/* ============================================================================
   ARROYO CUSTOM RIGS — PUBLIC STOREFRONT STYLES  (Demo A structure)
   Depends on arroyo.css for design tokens (:root). Public pages only.
   ============================================================================ */
*{box-sizing:border-box}
html,body{background:var(--bg);color:var(--ink);font-family:var(--font-body);-webkit-font-smoothing:antialiased}
/* overflow-x must be on BOTH: with `html` left visible the viewport still pans
   sideways on touch even though `body` is hidden. `clip` (not `hidden`) so we
   don't turn the root into a scroll container and break `position:sticky` nav. */
html{overflow-x:clip}
body{margin:0;overflow-x:clip;line-height:1.55}
/* The gradient is a placeholder that shows while a photo loads. It is painted BEHIND
   the image, so any transparent asset (the logo, future badges) shows it through the
   alpha as a dark box — opt those out with .is-transparent rather than dropping it. */
img{max-width:100%;display:block;background:linear-gradient(135deg,#1a1820 0%,#2a2430 50%,#1a1820 100%)}
img.is-transparent{background:none}
a{color:var(--ink);text-decoration:none}

/* Film grain overlay (subtle) */
body::before{
  content:"";position:fixed;inset:0;pointer-events:none;z-index:9999;opacity:.06;mix-blend-mode:overlay;
  background-image:url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='200' height='200'><filter id='n'><feTurbulence baseFrequency='0.9' numOctaves='2'/></filter><rect width='100%25' height='100%25' filter='url(%23n)'/></svg>");
}

/* Type */
.display{font-family:var(--font-display);font-weight:400;letter-spacing:-.01em;line-height:.92;text-transform:uppercase}
.stencil{font-family:var(--font-stencil);letter-spacing:.08em;text-transform:uppercase}
.eyebrow{font-family:var(--font-stencil);font-size:.78rem;letter-spacing:.22em;text-transform:uppercase;color:var(--gold)}
.eyebrow::before{content:"// ";color:var(--gold);opacity:.7}
h1,h2,h3,h4{font-family:var(--font-display);font-weight:400;letter-spacing:-.005em;line-height:.95;text-transform:uppercase;margin:0}

/* Skip */
.skip{position:absolute;left:-9999px;top:0;background:var(--pink);color:var(--ink);padding:.6rem 1rem;z-index:10000}
.skip:focus{left:1rem;top:1rem}

/* Brand mark. One asset serves both the nav and the footer because both grounds are
   --gold. The 2026-08-04 recreation was rolled back for two reasons that do not apply
   here: its keyline was white and read as a sticker on gold (this mark's keyline is
   red), and the footer was near-black then, which swallowed the black letterforms —
   the footer is gold now. If either ground ever goes dark again this needs a reversed
   variant, not a filter. See _designs/_LOGO-HANDOFF.md.
   Sized by height so the mark stays on one optical baseline as the bar shrinks; the
   width/height attributes on the <img> reserve the box so the sticky nav doesn't jump. */
.brand{display:inline-flex;align-items:center}
.brand-logo{display:block;height:55px;width:auto;background:none}
footer .brand-logo{height:72px}

/* Top utility bar (social + basket) */
.topbar{background:#000;border-bottom:1px solid var(--line)}
.topbar-inner{display:flex;align-items:center;justify-content:space-between;gap:1rem;max-width:var(--maxw);margin:0 auto;padding:.4rem 2rem;min-height:38px}
.topbar-social{display:flex;align-items:center;gap:1rem}
.topbar-tag{font-family:var(--font-stencil);font-size:.66rem;letter-spacing:.2em;text-transform:uppercase;color:var(--muted);margin-right:.4rem}
/* WCAG 2.2 AA — 2.5.8 Target Size (Minimum) is 24x24 CSS px, and these icons
   draw at 16px. The GLYPH stays 16px; only the tappable box grows, so nothing
   about the design changes — it just stops being a 16px target on a phone.
   (24 is the conformance bar. The 44px figure quoted by most tools is Apple's
   usability guidance, not WCAG.) */
.topbar .soc{display:inline-flex;align-items:center;justify-content:center;min-width:24px;min-height:24px;color:var(--muted);transition:color .15s, transform .15s}
.topbar .soc:hover{color:var(--gold);transform:translateY(-1px)}
.topbar-actions{display:flex;align-items:center;gap:1rem}
.topbar .topbar-cta{display:none}   /* desktop keeps the nav CTA; mobile shows this one */
.topbar .basket{position:relative;display:inline-flex;align-items:center;justify-content:center;min-width:24px;min-height:24px;color:var(--ink);transition:color .15s}
.topbar .basket:hover{color:var(--gold)}
.topbar .basket-count{position:absolute;top:-8px;right:-10px;min-width:16px;height:16px;padding:0 4px;background:var(--pink);color:#fff;border-radius:9px;font-family:var(--font-body);font-weight:800;font-size:.62rem;line-height:16px;text-align:center}
/* Call-the-shop dropdown — native <details>, opens on click only (never hover).
   Numbers come from config/shop.php; see partials/call-dropdown.blade.php. */
.calldrop{position:relative;display:inline-flex}
.calldrop-toggle{list-style:none;cursor:pointer;gap:.15rem;min-height:24px}
.calldrop-toggle::-webkit-details-marker{display:none}   /* Safari's default triangle */
.calldrop-caret{transition:transform .15s;opacity:.7}
.calldrop[open] .calldrop-toggle{color:var(--gold)}
.calldrop[open] .calldrop-caret{transform:rotate(180deg)}
.calldrop-panel{position:absolute;top:calc(100% + .55rem);left:50%;transform:translateX(-50%);z-index:200;
  min-width:210px;padding:.7rem;background:var(--panel);border:1px solid var(--line);border-radius:10px;
  box-shadow:0 18px 40px rgba(0,0,0,.55)}
/* Little pointer up to the icon. */
.calldrop-panel::before{content:"";position:absolute;top:-5px;left:50%;margin-left:-5px;width:9px;height:9px;
  background:var(--panel);border-left:1px solid var(--line);border-top:1px solid var(--line);transform:rotate(45deg)}
.calldrop-head{font-family:var(--font-stencil);font-size:.6rem;letter-spacing:.18em;text-transform:uppercase;
  color:var(--muted);margin:0 0 .5rem;padding:0 .35rem}
.calldrop-panel ul{list-style:none;margin:0;padding:0}
.calldrop-panel li + li{margin-top:.15rem}
.calldrop-panel li a{display:flex;flex-direction:column;gap:.05rem;padding:.4rem .35rem;border-radius:7px;
  color:var(--ink);text-decoration:none;transition:background .15s,color .15s}
.calldrop-panel li a:hover,.calldrop-panel li a:focus-visible{background:var(--panel-2);color:var(--gold)}
.calldrop-who{font-family:var(--font-stencil);font-size:.66rem;letter-spacing:.14em;text-transform:uppercase;color:var(--muted)}
.calldrop-panel li a:hover .calldrop-who,.calldrop-panel li a:focus-visible .calldrop-who{color:var(--gold)}
.calldrop-num{font-weight:700;font-size:.9rem;letter-spacing:.01em}
.calldrop-alt{display:block;margin-top:.5rem;padding:.4rem .35rem 0;border-top:1px solid var(--line);
  font-size:.72rem;color:var(--muted);text-decoration:none}
.calldrop-alt:hover,.calldrop-alt:focus-visible{color:var(--gold)}

@media (max-width: 720px){
  .topbar-inner{padding:.35rem 1.5rem}
  .topbar-tag{display:none}
  /* Move the CTA up to the top bar (after the social icons) */
  .topbar .topbar-cta{display:inline-flex;padding:.3rem .6rem;font-size:.58rem;letter-spacing:.1em;gap:.35rem;margin-left:.35rem}
  /* Hide the nav CTA so the row is just logo + hamburger → hamburger goes right */
  .nav .btn-pink{display:none}
  /* The social icons sit at the LEFT of the bar once .topbar-tag is hidden, so
     the panel anchors left — centring it (or anchoring right) pushes it off the
     screen edge and clips the numbers. */
  .calldrop-panel{left:0;right:auto;transform:none;min-width:190px;max-width:calc(100vw - 3rem)}
  .calldrop-panel::before{left:14px;margin-left:0}
}

/* Nav — gold bar with dark text */
.nav{position:sticky;top:0;z-index:100;background:var(--gold);border-bottom:1px solid rgba(0,0,0,.22)}
.nav-inner{display:flex;align-items:center;justify-content:space-between;padding:1.1rem 2rem;max-width:var(--maxw);margin:0 auto;gap:2rem}
.nav-menu{display:flex;gap:2rem;list-style:none;margin:0;padding:0}
.nav-menu a{font-family:var(--font-body);font-weight:700;font-size:.82rem;letter-spacing:.18em;text-transform:uppercase;color:#17110A;transition:color .15s,border-color .15s;border-bottom:2px solid transparent;padding-bottom:3px}
.nav-menu a:hover,.nav-menu a.active{color:#17110A;border-bottom-color:var(--red)}
/* Dark-on-gold treatment for the toggle inside the nav */
.nav .nav-toggle{color:#17110A}
.btn-pink{
  display:inline-flex;align-items:center;gap:.55rem;font-family:var(--font-body);font-weight:800;font-size:.78rem;letter-spacing:.2em;text-transform:uppercase;
  background:var(--pink);color:#fff;padding:.85rem 1.4rem;border:0;cursor:pointer;
  clip-path:polygon(0 0, calc(100% - 10px) 0, 100% 10px, 100% 100%, 10px 100%, 0 calc(100% - 10px));
  transition:background .15s, transform .15s;
}
.btn-pink:hover{background:var(--pink-2);transform:translateY(-1px)}
/* A disabled button has to LOOK disabled.
   Client, 2026-08-13, on /contact: "why is the send button active without me
   clicking on the radio?" — it was not active. consent-gate.js had already set
   disabled=true, but the button still rendered full brand red at opacity 1 with
   a pointer cursor, so a visitor who skipped the checkbox clicked a
   normal-looking button, got nothing, and had no idea why. On a contact form
   that is a lost customer.
   Greyed rather than dimmed: a translucent red still reads as "red button",
   where a grey one reads as "not yet". */
.btn-pink:disabled,
.btn-pink[disabled]{
  background:#8A8A8A;color:rgba(255,255,255,.75);
  cursor:not-allowed;transform:none;
}
.btn-pink:disabled:hover,
.btn-pink[disabled]:hover{background:#8A8A8A;transform:none}

/* Says WHY the button is off, next to the button. A disabled control fires no
   click event, so it cannot explain itself when pressed — the reason has to be
   on screen already. Hidden once the box is ticked. */
.consent-hint{
  display:block;margin-top:.6rem;font-size:.82rem;color:var(--muted,#9a9a9a);
}
.consent-hint[hidden]{display:none}
.btn-ghost{
  display:inline-flex;align-items:center;gap:.55rem;font-family:var(--font-body);font-weight:800;font-size:.78rem;letter-spacing:.2em;text-transform:uppercase;
  background:transparent;color:var(--ink);padding:.85rem 1.4rem;border:1px solid var(--ink);cursor:pointer;
  transition:background .15s, color .15s;
}
.btn-ghost:hover{background:var(--ink);color:var(--bg)}
.nav-toggle{display:none;background:none;border:0;color:var(--ink);font-size:1.5rem;cursor:pointer}

/* Hero slideshow */
/* The hero frame is 16:9 at EVERY viewport, not a slice of the viewport height.
   It used to be 92svh, which on a 1440x780 window makes a 1.85:1 box — a 16:9
   photo dropped into that loses ~17% off the top and bottom to `cover`, and the
   loss got worse the wider the window. A fixed 16:9 frame means a photo shot to
   the studio's landscape policy fills the frame exactly, edge to edge, with
   nothing cropped; anything NOT 16:9 (product art flagged "Show in hero" runs
   3:2 and 4:5) still covers, so the frame is never letterboxed.

   The cost, stated plainly: height now tracks WIDTH, so on a window shorter
   than 0.5625 x its width the bottom of the hero — and the dots with it — sits
   below the fold. Deliberate: the alternative is cropping the photo again. */
.hero-slider{position:relative;aspect-ratio:16/9;overflow:hidden}
/* WebGL distortion layer */
.hero-gl{position:absolute;inset:0;z-index:0;width:100%;height:100%;display:block}
.hero-slider.gl-active .hero-bg{display:none}
.hero-slider.gl-active .hero-slide{background:transparent}
/* Contrast box wrapping just the hero text (solid panel, no fade) */
.hero-box{display:inline-block;max-width:min(100%, 640px);
  background:rgba(11,10,12,.62);border-left:3px solid var(--gold);
  padding:1.8rem 2rem;
  -webkit-backdrop-filter:blur(16px) saturate(135%);
  backdrop-filter:blur(16px) saturate(135%);
  box-shadow:0 8px 40px rgba(0,0,0,.35),inset 0 1px 0 rgba(245,241,234,.06)}
/* Copy on the right for slides carrying --text-right. Flex rather than text-align:
   text-align would right-align the copy INSIDE the box too, and the headline, lede
   and CTA row all need to stay left-ranged. justify-content moves only the box.
   The gold rule stays on the left edge of the box on purpose — it reads as a margin
   marker against the copy, not as an outer frame, so it should not flip with the box.
   Reserves room for .hero-arrow.next (52px at right:1.2rem), which is centred
   vertically and would otherwise sit on top of the box on mid-width screens. */
.hero-slide--text-right .hero-content{display:flex;justify-content:flex-end;
  padding-right:clamp(0rem,calc((100vw - var(--maxw)) / -2 + 4.2rem),4.2rem)}
/* Hero accent text is gold (not red) */
.hero-slider .eyebrow,.hero-slider .eyebrow::before{color:var(--gold)}
.hero-track{position:relative;height:100%}
/* Fluid padding, not a flat 6rem/5rem: the frame is now as short as the window
   is narrow (a 900px-wide window gives a 506px-tall hero), and fixed padding
   plus the copy box overflowed it.

   The bottom padding is the fold guard. Frame height tracks WIDTH now, so on an
   ordinary landscape window the frame runs past the bottom of the screen and
   copy pinned to its foot goes with it — measured at 1440x723, the two CTAs sat
   at y=790 against a 723px fold. The second term is exactly that overhang:
   56.25% of the slide's width IS the frame height (percentage padding resolves
   against width, which is what makes this work), less the viewport, plus 10rem
   for the sticky header the hero starts below (130px desktop, ~107 on a
   landscape phone) and a little clearance. max() means it only ever engages
   when the frame actually overhangs;
   on a tall window the first term wins and the copy sits at the foot of the
   photo as designed. No breakpoint — it tracks the window continuously.

   vh, not the svh used for the old 92svh frame: measured in Chrome, svh here
   went stale across a window resize (it reported 907px against a 443px
   viewport, so the guard never engaged), and dvh would reflow the hero as the
   mobile URL bar slides. Portrait phones never reach this rule anyway — the
   split layout below replaces this padding outright — so vh's large-viewport
   reading only ever costs a little slack in mobile LANDSCAPE. */
.hero-slide{position:absolute;inset:0;height:100%;display:flex;align-items:flex-end;
  padding:clamp(2rem,5.5vw,6rem) 2rem max(clamp(1.75rem,4.5vw,5rem), calc(56.25% - 100vh + 10rem));
  opacity:0;visibility:hidden;transition:opacity .8s ease}
.hero-slide.is-active{opacity:1;visibility:visible;position:relative}
.hero-arrow{position:absolute;top:50%;transform:translateY(-50%);z-index:5;width:52px;height:52px;display:flex;align-items:center;justify-content:center;background:rgba(11,10,12,.5);border:1px solid var(--line);color:var(--ink);font-size:2rem;line-height:1;cursor:pointer;transition:background .15s,color .15s;backdrop-filter:blur(6px)}
.hero-arrow:hover{background:var(--pink);border-color:var(--gold);color:#fff}
.hero-arrow.prev{left:1.2rem}
.hero-arrow.next{right:1.2rem}
/* Off the hero at every size, at the client's request — but still in the DOM,
   still keyboard-reachable, and it flies in on focus. The slideshow advances on
   its own, so WCAG 2.2.2 needs *some* mechanism to stop it; this is the same
   show-on-focus treatment as .skip above, so nothing is visible to a mouse or
   touch visitor while keyboard and screen-reader users keep the control. */
.hero-pause{position:absolute;left:-9999px;top:0;z-index:6;background:rgba(11,10,12,.9);
  border:1px solid var(--line);color:var(--ink);font-family:var(--font-body);font-size:.7rem;
  font-weight:800;letter-spacing:.14em;text-transform:uppercase;padding:.55rem .9rem;cursor:pointer}
.hero-pause:focus{left:auto;right:1.2rem;top:1.2rem}
.hero-pause:hover,.hero-pause:focus{border-color:var(--gold);color:var(--gold)}
.hero-dots{position:absolute;bottom:1.6rem;left:50%;transform:translateX(-50%);z-index:5;display:flex;gap:.6rem}
/* 2.5.8: a 5px-tall dot is an unusable target on a phone. Transparent
   vertical padding lifts the hit box to 24px while background-clip keeps the
   painted dot exactly 5px — the design is unchanged, the target is not. */
.hero-dot{width:34px;height:5px;box-sizing:content-box;background:rgba(245,241,234,.3);background-clip:content-box;border:0;cursor:pointer;padding:9.5px 0;transition:background .2s}
.hero-dot.is-active{background:var(--gold)}
.hero-dot:hover{background:var(--gold)}
/* Hero (shared slide internals) */
.hero{position:relative;min-height:92vh;display:flex;align-items:flex-end;overflow:hidden;padding:6rem 2rem 4rem}
.hero-bg{position:absolute;inset:0;z-index:0}
.hero-bg img{width:100%;height:100%;object-fit:cover;object-position:center}
.hero-content{position:relative;z-index:2;max-width:var(--maxw);width:100%;margin:0 auto}
.hero h1{font-size:clamp(3.5rem,11vw,11rem);max-width:14ch}
.hero h1 .out{color:transparent;-webkit-text-stroke:2px var(--ink);display:block}
.hero .lede{max-width:48ch;color:var(--muted);font-size:1.05rem;margin:1.8rem 0 2.3rem;line-height:1.6}
/* The slideshow hero is .hero-slider > .hero-track > .hero-slide — there is no
   .hero ancestor, so none of the three rules above ever matched it and the
   headline fell back to the browser default 2em. It also needs its own scale
   rather than the boxless hero's 11vw: this copy lives inside .hero-box, which
   caps at 640px. */
.hero-slider :is(h1,h2){font-size:clamp(2.4rem,4vw + .8rem,4rem);max-width:16ch}
/* inline, not block: the outline word stays on the same line as the text before
   it ("DRIVEN BY YOU." on one line) at every breakpoint. The line break above it
   comes from the newline in the heading field, so editors still control it. */
.hero-slider :is(h1,h2) .out{color:transparent;-webkit-text-stroke:2px var(--ink);display:inline}
.hero-slider .lede{max-width:46ch;color:var(--muted);font-size:1.02rem;margin:1.2rem 0 1.8rem;line-height:1.6}
.hero-ctas{display:flex;gap:1rem;flex-wrap:wrap}
.hero-eyebrow{display:inline-flex;align-items:center;gap:.7rem;margin-bottom:1.5rem}
.hero-eyebrow .bar{width:48px;height:2px;background:var(--gold)}


/* ---- Hero in portrait: split layout -------------------------------------
   Desktop overlays the copy on the photo. That does not survive a portrait
   viewport: these are 16:9 shots, so `cover` on a 390×776 box crops to a ~28%
   centre strip of the frame — the subject ends up hidden behind .hero-box and
   what's left visible is out-of-focus background. Three further faults: the
   arrows sat at top:50%, i.e. straight through the copy; the dots and Pause
   were pinned to the bottom of a 92vh slider and so landed BELOW the fold on
   an 844px phone; and the slide kept its 6rem/5rem desktop padding.

   So in portrait the hero splits: the photo gets its own 16:9 band at the top
   (uncropped for a photo shot to the landscape policy), and the copy sits
   underneath on a solid panel — legible regardless of which photo the client
   uploads. Controls form one thumb-reachable row at the bottom. --band is
   consumed by .hero-slide's padding and by .hero-gl/.hero-bg, so the band
   height is set in exactly one place.

   Keyed on ORIENTATION, not a width breakpoint. The fault is a tall-and-narrow
   viewport, not a small one: iPad portrait at 768×1024 is wide enough to miss
   any phone breakpoint yet still crops the photo to 0.80 and pushes the dots
   past the fold. max-width caps it so a tall desktop window keeps the overlay.
   ------------------------------------------------------------------------ */
@media (orientation: portrait) and (max-width: 1024px){
  /* The band is a fixed 16:9 so a photo shot to the studio's landscape policy
     shows UNCROPPED. It has to be fixed rather than per-slide: slide 1 is the
     16:9 hero record, but the rest are generated from products flagged
     "Show in hero", whose art runs 3:2 and even 4:5 portrait. Sizing each band
     to its own image would swing the hero between ~219px and ~487px tall and
     shunt the page content down on every autoplay tick. */
  .hero-slider{--ar-w:16;--ar-h:9;--band:calc(100vw * var(--ar-h) / var(--ar-w));
    aspect-ratio:auto;min-height:0;background:var(--bg)}
  .hero-track{height:auto;min-height:0}
  /* display:block, not the desktop flex/align-end — copy flows under the band.
     Percentage padding resolves against the slide's own width, so the reserved
     space tracks the band exactly without depending on 100vw. */
  .hero-slide{height:auto;min-height:0;display:block;
    padding:calc(100% * var(--ar-h) / var(--ar-w)) 0 4.75rem}
  /* Confine the photo (WebGL canvas, or the <img> on the no-WebGL path) to the
     band instead of the whole slider. */
  .hero-gl,.hero-bg{top:0;bottom:auto;height:auto;width:100%;
    aspect-ratio:var(--ar-w)/var(--ar-h)}
  .hero-bg img{height:100%}
  .hero-content{max-width:none}
  /* The first slide right-aligns its box on desktop. In the split layout the copy
     is a full-width panel under the photo band, so there is nothing to align — undo
     it, or the box shrink-wraps to its text and sits off to one side. */
  .hero-slide--text-right .hero-content{display:block;padding-right:0}
  /* Opaque panel: with the photo no longer behind it, the blur+translucency
     was only costing legibility and paint time. */
  .hero-box{width:100%;max-width:none;padding:1.4rem 1.15rem 1.5rem;
    background:var(--panel);-webkit-backdrop-filter:none;backdrop-filter:none;
    box-shadow:none;border-left:3px solid var(--gold)}
  .hero-slider :is(h1,h2){font-size:clamp(2.1rem,9.5vw,3.1rem);max-width:none}
  .hero-slider .lede{font-size:.95rem;margin:.9rem 0 1.35rem;max-width:none}
  /* 2px stroke reads as a smear at phone headline sizes. */
  .hero-slider :is(h1,h2) .out{-webkit-text-stroke-width:1.5px}
  .hero-eyebrow{margin-bottom:.9rem}
  .hero-eyebrow .bar{width:26px}
  .hero-slider .eyebrow{font-size:.68rem;letter-spacing:.16em}
  /* Stacked, equal width — the two were 211px and 160px, which read as a
     mistake rather than a hierarchy. */
  .hero-ctas{flex-direction:column;gap:.6rem}
  .hero-ctas>a{width:100%;justify-content:center}

  /* Control row, below the copy */
  .hero-arrow{top:auto;bottom:1rem;transform:none;width:44px;height:44px;font-size:1.5rem}
  .hero-arrow.prev{left:1.15rem}
  .hero-arrow.next{right:1.15rem}
  /* Centred against the 44px arrows: 1rem + (44-5)/2 ≈ 2.2rem. */
  .hero-dots{bottom:2.2rem}
  .hero-dot{width:26px;height:5px}
  /* Nothing here for .hero-pause — it is hidden-until-focus at every size now
     (see the base rule). Arrows and dots are the only visible controls. */
}
/* Below ~380px the dots would collide with the arrows. */
@media (max-width: 380px){
  .hero-dot{width:18px}
  .hero-dots{gap:.45rem}
}

/* Portrait tablets get the split too, but not the phone's full-bleed copy:
   stretching the buttons to 753px reads as a mistake, and the lede hits ~90
   characters a line. Buttons go back side by side and the measure is capped. */
@media (orientation: portrait) and (min-width: 600px) and (max-width: 1024px){
  .hero-box{padding:1.8rem 2rem 2rem}
  .hero-slider .lede{max-width:62ch;font-size:1.02rem}
  .hero-ctas{flex-direction:row;flex-wrap:wrap;gap:.9rem}
  .hero-ctas>a{width:auto;min-width:210px}
}

/* Short phones (iPhone SE 375×667, 320×690): the band is fine, it's the copy
   stack that runs the CTAs past the fold. Compress the vertical rhythm rather
   than crop the photo. */
@media (orientation: portrait) and (max-width: 560px) and (max-height: 740px){
  .hero-slide{padding-bottom:4rem}
  .hero-box{padding:1.1rem 1.15rem 1.15rem}
  .hero-slider :is(h1,h2){font-size:clamp(1.9rem,8vw,2.5rem)}
  .hero-slider .lede{font-size:.9rem;margin:.7rem 0 1rem}
  .hero-eyebrow{margin-bottom:.7rem}
  .hero-ctas>a{padding:.7rem 1.4rem}
}

/* Phones and small tablets in LANDSCAPE: wide enough that the photo crops
   sanely, so the overlay is right — but far too short for the desktop hero's
   6rem/5rem padding and centre-height arrows. Tightened overlay. */
@media (orientation: landscape) and (max-height: 620px){
  /* Content-driven, not 86svh: on a 390px-tall landscape phone the nav alone
     eats 107px, so a viewport-proportional hero guarantees the CTAs fall off. */
  .hero-slider,.hero-track,.hero-slide{min-height:0}
  /* Bottom padding clears the control row (44px + 2×1rem). */
  /* Same fold guard as the base rule — the hero is a fixed 16:9 frame here too,
     so on an 844x390 landscape phone it is 475px tall against a 390px viewport
     and a flat 4.75rem left the CTAs off screen. 4.75rem stays as the floor: it
     is what clears the control row (44px + 2x1rem). */
  .hero-slide{padding:2.5rem 1.5rem max(4.75rem, calc(56.25% - 100vh + 10rem))}
  .hero-box{max-width:min(100%,520px);padding:1.2rem 1.5rem}
  .hero-slider :is(h1,h2){font-size:clamp(1.9rem,4.5vw,2.6rem)}
  .hero-slider .lede{font-size:.92rem;margin:.7rem 0 1.1rem;max-width:56ch}
  .hero-eyebrow{margin-bottom:.7rem}
  .hero-ctas>a{padding:.65rem 1.2rem}
  .hero-arrow{top:auto;bottom:1rem;transform:none;width:44px;height:44px}
  .hero-arrow.prev{left:1.5rem}
  .hero-arrow.next{right:1.5rem}
  .hero-dots{bottom:2.2rem}
}

/* Wide screens: run the whole headline on one line and let the box grow to suit.
   The break comes from a newline in the `heading` field (nl2br → <br>); hiding
   the <br> collapses it to the space nl2br leaves behind. Deliberately NOT
   white-space:nowrap — headings are admin-editable, and a long one would then
   punch out of the box instead of wrapping. */
@media (min-width: 1032px){
  .hero-slider :is(h1,h2) br{display:none}
  .hero-slider :is(h1,h2){max-width:none}
  .hero-box{max-width:min(100%,920px)}
  /* The box is ~780px wide now, so the old 46ch measure left the lede at three
     lines and it, not the headline, set the box height. 78ch takes it to two
     while staying inside a comfortable reading measure. */
  .hero-slider .lede{max-width:78ch}
}

/* Marquee */
.marquee{background:var(--pink);overflow:hidden;border-top:1px solid var(--bg);border-bottom:1px solid var(--bg);position:relative;z-index:3}
/* Seamless: spacing lives on each item (margin-right) so a -50% shift lands
   exactly on one duplicated tile — no half-gap jump at the loop restart. */
.marquee-track{display:flex;width:max-content;animation:scroll-x 34s linear infinite;padding:.9rem 0;white-space:nowrap;will-change:transform}
.marquee-item{font-family:var(--font-display);font-size:1.1rem;letter-spacing:.18em;text-transform:uppercase;color:#fff;display:flex;align-items:center;gap:2.5rem;margin-right:2.5rem}
.marquee-item::after{content:"";display:inline-block;width:10px;height:10px;background:#fff;border-radius:50%;flex-shrink:0}
@keyframes scroll-x{from{transform:translate3d(0,0,0)}to{transform:translate3d(-50%,0,0)}}
@media (prefers-reduced-motion: reduce){.marquee-track{animation:none}}

/* Stats */
.stats{padding:3.5rem 2rem;border-bottom:1px solid var(--line);background:var(--bg)}
/* Flex and centred, not the original fixed 4-column grid. That grid was written
   for four stats; at two (2026-08-08) the pair sat in columns 1-2 and stranded the
   right half of the band, reading as content that failed to load. Centring a short
   pair is what makes the empty space look chosen. It also scales: a third or fourth
   stat just joins the row, so this does not need revisiting if the copy grows. */
.stats-grid{max-width:var(--maxw);margin:0 auto;display:flex;flex-wrap:wrap;
  justify-content:center;align-items:center;row-gap:2.5rem}
.stat{text-align:center;padding:0 clamp(2rem,5vw,5rem)}
/* Hairline between adjacent stats. On the element rather than a gap pseudo-element
   so it cannot drift out of position when the row wraps. */
.stat + .stat{border-left:1px solid var(--line)}
.stat .n{font-family:var(--font-display);font-size:clamp(2.8rem,6vw,5rem);color:var(--amber);line-height:1;letter-spacing:-.02em}
.stat .l{font-family:var(--font-stencil);font-size:.78rem;letter-spacing:.22em;text-transform:uppercase;color:var(--muted);margin-top:.5rem}

/* Section base */
section{position:relative}
.container-x{max-width:var(--maxw);margin:0 auto;padding:0 2rem}
.section-head{display:flex;align-items:flex-end;justify-content:space-between;gap:2rem;margin-bottom:3rem;flex-wrap:wrap}
.section-head .num{font-family:var(--font-stencil);font-weight:700;font-size:1rem;color:var(--gold);letter-spacing:.3em;margin-bottom:.6rem;display:block}
.section-head h2{font-size:clamp(2.4rem,5.5vw,4.6rem)}
.section-head .link{font-family:var(--font-body);font-weight:800;font-size:.8rem;letter-spacing:.2em;text-transform:uppercase;color:var(--ink);border-bottom:2px solid var(--pink);padding-bottom:.25rem;align-self:flex-end}

/* Plate divider */
.plate{height:14px;background-image:
  radial-gradient(circle at 4px 4px, var(--panel-2) 1.5px, transparent 1.8px),
  radial-gradient(circle at 10px 10px, var(--panel-2) 1.5px, transparent 1.8px);
  background-size:12px 12px;background-color:var(--panel);border-top:1px solid var(--line);border-bottom:1px solid var(--line)}

/* Featured Builds / gallery cards */
.builds{padding:6rem 0;background:var(--bg)}
.builds-grid{display:grid;grid-template-columns:repeat(4,1fr);gap:1.2rem}
.build-card{position:relative;overflow:hidden;background:var(--panel);aspect-ratio:3/4;display:flex;flex-direction:column}
.build-card img{width:100%;height:100%;object-fit:cover;transition:transform .5s}
.build-card:hover img{transform:scale(1.04)}
/* Only overlay: the bottom caption strip where the text sits */
.build-card-overlay{position:absolute;inset:auto 0 0 0;padding:1.2rem 1.3rem;background:linear-gradient(180deg,transparent 0%,rgba(11,10,12,.95) 85%);z-index:2}
.build-card .tag{font-family:var(--font-stencil);font-size:.7rem;letter-spacing:.25em;color:var(--gold);text-transform:uppercase;margin-bottom:.4rem;display:block}
.build-card h2{font-size:1.5rem;color:var(--ink)}
/* Sits on the raw photo, above the bottom scrim, and measured 2.36:1 against a
   bright sky. It is a card ordinal — pure decoration under 1.4.3, and now
   aria-hidden so it isn't announced as a meaningless "01" — but "exempt" is not
   the same as "legible", and a shadow halo costs nothing and holds up over any
   photo the CMS throws at it. */
.build-card .num-corner{position:absolute;top:.9rem;left:1rem;font-family:var(--font-stencil);font-size:.8rem;color:var(--ink);letter-spacing:.2em;z-index:2;opacity:.85;text-shadow:0 1px 2px rgba(0,0,0,.95),0 0 8px rgba(0,0,0,.8)}
.build-card-overlay::before{content:"";position:absolute;left:0;right:0;top:0;height:3px;background:var(--pink)}

/* --- Shop category filters --------------------------------------------------
   These used to borrow .nav-menu, whose colour (#17110A) exists to sit on the
   GOLD nav bar — on the dark shop page that rendered near-black on near-black
   and the filters were effectively invisible. Gold on --bg instead, which is
   the same pairing the nav uses, just inverted. Active is white with the red
   underline so the current category still reads as the selected one. */
.shop-filters{display:flex;flex-wrap:wrap;gap:1.4rem;list-style:none;padding:0;margin:0 0 2.5rem}
.shop-filters a{display:inline-block;font-family:var(--font-body);font-weight:700;font-size:.82rem;letter-spacing:.18em;text-transform:uppercase;color:var(--gold);border-bottom:2px solid transparent;padding-bottom:3px;transition:color .15s,border-color .15s}
.shop-filters a:hover{color:var(--ink);border-bottom-color:var(--gold-2)}
.shop-filters a.active{color:var(--ink);border-bottom-color:var(--red)}
.shop-filters a:focus-visible{outline:2px solid var(--gold);outline-offset:3px}

/* --- Per-letter heading reveal ----------------------------------------------
   Paired with js/typewriter.js, which wraps the characters already in the
   markup. No cursor, per the brief — letters just arrive.

   steps(1,end) rather than a fade: a typewriter puts a letter down, it doesn't
   dissolve one in. The spans stay `inline` (no inline-block) so the heading is
   still one run of text for screen readers and line-breaking is unchanged.

   .tw-run is added by JS, so with JS off nothing here applies and the heading
   renders as ordinary text — the opacity:0 can never strand a heading.

   FAIL-SAFE BY CONSTRUCTION. This was first written with `animation: … forwards`
   and letters got STUCK INVISIBLE — on /builds, "Custom builds" rendered as
   "CUS  M BUILDS" with characters 4 and 5 permanently at opacity 0 even though
   the Web Animations API reported both animations `finished` with fill
   `forwards`. The held fill value simply never got committed for those two.

   So the resting state is no longer an animation's held value, it is a real
   declaration: `.tw-go .tw-c{opacity:1}`. The reveal is a transition INTO that
   state. If a transition is dropped, mis-timed or never fires, the letter
   still ends up at opacity 1 — the worst case is that it appears instantly
   instead of on cue. There is no longer any failure mode where a heading stays
   invisible, which is the only outcome that actually matters on an <h1>.

   .tw-done is the belt-and-braces: JS sets it once the run should be over and
   it pins every letter visible with transitions off. */
h1.tw-run .tw-c{opacity:0;transition:opacity .01s steps(1,end);transition-delay:var(--d,0ms)}
h1.tw-run.tw-go .tw-c{opacity:1}
h1.tw-run.tw-done .tw-c{opacity:1;transition:none}
@media (prefers-reduced-motion: reduce){
  h1.tw-run .tw-c{opacity:1;transition:none}
}

/* --- Perforated plate edge (global section divider) -------------------------
   The punched-metal strip that used to be baked into .story::before. Promoted
   to a utility class so any band can carry it (client, 2026-08-09: "i like
   that design as a global section devider"). Add class="perf-edge" to a
   <section> — it supplies its own position:relative, so it works on a section
   that isn't already positioned.

   The dot colour is --panel-2, which sits a step above BOTH surface tokens
   (--bg #0B0A0C and --panel #15131A), so one value reads on every band. A
   section on some future surface can override --perf-dot rather than
   redeclaring the gradients. aria-hidden by nature (a ::before with no text)
   and pointer-events:none so it can never eat a click near a section edge. */
.perf-edge{position:relative}
.perf-edge::before{content:"";position:absolute;top:0;left:0;right:0;height:14px;pointer-events:none;background-image:
  radial-gradient(circle at 4px 4px, var(--perf-dot, var(--panel-2)) 1.5px, transparent 1.8px),
  radial-gradient(circle at 10px 10px, var(--perf-dot, var(--panel-2)) 1.5px, transparent 1.8px);
  background-size:12px 12px}

/* About / Shop story */
.story{padding:7rem 0;background:var(--panel);position:relative}
.story-grid{display:grid;grid-template-columns:.9fr 1.1fr;gap:5rem;align-items:center}
/* A single landscape photo is much shorter than the text beside it, and
   centring left it floating with ~160px of dead space above — it read as a
   mistake. Top-aligning lines the photo up with the "Who we are" eyebrow.
   Scoped with :has(> img) so it applies ONLY to the single-photo case: the home
   page's 2x2 tile grid is nearly as tall as its text and still centres. */
.story-grid:has(.story-img > img){align-items:start}
.story-img{position:relative}
/* The single story photo keeps its OWN aspect ratio.
   This used to be a hard aspect-ratio:4/5 with object-fit:cover, chosen to suit
   shop-tools-bench.webp (1600x1999, which is exactly 4:5). The moment a
   different photo went in — the About page's 800x533 brackets shot — the crop
   squeezed a 1.50 image into 1.03 and cut away a third of its width.
   These photos get swapped by hand and from the admin, so the layout has to
   accept whatever shape arrives rather than assume one. height:auto + no
   aspect-ratio means the image is never cropped, whatever its proportions.
   (.story-tiles img below sets its own 4/3 for the home page grid, and wins on
   source order.) */
.story-img img{width:100%;height:auto;object-fit:cover;filter:brightness(.7) contrast(1.15) saturate(.85)}

/* Featured-products tile grid in the story slot. Four 4:3 product crops fill
   the same portrait block the single photo used to, which is the only way to
   show catalogue photos here without a brutal centre-crop: every product shot
   is LANDSCAPE by standing policy and the slot is 4:5. Each cell keeps its
   own aspect, the block as a whole stays portrait. The lighter filter than
   the stock photo's is deliberate — these are parts being sold, not mood. */
.story-tiles{display:grid;grid-template-columns:1fr 1fr;gap:.7rem;width:100%}
/* With a single photo the "20+ years" stamp hangs over a dead corner of the
   image, which is fine. Over the tile grid it landed on the fourth tile and
   buried its name plate — so in tile mode the stamp stops being absolute and
   sits under the grid instead, still tucked to the right edge. */
.story-img:has(.story-tiles){display:flex;flex-direction:column;align-items:flex-end}
.story-img:has(.story-tiles) .stamp{position:static;margin:.9rem -1rem 0 0}
.story-tiles a{display:block;position:relative;overflow:hidden;background:var(--panel-2)}
/* height:auto is load-bearing, not tidiness. The width/height attributes on
   the <img> are presentational hints, so with only `width` set in CSS the
   height attribute (675) stayed definite and aspect-ratio was ignored —
   every tile rendered 675px tall. */
.story-tiles img{width:100%;height:auto;aspect-ratio:4/3;object-fit:cover;filter:brightness(.82) contrast(1.08) saturate(.92);transition:transform .35s ease,filter .35s ease}
.story-tiles a:hover img,.story-tiles a:focus-visible img{transform:scale(1.05);filter:brightness(1) contrast(1.05) saturate(1)}
.story-tiles a:focus-visible{outline:2px solid var(--gold);outline-offset:2px}
/* Name plate over each tile — the point is to show WHAT the part is, not just
   that a photo exists. Gradient rather than a flat bar so the photo still
   reads through it. */
/* Measured 2026-08-11: with the old `to top, rgba(0,0,0,.88), transparent`
   ramp, a two-line tile name put its FIRST line where the gradient had already
   thinned out. Against the brightest photo in the set that came to 4.06:1 —
   under the 4.5 bar for 12px text. The others cleared it by roughly 1.2, which
   is the real problem: these photos come from the CMS, so a scrim that only
   just passes today fails the next time David uploads something brighter.
   The stops are in REM, not percentages, and that is the whole point: a
   percentage ramp stretches with the element, so a three-line product name
   pushed its first line up into the faded part and dropped to 2.6:1 while
   two-line names passed. Anchoring the fade to the 1.6rem top padding means the
   text area is always on .94 regardless of how long a name the admin types. */
.story-tiles .tile-name{position:absolute;left:0;right:0;bottom:0;padding:1.6rem .7rem .55rem;font-family:var(--font-body);font-weight:700;font-size:.72rem;letter-spacing:.04em;line-height:1.25;color:#fff;background:linear-gradient(to top,rgba(0,0,0,.94) 0,rgba(0,0,0,.94) calc(100% - 1.6rem),rgba(0,0,0,.5) calc(100% - .5rem),transparent 100%)}
@media (max-width: 520px){
  .story-tiles .tile-name{font-size:.66rem;padding:1.2rem .5rem .45rem}
}
.story-img .stamp{position:absolute;bottom:-1.3rem;right:-1rem;background:var(--pink);color:#fff;padding:1rem 1.4rem;font-family:var(--font-display);font-size:1.1rem;letter-spacing:.1em;text-transform:uppercase;clip-path:polygon(0 0,calc(100% - 12px) 0,100% 12px,100% 100%,12px 100%,0 calc(100% - 12px));text-align:center;line-height:1}
.story-img .stamp .big{font-size:2.6rem;display:block;line-height:.9;margin-bottom:.25rem}
.story h2{font-size:clamp(2.6rem,5.5vw,4.8rem);margin-bottom:1.5rem}
.story p{color:var(--muted);font-size:1.05rem;margin-bottom:2rem;max-width:55ch}
.story-list{list-style:none;padding:0;margin:0 0 2rem 0}
.story-list li{display:flex;gap:1rem;align-items:flex-start;padding:.85rem 0;border-bottom:1px solid var(--line)}
.story-list li::before{content:"";flex-shrink:0;width:18px;height:18px;margin-top:.3rem;background:var(--pink);clip-path:polygon(0 50%, 35% 100%, 100% 0)}
.story-list li span{font-family:var(--font-body);font-weight:600;font-size:1rem;color:var(--ink)}

/* Services */
.services{padding:7rem 0;background:var(--bg)}
.services-grid{display:grid;grid-template-columns:repeat(3,1fr);gap:1.2rem}
/* Flex column + stretched grid cells so .svc-foot can pin to the bottom and the
   badges/links align across a row even when the body copy runs to different lengths. */
.svc{background:var(--panel);padding:2.2rem 2rem;border:1px solid var(--line);position:relative;transition:border-color .2s, transform .2s;display:flex;flex-direction:column;height:100%}
.svc:hover{border-color:var(--gold);transform:translateY(-3px)}
.svc::before{content:"";position:absolute;top:.6rem;left:.6rem;width:6px;height:6px;background:var(--pink);border-radius:50%}
.svc::after{content:"";position:absolute;top:.6rem;right:.6rem;width:6px;height:6px;background:var(--pink);border-radius:50%}
.svc-thumb{display:block;width:100%;aspect-ratio:4/3;object-fit:cover;margin:0 0 1.4rem;border:1px solid var(--line)}
.svc .num{font-family:var(--font-stencil);font-size:.75rem;letter-spacing:.25em;color:var(--amber);margin-bottom:1rem;display:block}
.svc h3{font-size:1.65rem;margin-bottom:.75rem}
.svc p{color:var(--muted);font-size:.96rem;margin:0;line-height:1.55}
/* How a service is sold: "Kits available" vs quote-only (frame stretching).
   margin-top:auto pushes this block to the bottom of the flex card. */
.svc-foot{margin-top:auto;padding-top:1.5rem}
.svc-tag{display:inline-block;font-family:var(--font-body);font-size:.68rem;font-weight:800;letter-spacing:.16em;text-transform:uppercase;color:var(--gold);border:1px solid var(--gold);padding:.3rem .6rem}
.svc-tag-quote{color:var(--red-text);border-color:var(--red-text)}
.svc-link{display:flex;align-items:center;min-height:24px;margin-top:.9rem;font-family:var(--font-body);font-size:.78rem;font-weight:800;letter-spacing:.14em;text-transform:uppercase;color:var(--ink);border-bottom:1px solid transparent;width:fit-content}
.svc-link:hover{color:var(--gold);border-bottom-color:var(--gold)}

/* Brands We Build */
.brands{padding:7rem 0;background:var(--panel);border-top:1px solid var(--line)}
.brands-grid{display:grid;grid-template-columns:repeat(4,1fr);gap:1.2rem}
.brand-card{background:var(--bg);padding:2.4rem 2rem 2.2rem;border:1px solid var(--line);position:relative;transition:border-color .2s, transform .2s}
.brand-card:hover{border-color:var(--gold);transform:translateY(-3px)}
.brand-card::before{content:"";position:absolute;top:0;left:0;right:0;height:4px;background:var(--pink)}
.brand-name{font-family:var(--font-display);font-weight:400;font-size:1.9rem;color:var(--ink);text-transform:uppercase;letter-spacing:-.005em;line-height:.95;margin:0 0 1.3rem 0;padding-bottom:1rem;border-bottom:1px dashed var(--line)}
.brand-models{list-style:none;padding:0;margin:0}
.brand-models li{padding:.45rem 0;font-family:var(--font-stencil);font-weight:700;font-size:.95rem;letter-spacing:.18em;text-transform:uppercase;color:var(--muted);transition:color .15s;border-bottom:1px dashed var(--line)}
.brand-models li:last-child{border-bottom:0}
.brand-card:hover .brand-models li{color:var(--ink)}
.brand-card .all{font-family:var(--font-stencil);font-weight:700;font-size:.95rem;letter-spacing:.18em;text-transform:uppercase;color:var(--amber);padding:.45rem 0}

/* Process */
.process{padding:7rem 0;background:var(--panel)}
.process-grid{display:grid;grid-template-columns:repeat(4,1fr);gap:0;border-top:1px solid var(--line);border-left:1px solid var(--line)}
.step{padding:2.5rem 1.8rem;border-right:1px solid var(--line);border-bottom:1px solid var(--line);position:relative}
.step .num{font-family:var(--font-stencil);font-weight:700;font-size:4.5rem;color:var(--panel-2);line-height:.9;display:block;margin-bottom:1rem}
.step h3{font-size:1.3rem;margin-bottom:.7rem}
.step p{color:var(--muted);font-size:.92rem;margin:0;line-height:1.55}

/* Testimonials */
.quotes{padding:7rem 0;background:var(--bg)}
.quotes-grid{display:grid;grid-template-columns:repeat(3,1fr);gap:1.4rem}
.q{padding:2.2rem;background:var(--panel);border-left:3px solid var(--pink);position:relative}
.q::before{content:"\201C";position:absolute;top:.4rem;left:1rem;font-family:var(--font-display);font-size:5rem;color:var(--gold);opacity:.25;line-height:1}
.q .body{font-family:var(--font-body);font-size:1.05rem;font-weight:500;color:var(--ink);margin:0 0 1.4rem 0;line-height:1.55;position:relative;z-index:1}
.q .who{font-family:var(--font-stencil);font-size:.78rem;letter-spacing:.18em;color:var(--muted);text-transform:uppercase}
.q .who strong{color:var(--ink);font-weight:700}

/* Big CTA */
.cta{padding:9rem 2rem 5rem;background:var(--bg);position:relative;overflow:hidden;text-align:left;border-top:1px solid var(--line)}
.cta-bg-word{
  position:absolute;left:-2vw;bottom:-3vw;font-family:var(--font-display);font-size:32vw;color:transparent;
  -webkit-text-stroke:2px var(--panel-2);line-height:.85;letter-spacing:-.04em;pointer-events:none;user-select:none;z-index:0;text-transform:uppercase
}
.cta-inner{position:relative;z-index:2;max-width:var(--maxw);margin:0 auto;display:grid;grid-template-columns:1.1fr .9fr;gap:4rem;align-items:flex-end}
.cta h2{font-size:clamp(3rem,8vw,8rem)}
.cta .lede{color:var(--muted);font-size:1.1rem;max-width:50ch;margin:1.5rem 0}
.cta .contact-block{padding:2.2rem;background:var(--panel);border:1px solid var(--line)}
.cta .contact-block .item{padding:1rem 0;border-bottom:1px solid var(--line)}
.cta .contact-block .item:last-of-type{border-bottom:0}
.cta .contact-block .lab{font-family:var(--font-stencil);font-size:.74rem;letter-spacing:.22em;color:var(--gold);text-transform:uppercase;margin-bottom:.3rem;display:block}
.cta .contact-block .val{font-family:var(--font-display);font-size:1.7rem;color:var(--ink);text-transform:uppercase;letter-spacing:.01em}
.cta .contact-block .btn-pink{margin-top:1.4rem;width:100%;justify-content:center}

/* ---- Sub-page shell (builds gallery, shop, cms pages) ---- */
.page-hero{padding:5rem 0 3rem;background:var(--panel);border-bottom:1px solid var(--line);position:relative}
.page-hero .container-x{position:relative;z-index:1}
.page-hero h1{font-size:clamp(2.8rem,7vw,6rem)}
.page-hero .lede{color:var(--muted);font-size:1.05rem;max-width:60ch;margin-top:1.2rem}
.page-body{padding:5rem 0;background:var(--bg)}
.prose{max-width:70ch}
.prose p{color:var(--muted);font-size:1.05rem;margin:0 0 1.3rem}
.prose h2{font-size:2rem;margin:2.5rem 0 1rem}
.prose a{color:var(--gold)}

/* WCAG 1.4.1 (Use of Color) — a link sitting inside a sentence must be
   distinguishable WITHOUT colour. Gold on dark clears the contrast bar easily,
   but that only helps someone who can separate gold from grey; colour alone is
   not a cue. So in-text links are underlined.
   Scoped to running-text contexts on purpose: nav items, footer lists, product
   cards and buttons are standalone targets, not links embedded in prose, and
   underlining those would be noise. `.cookie-bar a` already did this — the rest
   of the site now matches it. */
.prose a,
.lede a,
.pd-install-note a,
.policy-gate a,
.cs-note a,
.form-note a{text-decoration:underline;text-underline-offset:.18em;text-decoration-thickness:1px}

/* A button that happens to live inside .prose keeps its own colours.
   `.prose a` is more specific than `.btn-pink`, so without this it repaints
   white button text gold — gold on the red fill fails 1.4.3, which is exactly
   what axe caught on the checkout success page. */
.prose a.btn-pink,
.prose a.btn-ghost{text-decoration:none}
.prose a.btn-pink{color:#fff}
.prose a.btn-ghost{color:var(--ink)}
.prose a.btn-ghost:hover{color:var(--bg)}
.crumbs{font-family:var(--font-stencil);font-size:.75rem;letter-spacing:.2em;text-transform:uppercase;color:var(--muted);margin-bottom:1rem}
.crumbs a{display:inline-flex;align-items:center;min-height:24px;color:var(--muted)}.crumbs a:hover{color:var(--gold)}

/* ---- Shop / product grid ---- */
.shop-grid{display:grid;grid-template-columns:repeat(3,1fr);gap:1.2rem}

/* Shop "All" view: one section per category. The page-level .section-head h2
   runs up to 4.6rem — too loud repeated five times down one page, so sections
   use a tighter scale and a slimmer bottom margin. */
.shop-section{margin-bottom:4.5rem}
.shop-section:last-child{margin-bottom:0}
/* A category carrying the perforated divider needs room above its heading, or
   the strip sits on top of the "// 02" eyebrow. The extra margin is what makes
   the strip read as a break BETWEEN categories rather than as decoration
   attached to the one below it. */
.shop-section.perf-edge{margin-top:1.5rem;padding-top:3rem}
.shop-section .section-head{margin-bottom:1.8rem}
.shop-section .section-head h2{font-size:clamp(1.9rem,3.5vw,2.8rem)}
.shop-section-lede{color:var(--muted);font-size:.98rem;max-width:64ch;margin-top:.5rem}
.product-card{background:var(--panel);border:1px solid var(--line);position:relative;display:flex;flex-direction:column;transition:border-color .2s, transform .2s}
.product-card:hover{border-color:var(--gold);transform:translateY(-3px)}
.product-card .thumb{aspect-ratio:4/3;overflow:hidden;background:var(--panel-2)}
.product-card .thumb img{width:100%;height:100%;object-fit:cover}
.product-card .pc-body{padding:1.3rem 1.4rem 1.5rem}
.product-card .cat{font-family:var(--font-stencil);font-size:.68rem;letter-spacing:.24em;text-transform:uppercase;color:var(--gold);display:block;margin-bottom:.5rem}
.product-card h3{font-size:1.25rem;color:var(--ink);margin-bottom:.6rem;font-family:var(--font-display)}
.product-card .price{font-family:var(--font-display);font-size:1.5rem;color:var(--amber)}
.product-card .price .was{font-family:var(--font-body);font-size:.95rem;color:var(--muted);text-decoration:line-through;margin-left:.5rem}
.product-card .price .mto{font-family:var(--font-stencil);font-size:.7rem;letter-spacing:.2em;color:var(--muted);text-transform:uppercase}

/* ---- Product detail ---- */
.pd-grid{display:grid;grid-template-columns:1.1fr .9fr;gap:3.5rem;align-items:start}
.pd-gallery img{width:100%;aspect-ratio:4/3;object-fit:cover;border:1px solid var(--line)}

/* Gallery controls — thumbs swap the main slot, main slot opens the lightbox */
.pd-main{display:block;width:100%;padding:0;border:0;background:none;cursor:zoom-in;position:relative}
/* Play badge — marks a video in the gallery. A ::after triangle rather than an
   icon file keeps it one less request and it inherits the accent colour. */
.pd-main.is-video,.pd-thumb.is-video{cursor:pointer}
.pd-main.is-video::after,.pd-thumb.is-video::after{content:'';position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);border-style:solid;border-color:transparent transparent transparent #fff;filter:drop-shadow(0 2px 6px rgba(0,0,0,.6))}
.pd-main.is-video::before,.pd-thumb.is-video::before{content:'';position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);border-radius:50%;background:rgba(6,5,9,.55);border:2px solid rgba(255,255,255,.9)}
.pd-main.is-video::before{width:76px;height:76px}
.pd-main.is-video::after{border-width:15px 0 15px 24px;margin-left:5px}
.pd-thumb{position:relative}
.pd-thumb.is-video::before{width:34px;height:34px;border-width:1.5px}
.pd-thumb.is-video::after{border-width:7px 0 7px 11px;margin-left:2px}
.pd-thumbs{display:grid;grid-template-columns:repeat(4,1fr);gap:1rem;margin-top:1rem}
.pd-thumb{padding:0;border:0;background:none;cursor:pointer;opacity:.55;transition:opacity .15s}
.pd-thumb:hover{opacity:.85}
.pd-thumb.is-active{opacity:1}
.pd-thumb.is-active img{border-color:var(--gold)}

/* Lightbox: a fullscreen transparent <dialog>; clicks on the dialog surface
   itself (outside the image/buttons) close it. The img rules override the
   generic .pd-gallery img crop above — the whole point of the lightbox is
   the uncropped master. */
.pd-lightbox{width:100vw;height:100dvh;max-width:none;max-height:none;margin:0;padding:0;border:0;background:transparent;display:none;align-items:center;justify-content:center;opacity:0;transform:scale(.965);transition:opacity .22s ease,transform .22s ease,display .22s allow-discrete,overlay .22s allow-discrete}
.pd-lightbox[open]{display:flex;opacity:1;transform:scale(1)}
/* @starting-style gives the OPEN transition a from-state; the allow-discrete
   entries above keep display/top-layer alive through the CLOSE fade. Browsers
   without these just snap open/closed — the old behavior, not a break. */
@starting-style{.pd-lightbox[open]{opacity:0;transform:scale(.965)}}
.pd-lightbox::backdrop{background:rgba(6,5,9,.94);opacity:0;transition:opacity .22s ease,display .22s allow-discrete,overlay .22s allow-discrete}
.pd-lightbox[open]::backdrop{opacity:1}
@starting-style{.pd-lightbox[open]::backdrop{opacity:0}}
.pd-lightbox img{width:auto;max-width:calc(100vw - 2rem);height:auto;max-height:calc(100dvh - 2rem);aspect-ratio:auto;object-fit:contain;border:0;transition:opacity .18s ease}
.pd-lightbox img.is-loading{opacity:.15}
/* WebGL layer: the same self-displacement transition as the home-page hero.
   It only appears once three.js has lazily loaded (.gl-active), so the <img>
   carries the first paint and there is never a blank frame. */
.pd-lb-gl{position:absolute;inset:0;width:100%;height:100%;display:block}
.pd-lightbox:not(.gl-active) .pd-lb-gl{display:none}
.pd-lightbox.gl-active img{visibility:hidden}

/* Video player — takes over the frame while a clip is showing. */
.pd-lb-player{position:relative;width:min(1400px,calc(100vw - 2rem));aspect-ratio:16/9;max-height:calc(100dvh - 2rem);background:#000}
.pd-lb-player iframe,.pd-lb-player video{position:absolute;inset:0;width:100%;height:100%;border:0;object-fit:contain;background:#000}
.pd-lightbox.is-playing .pd-lb-gl,.pd-lightbox.is-playing>img{display:none}
/* The arrows sit over a 16:9 player on desktop; on phones the player is short
   and they'd float in empty space, so drop them to the bottom corners. */
@media (max-width:720px){
  .pd-lightbox.is-playing .pd-lb-arrow{top:auto;bottom:1rem;transform:none}
}
@media (prefers-reduced-motion: reduce){
  .pd-lightbox,.pd-lightbox::backdrop,.pd-lightbox img{transition:none}
}
.pd-lightbox button{position:absolute;display:flex;align-items:center;justify-content:center;width:52px;height:52px;padding:0;border:1px solid var(--line);background:rgba(6,5,9,.7);color:var(--ink);font-size:1.9rem;line-height:1;cursor:pointer}
.pd-lightbox button:hover{border-color:var(--gold);color:var(--gold)}
.pd-lb-close{top:1rem;right:1rem}
.pd-lb-arrow{top:50%;transform:translateY(-50%)}
.pd-lb-arrow.prev{left:1rem}
.pd-lb-arrow.next{right:1rem}
.pd-lb-counter{position:absolute;bottom:1.1rem;left:50%;transform:translateX(-50%);font-family:var(--font-stencil);font-size:.8rem;letter-spacing:.25em;color:var(--muted)}
.pd-info h1{font-size:clamp(2rem,4vw,3.2rem);margin-bottom:1rem}
.pd-info .price{font-family:var(--font-display);font-size:2.4rem;color:var(--amber);margin:1rem 0}
.pd-info .price .was{font-family:var(--font-body);font-size:1.1rem;color:var(--muted);text-decoration:line-through;margin-left:.6rem}
.pd-meta{list-style:none;padding:0;margin:1.5rem 0;border-top:1px solid var(--line)}
.pd-meta li{display:flex;justify-content:space-between;gap:1rem;padding:.8rem 0;border-bottom:1px solid var(--line);font-size:.95rem}
.pd-meta li .k{font-family:var(--font-stencil);font-size:.72rem;letter-spacing:.2em;text-transform:uppercase;color:var(--muted)}
.pd-meta li .v{color:var(--ink);font-weight:600}

/* Add-on options — priced extras, selectable, above the buy row.
   A fieldset rather than a list: these are form controls now, and the legend
   is what a screen reader announces before the group. */
.pd-options{margin:1.8rem 0;border:1px solid var(--line);padding:1.4rem 1.5rem}
.pd-options legend{font-family:var(--font-stencil);font-size:.78rem;letter-spacing:.22em;text-transform:uppercase;color:var(--muted);padding:0 .5rem;margin-left:-.5rem}
.opt-row{display:grid;grid-template-columns:auto 1fr auto;align-items:baseline;gap:.85rem;padding:.7rem 0;border-top:1px solid var(--line);font-size:.95rem;cursor:pointer}
.pd-options .opt-row:first-of-type{border-top:0}
.opt-row input[type=checkbox]{width:1.05rem;height:1.05rem;accent-color:var(--amber);cursor:pointer;transform:translateY(.15rem)}
.opt-row:hover .opt-label{color:var(--amber)}
.opt-label{color:var(--ink);font-weight:600;transition:color .15s}
.opt-note{display:block;font-weight:400;font-size:.82rem;color:var(--muted);margin-top:.15rem}
.opt-price{font-family:var(--font-display);font-size:1.15rem;color:var(--amber);white-space:nowrap}

/* Buy row — quantity + add to basket */
.pd-buy{margin-top:1.5rem}
.pd-buy-row{display:flex;gap:1rem;align-items:stretch;flex-wrap:wrap}
.qty-field{display:flex;align-items:center;gap:.6rem;border:1px solid var(--line);padding:0 .9rem}
.qty-label{font-family:var(--font-stencil);font-size:.7rem;letter-spacing:.2em;text-transform:uppercase;color:var(--muted)}
.qty-field input{width:3.4rem;background:transparent;border:0;color:var(--ink);font-family:var(--font-display);font-size:1.1rem;padding:.7rem 0;text-align:center}
.qty-field input:focus{outline:2px solid var(--amber);outline-offset:2px}
.pd-buy-row .btn-pink{flex:1;min-width:14rem;border:0;cursor:pointer;font:inherit}
.pd-ask{display:inline-block;margin-top:1rem}
.pd-install-note{margin:1.2rem 0 0;font-size:.9rem;color:var(--muted);max-width:44ch}
.pd-install-note a{color:var(--gold)}

/* Specs — the per-product detail table. Added 2026-09-07 as the fix for
   near-identical kit pages: a <dl> of stated facts is what separates two kits
   that differ only by chassis, both for a reader scanning and for Google.
   Borrows .pd-meta's stencil-label/bold-value rhythm so it reads as the same
   family of information, but sits full-width below the description rather than
   in the buy column. */
.pd-specs{border-top:1px solid var(--line)}
.pd-specs-head{font-family:var(--font-stencil);font-size:.78rem;letter-spacing:.22em;text-transform:uppercase;color:var(--muted);margin:1.4rem 0 .2rem}
.pd-specs dl{margin:0}
/* Grid, not flex: the labels line up in a column so the values read as one
   list. minmax stops a long label ("Bag configuration") from squeezing the
   value into a two-character column. */
.pd-spec-row{display:grid;grid-template-columns:minmax(8rem,14rem) 1fr;gap:1rem;padding:.8rem 0;border-bottom:1px solid var(--line);font-size:.95rem}
.pd-spec-row dt{font-family:var(--font-stencil);font-size:.72rem;letter-spacing:.2em;text-transform:uppercase;color:var(--muted);padding-top:.15rem}
.pd-spec-row dd{margin:0;color:var(--ink);font-weight:600}
/* Phone: stack. Two columns at 360px gives the value about 12 characters. */
@media (max-width:560px){
  .pd-spec-row{grid-template-columns:1fr;gap:.25rem}
}

/* ---- Basket ---- */
.cart-grid{display:grid;grid-template-columns:1fr 22rem;gap:3rem;align-items:start}
.cart-line{display:grid;grid-template-columns:7rem 1fr auto;gap:1.4rem;padding:1.6rem 0;border-bottom:1px solid var(--line)}
.cart-line:first-child{border-top:1px solid var(--line)}
.cl-thumb img{width:7rem;height:5.25rem;object-fit:cover;display:block}
.cl-body h2{font-size:1.15rem;margin:0 0 .5rem}
.cl-body h2 a{color:var(--ink);text-decoration:none}
.cl-body h2 a:hover{color:var(--amber)}
.cl-options{list-style:none;padding:0;margin:.4rem 0 0;font-size:.88rem}
.cl-options li{display:flex;justify-content:space-between;gap:1rem;padding:.2rem 0;color:var(--muted);max-width:26rem}
.cl-options .opt-price{font-size:.95rem}
.cl-lead{margin:.6rem 0 0;font-size:.82rem;color:var(--muted)}
/* Qty / Update / Remove sit on one line. The two controls live in SEPARATE
   forms (a PATCH and a DELETE), so they are independent flex items whose
   natural heights differ — the qty form is as tall as its input, the remove
   form only as tall as its text. align-items:center alone left Update and
   Remove a couple of pixels apart. Giving every control the same box height
   makes them share a centre line regardless of which form they belong to. */
.cl-controls{display:flex;align-items:center;gap:1.4rem;margin-top:.9rem;flex-wrap:wrap}
.cl-controls form{display:flex;align-items:center;gap:.6rem;margin:0}
.cl-qty{display:flex;align-items:center;gap:.6rem}
.cl-qty label{display:flex;align-items:center;gap:.5rem}
.cl-qty input{width:3.2rem;background:transparent;border:1px solid var(--line);color:var(--ink);padding:.4rem;text-align:center;font-family:var(--font-display);line-height:1.2}
/* min-height matches the qty input's box (padding + border + line-height) so
   the text links centre against it, and clears the WCAG 2.5.8 24px target. */
.link-btn{display:inline-flex;align-items:center;min-height:2.15rem;background:none;border:0;padding:0;color:var(--muted);font-size:.82rem;letter-spacing:.06em;text-transform:uppercase;cursor:pointer;text-decoration:underline;font-family:var(--font-stencil)}
.link-btn:hover{color:var(--amber)}
.link-btn.danger:hover{color:#e0533f}
.cl-price{text-align:right;white-space:nowrap}
.cl-total{font-family:var(--font-display);font-size:1.35rem;color:var(--ink)}
.cl-each{font-size:.78rem;color:var(--muted);margin-top:.2rem}

.cart-summary{border:1px solid var(--line);padding:1.6rem;position:sticky;top:1.5rem}
.cart-summary h2{font-family:var(--font-stencil);font-size:.78rem;letter-spacing:.22em;text-transform:uppercase;color:var(--muted);margin:0 0 1.2rem}
.cs-row{display:flex;justify-content:space-between;align-items:baseline;padding-bottom:1rem;border-bottom:1px solid var(--line)}
.cs-amount{font-family:var(--font-display);font-size:1.6rem;color:var(--ink)}
.cs-note{font-size:.82rem;color:var(--muted);margin:1rem 0 1.4rem;line-height:1.55}
.policy-gate{display:grid;grid-template-columns:auto 1fr;gap:.7rem;align-items:start;font-size:.84rem;color:var(--muted);line-height:1.55;cursor:pointer}
.policy-gate input{width:1.05rem;height:1.05rem;accent-color:var(--amber);cursor:pointer;margin-top:.15rem}
.policy-gate a{color:var(--gold)}
.cs-checkout{width:100%;margin-top:1.3rem;border:0;cursor:pointer;font:inherit}
/* The gate made visible: a disabled button must LOOK unavailable, or people
   click it repeatedly and conclude the site is broken. */
.cs-checkout:disabled{opacity:.4;cursor:not-allowed}
.cs-continue{display:block;margin-top:1rem;text-align:center;font-size:.82rem;color:var(--muted)}
.field-error{color:#e0533f;font-size:.82rem;margin:.6rem 0 0}

@media(max-width:900px){
  .cart-grid{grid-template-columns:1fr;gap:2rem}
  .cart-summary{position:static}
}
@media(max-width:560px){
  .cart-line{grid-template-columns:4.5rem 1fr;gap:1rem}
  .cl-thumb img{width:4.5rem;height:3.4rem}
  .cl-price{grid-column:2;text-align:left;margin-top:.5rem}
}

.empty-note{padding:4rem 2rem;text-align:center;color:var(--muted);border:1px dashed var(--line);font-family:var(--font-stencil);letter-spacing:.15em;text-transform:uppercase;font-size:.85rem}

/* Footer */
/* Footer — inverted: gold background, dark text */
footer{padding:4rem 2rem 2rem;background:var(--gold);border-top:1px solid rgba(0,0,0,.22);color:#17110A}
footer .ft-logo p{color:rgba(23,17,10,.75) !important}
.ft-grid{max-width:var(--maxw);margin:0 auto;display:grid;grid-template-columns:1.4fr 1fr 1fr 1fr;gap:3rem;padding-bottom:3rem;border-bottom:1px solid rgba(0,0,0,.2)}
/* h2, not h4. These are top-level sections of the footer landmark, and on any
   page whose content stops at h1 or h2 an <h4> here was a skipped heading level
   — a screen-reader user navigating by heading hears a gap and cannot tell
   whether they missed something. Styling is unchanged; only the level moved. */
.ft-grid h2{font-family:var(--font-stencil);font-weight:700;font-size:.82rem;letter-spacing:.25em;color:#17110A;text-transform:uppercase;margin-bottom:1.1rem}
.ft-grid ul{list-style:none;padding:0;margin:0}
.ft-grid li{padding:.4rem 0;font-size:.95rem;color:rgba(23,17,10,.82)}
/* 2.5.8: these were 18px-tall targets. The link becomes the full-height box
   rather than just the glyph run — same look, tappable size. */
.ft-grid li a{display:inline-flex;align-items:center;min-height:24px;color:rgba(23,17,10,.82);transition:color .15s}
.ft-grid li a:hover{color:var(--red-text)}
/* <address> renders italic by default in every browser — reset it, this is a
   postal address and not an aside. */
.ft-address{font-style:normal;font-size:.95rem;line-height:1.55;color:rgba(23,17,10,.82);margin:0 0 .5rem}
/* Directions button. Dark plate on the gold footer with the same notched
   corner as .btn-pink, so it reads as the same family of control. Red on
   hover — the footer's only other accent. */
.btn-map{display:inline-flex;align-items:center;gap:.45rem;margin-top:.9rem;font-family:var(--font-body);font-weight:800;font-size:.72rem;letter-spacing:.16em;text-transform:uppercase;background:#17110A;color:var(--gold);padding:.62rem 1rem;
  clip-path:polygon(0 0,calc(100% - 8px) 0,100% 8px,100% 100%,8px 100%,0 calc(100% - 8px));
  transition:background .15s,color .15s,transform .15s}
.btn-map:hover{background:var(--red);color:#fff;transform:translateY(-1px)}
.btn-map:focus-visible{outline:2px solid #17110A;outline-offset:3px}
.ft-bottom{max-width:var(--maxw);margin:0 auto;padding-top:1.5rem;display:flex;justify-content:space-between;align-items:center;flex-wrap:wrap;gap:1rem}
.ft-bottom p{font-family:var(--font-stencil);font-size:.78rem;letter-spacing:.18em;color:rgba(23,17,10,.72);text-transform:uppercase;margin:0}
.ft-bottom a{display:inline-flex;align-items:center;min-height:24px;font-family:var(--font-stencil);font-size:.78rem;letter-spacing:.18em;color:#17110A;text-transform:uppercase}
.ft-bottom a:hover{color:var(--red-text)}

/* Footer legal row */
.ft-legal{max-width:var(--maxw);margin:0 auto;padding-top:1.5rem;display:flex;flex-wrap:wrap;gap:1.2rem}
.ft-legal a{display:inline-flex;align-items:center;min-height:24px;font-family:var(--font-stencil);font-size:.72rem;letter-spacing:.16em;text-transform:uppercase;color:rgba(23,17,10,.72)}
.ft-legal a:hover{color:var(--red-text)}

/* Cookie consent bar (server-side) */
.cookie-bar{position:fixed;left:1rem;right:1rem;bottom:1rem;z-index:9998;max-width:var(--maxw);margin:0 auto;
  display:flex;align-items:center;justify-content:space-between;gap:1.5rem;flex-wrap:wrap;
  background:var(--panel-2);border:1px solid var(--line);border-left:3px solid var(--gold);
  padding:1rem 1.3rem;box-shadow:0 10px 40px rgba(0,0,0,.5)}
.cookie-bar p{margin:0;color:var(--muted);font-size:.9rem}
.cookie-bar a{color:var(--gold);text-decoration:underline}
.cookie-bar form{margin:0}

/* Focus visibility */
a:focus-visible, button:focus-visible, input:focus-visible, select:focus-visible,
textarea:focus-visible, summary:focus-visible, [tabindex]:focus-visible{outline:2px solid var(--gold);outline-offset:3px}

/* Responsive */
@media (max-width: 1100px){
  /* 6 nav items + logo + CTA get tight before the 720px hamburger kicks in */
  .nav-inner{gap:1.2rem;padding:1.1rem 1.5rem}
  .nav-menu{gap:1.15rem}
  .nav-menu a{font-size:.76rem;letter-spacing:.12em}
  /* Never let the CTA break across lines — it blows the nav up to two rows long
     before it actually runs out of room. (The wordmark is an image now, so it
     shrinks by height instead of wrapping.) */
  .nav .btn-pink{white-space:nowrap}
  .brand-logo{height:48px}
  footer .brand-logo{height:62px}
  .builds-grid{grid-template-columns:repeat(2,1fr)}
  .services-grid{grid-template-columns:repeat(2,1fr)}
  .brands-grid{grid-template-columns:repeat(2,1fr)}
  .quotes-grid{grid-template-columns:1fr}
  .process-grid{grid-template-columns:repeat(2,1fr)}
  .ft-grid{grid-template-columns:1fr 1fr}
  .story-grid{grid-template-columns:1fr;gap:3rem}
  .cta-inner{grid-template-columns:1fr;gap:3rem}
  /* No .stats-grid rule here any more. It used to set grid-template-columns, which
     is inert now the row is flex, and .stat's gutter is a clamp() that already
     narrows with the viewport — an override here would only fight it. */
  .shop-grid{grid-template-columns:repeat(2,1fr)}
  .pd-grid{grid-template-columns:1fr;gap:2.5rem}
}
/* Last stop before the hamburger. At 768 the row (logo 138 + menu 375 + CTA)
   exactly fills the width, so both the CTA and the wordmark wrapped to two
   lines. Shrink the button's padding and tracking, and tighten the menu with
   it — the button alone can't recover enough width. */
@media (max-width: 900px){
  .nav-inner{gap:.9rem;padding:1rem 1.25rem}
  .brand-logo{height:43px}
  .nav-menu{gap:.9rem}
  .nav-menu a{font-size:.7rem;letter-spacing:.07em}
  .nav .btn-pink{padding:.6rem .85rem;font-size:.66rem;letter-spacing:.1em;gap:.35rem}
}
@media (max-width: 720px){
  /* Phone sizes are deliberately NOT on the desktop mark's scale — the nav here is
     width-constrained by the hamburger + CTA, not height-constrained, so growing the
     mark buys nothing and costs the CTA its room. Held at the pre-2026-08-08 sizes. */
  .brand-logo{height:40px}
  footer .brand-logo{height:46px}
  .nav-menu{display:none}
  .nav-menu.open{display:flex;position:absolute;top:100%;left:0;right:0;background:var(--gold);flex-direction:column;padding:1.5rem 2rem;border-bottom:1px solid rgba(0,0,0,.22);gap:1.2rem}
  .nav-toggle{display:block}
  .hero{padding:4rem 1.5rem 3rem;min-height:80vh}
  .container-x{padding:0 1.5rem}
  footer{padding:3rem 1.5rem 1.5rem}
  .cta{padding:6rem 1.5rem 3rem}
  .shop-grid{grid-template-columns:1fr}
  /* These three kept the 1100px two-column rule all the way down, leaving ~144px
     columns. Long unbreakable words at .18em tracking ("FREIGHTLINER") then blew
     past the container and were the source of the sideways scroll. */
  .services-grid{grid-template-columns:1fr}
  .brands-grid{grid-template-columns:1fr}
  .process-grid{grid-template-columns:1fr}
  /* Footer holds two columns down to ~360px, where a nav label no longer fits
     the ~128px track and gets clipped. */
  .ft-grid{gap:2.2rem}
  /* The stamp is hung outside its box by design; on a phone that overhang is
     what pokes past the viewport edge. */
  .story-img .stamp{right:0;bottom:-1rem}
}

@media (max-width: 420px){
  /* Below ~34px the outlined "Custom Rigs" script fills in and goes muddy, so this
     is the floor — the CTA gives up padding before the mark gives up height. */
  .brand-logo{height:34px}
  /* The stats row wraps around here. A stat below its neighbour is no longer
     beside it, so the divider would hang off the left of its own line. */
  .stat{padding:0}
  .stat + .stat{border-left:0}
  .ft-grid{grid-template-columns:1fr;gap:1.8rem}
  /* .25em tracking pushes the longest tag past a 320px card. */
  .build-card .tag{letter-spacing:.16em}
}

/* Grid/flex children default to min-width:auto, so an over-wide child stretches
   the track instead of shrinking. Belt-and-braces against future overflow. */
.builds-grid>*,.services-grid>*,.brands-grid>*,.process-grid>*,
.shop-grid>*,.quotes-grid>*,.stats-grid>*,.ft-grid>*{min-width:0}

/* --- Basket: fulfilment choice + shipping breakdown -------------------------
   Shipping became a real charge on 2026-08-12 (UPS flat rates), so the basket
   now asks how the customer wants it BEFORE they reach Stripe. Radios are
   full-width hit areas: WCAG 2.5.8 wants 24x24 minimum and a label people can
   hit with a thumb. */
/* margin-top clears the Total row above: a fieldset legend sits ON the border,
   so with no gap it collided with the total and read as part of it. */
.fulfil{border:1px solid var(--line);border-radius:10px;padding:.9rem 1rem;margin:1.4rem 0 1rem;background:rgba(255,255,255,.02)}
.fulfil legend{font-family:var(--font-stencil);font-size:.7rem;letter-spacing:.16em;text-transform:uppercase;color:var(--muted);padding:0 .35rem}
.fulfil-opt{display:flex;gap:.7rem;align-items:flex-start;padding:.55rem .25rem;cursor:pointer;min-height:44px}
.fulfil-opt+.fulfil-opt{border-top:1px solid var(--line)}
.fulfil-opt input{margin-top:.25rem;width:18px;height:18px;flex:0 0 auto;accent-color:var(--red)}
.fulfil-opt strong{display:block;color:var(--ink);font-size:.95rem}
.fulfil-opt em{display:block;font-style:normal;color:var(--muted);font-size:.85rem;margin-top:.1rem}
/* A summary row that is switched off must actually disappear.

   `.cs-row{display:flex}` beats the `hidden` ATTRIBUTE on specificity, so
   `row.hidden = true` did nothing and the row stayed on screen. That was a
   live bug before the tax row existed: choosing pickup was supposed to drop
   the shipping line and never did. The tax row made it visible — it showed
   $272.25 next to an untaxed total — because two rows now depend on it. */
.cs-row[hidden]{display:none}

/* Where the order is going. Sales tax is decided by this site (2026-08-22),
   so the destination has to be asked for before payment; this sits under the
   fulfilment radios and matches their card. */
.fulfil-state{margin:0 0 1rem;padding:.9rem 1rem;border:1px solid var(--line);border-radius:10px;background:rgba(255,255,255,.02)}
.fulfil-state[hidden]{display:none}
.fulfil-state label{display:block;font-family:var(--font-stencil);font-size:.7rem;letter-spacing:.16em;text-transform:uppercase;color:var(--muted);margin-bottom:.5rem}
/* Full width and 44px tall — a thumb target, same rule as the radios. The
   native arrow is replaced because the control otherwise renders as a tiny
   grey system widget against a dark panel and reads as broken. */
.fulfil-state select{width:100%;min-height:44px;padding:.55rem 2.2rem .55rem .7rem;background-color:rgba(0,0,0,.25);color:var(--ink);border:1px solid var(--line);border-radius:8px;font:inherit;font-size:.95rem;appearance:none;background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23c9c9c9' stroke-width='3' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='m6 9 6 6 6-6'/%3E%3C/svg%3E");background-repeat:no-repeat;background-position:right .8rem center;background-size:12px}
.fulfil-state select:focus-visible{outline:2px solid var(--gold);outline-offset:2px}
.fulfil-state option{background:#111;color:#fff}
.fulfil-state small{display:block;font-size:.78rem;color:var(--muted);margin-top:.5rem;line-height:1.5}
.fulfil-state .field-error{color:var(--red);margin-top:.4rem}

.cs-row-sub{font-size:.85rem;color:var(--muted);margin-top:-.35rem}
.cs-row-total{border-top:1px solid var(--line);margin-top:.6rem;padding-top:.6rem;font-weight:700}
.cs-row-total .cs-amount{color:var(--gold);font-size:1.15rem}
/* Composition note under a summary row (e.g. "$500 + $400 extras handling").
   Deliberately has NO amount in the money column: it explains how the figure
   beside it was reached, and an amount here read as a separate charge. */
.cs-breakdown{display:block;font-size:.75rem;color:var(--muted);margin-top:.15rem;letter-spacing:.02em}
/* The amount column uses the big display face because it holds money. When it
   holds a short phrase instead ("Calculated at checkout") that face is far too
   loud and wraps badly — this drops it back to body type without moving it out
   of the column. */
.cs-amount-note{font-family:var(--font-body);font-size:.82rem;font-weight:500;color:var(--muted);text-align:right;line-height:1.35;max-width:9rem}

/* Consent gate on the contact and quote forms. Reuses .policy-gate's layout
   (the basket's) so the three gates on the site look and behave alike; this
   only adds the spacing the standalone forms need.

   No max-width. It carried 52ch, which at .84rem is ~364px — narrower than the
   form itself, so a 66-character sentence broke onto a second line with one
   word stranded on it. The sentence is short enough to want the form's full
   width and to wrap on its own when the screen is genuinely narrow. */
.consent-gate{margin:1.5rem 0 .25rem}

/* Screen-reader-only. The site had no such utility, so a <caption> added for
   assistive tech rendered as visible body copy in the middle of the invoice.
   Clip rather than display:none — display:none removes it from the
   accessibility tree too, which defeats the point of having written it. */
.sr-only{
  position:absolute;width:1px;height:1px;padding:0;margin:-1px;
  overflow:hidden;clip:rect(0 0 0 0);clip-path:inset(50%);white-space:nowrap;border:0;
}

/* --- Invoice / proforma page (public, token-reached) ---------------------
   Client, 2026-08-13: proformas from quotes and invoices for phone orders,
   payable by card. This is the page the emailed link lands on.

   Uses the site's own tokens rather than new colours: a customer arriving
   here from an email needs to recognise it as Arroyo before they type a card
   number into the next screen. */
.invoice-sheet{max-width:52rem;margin-inline:auto}

.invoice-alert{
  margin:0 0 1.5rem;padding:.85rem 1rem;
  border-left:4px solid var(--red);background:rgb(206 32 41 / .08);
}
.invoice-paid{
  margin:0 0 1.5rem;padding:.85rem 1rem;
  border-left:4px solid var(--gold);background:rgb(242 184 7 / .1);
}

.invoice-meta{
  display:grid;grid-template-columns:repeat(auto-fit,minmax(13rem,1fr));
  gap:1.5rem;margin:0 0 2rem;
}
.invoice-meta p{margin:.25rem 0 0;line-height:1.55}
.invoice-meta__label{
  font-size:.72rem;letter-spacing:.1em;text-transform:uppercase;opacity:.65;
}

.invoice-lines{width:100%;border-collapse:collapse;margin:0 0 1.75rem}
.invoice-lines th,.invoice-lines td{padding:.85rem 0;text-align:left;vertical-align:top}
.invoice-lines thead th{
  font-size:.72rem;letter-spacing:.09em;text-transform:uppercase;opacity:.65;
  border-bottom:2px solid currentColor;padding-bottom:.5rem;
}
.invoice-lines tbody td{border-bottom:1px solid rgb(255 255 255 / .12)}
.invoice-lines .num{text-align:right;white-space:nowrap;font-variant-numeric:tabular-nums}
/* Options are part OF the item above, not separate purchases — the rule and
   indent say so without needing a word of explanation. */
.invoice-option{
  display:block;margin-top:.3rem;padding-left:.8rem;
  border-left:2px solid var(--gold);opacity:.8;font-size:.94em;
}
/* The shop's free-text description of a custom job. A neutral rule rather than
   the gold one used for priced options: this is explanation, not another
   charge, and the two must not read as the same kind of line.
   pre-line keeps the line breaks whoever typed it intended. */
.invoice-details{
  display:block;margin-top:.4rem;padding-left:.8rem;
  border-left:2px solid rgba(255,255,255,.18);
  opacity:.75;font-size:.94em;line-height:1.5;white-space:pre-line;
}
.invoice-lines tfoot th{text-align:right;font-weight:400;opacity:.8}
.invoice-lines tfoot .invoice-total th,
.invoice-lines tfoot .invoice-total td{
  padding-top:1.1rem;font-size:1.15rem;font-weight:700;opacity:1;
}
.invoice-lines tfoot .invoice-total td{color:var(--gold)}

.invoice-pay{margin:0}
.invoice-pay__btn{font-size:1.05rem;padding:1rem 1.9rem}
.invoice-fineprint{margin:1rem 0 0;font-size:.92rem;line-height:1.6;opacity:.75;max-width:52ch}

/* Narrow screens: the qty column is the one that can go, since every line
   on these invoices is a single fabricated item far more often than not. */
@media (max-width:32rem){
  .invoice-lines thead th:nth-child(2),
  .invoice-lines tbody td:nth-child(2){display:none}
}

/* ── The bot-trap honeypot, site-wide ────────────────────────────────────────
   ⚠️ This rule used to live ONLY inside /quote's own page-level <style>, scoped
   as `.quote-form .hp`. Any other page reusing the same `.hp` convention — the
   customer quote page below was the first — rendered the trap FULLY VISIBLE,
   labelled "Leave this blank".

   That is worse than having no honeypot at all: a real customer fills the
   field in, and the controller then silently discards their message as a bot.
   They get a success redirect and never find out. Unscoped here so the
   convention actually travels with the markup.
   ───────────────────────────────────────────────────────────────────────── */
.hp {
  position: absolute !important;
  left: -9999px !important;
  width: 1px; height: 1px; overflow: hidden;
}

/* ── The customer's quote page ───────────────────────────────────────────────
   /quote/{token}. Everything about the document itself reuses the invoice
   page's classes (.invoice-sheet, .invoice-lines, .invoice-total,
   .invoice-fineprint) so a customer's quote and their invoice read as the same
   shop's paperwork. Only the conversation below is genuinely new.
   ───────────────────────────────────────────────────────────────────────── */
.invoice-sheet h2 { margin-top: 2.25rem; }
.quote-doc__cols { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 2rem; }
@media (max-width: 720px) { .quote-doc__cols { grid-template-columns: minmax(0, 1fr); gap: .5rem; } }

.quote-thread { list-style: none; padding: 0; margin: 0 0 2rem; display: flex; flex-direction: column; gap: 1rem; }
.quote-thread__item {
  padding: 1rem 1.15rem; background: var(--panel);
  border-left: 3px solid var(--line); max-width: 44rem;
}
/* The shop's replies carry the gold rule and sit right; the customer's sit left.
   Same convention as every message app, so nobody has to be told which is which. */
.quote-thread__item--shop { border-left-color: var(--gold); margin-left: auto; }
.quote-thread__meta { font-size: .8rem; color: var(--muted); margin: 0 0 .35rem; }
.quote-thread__meta strong { color: var(--ink); }
.quote-thread__body { margin: 0; white-space: pre-line; line-height: 1.6; }

.quote-reply { display: flex; flex-direction: column; gap: .6rem; margin: 0 0 2rem; max-width: 44rem; }
.quote-reply label { font-size: .85rem; letter-spacing: .04em; text-transform: uppercase; color: var(--muted); }
.quote-reply textarea {
  width: 100%; padding: .8rem .9rem; font: inherit; line-height: 1.5;
  background: var(--bg); color: var(--ink); border: 1px solid var(--line);
}
/* ⚠️ NEVER `outline: none` WITHOUT A REPLACEMENT. This rule did exactly that
   until 2026-08-27 — it deleted the focus ring and recoloured the border instead,
   which is a colour-only cue and invisible in forced-colours/high-contrast mode.
   WCAG 2.4.7, and the single most common way sites fail it.

   Caught by `_site-tools/audit.js`, not by any test and not by looking at the
   page — a keyboard user would simply have lost track of where they were on the
   two screens where a customer actually writes to the shop. Both the quote page
   and the message page share this class, so it was both of them.

   `:focus-visible` so the ring appears for keyboard focus without boxing the
   textarea every time somebody clicks into it. The border colour stays as a
   secondary cue, never the only one. */
.quote-reply textarea:focus-visible {
  outline: 2px solid var(--gold);
  outline-offset: 2px;
  border-color: var(--red);
}
.quote-reply button { align-self: flex-start; }

/* "Email me a copy" — ticked by default, sitting with the reply it belongs to.
   It was a second BUTTON below Send until 2026-08-27; two buttons competed and
   neither read as the obvious thing to press. */
.quote-copy {
  display: flex; align-items: flex-start; gap: .6rem;
  margin: .2rem 0 .4rem; cursor: pointer;
  /* 44px tap target — this gets used on a phone. */
  min-height: 44px; padding-top: .35rem;
}
.quote-copy input { width: 1.1rem; height: 1.1rem; margin-top: .15rem; accent-color: var(--red); flex: none; }
.quote-copy span { font-size: .9rem; line-height: 1.4; }
.quote-copy em { font-style: normal; color: var(--muted); }

/* The "without replying" escape hatch. A link, not a button: the checkbox above
   covers the common case, and this only serves a finished conversation. */
.quote-transcript { margin: 0 0 1.5rem; }
.linkish {
  background: none; border: 0; padding: 0; cursor: pointer;
  font: inherit; font-size: .85rem; color: var(--muted);
  text-decoration: underline; text-underline-offset: 3px;
}
.linkish:hover { color: var(--gold); }

/* ── Accepting a quote ──────────────────────────────────────────────────────
   Sits between the terms and the conversation. Deliberately the loudest thing
   on the page after the total: it is the one action the page exists to invite,
   and the previous version of this screen offered no way to take it at all —
   a customer had to type "yes" into the reply box and hope somebody read it. */
.quote-accept {
  display: flex; flex-wrap: wrap; align-items: center; justify-content: space-between;
  gap: 1.25rem; margin: 2rem 0;
  padding: 1.25rem 1.4rem;
  background: var(--panel); border: 1px solid var(--line); border-left: 3px solid var(--gold);
}
.quote-accept__head { margin: 0 0 .3rem; font-weight: 700; font-size: 1.05rem; }
.quote-accept__note { margin: 0; color: var(--muted); font-size: .875rem; max-width: 46ch; line-height: 1.55; }
.quote-accept form { margin: 0; }

/* Once accepted, the same slot states the fact rather than offering the action
   again. Green rather than the brand red: this is a settled, good outcome, and
   red on a confirmation reads as a warning. */
.quote-accepted {
  margin: 2rem 0; padding: 1.25rem 1.4rem;
  background: var(--panel); border: 1px solid var(--line); border-left: 3px solid #4a8f4a;
}
.quote-accepted__head { margin: 0 0 .3rem; font-weight: 700; color: #7fc07f; }
.quote-accepted__note { margin: 0; color: var(--muted); font-size: .875rem; max-width: 56ch; line-height: 1.55; }

@media (max-width: 620px) {
  .quote-accept { flex-direction: column; align-items: stretch; }
  .quote-accept .btn-pink { width: 100%; }
}
