:root{
  /* DARK FIRST. The shop floor at night, lit by a worklight — and the service
     symbols are dashboard telltales, in the colours a real dash uses. */
  color-scheme:dark;
  --paper:#0A0C0F; --surface:#12161B; --surface-2:#181D24;
  /* ⭐ 9 Sep — owner: "make text a little bigger and brighter." The two
     secondary inks came up a step each. #6B7681 on the near-black surface
     measured 4.9:1 — legal, and thin at the small sizes it is used for
     (mono captions, the coupon disclosure). Measured after the lift:
     --ink-2 10.17:1 and --ink-3 6.35:1 against --paper.
     ⛔ Keep --ink-3 above 6:1 — the fine print must not depend on a good
        screen, and it is used below 14px. */
  --ink:#F2F5F8; --ink-2:#B2BCC6; --ink-3:#8A949E;
  --rule:#242C34; --rule-soft:#1A1F26;
  --accent:#E01B24; --accent-ink:#FFFFFF; --accent-soft:#2C1014;
  --pass:#3FD07A; --pass-soft:#122419;
  --todo:#E0B860; --todo-soft:#2A2113; --todo-line:#6B5322;
  /* telltale palette — red critical, amber caution, green ok, blue info */
  --tt-red:#FF4438; --tt-amber:#FFB020; --tt-green:#3FD07A;
  --tt-blue:#54A8FF; --tt-cyan:#2BD4D9; --tt-violet:#B98CFF;
  --shadow:0 1px 2px rgba(0,0,0,.5), 0 14px 40px -18px rgba(0,0,0,.85);
  --f-display:"Archivo","Helvetica Neue",Arial,sans-serif;
  --f-body:"Instrument Sans","Helvetica Neue",Arial,sans-serif;
  --f-mono:"JetBrains Mono",ui-monospace,"SF Mono",Menlo,monospace;
  --step-0:1.125rem; --step-1:1.3125rem;
  --step-2:clamp(1.5rem,1.15rem + 1.4vw,2rem);
  --step-3:clamp(1.9rem,1.35rem + 2.4vw,2.85rem);
  --step-4:clamp(2.5rem,1.5rem + 4.2vw,4.5rem);
  --rail:12.5rem; --gutter:clamp(1.25rem,4vw,3rem); --measure:64ch;
}
/* 🔴 THE AUTOMATIC LIGHT THEME IS GONE, 10 SEP. Owner, on his phone: "the
   mobile page is broken… its white background."
   The token swap itself was complete, but NOTHING BUILT SINCE SWITCHES WITH
   IT: the tachometer's gradients are baked into its SVG, the dashboard lamps
   glow against black, the alloy pedal is drawn in dark metal, the hero
   background is dark. On a phone set to light mode you got a white page with
   a dark cockpit sitting on it — which is exactly as broken as it sounds.
   ⭐ This design IS a dark instrument panel. Committing to that is the honest
      call; a light variant is not a palette, it is a second design.
   ⛔ Do not restore `@media (prefers-color-scheme:light)` without redrawing
      the gauge, the lamps, the pedal and the hero for it.
   ⭐ `color-scheme:dark` on :root tells the browser too, so form fields,
      scrollbars and the address bar stop rendering light against it.
   ⚠️ The explicit [data-theme="light"] block below is left in place and
      unreachable — nothing sets that attribute. It is the starting point if a
      real light theme is ever built. */

:root[data-theme="light"]{
    --paper:#E9EBEE; --surface:#FFFFFF; --surface-2:#F4F5F7;
    --ink:#0F161C; --ink-2:#414C56; --ink-3:#646E78;
    --rule:#C6CDD4; --rule-soft:#DCE1E6;
    --accent:#C0202B; --accent-ink:#FFFFFF; --accent-soft:#F7E1E3;
    --pass:#1F7A4D; --pass-soft:#DFF0E7;
    --todo:#8A5A00; --todo-soft:#FBF0DA; --todo-line:#E0B860;
    --tt-red:#C81E14; --tt-amber:#9A6B00; --tt-green:#1F7A4D;
    --tt-blue:#1C63C4; --tt-cyan:#0E7C8A; --tt-violet:#6B3FC4;
    --shadow:0 1px 2px rgba(19,26,33,.06), 0 8px 24px -12px rgba(19,26,33,.18);
}
*{box-sizing:border-box}
body{margin:0; background:var(--paper); color:var(--ink); font-family:var(--f-body); font-size:var(--step-0); line-height:1.6; -webkit-font-smoothing:antialiased}
/* 🔴 THERE WAS NO GLOBAL IMAGE CONSTRAINT ON THIS SITE. AT ALL.
   services.html carried work-panel-finish.jpg with no class on it, so it
   rendered at its natural 1168px and pushed the page 853px wide at 375 —
   the same horizontal-scrollbar fault the owner caught on the service pages
   once already, sitting on a different page the whole time.
   ⭐ The per-component widths were all correct; that is exactly why it went
      unnoticed. One image that missed a class had nothing to fall back on.
   ⛔ A build gate now refuses if this rule is ever absent. */
/* 🔴 <figure> CARRIES A 40px LEFT AND RIGHT MARGIN FROM THE BROWSER, and
   nothing here ever reset it. It is why the two band photographs would not go
   full width on a phone: the grid was edge to edge at 375px and the image
   inside it sat at x=40, w=295. Every figure on this site is a component with
   its own spacing — .shot, .plate, .shotband — so the UA margin is never what
   is wanted, it is just invisible until something needs to touch an edge.
   ⛔ Do not remove: some components set margin:0 themselves and some never
      did, which is exactly how this stayed hidden. */
figure{margin:0}
img,svg,video,canvas{max-width:100%}
img,video{height:auto}
h1,h2,h3,h4{font-family:var(--f-display); margin:0; text-wrap:balance; letter-spacing:-.02em; line-height:1.06}
p{margin:0}
a{color:inherit}
:focus-visible{outline:2.5px solid var(--accent); outline-offset:3px; border-radius:2px}
.mono{font-family:var(--f-mono); font-variant-numeric:tabular-nums}
.lbl{font-family:var(--f-mono); font-size:.6875rem; font-weight:700; letter-spacing:.14em; text-transform:uppercase; color:var(--ink-3)}
.shell{max-width:78rem; margin-inline:auto; padding-inline:var(--gutter)}


/* ⭐ STICKY. On a repair-shop site the header carries the only two things a
   visitor in trouble wants — the phone and Book a slot — so it follows them
   down instead of making them scroll back. It THINS on scroll rather than
   changing color, so nothing jumps: the .is-stuck class is set by one
   IntersectionObserver on a sentinel, never by a scroll handler.
   ⛔ backdrop-filter only, no opacity on the element itself — a translucent
      header over this dark ground turns the logo gray. */
.top{position:sticky; top:0; z-index:30; border-bottom:1px solid var(--rule);
  background:color-mix(in srgb, var(--surface) 88%, transparent);
  -webkit-backdrop-filter:saturate(1.4) blur(12px); backdrop-filter:saturate(1.4) blur(12px);
  transition:box-shadow .25s ease, border-color .25s ease}
.top.is-stuck{border-bottom-color:color-mix(in srgb, var(--accent) 38%, var(--rule));
  box-shadow:0 10px 30px -18px rgba(0,0,0,.9)}
.top.is-stuck .top__in{padding-block:.5rem}
.top.is-stuck .brand__logo{width:6.5rem}
.top__in,.brand__logo{transition:padding-block .25s ease, width .25s ease}
@media (prefers-reduced-motion:reduce){ .top,.top__in,.brand__logo{transition:none} }
.top__in{display:flex; align-items:center; gap:1.25rem; padding-block:.85rem; flex-wrap:wrap;
  position:relative}   /* the mobile menu panel hangs off this */
/* the logo is the home link — standard, and people reach for it before the nav */
.brand{display:flex; align-items:center; gap:.7rem; text-decoration:none; margin-right:auto; cursor:pointer; border-radius:.3rem}
.brand:hover .brand__logo{opacity:.85}
.brand__logo{transition:opacity .12s ease}
/* ⭐ His own logo, 240x127 and the largest that exists — shown at 132px so the
   source still has ~1.8x density on a retina screen. It is a real RGBA PNG, so
   it sits on the black ground without a plate behind it. On a light theme the
   silver wordmark would disappear, so there it gets a dark plate. */
.brand__logo{width:8.25rem; height:auto; flex:none; display:block}
.brand__txt{display:none}
/* 🔴 REMOVED WITH THE LIGHT THEME, 10 SEP. This gave the logo a dark plate
   behind it when the OS was in light mode. With the site now always dark,
   `--ink` is the light ink — so on an iPhone set to light mode this painted a
   WHITE BOX behind the logo on every page. The rule outlived the thing it
   was compensating for. ⛔ Anything keyed on prefers-color-scheme is now a
   leftover; there is one design. */
:root[data-theme="light"] .brand__logo{background:var(--ink); border-radius:.3rem; padding:.3rem .45rem}
@media (max-width:30rem){ .brand__logo{width:6.75rem} }
.brand__nm{font-family:var(--f-display); font-weight:800; font-size:1.0625rem; letter-spacing:-.03em; line-height:1.1}
.brand__sub{font-family:var(--f-mono); font-size:.625rem; letter-spacing:.13em; text-transform:uppercase; color:var(--ink-3)}

/* dropdown — the same telltales as the service grid, so the menu teaches the
   symbol set before you ever reach a service page */
.nav{position:relative}
.nav>ul{display:flex; gap:1.15rem; flex-wrap:wrap; list-style:none; margin:0; padding:0}
.nav li{position:relative}

/* ⭐ MOBILE MENU. Below 60rem the nav becomes a panel and the button becomes an X.
   The bars are three spans so the animation is a real transform, not a glyph swap —
   a font character cannot morph, and a morph is what makes it feel built. */
.burger{display:none; width:2.9rem; height:2.9rem; flex:none; align-items:center; justify-content:center;
  background:transparent; border:1px solid var(--rule); border-radius:.35rem; cursor:pointer; padding:0}
.burger:hover{border-color:var(--accent)}
/* ⭐ THE MOTOR — a 2D flat inline-3, and it is still three lines.
   Owner: "the mobile hamberger can be creative like a motor looking
   hamberger… make it 2d". So: three pistons on a crank. The bars ARE the
   hamburger bars — same count, same spacing, same affordance — with the
   crankshaft down the left and a journal on each rod. Read it fast and it is
   a menu icon; read it slow and it is an engine.
   ⛔ FLAT. No gradient, no bevel, no drop shadow, no isometric skew. Strokes
      and fills only, one weight throughout. */
.burger svg{display:block; width:1.5rem; height:1.5rem; overflow:visible}
.burger .pist{fill:var(--ink); transition:transform .32s cubic-bezier(.5,0,.2,1), opacity .2s ease, fill .32s ease}
.burger .crank{stroke:var(--ink-3); stroke-width:1.6; stroke-linecap:round; fill:none;
  transition:opacity .22s ease, stroke .32s ease}
.burger .jour{fill:var(--ink-3); transition:transform .32s cubic-bezier(.5,0,.2,1), opacity .22s ease, fill .32s ease}
.burger:hover .pist{fill:var(--accent)}
.burger:hover .crank{stroke:var(--accent)}
.burger:hover .jour{fill:var(--accent)}

/* 🔴 EVERY MOVING PART TURNS ABOUT ITS OWN CENTRE, so the rotation is set on
   each piece rather than the <svg>. transform-box:fill-box is what makes a
   percentage origin mean the SHAPE's box — without it the browser measures
   the whole viewBox and the two bars swing wide of each other. */
.burger .pist,.burger .jour{transform-box:fill-box; transform-origin:50% 50%}

/* open — the engine folds to a cross. Middle piston and the crank go; the
   outer two swing about the block's center. Origin is nudged off-center
   because the bars start 6px apart, not on top of one another. */
.burger[aria-expanded="true"] .crank,
.burger[aria-expanded="true"] .jour{opacity:0}
.burger[aria-expanded="true"] .p2{opacity:0; transform:scaleX(.2)}
.burger[aria-expanded="true"] .pist{fill:var(--accent)}
.burger[aria-expanded="true"] .p1{transform:translateY(6px) rotate(45deg)}
.burger[aria-expanded="true"] .p3{transform:translateY(-6px) rotate(-45deg)}
@media (prefers-reduced-motion:reduce){
  .burger .pist,.burger .jour,.burger .crank{transition:none}
}

@media (max-width:60rem){
  .burger{display:inline-flex}
  .top__in{flex-wrap:nowrap; gap:.6rem}
  /* 🔴 `flex-wrap:nowrap` ON THE HEADER ROW AND `flex-basis:100%` ON THE NAV
     ARE A CONTRADICTION, and the nav lost. It could not wrap to its own line,
     so it stayed on the header row and ran clean off the right edge — three
     of the seven links were sliced in half at 375px, and the whole header was
     shoved down to make room.
     ⭐ Taking it OUT OF FLOW settles it: the header row keeps its single line
        and the menu becomes a panel hung under the header, which is what a
        mobile menu should have been anyway. `.top` is already the sticky
        positioning context, so the panel travels with it.
     ⛔ Do not "fix" this by putting flex-wrap:wrap back — that returns the
        header to two rows the moment the menu opens. */
  .nav{order:10; display:none}
  .nav.open{display:block; position:absolute; top:100%; left:0; right:0; z-index:40;
    padding:.35rem clamp(1rem,4vw,1.6rem) .6rem;
    background:var(--surface); border-top:1px solid var(--rule);
    border-bottom:1px solid color-mix(in srgb, var(--accent) 30%, var(--rule));
    box-shadow:0 18px 40px -18px rgba(0,0,0,.8),
               0 0 34px -14px color-mix(in srgb, var(--accent) 60%, transparent);
    max-height:calc(100dvh - 100%); overflow-y:auto; overscroll-behavior:contain}
  .nav>ul{flex-direction:column; gap:0}
  .nav>ul>li{border-bottom:1px solid var(--rule-soft)}
  .nav>ul>li>a{display:flex; padding:.75rem .1rem; font-size:1rem}
  .nav .sub{display:grid; position:static; opacity:1; visibility:visible; transform:none;
    grid-template-columns:1fr 1fr; border:0; box-shadow:none; background:transparent;
    margin:0 0 .5rem; gap:.15rem}
  .nav .sub a{background:transparent; padding:.45rem .1rem; font-size:.8125rem}
  .nav .sub a:hover{background:transparent}
  .top__act .btn--go{padding:.55rem .8rem}
}

/* a menu that opens should say so — chevron flips when the panel is open */
.nav a[aria-haspopup]{display:inline-flex; align-items:center; gap:.4rem}
.nav a[aria-haspopup]::after{content:""; width:.4rem; height:.4rem; flex:none;
  border-right:2px solid currentColor; border-bottom:2px solid currentColor;
  transform:translateY(-2px) rotate(45deg); opacity:.7;
  transition:transform .15s ease, opacity .15s ease}
.nav li:hover>a[aria-haspopup]::after,
.nav li:focus-within>a[aria-haspopup]::after{transform:translateY(1px) rotate(-135deg); opacity:1}
.nav .sub{position:absolute; top:calc(100% + .55rem); left:-.75rem; z-index:30;
  display:grid; grid-template-columns:repeat(2,minmax(11.5rem,1fr)); gap:1px;
  background:var(--rule); border:1px solid var(--rule); border-radius:.45rem;
  box-shadow:var(--shadow); list-style:none; margin:0; padding:0; overflow:hidden;
  opacity:0; visibility:hidden; transform:translateY(-4px); transition:opacity .12s ease, transform .12s ease, visibility .12s}
.nav li:hover>.sub, .nav li:focus-within>.sub{opacity:1; visibility:visible; transform:none}
.nav .sub a{display:flex; align-items:center; gap:.6rem; background:var(--surface);
  padding:.6rem .8rem; border-bottom:0; font-size:.875rem; white-space:nowrap; color:var(--ink)}
.nav .sub a:hover{background:var(--surface-2); border-bottom:0}
.nav .sub .sym{width:1.85rem; height:1.85rem; border-radius:.3rem}
.nav .sub .sym svg{width:1.05rem; height:1.05rem}
@media (max-width:60rem){
  .nav .sub{position:static; opacity:1; visibility:visible; transform:none; grid-template-columns:1fr;
    box-shadow:none; margin:.4rem 0 .2rem; display:none}
  .nav li:focus-within>.sub{display:grid}
}

.nav a{text-decoration:none; font-size:.9375rem; font-weight:500; color:var(--ink-2); padding-block:.25rem; border-bottom:2px solid transparent}
.nav a:hover{color:var(--ink); border-bottom-color:var(--accent)}
.nav a[aria-current="page"]{color:var(--ink); border-bottom-color:var(--accent)}
.top__act{display:flex; align-items:center; gap:.85rem}
.tel{text-decoration:none; font-family:var(--f-mono); font-weight:700; font-size:.9375rem; white-space:nowrap}
.tel__cap{display:block; font-size:.625rem; letter-spacing:.13em; color:var(--ink-3); font-weight:400}
.tel__ico{display:none}

/* ⭐ 375px REFLOW. flex-wrap:nowrap on .top__in let the header run 23px past an iPhone
   on all 26 pages — invisible on screen, because the overflow is off-screen to the right.
   The phone becomes an icon rather than being DROPPED: for a repair shop the call IS the
   conversion, and the mobile menu carries the nav links only, so hiding it would put the
   number a full page-scroll away. 44px keeps the tap target legal (2.5.8). */
@media (max-width:30rem){
  .tel{width:2.75rem; height:2.75rem; display:inline-flex; align-items:center; justify-content:center;
    flex:none; border:1px solid var(--rule); border-radius:.35rem}
  .tel:hover{border-color:var(--accent)}
  .tel__ico{display:block; width:1.15rem; height:1.15rem; fill:none; stroke:var(--ink);
    stroke-width:1.7; stroke-linecap:round; stroke-linejoin:round}
  .tel__cap,.tel__num{display:none}
}
.btn{display:inline-flex; align-items:center; justify-content:center; gap:.5rem; min-height:2.75rem; padding:.6rem 1.15rem; border-radius:.25rem; font-family:var(--f-display); font-weight:700; font-size:.9375rem; text-decoration:none; border:1.5px solid transparent; cursor:pointer; transition:transform .12s ease, background-color .12s ease}
.btn--go{background:var(--accent); color:var(--accent-ink)}
.btn--go:hover{transform:translateY(-1px)}
.btn--line{border-color:var(--rule); color:var(--ink); background:transparent}
.btn--line:hover{border-color:var(--ink); background:var(--surface-2)}

.bay{display:grid; grid-template-columns:var(--rail) minmax(0,1fr); gap:clamp(1.5rem,4vw,3.5rem)}
@media (max-width:60rem){ .bay{grid-template-columns:minmax(0,1fr)} .rail{display:none} }
.rail{border-right:1px solid var(--rule); padding-right:1.25rem; padding-top:.4rem}

/* ═══════════════════════════════════════════════════════════════════════
   🔴 THE RAIL IS WHAT MADE THE INNER PAGES READ AS 1998. A 12.5rem column of
   mono labels down the left, then one long column of prose beside it — that
   is a newspaper sidebar, and on a thin page (privacy, FAQ, warranty) there
   is nothing else on the screen to carry it.

   ⭐ ON INNER PAGES IT BECOMES A STRIP. Same markup, same content, same
      component — turned on its side, given the full measure and equal
      columns. The home page keeps the sidebar, because there the rail sits
      beside photographs and plates that hold their own.
   ⛔ Scoped by a body class, not by page-guessing in CSS: build.py stamps
      page-home or page-inner, so this can never accidentally reach the one
      layout the owner already signed off.
   ═══════════════════════════════════════════════════════════════════════ */
.page-inner .bay{grid-template-columns:minmax(0,1fr); gap:0}
.page-inner .rail{border-right:0; padding-right:0; padding-top:0; margin-bottom:1.75rem}
/* 🔴 A <dl> IN A GRID DOES NOT PAIR ITSELF. dt and dd are separate children,
   so auto-placement filled the row label, value, label, value — "ESTIMATE |
   In writing | CHANGES | Re-approved first" reading across, with the last row
   half empty. The label and its value have to be told they belong together.
   ⭐ grid-auto-flow:column over exactly TWO rows does it with no extra markup:
      dt lands on row 1, its dd on row 2 beneath it, then the next pair starts
      a new column. grid-auto-columns keeps those columns equal. */
.page-inner .rail dl{display:grid; grid-auto-flow:column; grid-template-rows:auto auto;
  grid-auto-columns:minmax(0,1fr);
  gap:1px; background:var(--rule); border:1px solid var(--rule);
  border-radius:.5rem; overflow:hidden}
/* ⛔ Column flow cannot wrap, so on a phone it must go back to rows or five
   facts become five 60px slivers. */
@media (max-width:46rem){
  .page-inner .rail dl{grid-auto-flow:row; grid-template-rows:none;
    grid-template-columns:minmax(0,1fr)}
}
.page-inner .rail dt{background:var(--surface); padding:1rem 1.15rem .2rem; margin:0}
.page-inner .rail dd{background:var(--surface); padding:0 1.15rem 1.05rem; margin:0}
/* ⛔ dt and dd are separate grid items — each must paint its own background or
   the grid gap shows through as a hairline across every card. */
.rail dl{margin:0; display:grid; gap:1.35rem}
.rail dt{font-family:var(--f-mono); font-size:.625rem; font-weight:700; letter-spacing:.13em; text-transform:uppercase; color:var(--ink-3)}
.rail dd{margin:.25rem 0 0; font-family:var(--f-mono); font-size:.9375rem; font-weight:500; line-height:1.35}
.rail dd em{font-style:normal; color:var(--ink-3); font-size:.75rem; display:block; margin-top:.15rem}


/* ⭐ THE HERO IS AN INSTRUMENT CLUSTER. No stock photograph of a smiling mechanic —
   the shop has no photography yet, and a borrowed garage is worse than none. The
   gauge sweep is the one moment of motion on the page and it settles into a
   resting state that is fully legible with JS off and with motion reduced. */
.hero{position:relative; isolation:isolate; overflow:hidden}
.hero__bg{position:absolute; inset:0; z-index:-1; pointer-events:none}
.hero__bg::before{content:""; position:absolute; inset:0;
  background:radial-gradient(58% 62% at 78% 8%, color-mix(in srgb, var(--accent) 26%, transparent) 0%, transparent 70%);}
.hero__bg::after{content:""; position:absolute; inset:0; opacity:.5;
  background-image:radial-gradient(currentColor .6px, transparent .6px);
  background-size:22px 22px; color:var(--rule);
  -webkit-mask-image:linear-gradient(to bottom, #000 0%, transparent 78%);
  mask-image:linear-gradient(to bottom, #000 0%, transparent 78%);}
/* ⭐ Owner, 9 Sep: smaller, and slower. 38rem/64vw was reading as the subject
   of the hero rather than its instrument. Now 26rem/40vw and dimmer. */
/* 🔴 Owner, 9 Sep: "tucked way up in rt corner so only 1/2 shows". It was
   right:-8%/top:-6% — deliberately bled off two edges as background texture.
   Now that it is a real instrument it has to read as one, so it sits WHOLE:
   centred on the hero's vertical axis, clear of the right edge, and sized to
   fit the hero's height rather than the viewport's width. */
/* ⭐ 9 Sep: the figure and the dial were fighting for the same 610px — 384
   plus 384 cannot both fit beside a headline, so two thirds of him sat on top
   of it. The dial gives ground rather than the man: 24rem → 21rem and pushed
   to the edge, because he is the subject and it is the backdrop. */
/* ⭐ THE DIAL'S BOX IS A VARIABLE because two things now depend on it: the
   gauge and the pedal under it. Restating the clamp in the pedal rule would
   be two numbers that must be kept equal by hand, and they never stay
   equal. ⛔ Never re-type this clamp — read --dial. */
.hero{--dial:min(21rem,26vw,calc(100% - 36rem)); --dial-right:clamp(1.5rem,5vw,4rem);
  --dial-mid:44%}
.gauge{position:absolute; right:var(--dial-right); top:var(--dial-mid); translate:0 -50%;
  width:var(--dial); aspect-ratio:1;
  z-index:0; opacity:.92; filter:drop-shadow(0 26px 46px rgba(0,0,0,.75))}
.gauge svg{width:100%; height:100%; overflow:visible; display:block}
.gauge .band{fill:none; stroke:var(--accent); stroke-width:5; opacity:.85}
.gauge .tick{stroke:#AEB9C4; stroke-width:2.2; stroke-linecap:butt}
.gauge .tick--min{stroke:#5C6771; stroke-width:1.1}
.gauge .tick--hot{stroke:var(--accent)}
.gauge .num{fill:#D8E0E7; font-family:var(--f-mono); font-size:14px; font-weight:600}
.gauge .num--hot{fill:#FF5A50}
.gauge .unit{fill:#7C8892; font-family:var(--f-mono); font-size:8px;
  letter-spacing:.2em; text-transform:uppercase}
/* ⭐ Owner, 9 Sep: make the THAO more visible. It was #4E5862 at 10px — a
   maker's mark whispered onto the face. Lifted to the numerals' own tone one
   step down, and up to 12.5px, so it reads as branding rather than as dirt.
   ⛔ Not red: red is the redline on this face and means "past 8000". A brand
      mark in the warning color is a mixed signal on an instrument. */
/* ⭐ MATCHED TO THE LOGO, 9 Sep. Owner: "make that look the same font as the
   logo." It was already Archivo — the mismatch was never the family, it was
   the WEIGHT and the TRACKING. The wordmark is heavy and set almost solid;
   this was 700 at .3em, which reads as a spaced-out instrument label, not as
   THAO. 800 and near-solid tracking is as close as the loaded stack gets.
   ⛔ Not red: red is the redline on this face and means "past 8000". A brand
      mark in the warning color is a mixed signal on an instrument. */
/* the wordmark is an image now; .mk stays for the unit labels that use it */
.gauge .mkimg{opacity:.92}
.gauge .ndl{transform-origin:120px 120px; transform:rotate(var(--idle))}

/* ⭐ TICK-SPACE, so every angle below is readable as an rpm rather than a
   number of degrees. The needle art points UP, and tick 0 sits at 135°, so
   the rotation for tick t is 225 + 27t. */
.gauge{--idle:246.6deg;   /* t=0.8, a warm idle */
       --t2:279deg; --t5:360deg; --t6:387deg; --t7.5:427.5deg;
       --t9:468deg; --rest:284.4deg}

@media (prefers-reduced-motion:no-preference){
  /* ⭐ Owner, 9 Sep: "let it rev". Three throttle blips — each SNAPS up and
     FALLS BACK slower, which is the asymmetry a real needle has and the whole
     reason this reads as an engine rather than an animation. Runs ONCE, then
     idles: a needle looping forever beside the headline competes with the
     words for the entire visit. */
  /* ⭐ Owner, 9 Sep: "needs to move up slower". Two long pulls instead of
     four quick blips — the climb now takes ~1.2s where it used to take
     200ms. The per-keyframe timing functions keep the character: the RISE
     eases out (fast off idle, easing into the top), the FALL eases in and
     takes longer. That asymmetry is the whole reason it reads as an engine
     rather than a needle being dragged around a dial. */
  /* ⭐ Owner, 9 Sep: the descent 50% slower again. The RISES are unchanged at
     ~1.3s each; every FALL is stretched by half, which is why the total went
     7.4s → 9.8s. Falls now run 2.55s and 2.88s against rises of 1.26s and
     1.33s — roughly 2:1, which is close to what a real needle does when the
     throttle shuts and it drops back on engine braking rather than snapping.
     ⛔ Do not equalise these. Symmetric rise and fall is the single thing
        that makes a gauge read as a loading spinner. */
  /* ⭐ Owner, 9 Sep: ONE automatic rev. It used to blip twice on load, which
     on a page you visit repeatedly reads as fidgeting. One clean pull to the
     redline and a long fall onto the resting angle — then it is still, and
     everything after that is the visitor's own doing via the throttle.
     Rise 1.3s, fall 4.1s: the same ~1:3 asymmetry the throttle uses. */
  .gauge .ndl{animation:rev 5.4s linear .35s both}
  @keyframes rev{
    0%  {transform:rotate(246.6deg); animation-timing-function:cubic-bezier(.22,.72,.4,1)}
    24% {transform:rotate(473deg);   animation-timing-function:cubic-bezier(.34,0,.42,1)}
    100%{transform:rotate(284.4deg)}
  }
  /* ⭐ Owner: "when you hover it it revs up". A DIFFERENT animation name, so
     re-entering the gauge restarts it — the same keyframes would not replay.
     ⛔ The gauge must own pointer events for this to fire at all; it used to
        be pointer-events:none at z-index -1. */
  /* ═══════════════════════════════════════════════════════════════════
     HOLD THE THROTTLE OPEN. Owner, 9 Sep: "when i hover it it needs to stay
     throtoled like a game when i un hover then it slowly goes down."
     ⭐ THAT IS A STATE, NOT AN ANIMATION, and the difference is the whole
        implementation. A keyframe animation has a fixed length: it cannot
        wait for you. A TRANSITION between two states can be held open
        indefinitely and released whenever the pointer leaves — and if you
        let go halfway up, it falls from wherever the needle actually is
        rather than snapping to a keyframe. That is why it feels like a
        throttle instead of a clip being replayed.
     ⛔ The rise and the fall carry DIFFERENT durations on purpose: 1.15s up,
        3.6s down. Same 1:3 asymmetry the load rev uses.
     ═══════════════════════════════════════════════════════════════════ */
  .gauge .ndl{transition:transform 3.6s cubic-bezier(.32,.46,.38,1)}
  .gauge.is-rev .ndl{transform:rotate(473deg);
    transition:transform 1.15s cubic-bezier(.2,.7,.38,1)}
  /* The load rev is still an animation, and an animation OUTRANKS a
     transition on the same property for as long as it is applied. So the
     moment it finishes, JS stamps .rev-done and control passes to the
     transitions above. ⛔ Without this the throttle silently does nothing. */
  .gauge.rev-done .ndl{animation:none; transform:rotate(var(--rest))}
  .gauge.rev-done.is-rev .ndl{transform:rotate(473deg)}
}
/* 🔴 THE HIT AREA WAS A CIRCLE, AND THAT IS WHAT "BOUNCED".
   The dial only took pointer events over the rounded SVG, so anywhere near
   the corners the smallest mouse movement crossed the curve repeatedly —
   enter, leave, enter — and the needle lurched up and fell back on every
   crossing. The box itself never moved; I measured it, identical before and
   during hover. It was the pointer flickering across a curved boundary.
   ⭐ The whole square is the target now. The hero copy ends 70px to its left
      so a rectangular target still cannot eat a click meant for the text. */
.gauge{pointer-events:auto}
.gauge svg{pointer-events:auto}

/* 🔴 THE DIAL DOES NOT MOVE. Owner, 9 Sep: "dont let the tachometer move on
   hover, make it stay in place, only the gauge moves."
   A 4% scale on hover was tried and pulled back out. On a real instrument
   the housing is bolted down and the needle is the only thing with a bearing
   under it — the moment the whole dial breathes, it stops reading as an
   instrument and starts reading as a button.
   ⛔ Do not reintroduce a hover transform, scale, translate or shadow change
      on .gauge. The needle carries the entire interaction. */
/* ⭐ Owner: "make it touch screen for mobile also". It used to be
   display:none below 52rem — decoration that a phone did not need. Now that
   you can play with it, the phone gets it too: out of the corner, into the
   flow under the copy, at a size a thumb can hold. */
@media (max-width:52rem){
  .hero{display:flex; flex-direction:column}
  .hero .shell{order:1}
  /* ⭐ Owner, 9 Sep: small on mobile — "just a small gange". Shrinking an
     instrument is not only a width change, because the viewBox is fixed at
     240: every mark scales down with it. At 142px wide the 14px numerals
     would render at 8.3px and the minor ticks would alias into a gray
     smear. So the face is SIMPLIFIED as it shrinks — numerals scaled back
     up, minor ticks and the two legends dropped — which is exactly what a
     real small gauge does. */
  .gauge{order:2; position:static; translate:none; width:min(11rem,40vw);
    margin:1.4rem auto .25rem; opacity:1}
  .gauge .num{font-size:19px}
  .gauge .tick{stroke-width:3}
  .gauge .tick--min,.gauge .unit{display:none}
  /* ⭐ Owner: the small dial lost its THAO. It is kept, but re-sized in
     viewBox units — at 150px wide the desktop 10px mark would render at
     6.3px. 17px here lands at ~10.6px on a phone. */
  /* ⭐ the small dial keeps its mark, scaled up in viewBox units so it does
     not vanish at 150px wide — the same correction the numerals needed */
  .gauge .mkimg{width:86px; height:32.6px; x:77px; y:64px}
  .gauge .band{stroke-width:6}
  .hero .split > div:first-child{max-width:100%}

}
/* ═══════════════════════════════════════════════════════════════════════
   THE GAS PEDAL — a brushed-alloy race pedal, and it actually travels.
   Owner: "not so fake, make it move down and look like a real pedal when you
   push it… brushed aluminum pedal with little black lines inside like a
   race pedal."
   🔴 THE FIRST ONE WAS FAKE BECAUSE NOTHING MOVED BUT A COLOUR. It squashed
      8% and lit up red — a button pretending. A pedal is hinged at the floor,
      so pressing it does three things at once, and you need all three or none
      of them read: the pad FORESHORTENS toward the hinge, the whole thing
      DROPS, and the floor shadow TIGHTENS underneath it.
   ⛔ display:none is the default and the media query opts in — the other way
      round leaves a real focusable button in every desktop tab order. */
.pedal{display:none; justify-items:center; gap:.35rem; margin:.55rem auto 0;
  appearance:none; background:none; border:0; padding:.2rem .5rem; cursor:pointer;
  -webkit-tap-highlight-color:transparent; touch-action:manipulation;
  -webkit-user-select:none; user-select:none}
.pedal svg{width:4.2rem; height:auto; display:block; overflow:visible}

.pedal__hinge{fill:#171D23}
.pedal__shad{fill:#000; opacity:.5;
  transition:transform .17s cubic-bezier(.3,0,.2,1), opacity .17s ease;
  transform-box:fill-box; transform-origin:50% 50%}
.pedal__bez{stroke:#F6FAFD; stroke-opacity:.5; stroke-width:1.6;
  transition:stroke .18s ease, stroke-opacity .18s ease}

/* ⭐ ORIGIN AT THE HINGE. The pad is hinged at its foot, so every transform
   turns about 50% 100% — that is the difference between a pedal going down
   and a rectangle getting shorter in the middle of the screen. */
.pedal__body{transform-box:fill-box; transform-origin:50% 100%;
  transition:transform .17s cubic-bezier(.3,0,.2,1), filter .22s ease}

.pedal.is-down .pedal__body{transform:translateY(4px) scaleY(.83)}
.pedal.is-down .pedal__shad{transform:scale(.72,.6); opacity:.72}
.pedal.is-down .pedal__bez{stroke:var(--accent); stroke-opacity:1}
/* ⛔ THE GLOW IS AN EDGE, NOT A HALO. A 9px accent drop-shadow around the
   whole body drew a red rectangle in the dark and made the alloy look lit
   from inside. The bezel carries the color; the body just tightens. */
.pedal.is-down .pedal__body{filter:drop-shadow(0 1px 3px rgba(0,0,0,.6))}

.pedal__say{font-family:var(--f-mono); font-size:.5625rem; letter-spacing:.18em;
  text-transform:uppercase; color:var(--ink-3); transition:color .18s ease}
.pedal.is-down .pedal__say{color:var(--accent)}
.pedal:focus-visible{outline:2px solid var(--accent); outline-offset:3px; border-radius:.3rem}
@media (prefers-reduced-motion:reduce){
  .pedal__body,.pedal__shad{transition:none}
}

/* 🔴 THIS OPT-IN MUST SIT AFTER THE `display:none` ABOVE, AND ORIGINALLY DID
   NOT. I wrote it into the hero's own mobile block 200 lines earlier — where
   the caption it replaced had lived — then defined the base rule down here.
   Same specificity, later rule wins, so the pedal was display:none at every
   width and the owner said "cant see".
   ⛔ When a component's rules are split across a stylesheet, its media
      opt-in belongs with the component, not with the thing it replaced. */
/* 🔴 THE DESKTOP PEDAL WAS BUILT AND PULLED BACK OUT, 9 Sep — and the reason
   is worth keeping, because the obvious fix does not work.
   Hanging it off the dial's LAYOUT box put it 91px INSIDE the dial: .gauge
   carries a scroll parallax (`--sy` drives a vertical translate), so where
   it is PAINTED is not where it is LAID OUT, and `top: 44% + dial/2` lands
   at the untransformed position while the dial has drifted down from it.
   Matching it would mean giving the pedal the same drift — two elements
   sharing one transform, which is a wrapper, not a rule.
   ⭐ Not worth it: the owner's own call was "no desktop… too much", and the
      composition is already tight with the figure in there. Desktop revs on
      hover; the pedal is the touch affordance and stays touch-only.
   ⛔ If this ever comes back, wrap the dial AND the pedal in one positioned
      element and put the parallax on the wrapper. Do not re-derive `top`. */

@media (max-width:52rem){
  /* 🔴 AND IT NEEDS AN ORDER. The hero is a flex container on mobile and the
     dial is `order:2`; a new sibling defaults to `order:0`, so the pedal
     rendered as the FIRST item in the hero — 862px above the gauge it
     belongs to, off the top of the screen. It was in the right place in the
     DOM the whole time, which is why it looked like it had vanished.
     ⛔ Adding a sibling to a flex/grid container whose children carry
        explicit order values means giving the new one an order too. */
  .pedal{display:grid; order:3}
}

/* ⛔ Stop the browser treating a long press as a text selection or a
   double-tap zoom — both fight a press-and-hold gesture. */
.gauge svg{touch-action:manipulation; -webkit-tap-highlight-color:transparent;
  -webkit-user-select:none; user-select:none}

/* ═══════════════════════════════════════════════════════════════════════════
   MOTION. Two pieces, both cheap, both specific to this subject.

   ⛔ WHY THERE IS NO WebGL CAR. The best-known Three.js car showcase measures
      46.7s to first paint; Apple ships ZERO canvas on its product pages; and
      branded car models are licensed Editorial Use Only, so a recognisable
      badge is a legal problem on a commercial page. A slow hero loses more
      customers than a clever one wins.
   ⭐ So the depth is real but costs nothing: layered transforms, no library,
      no 3D model, no canvas.
   ═══════════════════════════════════════════════════════════════════════════ */

/* 1 ── THE BULB CHECK, REWRITTEN 9 Sep. Turn the key in any car built since
   the nineties and every warning lamp lights, one after another, as each
   system reports in — then they STAY lit until the system says otherwise.

   🔴 THE FIRST VERSION GOT ALL THREE THINGS WRONG, and the owner named them
      exactly: "they light up all over the place… then keep lit unless you
      scroll back up… only one in order though, not all at once."
      · IT TRIGGERED ON THE GRID, NOT THE ICON. One rect check on the whole
        .svc meant the moment its TOP crossed the line, all fifteen started
        their staggered run — including the ones twelve rows below the fold.
        By the time you scrolled to those they had already finished, so they
        read as lighting at random.
      · IT PUT THEM OUT AGAIN. The keyframe ended at 100% back on the dim
        value, so nothing stayed lit — the whole effect existed only during
        the 2.1s you were not looking at it.
      · IT WAS A CASCADE, NOT A SEQUENCE. Fifteen animations all started at
        once, separated only by animation-delay. Scroll past quickly and they
        overlap into a single flash.

   ⭐ SO IT IS A STATE NOW, NOT AN ANIMATION. `.lit` is added once per icon,
      in index order, one at a time, only when THAT icon is in view — and it
      is never removed. A transition carries the fade; there is no keyframe to
      run out and revert. */
.svc .sym{transition:box-shadow .45s ease, background-color .45s ease}
.svc a.lit .sym{
  background:color-mix(in srgb, currentColor 42%, transparent);
  box-shadow:0 0 0 1px color-mix(in srgb, currentColor 52%, transparent),
             0 0 22px -3px currentColor;
}

/* 2 ── HERO DEPTH. The gauge, the figure and the dot grid sit on three planes
   and move at three rates against the pointer. It is a flat cut-out; this is
   what makes it read as standing in a room. Scroll adds a slow vertical drift.
   ⛔ Everything is at its resting position with JS off — nothing is parked at
      opacity 0 waiting for an observer. */
/* 🔴 THE DIAL IS OUT OF THE PARALLAX — 9 Sep, and this is what the owner was
   actually seeing when he said the gauge still moves on hover. It was never
   my hover scale; it was THIS. The dial tracked the pointer by up to 16px and
   ROTATED 1.1 degrees with it, which on an instrument reads as the whole
   housing swimming.
   ⛔ AND IT IS WHY MY MEASUREMENT SAID "PINNED". I tested with synthetic
      pointerenter events, which never move a real cursor, so --px/--py stayed
      0 and the parallax never engaged. A dispatched event is not a mouse.
   ⭐ The dot grid keeps its drift — a background may swim, an instrument may
      not. */
.hero__bg{will-change:transform}
@media (prefers-reduced-motion:no-preference){
  .hero__bg{transform:translate3d(calc(var(--px,0) * 6px), calc(var(--py,0) * 5px + var(--sy,0) * 14px), 0);
    transition:transform .45s cubic-bezier(.2,.8,.3,1)}
  .hero.tracking .hero__bg{transition:none}
}

/* ⭐ FLAT, NOT LIT. He is a cut-out on a flat ground — no drop shadow pretending
   he is standing in a room, no card behind him. Cleaner, and it reads as a 2D
   graphic treatment rather than a photo-composite that does not quite convince. */
.shots .shot--cut{background:linear-gradient(180deg, color-mix(in srgb, var(--accent) 10%, transparent), transparent)}
.shots .shot--cut img{aspect-ratio:auto; object-fit:contain; max-height:15rem; margin-inline:auto; padding-top:.75rem}

/* ⭐ THE HERO FIGURE — a cut-out PNG with real alpha, so he stands on the black
   ground with the gauge glow behind rather than sitting in a box.
   ⚠️ This image is AI-GENERATED. It is the most prominent place on the site, so
   it is the FIRST one to replace with a photograph of Thao. The flag says so. */
.hero__fig{position:relative; display:grid; justify-items:center; align-self:end}
.hero__fig img{width:min(21rem,100%); height:auto; display:block;
}
.hero__fig .shot__flag{position:absolute; top:.4rem; right:.4rem; z-index:2}
.hero__fig::after{content:""; position:absolute; left:50%; bottom:-2px; translate:-50% 0;
  width:76%; height:2px; background:linear-gradient(90deg,transparent,var(--accent),transparent); opacity:.75}
@media (max-width:56rem){ .hero__fig{display:none} }
.ticketband{display:grid; grid-template-columns:minmax(0,.9fr) minmax(0,1.1fr);
  gap:clamp(1.5rem,4vw,3rem); align-items:center}
@media (max-width:56rem){ .ticketband{grid-template-columns:minmax(0,1fr)} }
.hero{padding-block:clamp(2.25rem,5vw,4rem) clamp(1.75rem,4vw,3rem); border-bottom:1px solid var(--rule)}
.eyebrow{display:flex; align-items:center; gap:.6rem; margin-bottom:1.1rem; flex-wrap:wrap}
.eyebrow .dot{width:.5rem; height:.5rem; border-radius:50%; background:var(--pass); flex:none}
/* 🔴 THE HEADLINE'S WIDTH IS SET HERE, NOT BY THE COLUMN. 16ch + text-wrap
   balance is what decides the wrap; the .split cap only ever clips it. I
   narrowed the column to make room for the figure and got SEVEN balanced
   short lines and 180px of dead air, because balance answers to the width it
   is given, not to the cap I was tuning.
   ⭐ So the type gives the ground instead: 4.5rem → 4rem at the top end. The
      same five-line break now happens in ~70px less width, which is exactly
      the room the figure needed. ⛔ Tune against the h1's real ink edge (a
      Range over its text), never its box. */
.hero h1{font-size:clamp(2.4rem,1.4rem + 3.6vw,4rem); font-weight:800;
  max-width:16ch; margin-bottom:1.1rem}
.hero h1 u{text-decoration:none; color:var(--accent)}
.hero__lede{font-size:var(--step-1); color:var(--ink-2); max-width:46ch; margin-bottom:1.75rem}
.hero__act{display:flex; gap:.75rem; flex-wrap:wrap; align-items:center}
.hero__note{font-size:.8125rem; color:var(--ink-3); max-width:36ch; margin-top:1rem}
.split{display:grid; grid-template-columns:minmax(0,1.05fr) minmax(0,.95fr); gap:clamp(1.75rem,4vw,3rem); align-items:start}
/* 🔴 9 Sep — THE HERO IS ONE COLUMN. It used to be a .split whose right half
   held a figure. When the fabricated "employee" came out, .split kept
   reserving that half and the hero rendered with a dead black column beside
   the headline on every screen wide enough to split — the gap the owner
   reported. The gauge is the hero's visual; it is positioned, not a column. */
.hero .split{grid-template-columns:minmax(0,1fr)}
/* ⭐ 34rem opened the gap but wrapped the headline to SEVEN lines and left
   180px of dead air. 36.5rem is the balance: the headline reads in five, and
   the figure still clears it comfortably. ⛔ Tune this against the h1's real
   ink edge (a Range over its text), never its box — the box runs to the cap
   whatever the letters do. */
.hero .split > div:first-child{max-width:min(100%, 36.5rem)}
/* ⭐ THE TECHNICIAN. Bottom-aligned to the hero so he stands on its floor,
   in front of the dial, and clear of the copy. Below 60rem the hero has no
   room for a figure and a headline at once, so he is dropped rather than
   shrunk into a thumbnail. */
/* ⭐ Owner: bigger, fill more of the space. 19rem → 24rem, and the right
   offset comes IN to match so he grows toward the dial rather than into the
   headline. At 1280 that puts him 717–1101 against a headline whose longest
   line ends at 670 — 47px of daylight, up from 12px when he was smaller. */
/* ⭐ 9 Sep: both moved off the right edge together. The dial had only 32px
   of margin, which reads as falling off the page rather than sitting on it.
   Each gains ~32px; the gap BETWEEN them is unchanged, so the composition
   holds and only its distance from the edge changes. */
/* ⭐ PUSHED LEFT, 9 Sep — owner: "push over man", then "to much". So this is
   the half-step: 19.5rem → 21.5rem, not the 24.5rem I tried first. */
.hero__man{position:absolute; right:clamp(9.5rem,21.5vw,21.5rem); bottom:0; z-index:1;
  width:min(24rem,30vw); height:auto; display:block; pointer-events:none;
  filter:drop-shadow(0 24px 34px rgba(0,0,0,.85))}
@media (max-width:60rem){ .hero__man{display:none} }
/* ⛔ The headline must CLEAR the gauge, not sit under it. At 46rem the word
   "whole" ran beneath the bezel at 1280px. The copy is capped and the dial
   bleeds further off the right edge, so the two never meet. */
@media (max-width:56rem){ .split{grid-template-columns:minmax(0,1fr)} }

/* page head for interior routes */
.phead{padding-block:clamp(2rem,4vw,3.25rem) clamp(1.5rem,3vw,2.25rem); border-bottom:1px solid var(--rule)}
.crumb{font-family:var(--f-mono); font-size:.75rem; color:var(--ink-3); margin-bottom:.9rem}
.crumb a{text-decoration:none}
.crumb a:hover{color:var(--accent)}
.phead h1{font-size:var(--step-3); font-weight:800; max-width:20ch; margin-bottom:.85rem}
/* ⭐ THE TELLTALE AT THE TOP OF A SERVICE PAGE LIGHTS AT LOAD — turn the key
   and the bulbs prove themselves. The grid's lamps are pumped by scroll; this
   one is already on screen, so there is nothing to wait for.
   ⛔ CSS ONLY, and it ENDS lit (`forwards`). With JS off, with the animation
      disabled, with prefers-reduced-motion — the lamp is on. Nothing here is
      parked dark waiting for a script. */
.phead h1 .sym{animation:bulbcheck 1.15s ease-out .25s both}
@keyframes bulbcheck{
  0%{background:color-mix(in srgb, currentColor 14%, transparent); box-shadow:none}
  38%{background:color-mix(in srgb, currentColor 62%, transparent);
      box-shadow:0 0 0 1px color-mix(in srgb, currentColor 70%, transparent), 0 0 30px -2px currentColor}
  100%{background:color-mix(in srgb, currentColor 42%, transparent);
       box-shadow:0 0 0 1px color-mix(in srgb, currentColor 52%, transparent), 0 0 22px -3px currentColor}
}
@media (prefers-reduced-motion:reduce){
  .phead h1 .sym{animation:none;
    background:color-mix(in srgb, currentColor 42%, transparent);
    box-shadow:0 0 0 1px color-mix(in srgb, currentColor 52%, transparent), 0 0 22px -3px currentColor}
}
.phead p{font-size:var(--step-1); color:var(--ink-2); max-width:52ch}

.ticket{background:var(--surface); border:1px solid var(--rule); border-radius:.4rem; box-shadow:var(--shadow); overflow:hidden}
.ticket__cap{display:flex; align-items:center; gap:.5rem; padding:.5rem 1.1rem; font-family:var(--f-mono); font-size:.6875rem; letter-spacing:.11em; text-transform:uppercase}
.ticket__cap{background:var(--surface-2); color:var(--ink-2); border-bottom:1px solid var(--rule)}
.ticket__hd{display:flex; align-items:baseline; gap:.75rem; padding:.85rem 1.1rem; border-bottom:1px solid var(--rule); background:var(--surface-2); flex-wrap:wrap}
.ticket__no{font-family:var(--f-mono); font-weight:700; font-size:.8125rem}
.ticket__veh{font-size:.8125rem; color:var(--ink-2); margin-left:auto; font-family:var(--f-mono)}
.ticket__body{padding:1.1rem}
.line{display:grid; grid-template-columns:auto minmax(0,1fr) auto; gap:.6rem .9rem; align-items:baseline; padding-block:.7rem; border-bottom:1px dashed var(--rule-soft)}
.line:last-of-type{border-bottom:0}
.line__code{font-family:var(--f-mono); font-weight:700; font-size:.8125rem; color:var(--accent); background:var(--accent-soft); padding:.1rem .4rem; border-radius:.2rem; white-space:nowrap}
.line__what b{display:block; font-weight:600; font-size:.9375rem}
.line__what span{display:block; font-size:.8125rem; color:var(--ink-2); margin-top:.1rem}
.line__amt{font-family:var(--f-mono); font-weight:700; font-size:.9375rem; white-space:nowrap; font-variant-numeric:tabular-nums}
.line__amt em{display:block; font-style:normal; font-weight:400; font-size:.6875rem; color:var(--ink-3); text-align:right}
.ticket__ft{display:flex; align-items:center; gap:.75rem; padding:.9rem 1.1rem; border-top:1px solid var(--rule); background:var(--surface-2); flex-wrap:wrap}
.stamp{display:inline-flex; align-items:center; gap:.4rem; font-family:var(--f-mono); font-size:.75rem; font-weight:700; letter-spacing:.06em; text-transform:uppercase; color:var(--pass); background:var(--pass-soft); border:1px solid color-mix(in srgb, var(--pass) 30%, transparent); padding:.28rem .55rem; border-radius:.2rem}
.ticket__tot{margin-left:auto; font-family:var(--f-mono); font-weight:700; font-size:1.0625rem}
.ticket__tot em{font-style:normal; font-size:.6875rem; color:var(--ink-3); font-weight:400; letter-spacing:.1em; text-transform:uppercase; margin-right:.4rem}

.sec{padding-block:clamp(2.5rem,5vw,4rem); border-bottom:1px solid var(--rule)}
.sec:last-child{border-bottom:0}
.sec__hd{max-width:var(--measure); margin-bottom:2rem}
.sec__hd h2{font-size:var(--step-3); font-weight:800; margin-block:.6rem .7rem}
.sec__hd p{color:var(--ink-2); font-size:var(--step-1)}
.prose{max-width:var(--measure); display:grid; gap:1.15rem}
.prose h3{font-size:1.25rem; font-weight:700; margin-top:.75rem}
.prose p{color:var(--ink-2)}
.prose ul{margin:0; padding-left:1.15rem; color:var(--ink-2); display:grid; gap:.5rem}
.prose strong{color:var(--ink); font-weight:600}

/* ═══════════════════════════════════════════════════════════════════════
   ILLUMINATION — one lighting language for the whole site.
   Owner: "even on check light come on can be lit up… make it so things like
   this just light up like a high tech car would, tesla style."
   A modern dash does not blink things at you. A lamp comes up smoothly, sits
   slightly hot for a moment, then settles and STAYS ON. That is the whole
   behavior, and it is why this is a state and not a loop.

   🔴 THE UNLIT STATE MUST BE FULLY LEGIBLE. Nothing here is parked at
      opacity 0 or hidden waiting for a scroll handler. Unlit is "dimmer",
      never "absent" — with JS dead the page reads exactly the same, just
      without the light. ⛔ Never make a lamp the only thing revealing text.
   ⭐ --lamp carries the color, so a green PASS chip and an amber telltale
      share one rule and differ by one variable. */
[data-lamp]{--lamp:var(--accent);
  transition:box-shadow .55s cubic-bezier(.2,.6,.2,1), border-color .55s ease,
             color .45s ease, background-color .55s ease}
[data-lamp].lit{
  border-color:color-mix(in srgb, var(--lamp) 55%, var(--rule));
  animation:lampon .85s cubic-bezier(.2,.7,.25,1) both}
@keyframes lampon{
  0%{box-shadow:0 0 0 1px color-mix(in srgb,var(--lamp) 0%,transparent), 0 0 0 0 transparent}
  55%{box-shadow:0 0 0 1px var(--lamp), 0 0 34px -4px var(--lamp)}
  100%{box-shadow:0 0 0 1px color-mix(in srgb,var(--lamp) 45%,transparent),
                 0 0 22px -7px var(--lamp)}
}
/* the green chips carry the PASS hue, not the brand red */
.stamp{--lamp:var(--pass)}
/* ⭐ A READOUT LIGHTS LINE BY LINE — that sequence is the whole effect, and
   it costs nothing but the pump's 90ms stride.
   ⛔ The code chips keep their OWN hue. An earlier pass had them lighting
      green on top of the red-tinted background they already carry, which is
      two colours arguing inside one 40px chip. --lamp defaults to accent and
      that is correct here; do not override it. */
.line__code{border:1px solid transparent}
@media (prefers-reduced-motion:reduce){
  [data-lamp].lit{animation:none;
    box-shadow:0 0 0 1px color-mix(in srgb,var(--lamp) 45%,transparent), 0 0 22px -7px var(--lamp)}
}

/* 🔴 `.done` WAS ADDED BY JAVASCRIPT AND STYLED BY NOTHING. The march handler
   has been tagging every .step on scroll since the day it was written and
   there has never been a single rule matching it, so the owner's "have the
   #1 2 3 4 5 light up green as you scroll down" shipped as pure dead weight.
   ⭐ A gate in build.py now refuses any state class the runtime sets that the
      stylesheet does not answer. */
.step__n{transition:color .4s ease, text-shadow .5s ease}
.step.done .step__n{color:var(--pass);
  text-shadow:0 0 14px color-mix(in srgb,var(--pass) 70%,transparent)}
.step__n::after{content:""; display:block; width:1.6rem; height:2px; margin-top:.45rem;
  background:var(--rule); transition:background-color .45s ease, box-shadow .5s ease}
.step.done .step__n::after{background:var(--pass);
  box-shadow:0 0 12px -1px var(--pass)}

.steps{display:grid}
.step{display:grid; grid-template-columns:3.25rem minmax(0,1fr); gap:1.25rem; padding-block:1.35rem; border-top:1px solid var(--rule-soft); align-items:start}
.step:first-child{border-top:0; padding-top:0}
.step__n{font-family:var(--f-mono); font-weight:700; font-size:.8125rem; color:var(--n-col,var(--ink-3)); padding-top:.2rem}
.step h3{font-size:1.1875rem; font-weight:700; margin-bottom:.35rem}
.step p{color:var(--ink-2); max-width:58ch; font-size:.9375rem}
.step__meta{margin-top:.5rem; font-family:var(--f-mono); font-size:.75rem; color:var(--ink-3)}


/* ⭐ THE SPECIAL IS A WORK ORDER, NOT A COUPON. Every shop runs an oil-change
   offer and every one of them prints the same starburst. This one is set as a
   torn-off ticket so it belongs to the same world as the repair order in the
   hero — the page has one idea, not two. */
/* ⭐ A COUPON THAT IS ACTUALLY A COUPON. Perforated edge, tear-off stub, and it
   prints on its own — @media print hides the entire site and leaves this one card
   on the page, so "print" produces a coupon and not 26 pages of website.
   ⛔ The dashes are a repeating-linear-gradient border, not an image: it stays
   crisp at any zoom and costs nothing. */
/* ⭐ THE SPEC PLATE. Every service page gets a drawing instead of a stock photo,
   because we have three usable photographs and fifteen service pages — and the
   same borrowed garage repeated fifteen times is worse than no picture at all.
   Each plate is the service's own telltale on an engineering grid, with that
   service's real figures set as dimension callouts. It teaches something a stock
   image cannot, and it costs about 2 KB. */
.plate{position:relative; border:1px solid var(--rule); border-radius:.45rem;
  background:var(--surface); overflow:hidden; display:grid;
  grid-template-columns:minmax(0,1fr) minmax(0,1.05fr); align-items:center; gap:0}
@media (max-width:44rem){ .plate{grid-template-columns:minmax(0,1fr)} }
.plate__draw{position:relative; display:grid; place-items:center; padding:clamp(1.5rem,4vw,2.5rem);
  background:
    linear-gradient(to right, color-mix(in srgb,var(--rule) 45%,transparent) 1px, transparent 1px) 0 0/28px 28px,
    linear-gradient(to bottom, color-mix(in srgb,var(--rule) 45%,transparent) 1px, transparent 1px) 0 0/28px 28px,
    var(--surface-2)}
.plate__draw .sym{width:7.5rem; height:7.5rem; border-radius:.9rem;
  background:color-mix(in srgb, currentColor 12%, transparent);
  border:1px solid color-mix(in srgb, currentColor 34%, transparent)}
.plate__draw .sym svg{width:4.2rem; height:4.2rem; stroke-width:1.5}
.plate__specs{padding:clamp(1.25rem,3vw,2rem); display:grid; gap:0; align-content:center}
.plate__row{display:grid; grid-template-columns:auto minmax(0,1fr) auto; align-items:baseline;
  gap:.6rem; padding-block:.7rem; border-bottom:1px dashed var(--rule-soft)}
.plate__row:last-child{border-bottom:0}
.plate__k{font-family:var(--f-mono); font-size:.6875rem; letter-spacing:.11em;
  text-transform:uppercase; color:var(--ink-3); white-space:nowrap}
.plate__lead{height:1px; background:repeating-linear-gradient(to right,
  color-mix(in srgb,var(--rule) 90%,transparent) 0 4px, transparent 4px 8px); align-self:center}
.plate__v{font-family:var(--f-mono); font-weight:700; font-size:.9375rem; white-space:nowrap}
/* ⭐ 320px. A three-column spec row with a nowrap value cannot fit the narrowest
   viewport WCAG 1.4.10 names; the value was being pushed out of sight rather than
   scrolled to. Stack the row instead — a spec reads fine on two lines. */
@media (max-width:24rem){
  .plate__row{grid-template-columns:minmax(0,1fr)}
  .plate__v{white-space:normal; overflow-wrap:anywhere}
}
.plate__cap{position:absolute; left:.7rem; top:.7rem; font-family:var(--f-mono);
  font-size:.625rem; letter-spacing:.12em; text-transform:uppercase; color:var(--ink-3)}

/* ═══════════════════════════════════════════════════════════════════════
   THE OIL CHANGE — the home page's own ticket.
   Owner: "make the home oil change element way better."
   🔴 WHAT WAS WRONG WAS NOT DECORATION. The old section stated $45/$60 three
      separate times — a reference rail above it, the h2, and the stub — in a
      1065px column with the stub stacked underneath, so the ticket read as a
      long gray slab and the price was nowhere in particular. Saying a number
      three times is how you make it stop being the number.
   ⭐ ONE TICKET. The left half sells the visit and never mentions money; the
      stub carries the price, the expiry, the terms and the actions. That is
      also how a real coupon is organized, and why the stub is the half people
      photograph.
   ⛔ Flat, like the popup — solid fills, a dashed seam, notches cut with
      border-radius. No gradient, no bevel. The only shadow is a glow. */
/* ═══════════════════════════════════════════════════════════════════════
   THE REVIEWS CAROUSEL
   ⭐ scroll-snap IS the carousel. The track is a real horizontally scrolling
      list, so a thumb swipes it, a trackpad flicks it, Tab reaches it and the
      arrow keys move it — all for free, and it still works with JS dead. The
      buttons only call scrollBy.
   ⛔ No autoplay. A testimonial that slides away mid-sentence is a reading
      problem, not a feature. */
.rvw{display:grid; gap:1rem}
.rvw__hd{display:flex; align-items:center; justify-content:space-between; gap:1rem; flex-wrap:wrap}
.rvw__score{margin:0; display:flex; align-items:center; gap:.6rem; flex-wrap:wrap;
  font-family:var(--f-mono); font-size:.8125rem; color:var(--ink-3)}
.rvw__score b{font-size:1.5rem; color:var(--ink); font-weight:700; line-height:1}
.rvw__stars{display:inline-flex; gap:.1rem}
.rvw__stars svg{width:.95rem; height:.95rem; fill:#F2B01E}
.rvw__nav{display:flex; gap:.4rem}
.rvw__btn{appearance:none; cursor:pointer; width:2.4rem; height:2.4rem; border-radius:.35rem;
  border:1px solid var(--rule); background:var(--surface); color:var(--ink-2);
  font-size:1rem; line-height:1; transition:color .2s ease, border-color .2s ease}
.rvw__btn:hover{color:var(--accent); border-color:var(--accent)}
.rvw__btn:focus-visible{outline:2px solid var(--accent); outline-offset:2px}

.rvw__track{display:grid; grid-auto-flow:column; grid-auto-columns:minmax(17rem,20rem);
  gap:1rem; overflow-x:auto; scroll-snap-type:x mandatory; scroll-behavior:smooth;
  padding-bottom:.5rem; scrollbar-width:thin}
.rvw__track:focus-visible{outline:2px solid var(--accent); outline-offset:4px; border-radius:.4rem}
.rvw__card{scroll-snap-align:start; margin:0; display:grid; gap:.7rem; align-content:start;
  padding:1.25rem 1.3rem; border-radius:.5rem;
  background:var(--surface); border:1px solid var(--rule)}
.rvw__card blockquote{margin:0; font-size:.9375rem; line-height:1.6; color:var(--ink-2)}
.rvw__card figcaption{font-family:var(--f-mono); font-size:.75rem; color:var(--ink);
  display:grid; gap:.1rem}
.rvw__card figcaption span{font-size:.625rem; letter-spacing:.12em; text-transform:uppercase; color:var(--ink-3)}
.rvw__note{margin:0; font-size:.75rem; color:var(--ink-3)}
@media (prefers-reduced-motion:reduce){ .rvw__track{scroll-behavior:auto} }

/* the case for this shop, on each service page */
.whyus{margin-top:2.75rem; padding:clamp(1.5rem,3.5vw,2.25rem);
  background:var(--surface); border:1px solid var(--rule); border-radius:.55rem;
  border-left:3px solid var(--accent)}
.whyus h2{font-size:clamp(1.25rem,1rem + 1vw,1.6rem); font-weight:800;
  margin-bottom:.7rem; max-width:26ch}
.whyus p{color:var(--ink-2); max-width:68ch}
.whyus p + p{margin-top:1rem}
.whyus__local{font-size:.9375rem; color:var(--ink-3); padding-top:1rem;
  border-top:1px solid var(--rule)}

.coupon-sec{scroll-margin-top:5rem}
.oil{position:relative; display:grid; grid-template-columns:minmax(0,1fr) 19rem;
  background:var(--surface); border:1px solid var(--rule); border-radius:.6rem;
  box-shadow:0 0 0 1px color-mix(in srgb,var(--accent) 16%, transparent),
             0 0 70px -26px var(--accent)}
@media (max-width:56rem){ .oil{grid-template-columns:minmax(0,1fr)} }

.oil__main{padding:clamp(1.5rem,3.4vw,2.5rem)}
.oil__tag{display:flex; align-items:center; gap:.55rem; margin-bottom:1rem;
  font-family:var(--f-mono); font-size:.6875rem; letter-spacing:.14em;
  text-transform:uppercase; color:var(--accent)}
.oil__lamp{width:.6rem; height:.6rem; border-radius:50%; background:var(--accent);
  box-shadow:0 0 0 3px color-mix(in srgb,var(--accent) 22%,transparent),
             0 0 12px -1px var(--accent);
  animation:lampidle 2.6s ease-in-out infinite}
.oil__h{font-size:clamp(1.6rem,1.1rem + 1.9vw,2.35rem); font-weight:800;
  line-height:1.1; margin-bottom:.85rem; max-width:20ch}
.oil__h span{color:var(--ink-2)}
.oil__lede{color:var(--ink-2); max-width:52ch; margin-bottom:1.5rem}

.oil__inc{list-style:none; margin:0; padding:0; display:grid; gap:.55rem}
@media (min-width:40rem){ .oil__inc{grid-template-columns:1fr 1fr; gap:.55rem 1.6rem} }
.oil__inc li{display:grid; grid-template-columns:1.55rem minmax(0,1fr); gap:.6rem;
  align-items:start; font-size:.9375rem; color:var(--ink-2);
  border-radius:.3rem; padding:.15rem .2rem}
.oil__inc b{display:block; color:var(--ink); font-weight:600}
/* ⭐ the ticks report in one at a time as you reach the section — the site's
   own [data-lamp] pump, so this needs no JavaScript of its own */
.oil__ick{display:grid; place-items:center; width:1.55rem; height:1.55rem;
  border-radius:.3rem; border:1px solid var(--rule);
  background:color-mix(in srgb,var(--pass) 8%,transparent)}
.oil__inc li{--lamp:var(--pass)}
.oil__ick svg{width:.95rem; height:.95rem; stroke:var(--pass); fill:none; stroke-width:2.4;
  stroke-linecap:round; stroke-linejoin:round; opacity:.45; transition:opacity .45s ease}
.oil__inc li.lit .oil__ick svg{opacity:1}

/* the stub — the half that carries the money */
.oil__stub{position:relative; display:grid; align-content:start; justify-items:center;
  gap:.15rem; text-align:center; padding:clamp(1.5rem,3vw,2rem) 1.35rem;
  background:var(--surface-2); border-radius:0 .5rem .5rem 0;
  background-image:linear-gradient(var(--rule) 50%, transparent 0);
  background-size:2px 9px; background-repeat:repeat-y; background-position:left 1rem}
.oil__stub::before,.oil__stub::after{content:""; position:absolute; left:0;
  width:1.3rem; height:1.3rem; border-radius:50%; background:var(--paper);
  border:1px solid var(--rule)}
.oil__stub::before{top:0; transform:translate(-50%,-50%); border-top:0; border-left:0}
.oil__stub::after{bottom:0; transform:translate(-50%,50%); border-bottom:0; border-left:0}
@media (max-width:56rem){
  /* ⭐ THE STUB IS FULL WIDTH NOW, SO THE TIERS GO SIDE BY SIDE. Stacked they
     ran the section to 1317px — taller than the two-column desktop version,
     which is the wrong way round for the narrower screen. */
  .oil__tiers{grid-template-columns:1fr 1fr; gap:0}
  .oil__tier + .oil__tier{padding-top:0; border-top:0;
    border-left:1px solid var(--rule)}
  .oil__act{grid-template-columns:1fr 1fr}
  .oil__who{padding-top:.8rem}
  .oil__stub{border-radius:0 0 .5rem .5rem;
    background-image:linear-gradient(90deg, var(--rule) 50%, transparent 0);
    background-size:9px 2px; background-repeat:repeat-x; background-position:1rem top}
  .oil__stub::before{left:0; top:0; transform:translate(-50%,-50%)}
  .oil__stub::after{left:auto; right:0; top:0; bottom:auto; transform:translate(50%,-50%)}
}

.oil__cap{font-family:var(--f-mono); font-size:.625rem; letter-spacing:.13em;
  text-transform:uppercase; color:var(--ink-3)}
.oil__tiers{display:grid; gap:.7rem; width:100%; margin:.9rem 0 1rem}
.oil__tier{display:grid; gap:.08rem; justify-items:center}
.oil__tier + .oil__tier{padding-top:.7rem; border-top:1px solid var(--rule)}
.oil{--oil-price:clamp(2.1rem,1.6rem + 2.1vw,2.75rem)}
.oil__price{font-family:var(--f-mono); font-weight:700; line-height:1;
  font-size:var(--oil-price); color:var(--ink)}

/* 🔴 A BIG PRICE DEMANDS A BIG DISCLOSURE — THAT IS THE TRADE, NOT A BUG.
   §3372.1(d) wants the disclosure at least HALF the price type size, so a
   51px price forces 25.6px terms. The answer is not to shrink the ratio, it
   is to shorten the SENTENCE: the stub says the terse version at whatever
   size the price demands, and the full wording sits in .oil__fine below.
   ⭐ Both sizes read --oil-price, so the ratio cannot drift when someone
      resizes the price. max() floors it for readability and can only ever
      exceed the legal minimum. ⛔ Never hard-code either number. */
.oil__tax{margin:0 0 .9rem; padding:.5rem .6rem; text-align:left;
  font-size:max(.75rem, calc(var(--oil-price) / 2)); line-height:1.35;
  color:var(--ink); background:color-mix(in srgb,var(--accent) 12%,transparent);
  border-left:2px solid var(--accent); border-radius:0 .25rem .25rem 0}
.oil__exp{display:grid; gap:.1rem; margin:0 0 1rem}
.oil__exp b{font-family:var(--f-mono); font-size:1rem; color:var(--ink)}
.oil__who{font-family:var(--f-mono); font-size:.6875rem; line-height:1.7;
  color:var(--ink-3); padding-top:.9rem; border-top:1px solid var(--rule); width:100%}
.oil__act{display:grid; gap:.5rem; width:100%; margin-top:1.1rem}

.oil__fine{margin-top:1rem; font-size:.75rem; line-height:1.6; color:var(--ink-3);
  max-width:78ch}


/* 🔴 16 CCR §3372.1(d) — CALIFORNIA, AND IT IS A MEASURED RULE, NOT A VIBE.
   An advertised repair price whose disclosure is too small to read is a
   misleading price advert: "The type size of the disclosure statement shall be
   at least 1/2 the type size used in the advertised price and the statement
   shall either be shown near the price or shall be prominently footnoted."
   ⛔ It was .75rem (12px) against a 2.1-2.9rem price — 71% of the legal floor.
   ⭐ Both sizes now derive from ONE variable, so the ratio cannot drift when
      somebody changes the price size. This is B&P §9884.7(a)(6) territory:
      a regulation breach is a ground to discipline the ARD registration.
   ⚠️ Still owner-dependent: the coupon has NO EXPIRY and does not say whether
      $45 is out-the-door. §3372.1(d) also wants "the extra cost may be
      substantial" wording if extras routinely exceed 25% of the advertised
      price (>$11.25 here). ⛔ And per §3357 a disposal fee may not be charged
      at all unless the shop's EPA identification number is on the paperwork. */
/* ═══════════════════════════════════════════════════════════════════════
   THE OFFER BAR — the oil change, on every page, directly under the header.
   Owner, 9 Sep: "put oil change on top of the pages somewhere… so they can
   always see the oil change special."

   🔴 A PRICE ON 25 PAGES IS 25 ADVERTS. 16 CCR §3372.1(d) does not care that
      this is a strip rather than a coupon: wherever the price appears, the
      disclosure must be at least HALF its type size and sit near it. Putting
      "$45" in a slim bar with a 10px footnote would have created twenty-five
      fresh violations of the rule I fixed on the coupon this morning.
   ⭐ SO THE BAR'S PRICE IS DELIBERATELY SMALL. Not for taste — a smaller
      price makes a compliant disclosure small enough to fit on one line. The
      two sizes come off ONE variable, exactly like the coupon, so the ratio
      cannot drift when somebody restyles it.
   ⛔ Do not enlarge --bar-price without enlarging the disclosure with it. A
      build gate below refuses if the ratio is ever broken.
   ═══════════════════════════════════════════════════════════════════════ */
.offerbar{--bar-price:clamp(.9rem, .68rem + .58vw, 1.125rem);
  border-bottom:1px solid var(--rule);
  background:linear-gradient(90deg,
    color-mix(in srgb,var(--accent) 16%,var(--surface)) 0%,
    var(--surface) 62%)}
.offerbar__in{display:flex; align-items:center; gap:.55rem 1rem; flex-wrap:wrap;
  padding-block:.6rem}
.offerbar__tag{font-family:var(--f-mono); font-size:.625rem; letter-spacing:.14em;
  text-transform:uppercase; color:var(--accent); flex:none}
/* 🔴 `flex:none` ON A FIXED STRING IS AN OVERFLOW WAITING FOR A NARROW PHONE.
   "Oil change $45 · full synthetic $60" in 1.125rem mono measures 378px. At
   375px that is a horizontal scrollbar on every one of 26 pages, and it was
   there from the moment I built the bar.
   ⭐ It may now shrink and wrap. And the SIZE comes down with the viewport —
      which matters beyond looks: §3372.1(d) ties the disclosure to half the
      price type size off `--bar-price`, so shrinking the variable carries the
      ratio down with it automatically.
   ⛔ Never hard-code a px font-size on either of these two. The ratio is the
      compliance, not the number. */
.offerbar__price{font-family:var(--f-mono); font-weight:700; font-size:var(--bar-price);
  color:var(--ink); flex:0 1 auto; min-width:0; line-height:1.15}
.offerbar__fine{font-size:max(.6875rem, calc(var(--bar-price) / 2)); line-height:1.4;
  color:var(--ink-3); flex:1 1 14rem; min-width:0}
/* the visible short form is a BUTTON so the footnote is reachable by keyboard,
   but it must still read as text, not a control */
/* 🔴 HALF THE PRICE IS A FLOOR, NOT A TARGET. §3372.1(d) sets a MINIMUM, and
   reading it as a formula produced 7.2px disclosure text on a phone — compliant
   to the letter and unreadable, which is the opposite of what the rule is for.
   ⭐ `max()` keeps whichever is larger, so the ratio can only ever be exceeded,
      never missed. ⛔ Do not "simplify" this back to a bare division. */
.offerbar__fine{appearance:none; background:none; border:0; text-align:left;
  font:inherit; font-size:max(.6875rem, calc(var(--bar-price) / 2)); line-height:1.4;
  color:var(--ink-3); flex:1 1 14rem; min-width:0; cursor:pointer; padding:0}
.offerbar__fine:hover .offerbar__star,.offerbar__fine:focus-visible .offerbar__star{color:var(--accent)}
.offerbar__star{color:var(--ink-2); text-decoration:underline; text-underline-offset:2px; white-space:nowrap}

/* ═══════════════════════════════════════════════════════════════════════
   THE TICKET — the oil-change coupon, as a coupon.
   Owner: "make coupon be a cool element 2d style let up."
   🔴 FLAT MEANS FLAT. The whole shape is built from things that have no
      depth: solid fills, a dashed rule, and two notches cut with radial
      gradients. There is no bevel, no inset shadow, no paper grain, no
      perspective. The only shadow in here is a GLOW, which is light, not
      dimension — that is the "light up".
   ⛔ Do not add a gradient to the body. The moment it has a light source it
      stops being a 2D ticket and starts being a bad photograph of one. */
/* 🔴 THE PADDING IS LOAD-BEARING, NOT DECORATION.
   The notches overhang their element by half their width, and the close
   button sits outside the card's corner. Measured at 375px that pushed the
   PAGE 23px wide — a horizontal scrollbar on a phone, which is the exact
   fault the owner caught on the service pages once already.
   ⭐ So the dialog carries a gutter and the overhang lands INSIDE it. That
      also makes `overflow:auto` safe: a scroll container clips its overflow
      in BOTH axes — `overflow-y:auto` silently computes overflow-x to auto
      too — so an overhang outside the padding box would have been sheared
      off the moment the ticket got tall enough to scroll.
   ⛔ Never trade this padding for margin. Margin is outside the scroll box. */
.tkt{border:0; padding:.85rem; box-sizing:border-box;
  width:min(31rem, calc(100vw - 2rem)); max-width:none;
  max-height:calc(100dvh - 2rem); overflow:auto; overscroll-behavior:contain;
  background:transparent; color:var(--ink)}
.tkt::backdrop{background:rgba(0,0,0,.72)}

.tkt__in{position:relative; display:grid; grid-template-columns:minmax(0,1fr) 8.5rem;
  background:var(--surface); border:1px solid var(--rule); border-radius:.55rem;
  box-shadow:0 0 0 1px color-mix(in srgb,var(--accent) 30%,transparent),
             0 0 60px -12px var(--accent);
  animation:tktglow .9s ease-out both}
@keyframes tktglow{
  0%{box-shadow:0 0 0 1px color-mix(in srgb,var(--accent) 0%,transparent), 0 0 0 0 transparent}
  45%{box-shadow:0 0 0 1px var(--accent), 0 0 90px -8px var(--accent)}
  100%{box-shadow:0 0 0 1px color-mix(in srgb,var(--accent) 30%,transparent),
                 0 0 60px -12px var(--accent)}
}

/* the perforation — a dashed rule with a notch bitten out of each end.
   🔴 THE NOTCHES HANG OFF THE STUB, NOT OFF THE CARD. First version put them
      on .tkt__in at a hard-coded offset from the edge, so the moment the
      layout stacked they sat 160px clear of the tear line they were supposed
      to terminate. Anchored to .tkt__stub they find the seam themselves in
      both directions, because the seam IS that element's edge.
   ⛔ Never position a notch by a measurement that has to be kept in step
      with a column width by hand. */
.tkt__stub::after,.tkt__stub::before{content:""; position:absolute;
  width:1.1rem; height:1.1rem; border-radius:50%; background:var(--bg);
  border:1px solid var(--rule); z-index:1}
/* desktop: the seam is the stub's LEFT edge, so the notches cap it top and bottom */
.tkt__stub::before{left:0; top:0; transform:translate(-50%,-50%); border-left:0; border-top:0}
.tkt__stub::after{left:0; bottom:0; transform:translate(-50%,50%); border-left:0; border-bottom:0}

.tkt__main{padding:1.5rem 1.4rem 1.35rem}
.tkt__tag{display:flex; align-items:center; gap:.5rem; margin:0 0 .75rem;
  font-family:var(--f-mono); font-size:.625rem; letter-spacing:.14em;
  text-transform:uppercase; color:var(--accent)}
/* ⭐ the lamp does a bulb check on open, exactly like the service-page
   telltale — one language for "this is lit" across the whole site */
.tkt__lamp{width:.55rem; height:.55rem; border-radius:50%; background:var(--accent);
  animation:tktlamp 1.1s ease-out .1s both}
@keyframes tktlamp{
  0%{opacity:.25; box-shadow:none}
  40%{opacity:1; box-shadow:0 0 0 4px color-mix(in srgb,var(--accent) 30%,transparent), 0 0 16px var(--accent)}
  100%{opacity:1; box-shadow:0 0 0 3px color-mix(in srgb,var(--accent) 20%,transparent), 0 0 10px -1px var(--accent)}
}
.tkt__h{margin:0 0 .9rem; font-size:1.6rem; font-weight:800; line-height:1}

.tkt__tiers{display:flex; gap:1.4rem; margin-bottom:.9rem}
.tkt__tier{display:grid; gap:.05rem}
.tkt__cap{font-family:var(--f-mono); font-size:.5625rem; letter-spacing:.13em;
  text-transform:uppercase; color:var(--ink-3)}
.tkt__price{font-family:var(--f-mono); font-weight:700; font-size:2.1rem;
  line-height:1; color:var(--ink)}

/* 🔴 THE TAX LINE SITS ON THE PRICE. "$45" reads as a total unless something
   adjacent says otherwise, and four items down a list is not adjacent. */
.tkt__tax{margin:0 0 .9rem; padding:.5rem .65rem; font-size:.75rem; line-height:1.4;
  color:var(--ink); background:color-mix(in srgb,var(--accent) 12%,transparent);
  border-left:2px solid var(--accent); border-radius:0 .25rem .25rem 0}

.tkt__inc{list-style:none; margin:0; padding:0; display:grid; gap:.3rem;
  font-size:.8125rem; color:var(--ink-2)}
.tkt__inc li{display:flex; align-items:baseline; gap:.5rem}
.tkt__inc li::before{content:""; width:.3rem; height:.3rem; flex:none;
  background:var(--accent); transform:rotate(45deg)}

.tkt__stub{position:relative; display:grid; align-content:start; justify-items:center;
  gap:.15rem; padding:1.5rem .8rem 1.35rem; text-align:center;
  background:var(--surface-2); border-radius:0 .5rem .5rem 0}
/* the dashed seam itself — a background, so it does not compete with the two
   pseudo-elements the notches need */
.tkt__stub{background-image:linear-gradient(var(--rule) 50%, transparent 0);
  background-size:2px 9px; background-repeat:repeat-y; background-position:left .8rem}
.tkt__date{font-family:var(--f-mono); font-weight:700; font-size:1.05rem;
  color:var(--ink); margin-bottom:.7rem}
.tkt__who{line-height:1.6; margin-bottom:.8rem}
.tkt__call{font-size:.75rem; padding:.45rem .9rem}

.tkt__fine{margin-top:.9rem; padding:.9rem 1.1rem; border-radius:.45rem;
  background:var(--surface); border:1px solid var(--rule);
  font-size:.6875rem; line-height:1.55; color:var(--ink-3)}
.tkt__fine p{margin:0 0 .5rem}
.tkt__fine p:last-child{margin:0}
.tkt__fine b{color:var(--ink-2)}

.tkt__close{position:absolute; top:0; right:0; margin:0; z-index:2}
.tkt__close button{appearance:none; cursor:pointer; width:2rem; height:2rem;
  border-radius:50%; border:1px solid var(--rule); background:var(--surface);
  color:var(--ink-2); font-size:1.1rem; line-height:1;
  transition:color .2s ease, border-color .2s ease}
.tkt__close button:hover{color:var(--accent); border-color:var(--accent)}

@media (max-width:34rem){
  .tkt__in{grid-template-columns:minmax(0,1fr)}
  /* stacked: the seam is the stub's TOP edge, so the notches move to its
     two top corners and the dashed rule turns horizontal */
  .tkt__stub::before{left:0; top:0; transform:translate(-50%,-50%)}
  .tkt__stub::after{left:auto; right:0; top:0; bottom:auto;
    transform:translate(50%,-50%); border-bottom:1px solid var(--rule); border-left:1px solid var(--rule)}
  .tkt__stub{border-radius:0 0 .5rem .5rem; padding-top:1.2rem;
    background-image:linear-gradient(90deg, var(--rule) 50%, transparent 0);
    background-size:9px 2px; background-repeat:repeat-x; background-position:.8rem top}
  .tkt__price{font-size:1.8rem}
}
@media (prefers-reduced-motion:reduce){
  .tkt__in,.tkt__lamp{animation:none}
  .tkt__lamp{box-shadow:0 0 0 3px color-mix(in srgb,var(--accent) 20%,transparent)}
}

.offerbar__go{appearance:none; -webkit-appearance:none; cursor:pointer;
  display:inline-flex; align-items:center; gap:.5rem; flex:none; white-space:nowrap;
  font-family:var(--f-display); font-weight:700; font-size:.8125rem; line-height:1;
  padding:.5rem .85rem .5rem .75rem; border-radius:.3rem; text-decoration:none;
  color:var(--accent); background:color-mix(in srgb,var(--accent) 10%,transparent);
  border:1px solid color-mix(in srgb,var(--accent) 38%,transparent);
  transition:background-color .2s ease, border-color .2s ease, color .2s ease,
             box-shadow .3s ease}

/* ⭐ THE LAMP. Same language as the dashboard telltales the whole site is
   built on — a warning light on the dash, not a marketing badge. It idles at
   half brightness and comes up full on hover; the ring is a flat ::after, so
   this is still 2D. ⛔ No gradient, no bevel. */
.offerbar__go::before{content:""; width:.5rem; height:.5rem; border-radius:50%;
  flex:none; background:var(--accent);
  box-shadow:0 0 0 3px color-mix(in srgb,var(--accent) 22%,transparent),
             0 0 10px -1px var(--accent);
  animation:lampidle 2.6s ease-in-out infinite}
@keyframes lampidle{0%,100%{opacity:.5} 50%{opacity:1}}

.offerbar__go:hover,.offerbar__go:focus-visible{
  color:var(--surface); background:var(--accent); border-color:var(--accent);
  box-shadow:0 0 0 1px var(--accent), 0 0 24px -4px var(--accent)}
.offerbar__go:hover::before,.offerbar__go:focus-visible::before{
  animation:none; opacity:1; background:var(--surface);
  box-shadow:0 0 0 3px color-mix(in srgb,var(--surface) 30%,transparent)}
.offerbar__go:active{transform:translateY(1px)}
@media (prefers-reduced-motion:reduce){ .offerbar__go::before{animation:none; opacity:1} }
/* ⭐ OFF THE PHONE HEADER, 10 SEP — owner's call. On a narrow screen it ate
   four lines above the fold before the visitor reached the page they asked
   for. The offer still has the whole section on the home page, with the same
   price, the same terms and the same expiry.
   ⛔ The markup STAYS in the page — it is only hidden. The coupon gate is
      keyed on the price appearing in the HTML, and the dialog and its trigger
      travel with it, so the terms are still one tap away wherever the price
      is shown. */
@media (max-width:52rem){ .offerbar{display:none} }
@media print{ .offerbar{display:none!important} }


/* ⭐ BOTH PRICES ARE THE SAME SIZE — a customer choosing between them is not
   being upsold, they are being told what each one costs. */


@media print{
  body > *:not(main), .top, .foot, .draft, .launch, .chat, .rail, .sec:not(.coupon-sec){display:none!important}
  .coupon-sec, .coupon-sec *{visibility:visible}
  /* 🔴 THESE ARE NOT DEAD RULES — "Print or save" IS A REAL BUTTON. When the
     section was rebuilt from .coupon__* to .oil__*, four print rules were
     left pointing at classes no longer in the markup, and the coupon would
     have printed in screen colours: white text, glow, dark stub. Nearly
     deleted them as dead CSS instead of porting them. ⛔ Before removing a
     rule because the class is gone, check WHICH media it belongs to. */
  .oil{box-shadow:none!important; border-color:#000; color:#000; background:#fff}
  .oil__main,.oil__h,.oil__lede,.oil__inc,.oil__inc b{color:#000}
  .oil__stub{background:#f4f4f4; color:#000}
  .oil__stub::before,.oil__stub::after{background:#fff}
  .oil__price,.oil__exp b,.oil__cap,.oil__who,.oil__tax,.oil__fine{color:#000}
  .oil__tax{background:#fff; border-left-color:#000}
  .oil__lamp,.oil__act{display:none!important}
  @page{margin:12mm}
}

.offer{display:grid; grid-template-columns:minmax(0,1fr) auto; gap:clamp(1.25rem,3vw,2.5rem);
  align-items:center; background:var(--surface); border:1px solid var(--rule);
  border-left:4px solid var(--accent); border-radius:.45rem; padding:clamp(1.25rem,3vw,2rem);
  box-shadow:var(--shadow); position:relative; overflow:hidden}
@media (max-width:46rem){ .offer{grid-template-columns:minmax(0,1fr)} }
.offer__tag{display:inline-flex; align-items:center; gap:.45rem; font-family:var(--f-mono);
  font-size:.6875rem; font-weight:700; letter-spacing:.13em; text-transform:uppercase;
  color:var(--accent); margin-bottom:.7rem}
.offer h2{font-size:var(--step-2); font-weight:800; max-width:18ch; margin-bottom:.65rem}
.offer__lede{color:var(--ink-2); max-width:48ch; margin-bottom:1.1rem}
.offer__list{list-style:none; margin:0 0 1.25rem; padding:0; display:grid; gap:.4rem;
  font-size:.9375rem; color:var(--ink-2)}
.offer__list li{display:grid; grid-template-columns:1.1rem minmax(0,1fr); gap:.55rem; align-items:start}
.offer__list svg{width:1.1rem; height:1.1rem; stroke:var(--pass); fill:none; stroke-width:2;
  stroke-linecap:round; stroke-linejoin:round; margin-top:.25rem}
.offer__fine{font-size:.75rem; color:var(--ink-3); max-width:52ch}
.offer__price{display:grid; justify-items:center; gap:.15rem; text-align:center;
  border:1px dashed var(--rule); border-radius:.4rem; padding:1.1rem 1.4rem;
  background:var(--surface-2); min-width:11rem}
.offer__price .amt{font-family:var(--f-mono); font-weight:700; font-size:clamp(2rem,1.4rem+2vw,2.75rem); line-height:1}
.offer__price .cap{font-family:var(--f-mono); font-size:.625rem; letter-spacing:.13em;
  text-transform:uppercase; color:var(--ink-3)}
.offer__act{margin-top:.9rem; display:flex; gap:.6rem; flex-wrap:wrap; justify-content:center}
.svc{display:grid; grid-template-columns:repeat(auto-fill,minmax(15.5rem,1fr)); gap:1px; background:var(--rule); border:1px solid var(--rule); border-radius:.4rem; overflow:hidden}
.svc a{background:var(--surface); padding:1.15rem; text-decoration:none; align-content:start; transition:background-color .12s ease}
.svc a:hover{background:var(--surface-2)}
.svc a{grid-template-columns:auto minmax(0,1fr); column-gap:.85rem}
.svc h3,.svc .svc__n{font-size:1.0625rem; font-weight:700; display:block}
.sym{width:2.5rem; height:2.5rem; flex:none; border-radius:.45rem; display:grid; place-items:center;
     background:color-mix(in srgb, currentColor 13%, transparent);
     border:1px solid color-mix(in srgb, currentColor 30%, transparent)}
.sym svg{width:1.4rem; height:1.4rem; stroke:currentColor; fill:none; stroke-width:1.7;
     stroke-linecap:round; stroke-linejoin:round}
.svc a{display:flex; align-items:flex-start; gap:.9rem}
.svc .txt{display:grid; gap:.3rem; min-width:0}
.sym--lg{width:3.25rem; height:3.25rem; border-radius:.55rem}
.sym--lg svg{width:1.85rem; height:1.85rem}
.svc p{font-size:.875rem; color:var(--ink-2)}
.svc .spec{font-family:var(--f-mono); font-size:.6875rem; color:var(--ink-3); margin-top:.4rem}


/* ⭐ PHOTOGRAPHY BAND. A real photograph earns its place by showing the WORK, not
   by decorating the page. Sized and cropped here so a replacement drops in without
   the layout moving — the aspect ratio is fixed, the image covers it. */
/* ⭐ THE PLATES CARRY THE COLOUR. Each .sym is tinted by its own telltale
   variable via currentColor, so a row of plates is a row of different hues
   that all mean something — red critical, amber caution, green ok, blue info.
   ⛔ Never restate the hue here; it comes from sym() and nowhere else. */
/* ⭐ Three columns that are genuinely three EQUAL claims — so they get equal
   width, one rule, and the hue comes from sym() as everywhere else. */
/* ═══════════════════════════════════════════════════════════════════════
   THE INTAKE FORM. Built for a phone in a car park with one hand, which is
   where most of these get filled in: 44px targets, real labels, and the
   fork answered by tapping a card rather than reading a dropdown.
   ═══════════════════════════════════════════════════════════════════════ */
.intake{display:grid; gap:1.6rem; max-width:44rem}
.fset{border:0; margin:0; padding:0; display:grid; gap:.85rem}
.fset__q{display:flex; align-items:center; gap:.7rem; padding:0; margin-bottom:.2rem;
  font-family:var(--f-display); font-weight:700; font-size:1.125rem; color:var(--ink)}
.fset__n{display:grid; place-items:center; width:1.65rem; height:1.65rem; flex:none;
  border-radius:50%; background:var(--accent-soft); color:var(--accent);
  font-family:var(--f-mono); font-size:.75rem; font-weight:600}
.picks{display:grid; grid-template-columns:repeat(auto-fit,minmax(13rem,1fr)); gap:.6rem}
.pick{position:relative; display:block; cursor:pointer}
.pick input{position:absolute; opacity:0; width:100%; height:100%; margin:0; cursor:pointer}
.pick__in{display:grid; gap:.2rem; min-height:2.75rem; padding:.85rem .95rem;
  border:1.5px solid var(--rule); border-radius:.4rem; background:var(--surface);
  transition:border-color .12s ease, background-color .12s ease}
.pick__in b{font-family:var(--f-display); font-size:.9375rem; color:var(--ink)}
.pick__in span{font-size:.8125rem; color:var(--ink-2)}
.pick input:checked + .pick__in{border-color:var(--accent);
  background:color-mix(in srgb, var(--accent) 11%, var(--surface))}
.pick input:focus-visible + .pick__in{outline:2px solid var(--accent); outline-offset:2px}
.branch__in{border:1px solid color-mix(in srgb,var(--accent) 40%,var(--rule));
  border-left-width:3px; border-radius:.4rem; padding:1.1rem 1.15rem;
  background:color-mix(in srgb,var(--accent) 8%,var(--surface)); display:grid; gap:.6rem}
.branch__in b{font-family:var(--f-display); font-size:1rem; color:var(--ink)}
.branch__in p{margin:0; font-size:.9375rem; color:var(--ink-2)}
.branch__in .btn{justify-self:start}
.grid2{display:grid; grid-template-columns:repeat(auto-fit,minmax(11rem,1fr)); gap:.6rem}
.grid3{display:grid; grid-template-columns:repeat(auto-fit,minmax(7.5rem,1fr)); gap:.6rem}
.fld{display:grid; gap:.35rem}
.fld > span{font-family:var(--f-mono); font-size:.6875rem; letter-spacing:.1em;
  text-transform:uppercase; color:var(--ink-3)}
.fld > span em{font-style:normal; text-transform:none; letter-spacing:0; opacity:.75}
.fld input,.fld textarea{width:100%; min-height:2.75rem; padding:.65rem .8rem;
  border:1.5px solid var(--rule); border-radius:.35rem; background:var(--surface-2);
  color:var(--ink); font:inherit; font-size:1rem}
.fld textarea{min-height:6.5rem; resize:vertical; line-height:1.5}
.fld input:focus-visible,.fld textarea:focus-visible{outline:2px solid var(--accent);
  outline-offset:1px; border-color:var(--accent)}
.fld input[aria-invalid="true"],.fld textarea[aria-invalid="true"]{border-color:var(--accent)}
.fld__err{font-size:.8125rem; color:var(--accent); margin:.1rem 0 0}
.fld__hint{margin:.1rem 0 0; font-size:.8125rem; color:var(--ink-3); max-width:58ch}
.intake__act{display:flex; flex-wrap:wrap; gap:.7rem; align-items:center}
.intake__note{margin:0; font-size:.8125rem; color:var(--ink-3); max-width:62ch}
.outcome{border:1px solid var(--rule); border-radius:.45rem; padding:1.4rem 1.5rem;
  background:var(--surface); display:grid; gap:.9rem; max-width:44rem}
.outcome h3{margin:0; font-family:var(--f-display); font-size:1.25rem}
.outcome dl{display:grid; grid-template-columns:auto minmax(0,1fr); gap:.4rem .9rem; margin:0}
.outcome dt{font-family:var(--f-mono); font-size:.6875rem; letter-spacing:.1em;
  text-transform:uppercase; color:var(--ink-3); padding-top:.15rem}
.outcome dd{margin:0; color:var(--ink)}

.trades{display:grid; grid-template-columns:repeat(auto-fit,minmax(17rem,1fr));
  gap:1rem; margin-top:1.9rem}
.trade{border:1px solid var(--rule); border-radius:.45rem; padding:1.4rem 1.35rem 1.5rem;
  background:linear-gradient(180deg,
    color-mix(in srgb, currentColor 9%, var(--surface)) 0%, var(--surface) 58%);
  display:grid; align-content:start; gap:.9rem}
.trade__top{display:flex; align-items:center; gap:.85rem}
.trade__top .sym{width:3.1rem; height:3.1rem; border-radius:.8rem; flex:none;
  background:color-mix(in srgb, currentColor 14%, transparent);
  border:1px solid color-mix(in srgb, currentColor 36%, transparent)}
.trade__top .sym svg{width:1.7rem; height:1.7rem}
.trade__top b{display:block; font-family:var(--f-display); font-size:1.125rem;
  font-weight:700; color:var(--ink); line-height:1.15}
/* 🔴 THIS RULE WAS `.trade__top span` AND IT BROKE THE ICONS.
   The icon is a <span class="sym">, and `.trade__top span` is a class PLUS an
   element — higher specificity than the bare `.sym` — so it beat
   `.sym{display:grid; place-items:center}` and set the box to display:block.
   The artwork was fine; the SVG simply sat in the top-left corner of a 50px
   box, 10px up and 10px left of where it belonged, on all three cards.
   ⭐ `> div span` hits only the caption, which is nested; the icon is a
      DIRECT child and is now out of reach.
   ⛔ An element selector under a class outranks a component class. Never
      style a bare element inside a block that also carries components. */
.trade__top > div span{display:block; font-family:var(--f-mono); font-size:.625rem;
  letter-spacing:.14em; text-transform:uppercase; color:currentColor; margin-top:.28rem}
.trade p{margin:0; font-size:.9375rem; color:var(--ink-2)}
.trades__note{margin-top:1.4rem; padding-top:1.2rem; border-top:1px solid var(--rule-soft);
  color:var(--ink-3); font-size:.9375rem; max-width:68ch}

.plates{display:grid; grid-template-columns:repeat(auto-fit,minmax(16rem,1fr));
  gap:1rem; margin-top:.25rem}
/* 🔴 .plate IS A TWO-COLUMN GRID (draw | specs) — it was built for the wide
   service pages. Setting only grid-template-rows here left those columns in
   place, so inside a 390px card the symbol took half the width and the copy
   was crushed into ~190px, wrapping "a circuit, not a part" over four lines.
   That is the "stuffed in the corner" the owner saw. ⛔ Always reset the axis
   you are NOT using when you reuse a grid component in a narrower box. */
.plates .plate{background:linear-gradient(180deg,
    color-mix(in srgb, currentColor 7%, var(--surface)) 0%, var(--surface) 62%);
  display:grid; grid-template-columns:minmax(0,1fr); grid-template-rows:auto auto;
  gap:0; overflow:hidden; align-items:stretch}
.plates .plate__draw{padding:1.5rem 1rem 1.1rem;
  background:radial-gradient(70% 90% at 50% 0%,
    color-mix(in srgb, currentColor 16%, transparent), transparent 72%)}
.plates .plate__draw .sym{width:4.6rem; height:4.6rem; border-radius:1rem}
.plates .plate__draw .sym svg{width:2.5rem; height:2.5rem}
.plates .plate__specs{padding:0 1.1rem 1.1rem}
.plates .plate__row{grid-template-columns:auto minmax(0,1fr); row-gap:.15rem;
  padding:.7rem 0; align-items:baseline}
.plates .plate__row b{font-family:var(--f-display); font-size:.9375rem; color:var(--ink)}
.plates .plate__row span{font-size:.875rem; color:var(--ink-2); grid-column:1/-1}
.plates .plate__row em{font-family:var(--f-mono); font-style:normal; font-size:.6875rem;
  letter-spacing:.08em; text-transform:uppercase; color:currentColor; justify-self:end}

/* ⭐ A BAND, NOT A THUMBNAIL. Its own class rather than a .shot modifier —
   .shot img carries aspect-ratio:5/4 at the same specificity, so a modifier
   was decided by source order and lost. A separate class cannot be outranked
   by accident. */

/* ── THE SERVICE PAGE, in the home page's vocabulary ─────────────────────
   ⭐ The strip is the old sidebar rail turned on its side. The same four
      facts, but as equal columns across the full measure instead of a skinny
      ribbon — which is what made the page read as a 90s article. */
.specstrip{display:grid; grid-template-columns:repeat(auto-fit,minmax(11rem,1fr));
  gap:1px; background:var(--rule); border:1px solid var(--rule);
  border-radius:.5rem; overflow:hidden; margin-bottom:1.75rem}
.spec2{background:var(--surface); padding:1rem 1.15rem 1.1rem; display:grid; gap:.3rem}
.spec2 span{font-family:var(--f-mono); font-size:.625rem; letter-spacing:.14em;
  text-transform:uppercase; color:var(--ink-3)}
.spec2 b{font-family:var(--f-display); font-size:1.0625rem; font-weight:700; color:var(--ink)}

/* ═══════════════════════════════════════════════════════════════════════
   PROSE AS CARDS. warranty, privacy, about and visit each carry five to ten
   h3-and-paragraph sections down one ribbon — the structure that made them
   read as a 1998 article with nothing to say. Same copy, laid into the same
   card the service pages use, two across so 500 words fill the measure.
   ⛔ The LEAD spans both columns: it introduces the cards, it is not one.
   ⛔ Cards are NOT numbered. These are parallel topics ("The repair warranty",
      "Estimates and approvals", "Your old parts") — numbering would assert a
      sequence the content does not have.
   ═══════════════════════════════════════════════════════════════════════ */
.prose--cards{display:grid; grid-template-columns:repeat(auto-fit,minmax(19rem,1fr));
  gap:1rem; max-width:none}
.pcard{border:1px solid var(--rule); border-radius:.5rem; padding:1.4rem 1.4rem 1.5rem;
  background:var(--surface); display:grid; align-content:start; gap:.65rem}
.pcard--lead{grid-column:1/-1; background:none; border:0; padding:0 0 .35rem}
.pcard--lead p{font-size:1.0625rem; color:var(--ink-2); max-width:62ch}
.pcard h3{margin:0; font-family:var(--f-display); font-size:1.0625rem; font-weight:700;
  color:var(--ink); line-height:1.25}
.pcard h3::after{content:""; display:block; width:2rem; height:2px; border-radius:2px;
  background:var(--accent); margin-top:.6rem; opacity:.75}
.pcard p{margin:0; font-size:.9375rem; color:var(--ink-2)}
.pcard ul{margin:0; padding-left:1.1rem; font-size:.9375rem; color:var(--ink-2)}
.pcard li{margin-bottom:.4rem}
.pcard .mono{font-size:.8125rem}
/* ⛔ A card must not inherit the ribbon's max-width or it re-narrows inside
   its own box. */
.prose--cards .pcard > *{max-width:none}

/* Twelve neighborhoods, equal cards, three across — the same grid language
   as the trades and the service argument. */
.areagrid{display:grid; grid-template-columns:repeat(auto-fit,minmax(16rem,1fr));
  gap:.75rem; margin-top:1.5rem}
.areacard{border:1px solid var(--rule); border-radius:.45rem; padding:1.1rem 1.2rem 1.2rem;
  background:var(--surface); display:grid; align-content:start; gap:.3rem}
.areacard b{font-family:var(--f-display); font-size:1rem; font-weight:700; color:var(--ink)}
.areacard span{font-family:var(--f-mono); font-size:.625rem; letter-spacing:.12em;
  text-transform:uppercase; color:var(--tt-blue)}
.areacard p{margin:.35rem 0 0; font-size:.875rem; color:var(--ink-2)}

.argue{display:grid; grid-template-columns:repeat(auto-fit,minmax(17rem,1fr));
  gap:1rem; margin-top:1.75rem}
.argue__c{border:1px solid var(--rule); border-radius:.5rem; padding:1.5rem 1.4rem 1.6rem;
  background:linear-gradient(180deg,
    color-mix(in srgb, currentColor 8%, var(--surface)) 0%, var(--surface) 62%);
  display:grid; align-content:start; gap:.7rem}
.argue__c h2{font-size:1.1875rem; line-height:1.2; color:var(--ink); margin:0}
.argue__c p{margin:0; font-size:.9375rem; color:var(--ink-2)}
/* the hue reads as a rule under the heading, not as coloured text */
.argue__c h2::after{content:""; display:block; width:2.25rem; height:2px;
  background:currentColor; margin-top:.7rem; border-radius:2px}
/* ⭐ ITS OWN CLASS, not a .shot modifier — .shot img carries aspect-ratio:5/4
   at identical specificity, so a modifier is decided by source order and
   loses.
   ⛔ A FIXED HEIGHT, NOT AN ASPECT RATIO. An aspect ratio grows with the
      column, which is exactly how this reached 493px on a wide screen and
      read as a hero. 200px stays 200px.
   🔴 width:100% IS LOAD-BEARING. Without it the img renders at its natural
      1000px and forces the whole page 1060px wide — horizontal scroll at
      1024, 768 and 375. That is what "completely broke" looked like. */
/* ⭐ A HERO, OR NOTHING. Owner, 9 Sep: "if you going to make them big just
   make it a hero — you cut off 1/2 the pic."
   He is right about the arithmetic. A fixed 200px band across a 4:3 source
   shows roughly a FIFTH of the frame: the photograph is doing almost no work
   and the customer sees a stripe. Either commit to the picture or leave it
   out; a sliver is the worst of both.
   ⭐ 16:9 keeps about three quarters of a 4:3 frame — the subject survives —
      and at the top of the article it reads as the page's opening image
      rather than an ornament wedged mid-column.
   ⛔ NOT a fixed pixel height. That was the bug that made it a stripe: 200px
      is a different fraction of every source. A RATIO scales with the column
      and keeps the same share of every photograph.
   ⛔ Each photo keeps its own object-position — see the SHOT map. */
/* ═══════════════════════════════════════════════════════════════════════
   THE SHOWCASE — a framed photograph beside the page's opening argument.
   ⛔ NO aspect-ratio AND NO fixed height on the image. Both of those crop,
      and cropping is what turned this into a stripe twice already. In its own
      column it can simply BE its own shape: height:auto, 100% of the frame,
      whatever ratio the photograph happens to have.
   ⭐ The frame is two rings and a lift, not a heavy border — a hairline in the
      rule color, an inset highlight so the image reads as glazed rather than
      pasted on, and a soft shadow to sit it above the ground.
   ═══════════════════════════════════════════════════════════════════════ */
.showcase{display:grid; grid-template-columns:minmax(0,0.95fr) minmax(0,1.05fr);
  gap:clamp(1.25rem,3vw,2.25rem); align-items:center; margin:0 0 2rem}
.showcase__pic{margin:0; border-radius:.85rem; overflow:hidden;
  background:var(--surface); border:1px solid var(--rule);
  box-shadow:0 1px 0 color-mix(in srgb,#fff 6%,transparent) inset,
             0 18px 34px -22px rgba(0,0,0,.95)}
.showcase__glass{position:relative; display:block}
.showcase__pic img{width:100%; height:auto; display:block}
/* ⭐ A LITTLE SHEEN — one soft diagonal sweep, the way light falls across
   glass. ⛔ Kept under 10%: a gloss you can NAME is a gloss that is too
   strong, and these are photographs of real work, not product shots.
   ⛔ pointer-events:none so it never eats a tap on the image. */
.showcase__glass::after{content:""; position:absolute; inset:0; pointer-events:none;
  background:linear-gradient(114deg,
    color-mix(in srgb,#fff 9%,transparent) 0%,
    color-mix(in srgb,#fff 4%,transparent) 22%,
    transparent 46%,
    transparent 78%,
    color-mix(in srgb,#fff 3%,transparent) 100%);
  mix-blend-mode:screen}
@media (prefers-reduced-transparency:reduce){ .showcase__glass::after{display:none} }
.showcase__pic figcaption{padding:.7rem 1rem .8rem; font-family:var(--f-mono);
  font-size:.6875rem; letter-spacing:.09em; color:var(--ink-3);
  border-top:1px solid var(--rule); background:var(--surface-2)}
.showcase__say h2{margin:0 0 .7rem; font-size:clamp(1.25rem,1rem + 1vw,1.6rem);
  line-height:1.2; color:var(--ink)}
.showcase__say h2::after{content:""; display:block; width:2.25rem; height:2px;
  background:currentColor; opacity:.75; border-radius:2px; margin-top:.7rem}
.showcase__say p{margin:0; font-size:1rem; color:var(--ink-2); max-width:52ch}
/* ⛔ Below 52rem the two columns become two slivers — stack, picture first. */
@media (max-width:52rem){
  .showcase{grid-template-columns:minmax(0,1fr); gap:1.1rem}
}

/* ⭐ SYMMETRY IS TWO EQUAL HALVES, not "as many as fit". auto-fit would give
   three columns on a wide screen and two on a medium one — the count changing
   with the viewport is exactly what stops a row reading as balanced. Two
   photographs, two columns, one seam down the middle. */
/* 🔴 THIS WAS `1fr 1fr` WITH NO BREAKPOINT, so it forced two columns at every
   width — including 375px, where each card became about 165px wide and the
   captions wrapped to one and two words a line. The base `.shots` grid uses
   auto-fit/minmax and collapses on its own; this override threw that away.
   ⛔ A fixed column count needs a width to stop at. Every one of them. */
.sec--band .shots{grid-template-columns:1fr 1fr}
@media (max-width:46rem){
  /* ⭐ FULL BLEED ON A PHONE, and no box. Owner: "those two pics need to be
     full mobile screen, no gray box around them."
     The grid's own border and its 1px gap over a --rule background are what
     drew that box; both go, and the grid steps out of the shell's gutter with
     `margin-inline: calc(50% - 50vw)` — the standard full-bleed, and safe
     here because it is inside a phone-width media query where scrollbars are
     overlaid rather than taking width.
     ⛔ Never use this outside a narrow media query: on desktop 100vw includes
        the scrollbar and it overflows the page. */
  .sec--band .shots{
    grid-template-columns:1fr; gap:1.75rem;
    background:transparent; border:0; border-radius:0;
    margin-inline:calc(50% - 50vw);
  }
  /* the picture goes edge to edge; only the words keep the gutter */
  .sec--band .shot{background:transparent}
  .sec--band .shot img{aspect-ratio:16/10; border-radius:0}
  .sec--band .shot figcaption{padding-inline:var(--gutter)}
}
.shots{display:grid; grid-template-columns:repeat(auto-fit,minmax(15rem,1fr)); gap:1px;
  background:var(--rule); border:1px solid var(--rule); border-radius:.45rem; overflow:hidden}
/* 🔴 A GRID ITEM STRETCHES, AND STRETCHING BEATS aspect-ratio.
   .shot is display:grid, so with the default align-items:stretch the <img>
   was pulled to the full row height — 219x800 instead of 219x175 — and
   object-fit:cover then cropped a portrait sliver out of a landscape photo.
   aspect-ratio does not win that fight; it only applies when the box is
   free to size itself. ⭐ Pin the rows and let the image size itself. */
.shot{position:relative; background:var(--surface); display:grid;
  grid-template-rows:auto 1fr; align-content:start}
.shot img{width:100%; height:auto; aspect-ratio:5/4; object-fit:cover; display:block;
  align-self:start}
.shot figcaption{padding:.7rem .9rem .85rem; font-size:.8125rem; color:var(--ink-2)}
.shot figcaption b{display:block; color:var(--ink); font-weight:600; font-size:.875rem; margin-bottom:.15rem}
.shot__flag{position:absolute; top:.6rem; left:.6rem; font-family:var(--f-mono); font-size:.625rem;
  font-weight:700; letter-spacing:.11em; text-transform:uppercase; color:var(--todo);
  background:color-mix(in srgb, var(--todo-soft) 92%, transparent); border:1px solid var(--todo-line);
  padding:.25rem .5rem; border-radius:.2rem}
.shot--empty{display:grid; place-items:center; text-align:center; gap:.3rem; padding:1.5rem 1rem;
  aspect-ratio:5/4; border:1.5px dashed var(--rule); background:var(--surface-2)}
.shot--empty b{font-family:var(--f-mono); font-size:.75rem; letter-spacing:.1em; text-transform:uppercase; color:var(--ink-2)}
.shot--empty span{font-size:.8125rem; color:var(--ink-3); max-width:26ch}

.mapcard{display:flex; align-items:center; gap:.9rem; text-decoration:none;
  background:var(--surface); border:1px solid var(--rule); border-left:3px solid var(--accent);
  border-radius:.45rem; padding:1.1rem 1.25rem; min-height:9rem}
.mapcard:hover{background:var(--surface-2)}
.mapcard__pin{width:2.75rem; height:2.75rem; flex:none; border-radius:.45rem; display:grid; place-items:center;
  background:color-mix(in srgb, var(--accent) 16%, transparent); color:var(--accent)}
.mapcard__pin svg{width:1.5rem; height:1.5rem; fill:currentColor}
.mapcard b{display:block; font-family:var(--f-display); font-size:1.0625rem; font-weight:700}
.mapcard span{font-size:.9375rem; color:var(--ink-2)}
.mapcard em{display:block; font-style:normal; font-family:var(--f-mono); font-size:.6875rem;
  letter-spacing:.11em; text-transform:uppercase; color:var(--accent); margin-top:.35rem}
.mount{border:1px solid var(--rule); border-radius:.45rem; background:var(--surface-2);
  display:grid; place-items:center; text-align:center; gap:.3rem; padding:2.25rem 1rem; min-height:10rem}
.mount b{font-family:var(--f-mono); font-size:.75rem; letter-spacing:.1em; text-transform:uppercase; color:var(--ink-2)}
.mount span{font-size:.8125rem; color:var(--ink-3); max-width:34ch}
.slot{border:1.5px dashed var(--rule); border-radius:.4rem; background:var(--surface-2); display:grid; place-items:center; text-align:center; padding:1.5rem 1rem; gap:.3rem; min-height:11rem}
.slot b{font-family:var(--f-mono); font-size:.75rem; letter-spacing:.1em; text-transform:uppercase; color:var(--ink-2)}
.slot span{font-size:.8125rem; color:var(--ink-3); max-width:32ch}

/* 🔴 THE RIGHT HALF OF THIS BLOCK WAS EMPTY ON 25 PAGES. .cta was a single
   grid column and the copy carries its own max-width, so everything past
   ~52ch was black. It looked like a layout bug because it was one.
   ⭐ Two columns now, and the second one is the map. */
.cta{color:var(--ink); border-radius:.5rem; padding:clamp(1.75rem,4vw,2.75rem);
  display:grid; gap:clamp(1.5rem,3vw,2.5rem); align-items:center;
  grid-template-columns:minmax(0,1fr) minmax(0,21rem)}
@media (max-width:56rem){ .cta{grid-template-columns:minmax(0,1fr)} }
.cta__say{display:grid; gap:1.25rem; align-content:start}

.cta__map{min-width:0}
/* 🔴 THIS WAS CALLED .mapcard AND IT CLOBBERED AN EXISTING COMPONENT.
   The Visit page has had a `.mapcard` since it was built — a flex row with a
   pin and the address, linking to Google Maps. I reused the name for the
   booking panel thirty lines further down, so mine won the cascade and turned
   that link into a padded grid box. The Visit page looked broken and the
   cause was nowhere near it.
   ⛔ Before naming a component, grep the stylesheet for the name. A build
      gate now refuses two definitions of one class that disagree on
      `display`. */
.mappanel{display:grid; gap:.35rem; padding:1.35rem 1.4rem 1.25rem;
  background:var(--surface); border:1px solid var(--rule); border-radius:.5rem;
  box-shadow:0 0 0 1px color-mix(in srgb,var(--accent) 12%,transparent),
             0 0 44px -22px var(--accent)}
.mappanel__lbl{font-family:var(--f-mono); font-size:.625rem; letter-spacing:.14em;
  text-transform:uppercase; color:var(--accent); margin:0}
.mappanel__addr{margin:.35rem 0 0; font-family:var(--f-mono); font-weight:700;
  font-size:1rem; line-height:1.5; color:var(--ink)}
.mappanel__hrs{margin:.15rem 0 0; font-family:var(--f-mono); font-size:.75rem; color:var(--ink-3)}
.mappanel__act{display:grid; gap:.5rem; margin-top:1rem}
.mappanel__act .btn{color:inherit; border-color:currentColor; text-align:center}
.mappanel__note{margin:.85rem 0 0; font-size:.6875rem; line-height:1.5; color:var(--ink-3)}

/* once loaded, the panel becomes the frame around the map */
.mappanel.is-live{padding:0; overflow:hidden; gap:0}
.mappanel.is-live > :not(iframe){display:none}
.mappanel iframe{display:block; width:100%; aspect-ratio:4/3; border:0}
.cta{background:var(--surface-2); border:1px solid var(--rule)}
.cta h2{font-size:var(--step-2); font-weight:800; max-width:22ch}
.cta p{max-width:52ch}
.cta p{color:var(--ink-2)}
.cta__row{display:flex; gap:.75rem; flex-wrap:wrap; align-items:center}

/* faq */
.faq{border-top:1px solid var(--rule-soft); max-width:var(--measure)}
.faq details{border-bottom:1px solid var(--rule-soft)}
.faq summary{cursor:pointer; padding:1rem 0; font-family:var(--f-display); font-weight:700; font-size:1.0625rem; list-style:none; display:flex; gap:.75rem; align-items:flex-start; min-height:2.75rem}
.faq summary::-webkit-details-marker{display:none}
.faq summary::before{content:"+"; font-family:var(--f-mono); color:var(--accent); font-weight:700; flex:none; line-height:1.4}
.faq details[open] summary::before{content:"\2212"}
.faq p{padding:0 0 1.1rem 1.6rem; color:var(--ink-2); max-width:58ch}

/* tables */
.tbl-wrap{overflow-x:auto; border:1px solid var(--rule); border-radius:.4rem; background:var(--surface)}
table{border-collapse:collapse; width:100%; min-width:34rem; font-size:.9375rem}
th,td{text-align:left; padding:.75rem .9rem; border-bottom:1px solid var(--rule-soft); vertical-align:top}
th{font-family:var(--f-mono); font-size:.6875rem; letter-spacing:.11em; text-transform:uppercase; color:var(--ink-3); background:var(--surface-2)}
tr:last-child td{border-bottom:0}
td.num{font-family:var(--f-mono); font-variant-numeric:tabular-nums; white-space:nowrap}

.areas{display:flex; flex-wrap:wrap; gap:.5rem; list-style:none; margin:0; padding:0}
.areas li{font-family:var(--f-mono); font-size:.8125rem; border:1px solid var(--rule); border-radius:1.25rem; padding:.35rem .75rem; background:var(--surface)}


/* his own channels — ⛔ Yelp is deliberately absent, it is barred estate-wide */
.social{display:flex; gap:.5rem; flex-wrap:wrap; margin-top:.9rem}
.social a{display:inline-flex; align-items:center; gap:.4rem; text-decoration:none;
  border:1px solid var(--rule); border-radius:1.25rem; padding:.35rem .75rem;
  font-size:.8125rem; color:var(--ink-2); background:var(--surface)}
.social a:hover{border-color:var(--accent); color:var(--ink)}
.social svg{width:.95rem; height:.95rem; fill:currentColor}
.foot{padding-block:2.5rem 3rem; font-size:.875rem; color:var(--ink-2); border-top:1px solid var(--rule)}
.foot__grid{display:grid; grid-template-columns:repeat(auto-fit,minmax(12rem,1fr)); gap:1.75rem; margin-bottom:2rem}
.foot h4,.foot .foot__h{font-family:var(--f-mono); font-size:.6875rem; letter-spacing:.13em; text-transform:uppercase; color:var(--ink-3); margin-bottom:.6rem}
.foot ul{list-style:none; margin:0; padding:0; display:grid; gap:.35rem}
.foot a{text-decoration:none; color:var(--ink-2)}
.foot a:hover{color:var(--ink); text-decoration:underline}

/* ⭐ A CIRCLE, NOT A PILL. 3.5rem is comfortably over the 44px touch-target
   floor, and the label moves to aria-label so a screen reader still gets
   "Describe a symptom" while the corner stays quiet. The tooltip on hover
   keeps the meaning discoverable for everyone else. */
.launch{position:fixed; right:1.15rem; bottom:1.15rem; z-index:40;
  width:3.5rem; height:3.5rem; border-radius:50%; padding:0;
  display:inline-flex; align-items:center; justify-content:center;
  background:var(--accent); color:var(--accent-ink); border:0; cursor:pointer;
  box-shadow:0 6px 20px -4px rgba(0,0,0,.55), 0 0 0 1px color-mix(in srgb,var(--accent) 55%,transparent);
  transition:transform .15s cubic-bezier(.3,1.3,.5,1), box-shadow .15s ease}
.launch:hover{transform:scale(1.06)}
.launch:active{transform:scale(.96)}
.launch svg{width:1.55rem; height:1.55rem; fill:none; stroke:currentColor;
  stroke-width:1.9; stroke-linecap:round; stroke-linejoin:round}
.launch::after{content:attr(data-label); position:absolute; right:calc(100% + .6rem);
  white-space:nowrap; background:var(--surface); color:var(--ink);
  border:1px solid var(--rule); border-radius:.3rem; padding:.35rem .6rem;
  font-family:var(--f-body); font-size:.8125rem; box-shadow:var(--shadow);
  opacity:0; pointer-events:none; transform:translateX(4px); transition:opacity .14s ease, transform .14s ease}
.launch:hover::after,.launch:focus-visible::after{opacity:1; transform:none}
@media (max-width:30rem){ .launch::after{display:none} }
.chat{position:fixed; right:1rem; bottom:1rem; z-index:41; width:min(23rem, calc(100vw - 2rem)); background:var(--surface); border:1px solid var(--rule); border-radius:.5rem; box-shadow:var(--shadow); overflow:hidden; display:grid; grid-template-rows:auto 1fr auto auto; max-height:min(32rem, calc(100vh - 2rem))}
.chat__hd{display:flex; align-items:center; gap:.6rem; padding:.75rem .9rem; border-bottom:1px solid var(--rule); background:var(--surface-2)}
.chat__hd b{font-family:var(--f-display); font-size:.9375rem}
.chat__hd .lbl{margin-left:auto}
.chat__x{background:none; border:0; cursor:pointer; color:var(--ink-2); font-size:1.15rem; line-height:1; padding:.2rem .3rem; min-height:2rem}
.chat__log{padding:.9rem; display:grid; gap:.7rem; overflow-y:auto; align-content:start}
.msg{max-width:85%; padding:.55rem .75rem; border-radius:.45rem; font-size:.875rem; line-height:1.45}
.msg--bot{background:var(--surface-2); border:1px solid var(--rule-soft); justify-self:start; border-bottom-left-radius:.1rem}
.msg--me{background:var(--accent); color:var(--accent-ink); justify-self:end; border-bottom-right-radius:.1rem}
.chat__opts{display:flex; flex-wrap:wrap; gap:.4rem; padding:0 .9rem .9rem}
.chip{background:transparent; border:1.5px solid var(--rule); color:var(--ink); border-radius:1.25rem; padding:.4rem .75rem; font-size:.8125rem; font-family:var(--f-body); cursor:pointer; min-height:2rem}
.chip:hover{border-color:var(--accent); color:var(--accent)}
.chat__ft{padding:.6rem .9rem .75rem; border-top:1px solid var(--rule); font-size:.6875rem; color:var(--ink-3); font-family:var(--f-mono)}
[hidden]{display:none!important}
@media (prefers-reduced-motion:reduce){ *{animation:none!important; transition:none!important} }
