    @font-face { font-family: "Archivo"; font-weight: 400; src: url("../fonts/Archivo-400.woff2") format("woff2"); font-display: swap; }
    @font-face { font-family: "Archivo"; font-weight: 700; src: url("../fonts/Archivo-700.woff2") format("woff2"); font-display: swap; }
    @font-face { font-family: "IBM Plex Mono"; font-weight: 400; src: url("../fonts/IBMPlexMono-400.woff2") format("woff2"); font-display: swap; }
    @font-face { font-family: "IBM Plex Mono"; font-weight: 700; src: url("../fonts/IBMPlexMono-700.woff2") format("woff2"); font-display: swap; }
    @font-face {
      font-family: "Bootzy";
      src: url("../fonts/BootzyTM.woff2") format("woff2"), url("../fonts/BootzyTM.woff") format("woff");
      font-display: swap;
    }

    :root {
      --paper: #e9e1d2;
      --paper-dark: #d4c9b6;
      --ink: #171512;
      --ink-soft: #5b554c;
      /* ACCENT — RULED #FF4013 (owner, 26 Jul 2026), baked. Red #cf3f27
         survives only as the ERROR colour (semantic, accent-independent). */
      --accent-choice: #FF4013;
      --red: var(--accent-choice);   /* legacy alias: every decorative accent usage */
      --error: #a83318;              /* semantic only: errors + required marks — darkened from #cf3f27 (3.66:1) to 5.1:1 on paper; RULED 26 Jul */
      --rule: rgba(23, 21, 18, 0.28);
      /* PROCESS COLOURS (L2f Addendum A) — the hero dot strip's four inks.
         Named tokens rather than literals scattered through the markup, per
         the Task 5 ruling. These are the sRGB renderings of the actual
         process inks, NOT the video primaries: #00FFFF / #FF00FF are screen
         cyan and magenta and read as neon next to a real yellow.
         K is the site's own --ink, deliberately: at 14px it is
         indistinguishable from #000 against this strip, and reusing the
         existing token keeps one black on the page instead of two.
         DECORATIVE ONLY — no brand meaning attaches to these (ruled). */
      --process-cyan: #00AEEF;      /* Process Cyan    */
      --process-magenta: #EC008C;   /* Process Magenta */
      --process-yellow: #FFF200;    /* Process Yellow  */
      --process-key: var(--ink);    /* Key — the site's ink, not a second black */
      /* Masthead height — ONE source of truth (L2p Task 8). Verified against
         the rendered box, not assumed: 78px at >=561px and 64px at <=560px,
         measured as both rect height and offsetHeight. `* { box-sizing:
         border-box }` means the 1px bottom border is INSIDE these numbers, so
         this is exactly the space the sticky bar occupies.
         Used by three things that must never disagree: the masthead's own
         min-height, the hero's viewport calculation, and the anchor scroll
         offset. The mobile override lives in the max-width: 560px block
         alongside the masthead's own, so the two cannot drift apart. */
      --masthead-h: 78px;
      --display: "Bootzy", "Arial Narrow", sans-serif;
      --display-tracking: 0.015em;  /* L2p Task 1 — one source of truth for Bootzy tracking. Started at 0.01em and tuned by eye to the ruled 0.015em ceiling: 0.01em left the E–E of SCREEN still touching at services-card size. See the shared rule at the end of the base styles. */
      --body: "Archivo", "Helvetica Neue", Arial, sans-serif;   /* RULED 26 Jul — OFL, self-hosted */
      --mono: "IBM Plex Mono", "Courier New", monospace;   /* RULED 26 Jul — OFL, self-hosted */
      --ledger-columns: minmax(0, .72fr) minmax(0, 1.28fr);
    }

/* hover affordances independent of accent strength */
.faq-question:hover .faq-label, .faq-question:focus-visible .faq-label { text-decoration: underline; text-underline-offset: 3px; }
.footer-start:hover, .footer-start:focus-visible { text-decoration: underline; text-underline-offset: 4px; }

    * { box-sizing: border-box; }
    html { scroll-behavior: smooth; }
    /* Anchor scroll offset (L2p Task 8).
       The masthead is sticky, so an anchor target scrolled to y=0 sits BEHIND
       it and its heading is hidden. scroll-margin-top reserves the bar's own
       height plus 8px of breathing room.

       The offset is derived from --masthead-h, so it tracks the bar at every
       breakpoint automatically and cannot fall out of step with it.

       Smooth scrolling above is PRE-EXISTING and deliberately untouched, as is
       the reduced-motion rule at the end of this file that switches it to
       auto. This task changes the offset only — it does not re-add either.

       EXACT TARGET LIST — every in-page href on the page resolves to one of
       these seven, enumerated from the document rather than assumed:
         #top        -> <main>      wordmark
         #proofs     -> <section>   nav "Work"
         #services   -> <section>   nav "Services"
         #live       -> <section>   nav "Live"
         #suppliers  -> <section>   nav "Garments"
         #faq        -> <section>   nav "FAQ"
         #quote      -> <section>   nav "Start a job" + footer "Start a job" */
    #top, #proofs, #services, #live, #suppliers, #faq, #quote {
      scroll-margin-top: calc(var(--masthead-h) + 8px);
    }
    body {
      margin: 0;
      background: var(--paper);
      color: var(--ink);
      font-family: var(--body);
      font-size: 16px;
      line-height: 1.5;
    }
    body::before {
      content: "";
      position: fixed;
      inset: 0;
      pointer-events: none;
      z-index: 20;
      opacity: 0.14;
      background:
        repeating-linear-gradient(0deg, transparent 0 3px, rgba(23,21,18,.035) 3px 4px),
        repeating-linear-gradient(90deg, transparent 0 5px, rgba(255,255,255,.14) 5px 6px);
      mix-blend-mode: multiply;
    }
    a { color: inherit; }
    img { display: block; width: 100%; }
    .shell { width: min(1440px, 100%); margin: 0 auto; border-inline: 1px solid var(--rule); }

    .masthead {
      --masthead-optical: 0px;   /* optical drop — neutral; tune by eye if needed */
      min-height: var(--masthead-h);
      display: grid;
      grid-template-columns: 1fr auto 1fr;
      align-items: center;
      gap: 24px;
      padding: 16px 28px;
      border-bottom: 1px solid var(--ink);
      position: sticky;
      top: 0;
      z-index: 10;
      background: color-mix(in srgb, var(--paper) 92%, transparent);
      backdrop-filter: blur(8px);
    }
    .masthead-meta, .masthead-nav {
      font: 700 10px/1.35 var(--mono);
      text-transform: uppercase;
      letter-spacing: .08em;
      transform: translateY(var(--masthead-optical));
    }
    .masthead-nav { display: flex; justify-content: flex-end; gap: 22px; align-items: center; }
    .masthead-nav a { text-decoration: none; border-bottom: 1px solid transparent; }
    .masthead-nav a:hover, .masthead-nav a:focus-visible { border-color: currentColor; }
    .wordmark {
      font-family: var(--display);
      font-size: clamp(25px, 3vw, 42px);
      line-height: 1;               /* was .8 — single nowrap line; .8 clipped the em box upward */
      transform: translateY(var(--masthead-optical));
      /* letter-spacing REMOVED (L2p Task 1): this was .025em. Tracking now
         comes solely from --display-tracking via the shared display rule.
         Do not reintroduce a per-selector value here. */
      text-transform: uppercase;
      text-decoration: none;
      white-space: nowrap;
    }
    .quote-link {
      background: var(--red);
      color: var(--ink);   /* was #fff8ec — 3.35:1 at 10px; ink-on-accent = 5.2:1 */
      padding: 9px 14px;   /* symmetric — was 10/8, part of the high-riding annotation */
      border-bottom: 0 !important;
    }

    .hero {
      display: grid;
      grid-template-columns: minmax(0, 1.05fr) minmax(390px, .95fr);
      min-height: calc(100svh - var(--masthead-h));
      border-bottom: 1px solid var(--ink);
    }
    .hero-copy {
      position: relative;
      padding: clamp(40px, 7vw, 112px) clamp(24px, 5vw, 76px) 48px;
      display: flex;
      flex-direction: column;
      justify-content: space-between;
      overflow: hidden;
    }
    .reg-mark {
      position: absolute;
      top: 24px;
      right: 24px;
      width: 28px;
      height: 28px;
      border: 1px solid var(--ink);
      border-radius: 50%;
    }
    .reg-mark::before, .reg-mark::after {
      content: "";
      position: absolute;
      background: var(--ink);
    }
    .reg-mark::before { width: 40px; height: 1px; left: -7px; top: 13px; }
    .reg-mark::after { height: 40px; width: 1px; top: -7px; left: 13px; }
    .section-reg { pointer-events: none; }
    .reg-mark.light { border-color: var(--paper); }
    .reg-mark.light::before, .reg-mark.light::after { background: var(--paper); }
    /* Registration-mark hover spin (L2p Task 7).
       The marks are CSS shapes — a bordered circle plus two pseudo-element
       rules — so there is no SVG here and no SVG guidance applies.

       The ENTIRE behaviour, pointer-events included, sits behind this query.
       Touch devices and reduced-motion users therefore keep the existing
       pointer-events: none exactly as before, which is both the accessible
       answer and the safe one: nothing new can intercept a tap.

       Placed AFTER `.section-reg { pointer-events: none }` on purpose. Both
       selectors are one class, so specificity ties and source order decides;
       a media query adds no specificity of its own. Moving this block above
       that rule would silently disable the hover target on all eight section
       marks while leaving the hero mark working — a confusing half-failure.

       Scoped to `.reg-mark` itself so ONLY the mark becomes interactive; no
       container, and nothing else on the page, gains pointer-events. */
    @media (hover: hover) and (pointer: fine) and (prefers-reduced-motion: no-preference) {
      .reg-mark {
        pointer-events: auto;
        transform-origin: center;
        transition: transform 0.6s ease;
      }
      .reg-mark:hover { transform: rotate(360deg); }
    }
    .kicker {
      font: 700 11px/1.4 var(--mono);
      letter-spacing: .12em;
      text-transform: uppercase;
      margin: 0 0 28px;
    }
    .hero-title {
      font-family: var(--display);
      font-size: clamp(94px, 13vw, 198px);
      font-weight: 400;
      line-height: .72;
      /* letter-spacing REMOVED (L2p Task 1): this was -.025em and defeated
         the shared display-tracking rule. Tracking now comes solely from
         --display-tracking. Do not reintroduce a per-selector value here. */
      text-transform: uppercase;
      margin: 0;
    }
    .hero-title .red { color: var(--red); }
    .hero-intro {
      display: grid;
      grid-template-columns: 70px 1fr;
      gap: 22px;
      max-width: 590px;
      margin-top: 72px;
      padding-top: 16px;
      border-top: 1px solid var(--ink);
    }
    .hero-intro .index { font: 700 12px var(--mono); color: var(--ink); }  /* was accent — 2.7:1 on paper, split ruled */
    .hero-intro p { margin: 0; font-size: clamp(18px, 2vw, 26px); line-height: 1.28; letter-spacing: -.025em; }

    .hero-visual { position: relative; min-height: 720px; background: var(--ink); overflow: hidden; }
    .hero-carousel, .hero-track, .hero-slide { height: 100%; min-height: inherit; }
    .hero-carousel { position: absolute; inset: 0; }
    .hero-track { display: flex; transition: transform .7s cubic-bezier(.22,.61,.36,1); will-change: transform; }
    /* Reduced motion (L2p Task 6). The initializer already withholds the
       auto-advance timer, but that alone left MANUAL next/previous still
       sliding for 0.7s — an animated transition the user asked not to see.
       Killing it here covers every path into showSlide, and unlike the JS
       check this one is live: it follows the setting without a reload. */
    @media (prefers-reduced-motion: reduce) {
      .hero-track { transition: none; }
    }
    .hero-slide { min-width: 100%; position: relative; margin: 0; }
    .hero-slide img { height: 100%; object-fit: cover; }   /* filter: see [data-carousel-track] img (Addendum G) */
    .hero-controls, .live-controls {
      position: absolute;
      z-index: 4;
      top: 24px;
      right: 24px;
      display: flex;
      align-items: center;
      background: var(--paper);
      border: 1px solid var(--ink);
    }
    .hero-control, .live-control {
      width: 44px;
      height: 42px;
      border: 0;
      border-right: 1px solid var(--ink);
      border-radius: 0;
      background: transparent;
      color: var(--ink);
      cursor: pointer;
      font: 700 16px var(--mono);
    }
    .hero-control:last-child, .live-control:last-child { border-right: 0; }
    .hero-control:hover, .hero-control:focus-visible, .live-control:hover, .live-control:focus-visible { background: var(--red); color: #fff8ec; outline: none; }
    /* color is EXPLICIT (rev 5, 20 Aug 2026): the strip's background is
       always var(--paper), but the count previously INHERITED its colour
       from the section — fine on the cream hero, invisible (cream on
       cream) inside the dark live and services sections. The buttons never
       had this bug because .hero-control/.live-control set color: var(--ink)
       explicitly; the count now does the same. */
    .hero-count, .live-count { min-width: 70px; padding: 0 12px; text-align: center; font: 700 10px var(--mono); letter-spacing: .08em; color: var(--ink); }
    /* CAROUSEL FRAME (L2f Addendum C) — the frosted inset frame, unified.
       Was `.hero-visual::after`, i.e. hero-only; L2p proved that was
       pre-existing rather than a defect, and Joel ruled it should apply to
       every instance. Keyed on the component root attribute so hero, services
       and the live instance all inherit it with no per-instance declaration.

       Hero is byte-identical across this change: [data-carousel] matches the
       same .hero-visual element, at the same specificity (0,1,0) and the same
       position in the cascade.

       ::after specifically, NOT ::before — the frame must paint OVER the
       slides. Both are absolutely positioned, so within the positioned layer
       they paint in DOM order; ::before would fall UNDER the track and vanish.
       This is why the live section's badge had to move to ::before (below):
       one element has only one ::after, and at equal specificity the later
       .live-image rule would have won, silently leaving live unframed. */
    [data-carousel]::after {
      content: "";
      position: absolute;
      inset: 0;
      box-shadow: inset 0 0 0 14px rgba(233,225,210,.18);
      pointer-events: none;
    }
    /* CAROUSEL VIEWPORT CLIPPING (L2f Task 4 corrective) — part of the same
       component contract as the frame above.

       The track is a flex row of full-width slides moved with translateX, so
       every slide other than the active one sits OUTSIDE the viewport box and
       must be clipped. Without this they paint straight across whatever sits
       beside the carousel.

       This was previously supplied by accident, not by contract: `.hero-visual`
       carries its own `overflow: hidden` and `.services-finish` likewise, so
       both instances clipped for reasons that had nothing to do with being
       carousels. The live root has no such rule and never needed one while it
       held a single static photograph — so slides 4-6 painted over the
       "LIVE PRINTING, DONE RIGHT." copy column. Clipping belongs on the
       viewport, where the translation happens, not on whatever root an
       instance happens to sit in. */
    [data-carousel-viewport] { overflow: hidden; }
    /* CAROUSEL PHOTO TREATMENT (L2f Addendum G) — declared ONCE for every
       instance, same principle as the frame and the viewport clipping above.

       The three instances had drifted to three different values: hero
       saturate(.86) contrast(1.05), services saturate(.82) contrast(1.04),
       live contrast(1.15). That read as intentional while live was greyscale;
       once live went to colour (Addendum F) it just looked like drift. Ruled
       to the HERO's value — the most prominent instance and the one the design
       was tuned against. Live's contrast(1.15) is dropped: it existed to give
       the greyscale treatment punch and is not needed in colour.

       NO per-instance filter declarations remain. This rule sits with the
       other component rules, EARLY in the cascade, deliberately: it and a
       per-instance rule like `.hero-slide img` are both (0,1,1), so a future
       instance needing a different treatment can override simply by declaring
       it later — a deliberate, documented override rather than a fourth
       independent rule that drifts. */
    [data-carousel-track] img { filter: saturate(.86) contrast(1.05); }
    .job-ticket {
      position: absolute;
      left: 24px;
      right: 24px;
      bottom: 24px;
      display: grid;
      grid-template-columns: 1.2fr 1fr 1fr;
      background: var(--paper);
      /* Explicit colour, NOT inherited (L2p Task 4). The ticket paints ink on
         its own cream panel, so it must not take the surrounding section's
         text colour. The hero happened to inherit --ink and looked fine; the
         services section sets `color: var(--paper)`, which rendered the
         values cream-on-cream, i.e. invisible. Setting it here makes the
         component work in ANY section — required now that it is reusable. */
      color: var(--ink);
      border: 1px solid var(--ink);
      z-index: 4;
    }
    .job-ticket > div { padding: 12px 14px; border-right: 1px solid var(--ink); }
    .job-ticket > div:last-child { border: 0; }
    .ticket-label { display: block; font: 700 9px var(--mono); text-transform: uppercase; letter-spacing: .1em; color: var(--ink-soft); margin-bottom: 4px; }
    .ticket-value { display: block; font: 700 12px var(--mono); text-transform: uppercase; }
    /* A pending cell is deliberately BLANK (unknown value — see updateTicket).
       An empty block generates no line box, so without this the ticket would
       shrink when every cell is pending. Scoped to the pending state only, so
       populated cells are byte-for-byte untouched. */
    .ticket-value[data-ticket-state="pending"] { min-height: 1.2em; }

    .ink-strip {
      display: grid;
      grid-template-columns: auto 1fr auto;
      align-items: center;
      gap: 28px;
      padding: 14px 28px;
      border-bottom: 1px solid var(--ink);
      background: var(--red);
      color: var(--ink)   /* was cream — small text on accent, 3.35:1 */;
      font: 700 10px var(--mono);
      text-transform: uppercase;
      letter-spacing: .1em;
    }
    .colour-chips { display: flex; justify-content: center; gap: 10px; }
    .colour-chips i { width: 14px; height: 14px; border: 1px solid currentColor; border-radius: 50%; background: var(--chip); }
    /* The --chip indirection is retained so the rule above is unchanged; these
       classes just supply it from a named token instead of an inline literal. */
    .chip-c { --chip: var(--process-cyan); }
    .chip-m { --chip: var(--process-magenta); }
    .chip-y { --chip: var(--process-yellow); }
    .chip-k { --chip: var(--process-key); }

    .studio-copy {
      display: grid;
      grid-template-columns: var(--ledger-columns);
      border-bottom: 1px solid var(--ink);
    }
    .studio-copy-heading {
      position: relative;
      padding: clamp(48px, 7vw, 96px) clamp(24px, 4vw, 58px);
      border-right: 1px solid var(--ink);
    }
    .studio-copy-heading h2 { margin: 0; font: 400 clamp(62px, 8vw, 118px)/.76 var(--display); text-transform: uppercase; }
    .studio-copy-body {
      padding: clamp(48px, 7vw, 96px) clamp(24px, 5vw, 76px);
      display: grid;
      gap: 28px;
    }
    .studio-copy-body p { margin: 0; max-width: 760px; font-size: clamp(18px, 2vw, 25px); line-height: 1.38; letter-spacing: -.02em; }
    .studio-copy-body .craft-line { color: var(--red); font: 400 clamp(42px, 6vw, 84px)/.82 var(--display); text-transform: uppercase; }

    /* L2p Task 2 — optical balance of the logo band, measured not guessed.
       The old 22/26 + 18 nominal values MISLEAD, because neither renders at
       its face value:
         - the 18px under the label rendered as a ~21px visible gutter, since
           the 10px mono cap ink ends ~3px above the heading box bottom;
         - the 26px bottom rendered as 27px, because border-bottom sits
           BELOW the padding.
       Net: 21px above the logo row vs 27px below it — the row sat 6px high,
       leaving the deeper gap underneath that read as bottom-heavy. Adding
       bottom padding would have made it worse.
       Fix: bring both to an even 24px rhythm (24 above the label ink /
       24 gutter / 24 below the logos). Band height is UNCHANGED at 118px
       desktop and 110px mobile, so page height and the baselines are
       unaffected. Verified at 1440 and 390. */
    .trusted {
      overflow: hidden;
      background: var(--ink);
      color: var(--paper);
      border-bottom: 1px solid var(--ink);
      padding: 22px 0 23px;   /* 23 + 1px border = 24 visible below the logos */
    }
    .trusted-heading { padding: 0 28px 21px; font: 700 10px var(--mono); letter-spacing: .15em; text-transform: uppercase; }   /* 21 + ~3px cap-to-box-bottom = 24 visible gutter */
    .logo-ticker { overflow: hidden; }
    /* SEAMLESS LOOP GEOMETRY (rev 6, 21 Aug 2026). The spacing is a
       margin-right on EVERY logo, deliberately NOT a flex gap on the track.
       With gap, the track is copy + gap + copy: translateX(-50%) then moves
       half a gap LESS than one full copy period, so the strip snapped
       sideways by gap/2 (29px desktop, 20px mobile) at every loop — the
       reported "glitch". With the margin carried by each img, each copy
       includes its own trailing space, the track is exactly two periods
       wide, and -50% lands identically on the seam. */
    .logo-track { display: flex; align-items: center; width: max-content; animation: logo-ticker 32s linear infinite; }
    .logo-track:hover { animation-play-state: paused; }
    .logo-track img { width: auto; height: 38px; max-width: 118px; object-fit: contain; opacity: .78; flex: 0 0 auto; margin-right: 58px; }
    .logo-track img:hover { opacity: 1; }
    @keyframes logo-ticker { to { transform: translateX(-50%); } }

    .proofs { display: grid; grid-template-columns: var(--ledger-columns); border-bottom: 1px solid var(--ink); }
    .section-intro { position: relative; padding: clamp(42px, 6vw, 88px) clamp(24px, 4vw, 58px); border-right: 1px solid var(--ink); }
    .section-number { display: block; color: var(--ink); font: 700 11px var(--mono); margin-bottom: 24px; }  /* split ruled */
    .section-title { margin: 0; font: 400 clamp(56px, 7vw, 106px)/.78 var(--display); text-transform: uppercase; }
    .section-intro p { max-width: 360px; margin: 38px 0 0; color: var(--ink-soft); }
    .proof-grid { display: grid; grid-template-columns: repeat(2, 1fr); }
    .proof { margin: 0; padding: 24px; border-left: 1px solid var(--rule); border-bottom: 1px solid var(--rule); }
    .proof:nth-child(odd) { border-left: 0; }
    .proof:nth-last-child(-n+2) { border-bottom: 0; }
    .proof-image { aspect-ratio: 4 / 5; overflow: hidden; background: var(--paper-dark); }
    .proof-image img { height: 100%; object-fit: cover; transition: transform .5s ease; }
    .proof:hover img { transform: scale(1.025); }
    .proof figcaption { display: grid; grid-template-columns: 48px 1fr; gap: 16px; padding-top: 14px; }
    .proof-no { font: 700 11px var(--mono); color: var(--ink); }  /* split ruled */
    .proof-name { margin: 0 0 5px; font: 400 26px/.9 var(--display); text-transform: uppercase; }
    .proof-spec { margin: 0; font: 700 9px/1.6 var(--mono); text-transform: uppercase; letter-spacing: .05em; color: var(--ink-soft); }

    /* ── PRINT LIGHTBOX (L2b Task 5) ──────────────────────────────────────
       The "View print" button is a real control with a visible affordance —
       the brief rules out click handling on a bare figure. It is created by
       the script, so it only ever exists where it can actually work. */
    .proof-view {
      grid-column: 2;
      justify-self: start;
      margin-top: 12px;
      padding: 8px 14px;
      border: 1px solid var(--rule);
      background: transparent;
      color: var(--ink);
      font: 700 9px/1 var(--mono);
      letter-spacing: .08em;
      text-transform: uppercase;
      cursor: pointer;
      transition: background .15s, color .15s, border-color .15s;
    }
    .proof-view:hover, .proof-view:focus-visible { border-color: var(--ink); background: var(--ink); color: var(--paper); outline: none; }
    .proof-view:focus-visible { outline: 2px solid var(--ink); outline-offset: 2px; }

    /* DESKTOP SIZING — corrected after Joel's Safari hand-check, 6 Aug 2026.
       The dialog was capped at 1080px and the panel split it 1.1fr/1fr, so the
       photograph got ~521px against a 412px grid tile: 1.26x, and IDENTICAL at
       1920x1080 as at 1440x900, because the cap never moved. The lightbox
       added no magnification on exactly the screens with room to spare.
       Measured, not guessed — max-height was never the constraint (852px
       available against a 448px dialog) and the asset was not either (521px
       shown of 1000px native).

       --lightbox-chrome is the vertical space the dialog spends on anything
       that is not the photograph: 48px of viewport inset (the max-height
       below), 56px of panel padding, and ~40px of controls-plus-margin. The
       image's max-height is derived from it, so height finally does work
       instead of sitting idle. */
    .print-lightbox { --lightbox-chrome: 160px; width: min(1600px, calc(100vw - 48px)); max-height: calc(100vh - 48px); padding: 0; border: 1px solid var(--ink); background: var(--paper); color: var(--ink); overflow: auto; }
    .print-lightbox::backdrop { background: rgba(23, 21, 18, .72); }
    /* STACKED BY DEFAULT — side-by-side is the exception, above 1150px only.
       Ruled 6 Aug 2026 after Joel's second Safari check: at in-between widths
       the two-column layout held on far too long, squeezing the photograph
       into a small box while the description ran as a very tall, narrow
       column.

       THRESHOLD DERIVED BY MEASUREMENT, NOT PICKED. The text column computes
       to (viewport - 132) / 3, so at a 15px body font the line length falls
       to 45 CHARACTERS — the lower bound of the comfortable reading measure —
       at a viewport of ~1145px. Measured: 43 characters at 1100px, 47 at
       1200px. 1150px is that crossing, rounded.

       The symptom Joel saw is the same fault further along: below ~1024px the
       description's natural height overtakes the image's outright — 490px of
       text against a 383px image at 900px wide, 876px against 213px at 561px.

       At the threshold the image column is still 679px, a strong enlargement;
       below it, stacking hands the photograph the FULL dialog width instead.
       The mobile full-screen takeover at <=560px is then simply the same
       stacked layout continuing, not a separate design. */
    .print-lightbox-panel { position: relative; display: grid; grid-template-columns: minmax(0, 1fr); gap: 28px; padding: 28px; }
    @media (min-width: 1150px) {
      /* Image takes the majority: 2fr/1fr, not the original 1.1fr/1fr. */
      .print-lightbox-panel { grid-template-columns: minmax(0, 2fr) minmax(0, 1fr); }
      /* Text block rides at the photo's midpoint (Joel's ruling, 20 Aug
         2026): a top-anchored title beside a tall 4:5 photo left a void
         under short descriptions. The padding-bottom nudges the optical
         centre up to compensate for the controls row under the photo.
         Desktop-only — the mobile takeover stacks and is untouched. */
      .print-lightbox-text { align-self: center; padding-bottom: 40px; }
    }
    .print-lightbox-close {
      position: absolute; top: 14px; right: 14px; z-index: 2;
      padding: 8px 12px; border: 1px solid var(--rule);
      background: var(--paper); color: var(--ink);
      font: 700 9px/1 var(--mono); letter-spacing: .08em; text-transform: uppercase; cursor: pointer;
    }
    .print-lightbox-close:hover, .print-lightbox-close:focus-visible { border-color: var(--ink); background: var(--ink); color: var(--paper); outline: none; }
    /* Mobile keeps `width: 100%` — the full-screen takeover is correct and is
       deliberately untouched. Desktop sizing is scoped to min-width: 561px,
       the complement of the mobile block's max-width: 560px. */
    .print-lightbox-image img { display: block; width: 100%; height: auto; background: var(--paper-dark); }
    @media (min-width: 561px) {
      /* Sized against the VIEWPORT, not the column. width:auto with paired
         max-width/max-height lets the photograph grow until it hits whichever
         edge binds first — the column for a landscape, the viewport height for
         a portrait. object-fit: contain is belt-and-braces: with both
         dimensions auto the ratio is already preserved, but it guarantees no
         distortion if a future rule pins one side.
         --photo-w / --photo-h are published per photo by js/main.js
         (showPhoto). CSS cannot read an image's intrinsic size, so the native
         ceiling arrives that way. The min() keeps every limit live at once:
         the column, the viewport height, and the file's own pixels — whichever
         is smallest wins, so THE PHOTOGRAPH IS NEVER UPSCALED. The fallbacks
         make the rule safe if a photo ever arrives without dimensions. */
      .print-lightbox-image img {
        width: auto;
        max-width: min(100%, var(--photo-w, 100%));
        max-height: min(calc(100vh - var(--lightbox-chrome)), var(--photo-h, 100vh));
        object-fit: contain;
        margin-inline: auto;
      }
      /* Readable measure when STACKED. One rule covers both layouts and needs
         no second breakpoint: side-by-side the text column is 355-435px, well
         under this cap, so the rule is inert there. Stacked it would otherwise
         run the full dialog width — about 139 characters at 1149px, as
         unreadable in the other direction as the narrow column it replaced. */
      .print-lightbox-body { max-width: 60ch; }
    }
    .print-lightbox-controls { display: flex; align-items: center; justify-content: space-between; gap: 12px; margin-top: 12px; }
    /* [hidden] must beat the flex display above — a single-photo print has
       these REMOVED, not merely dimmed, so they never take a tab stop. */
    .print-lightbox-controls[hidden] { display: none; }
    .print-lightbox-nav {
      padding: 8px 12px; border: 1px solid var(--rule); background: transparent; color: var(--ink);
      font: 700 9px/1 var(--mono); letter-spacing: .08em; text-transform: uppercase; cursor: pointer;
    }
    .print-lightbox-nav:hover, .print-lightbox-nav:focus-visible { border-color: var(--ink); background: var(--ink); color: var(--paper); outline: none; }
    .print-lightbox-count { margin: 0; font: 700 9px/1 var(--mono); letter-spacing: .08em; text-transform: uppercase; color: var(--ink-soft); }
    /* padding-right clears the absolutely-positioned close button — without
       it a long print title runs underneath the control. */
    .print-lightbox-title { margin: 0 0 14px; padding-right: 84px; font: 400 34px/.95 var(--display); letter-spacing: var(--display-tracking); text-transform: uppercase; }
    .print-lightbox-body { margin: 0; font-size: 15px; line-height: 1.65; color: var(--ink); }

    /* Scroll lock while the dialog is open. Native modal dialogs do not
       reliably stop the page behind them scrolling, so this is set
       explicitly and removed on close. */
    html.lightbox-open, html.lightbox-open body { overflow: hidden; }

    /* Open animation ONLY where motion is welcome. The global reduce block
       also neutralises transitions, so this is belt-and-braces — and it
       states the intent where someone would look for it. */
    @media (prefers-reduced-motion: no-preference) {
      .print-lightbox[open] { animation: lightbox-in .18s ease-out; }
      @keyframes lightbox-in { from { opacity: 0; transform: translateY(6px); } to { opacity: 1; transform: none; } }
    }

    .services {
      background: var(--ink);
      color: var(--paper);
      border-bottom: 1px solid var(--ink);
    }
    .services-header {
      display: grid;
      grid-template-columns: var(--ledger-columns);
      border-bottom: 1px solid rgba(233,225,210,.32);
    }
    .services-heading {
      position: relative;
      padding: clamp(48px, 7vw, 96px) clamp(24px, 5vw, 76px);
      border-right: 1px solid rgba(233,225,210,.32);
    }
    .services-eyebrow {
      display: block;
      margin-bottom: 26px;
      color: var(--red);
      font: 700 10px var(--mono);
      letter-spacing: .14em;
      text-transform: uppercase;
    }
    .services-heading h2 {
      margin: 0;
      max-width: none;
      font: 400 clamp(70px, 7.5vw, 112px)/.74 var(--display);
      text-transform: uppercase;
    }
    .services-deck {
      margin-top: clamp(42px, 6vw, 76px);
      padding-top: 18px;
      border-top: 1px solid rgba(233,225,210,.32);
    }
    .services-deck p {
      max-width: 390px;
      margin: 0;
      color: #c7beaf;
      font-size: clamp(17px, 1.6vw, 22px);
      line-height: 1.35;
      letter-spacing: -.02em;
    }
    .services-finish {
      position: relative;
      min-height: clamp(440px, 42vw, 610px);
      margin: 0;
      overflow: hidden;
      background: #26231f;
    }
    /* The track/slide wrappers were added at L2p Task 6, when this image became
       the second initCarousel instance. They reproduce the geometry the bare
       <img> had before — the track fills the figure and the single slide fills
       the track — so the existing `.services-finish img` rule below still
       applies unchanged and the rendering is untouched. */
    .services-viewport { position: absolute; inset: 0; overflow: hidden; }
    .services-track { position: absolute; inset: 0; display: flex; transition: transform .6s cubic-bezier(.4, 0, .2, 1); }
    @media (prefers-reduced-motion: reduce) { .services-track { transition: none; } }
    /* Controls reuse the live-carousel control styling; pinned bottom-right
       of the photo, clear of the job ticket. */
    /* Controls take the base .live-controls position (top-right) — the same
       placement as the hero and live instances, and clear of the job ticket,
       which owns the bottom edge on mobile. */
    .services-slide { position: relative; min-width: 100%; height: 100%; }
    .services-finish img {
      position: absolute;
      inset: 0;
      height: 100%;
      object-fit: cover;
      object-position: 50% 48%;
    }
    /* The .services-finish figcaption rules were REMOVED at L2p Task 4: the
       static "07 / Finish" caption is replaced by the shared .job-ticket
       component now that Joel has ratified the values for this image. The
       ticket is styled solely by the .job-ticket block above — one
       implementation, no per-section variant. */
    .services-list {
      display: grid;
      grid-template-columns: minmax(0, .72fr) repeat(2, minmax(0, .64fr));
    }
    .service {
      display: flex;
      flex-direction: column;
      align-items: flex-start;
      gap: 10px;
      min-height: 150px;
      padding: 30px clamp(24px, 4vw, 58px);
      border-right: 1px solid rgba(233,225,210,.22);
      border-bottom: 1px solid rgba(233,225,210,.22);
    }
    .service:nth-child(3n) { border-right: 0; }
    .service:nth-last-child(-n+3) { border-bottom: 0; }
    .service-no { color: var(--red); font: 700 10px var(--mono); }
    .service h3 { margin: 0; font: 400 clamp(31px, 3vw, 48px)/.9 var(--display); text-transform: uppercase; max-width: 100%; overflow-wrap: break-word; }
    .service p { margin: 0; color: #a9a092; font-size: 13px; line-height: 1.5; max-width: 440px; }

    .faq {
      display: grid;
      grid-template-columns: var(--ledger-columns);
      border-bottom: 1px solid var(--ink);
      background: var(--paper);
    }
    .faq-intro {
      position: relative;
      padding: clamp(48px, 7vw, 96px) clamp(24px, 4vw, 58px);
      border-right: 1px solid var(--ink);
      background: var(--paper);
    }
    .faq-intro-inner { position: sticky; top: 112px; }
    .faq-intro h2 {
      margin: 0;
      font: 400 clamp(62px, 8vw, 118px)/.76 var(--display);
      text-transform: uppercase;
    }
    .faq-intro p { max-width: 350px; margin: 30px 0 0; color: var(--ink-soft); }
    .faq-list { padding: clamp(36px, 5vw, 70px) clamp(24px, 5vw, 76px); }
    .faq-group-label { font: 700 10px var(--mono); text-transform: uppercase; letter-spacing: .14em; color: var(--ink-soft); padding: 34px 0 12px; }
    .faq-group-label:first-child { padding-top: 0; }
    .faq-item { border-top: 1px solid var(--ink); }
    .faq-item:last-child { border-bottom: 1px solid var(--ink); }
    .faq-question {
      width: 100%;
      padding: 22px 0;
      display: grid;
      grid-template-columns: 34px 1fr 24px;
      gap: 14px;
      align-items: center;
      border: 0;
      background: transparent;
      color: var(--ink);
      cursor: pointer;
      text-align: left;
      font: 400 clamp(27px, 3vw, 40px)/.9 var(--display);
      text-transform: uppercase;
    }
    .faq-index { color: var(--ink); font: 700 9px var(--mono); }  /* split ruled */
    .faq-icon {
      font: 400 28px/1 var(--body);
      text-align: center;
      transition: transform .2s ease;
    }
    /* hover keeps the underline affordance; accent colour dropped (2.7:1 small) */
    .faq-question:focus-visible { outline: 2px solid var(--ink); outline-offset: 5px; }
    .faq-item.open .faq-icon { transform: rotate(45deg); }
    .faq-answer {
      display: grid;
      grid-template-rows: 0fr;
      transition: grid-template-rows .28s ease;
    }
    .faq-answer > div { overflow: hidden; }
    .faq-answer p {
      max-width: 680px;
      margin: 0;
      padding: 0 38px 24px 48px;
      color: var(--ink-soft);
      font-size: 15px;
      line-height: 1.6;
    }
    .faq-item.open .faq-answer { grid-template-rows: 1fr; }

    .suppliers {
      display: grid;
      grid-template-columns: var(--ledger-columns);
      border-bottom: 1px solid var(--ink);
      background: var(--ink);
      color: var(--paper);
    }
    .suppliers-intro {
      position: relative;
      padding: clamp(48px, 7vw, 96px) clamp(24px, 4vw, 58px);
      border-right: 1px solid rgba(233,225,210,.3);
    }
    .suppliers .section-number { color: var(--paper); }
    .suppliers-intro h2 {
      max-width: 100%;
      margin: 0;
      font: 400 clamp(48px, 5.3vw, 84px)/.78 var(--display);
      text-transform: uppercase;
    }
    .suppliers-intro p { max-width: 380px; margin: 30px 0 0; color: #c7beaf; }
    .supplier-list { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); }
    .supplier-link {
      min-height: 104px;
      padding: 19px 22px;
      display: grid;
      grid-template-columns: minmax(0, 1fr) auto;
      align-items: center;
      gap: 14px;
      border-right: 1px solid rgba(233,225,210,.22);
      border-bottom: 1px solid rgba(233,225,210,.22);
      color: var(--paper);
      text-decoration: none;
      font: 400 clamp(25px, 2.5vw, 39px)/.84 var(--display);
      text-transform: uppercase;
      transition: background .18s, color .18s;
    }
    .supplier-link:nth-child(2n) { border-right: 0; }
    .supplier-link:nth-last-child(-n+2) { border-bottom: 0; }   /* even count (14): last ROW loses its bottom border; the old odd-count last-child span is retired */
    .supplier-link:hover, .supplier-link:focus-visible { background: var(--paper); color: var(--ink); outline: none; }
    .supplier-arrow { font: 700 13px var(--mono); }
    .supplier-badge {
      grid-column: 1 / -1;
      justify-self: start;
      margin-top: -5px;
      padding: 5px 7px 4px;
      background: var(--red);
      color: var(--ink);   /* was cream — 8px on accent, 3.35:1; ink = 5.2:1 */
      font: 700 8px var(--mono);
      letter-spacing: .1em;
      text-transform: uppercase;
    }

    .live { display: grid; grid-template-columns: var(--ledger-columns); background: var(--paper); color: var(--ink); border-bottom: 1px solid var(--ink); }
    .live-image { min-height: 620px; position: relative; border-right: 1px solid var(--ink); }
    /* Live carousel geometry (L2f Task 4) — mirrors the services instance:
       the track fills the figure and each slide fills the track, so the
       existing `.live-image img` rule below still applies unchanged and the
       section's rendering and layout are untouched. */
    .live-carousel { position: absolute; inset: 0; }
    .live-track { position: absolute; inset: 0; display: flex; transition: transform .7s cubic-bezier(.22,.61,.36,1); will-change: transform; }
    @media (prefers-reduced-motion: reduce) {
      .live-track { transition: none; }   /* same rule the hero track carries — see L2p Task 6 */
    }
    .live-slide { position: relative; min-width: 100%; height: 100%; margin: 0; }
    /* Addendum F (L2f, ruled 29 Jul 2026): the greyscale treatment is REMOVED —
       all six live slides render in full colour. `grayscale(1)` dropped;
       `contrast(1.15)` retained, since the ruling was to remove the black-and-
       white treatment, not the contrast lift.
       Scope: this selector is live-only and the greyscale was declared exactly
       once here, so nothing else on the page is affected. The hero
       (`saturate(.86) contrast(1.05)`) and services (`saturate(.82)
       contrast(1.04)`) treatments are untouched — each instance carries its
       own filter; none is shared. */
    .live-image img { height: 100%; object-fit: cover; }   /* filter: see [data-carousel-track] img (Addendum G) */
    /* ADDENDUM H (L2b Task 7) — the live-printing rate badge that sat here has
       been REMOVED. It was a .live-image::before with z-index 1, absolutely
       positioned at 20px/20px in the accent colour, carrying a `content`
       string naming the hourly print rate.

       Its exact content string is deliberately NOT quoted in this comment:
       the gate verifies clean removal BY GREP, and repeating the string here
       would return a hit and defeat the check. It is recoverable from git
       history if ever needed. (Third instance of this trap in the arc, after
       the netlify.app comment and the JSON-LD script tag.)

       Removed because it COLLIDED ON MOBILE, which is what Addendum H was
       raised to fix. Ratified; not a stylistic preference.

       ::after on this element is still in use for the unified carousel frame
       (L2f Addendum C) and is untouched — the badge was moved ::after ->
       ::before at L2f Task 2 precisely to free it, and that arrangement
       stands. Do not reinstate a ::before here without a fresh ruling: the
       claim "100+ prints per hour" also appears in the live-specs band below,
       so removing the badge lost the badge, not the fact. */
    .live-copy { position: relative; padding: clamp(44px, 7vw, 100px); }
    .live-copy h2 { margin: 0; font: 400 clamp(70px, 9vw, 138px)/.73 var(--display); text-transform: uppercase; }
    .live-copy p { max-width: 520px; color: var(--ink-soft); font-size: 18px; }
    .live-specs {
      grid-column: 1 / -1;
      display: grid;
      grid-template-columns: minmax(0, .72fr) repeat(2, minmax(0, .64fr));
      background: var(--paper);
      color: var(--ink);
      border-top: 1px solid var(--ink);
    }
    .live-specs div {
      min-height: 132px;
      padding: 28px clamp(24px, 4vw, 58px);
      border-right: 1px solid var(--rule);
      font: 700 9px/1.45 var(--mono);
      text-transform: uppercase;
      letter-spacing: .06em;
    }
    .live-specs div:last-child { border-right: 0; }
    /* Stat VALUES in the accent (L2p Task 5). OWNER OVERRIDE — Joel,
       27 Jul 2026 — of the earlier ruling that flipped these to dark ink
       when the redesign moved the strip onto paper.
       Measured 2.70:1 for #FF4013 on the #e9e1d2 cream, against 14.03:1 for
       ink. That shortfall against the 3:1 large-text bar is EXPLICITLY
       ACCEPTED, and only here: this is a 30px display-scale band.
       NOT PRECEDENT for accent-on-cream at smaller sizes — the standing
       accent-for-large / ink-for-small split holds everywhere else.
       There is deliberately NO darker fallback variant; the override is the
       ruling. Sub-labels are untouched and stay ink.
       var(--red) resolves to --accent-choice (#FF4013), keeping the accent
       single-sourced as every other accent usage does. */
    .live-specs strong { display: block; color: var(--red); font-size: 30px; margin-bottom: 7px; }

    .quote-section {
      display: grid;
      grid-template-columns: var(--ledger-columns);
      border-bottom: 1px solid var(--ink);
      background: var(--paper);
    }
    .quote-intro {
      padding: clamp(48px, 7vw, 96px) clamp(24px, 4vw, 58px);
      border-right: 1px solid var(--ink);
      position: relative;
    }
    .quote-intro-inner { position: sticky; top: 112px; }
    .quote-intro h2 { margin: 0 0 28px; padding-left: .05em; font: 400 clamp(62px, 7vw, 104px)/.76 var(--display); text-transform: uppercase; }
    .quote-intro p { max-width: 360px; color: var(--ink-soft); }
    .quote-meta { margin-top: 34px; padding-top: 16px; border-top: 1px solid var(--ink); font: 700 10px/1.7 var(--mono); text-transform: uppercase; }
    .quote-form-wrap { padding: clamp(48px, 6vw, 84px) clamp(24px, 5vw, 76px); }
    .form-section { margin-bottom: 42px; }
    .form-section-label { display: block; margin-bottom: 16px; padding-bottom: 9px; border-bottom: 1px solid var(--rule); color: var(--ink-soft); font: 700 10px var(--mono); letter-spacing: .14em; text-transform: uppercase; }
    .req { color: var(--error); }
    .two-col { display: grid; grid-template-columns: 1fr 1fr; gap: 14px; }
    .field { margin-bottom: 16px; }
    /* .field-label is the chip-group heading (L3). A chip group has no single
       control to point a <label for=> at, so the ten of them were bare
       <label> elements labelling nothing — invalid HTML, and the group's name
       never reached a screen reader. They are now spans carrying an id, and
       each chip grid is role="group" aria-labelledby that id. Styling is
       shared with the real labels, so the change is visually a no-op. */
    .field label, .field .field-label { display: block; margin-bottom: 7px; font: 700 10px var(--mono); letter-spacing: .08em; text-transform: uppercase; }
    .field input, .field select, .field textarea {
      width: 100%;
      padding: 12px 13px;
      border: 1px solid var(--rule);
      border-radius: 0;
      background: rgba(255,255,255,.16);
      color: var(--ink);
      font: 15px var(--body);
    }
    .field textarea { min-height: 116px; resize: vertical; }
    .field input:focus, .field select:focus, .field textarea:focus { border-color: var(--ink); outline: 2px solid transparent; box-shadow: 0 0 0 2px rgba(207,63,39,.2); }
    .hint { margin-top: 6px; color: var(--ink-soft); font: 10px/1.5 var(--mono); }
    .choice-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 8px; }
    .choice-tile, .chip {
      border: 1px solid var(--rule);
      border-radius: 0;
      background: transparent;
      color: var(--ink);
      cursor: pointer;
      text-align: left;
      transition: background .15s, color .15s, border-color .15s;
    }
    .choice-tile { padding: 17px 14px; }
    .choice-name { display: block; font: 700 11px var(--mono); letter-spacing: .08em; text-transform: uppercase; }
    .choice-sub { display: block; margin-top: 5px; color: var(--ink-soft); font-size: 11px; }
    .choice-tile:hover, .choice-tile:focus-visible, .chip:hover, .chip:focus-visible { border-color: var(--ink); outline: none; }
    .choice-tile.selected, .chip.selected { background: var(--ink); border-color: var(--ink); color: var(--paper); }
    .choice-tile.selected .choice-sub { color: var(--paper-dark); }
    .chip-grid { display: flex; flex-wrap: wrap; gap: 7px; }
    .chip { padding: 9px 13px; font: 700 10px var(--mono); letter-spacing: .06em; text-transform: uppercase; }
    .conditional { display: none; margin-top: 18px; padding: 18px; border-left: 3px solid var(--red); background: rgba(255,255,255,.16); }
    .conditional.show { display: block; }
    #embroideryFields.show { margin-bottom: 42px; }
    /* ── L3 part 1b: positions flow, colour rows, supplied-garments notice ──
       Restores the pre-redesign enquiry journey in the current design
       language. Everything uses the existing chip/button styles; only the
       pieces the old form drew with inline styles get classes here. */
    .pos-hint { font: 400 10px var(--mono); letter-spacing: .06em; color: var(--ink-soft); text-transform: none; }
    .colour-row { display: flex; align-items: center; gap: 10px; margin-bottom: 10px; flex-wrap: wrap; }
    .colour-row-label { flex-shrink: 0; width: 90px; font: 700 10px var(--mono); letter-spacing: .08em; text-transform: uppercase; color: var(--ink-soft); }
    .colour-row .chip-grid { gap: 4px; }
    .colour-row .chip { padding: 5px 10px; font-size: 10px; }
    .note-box { margin-top: 10px; padding: 10px 12px; border-left: 3px solid var(--rule); background: rgba(255,255,255,.16); font-size: 12px; color: var(--ink-soft); line-height: 1.6; }
    .note-kicker { display: block; margin-bottom: 4px; font: 700 9px var(--mono); letter-spacing: .1em; text-transform: uppercase; color: var(--ink); }
    .notice-box { margin: 4px 0 16px; padding: 16px 18px; border: 1.5px solid var(--red); background: rgba(255,255,255,.16); }
    .notice-box[hidden] { display: none; }
    .notice-kicker { margin-bottom: 10px; font: 700 9px var(--mono); letter-spacing: .15em; text-transform: uppercase; color: var(--red); }
    .notice-body { margin-bottom: 12px; font-size: 13px; line-height: 1.7; }
    .ack-row { display: flex; align-items: flex-start; gap: 10px; padding: 10px 12px; background: rgba(255,255,255,.3); cursor: pointer; font-size: 12px; line-height: 1.5; }
    .ack-row input[type="checkbox"] { flex-shrink: 0; width: 16px; height: 16px; margin: 1px 0 0; accent-color: var(--ink); }
    .privacy-note { margin: 14px 0 0; color: var(--ink-soft); font: 10px/1.55 var(--mono); }
    .form-error { display: none; margin-top: 6px; color: var(--error); font: 700 11px var(--mono); }
    .form-error.show { display: block; }
    .submit-row { padding-top: 24px; border-top: 2px solid var(--ink); }
    .submit-button {
      width: 100%;
      padding: 20px;
      border: 1px solid var(--red);
      background: var(--red);
      color: #fff8ec;
      cursor: pointer;
      font: 400 28px/.9 var(--display);
      letter-spacing: .06em;
      text-transform: uppercase;
    }
    .submit-button:hover, .submit-button:focus-visible { background: var(--ink); border-color: var(--ink); outline: none; }
    .submit-button:disabled { opacity: .55; cursor: wait; }
    .success-panel { display: none; padding: 48px 0; text-align: center; }
    .success-panel.show { display: block; }
    /* Option A (L3 part 2): the unmistakable artwork step. Styled like the
       submit button so the eye lands on it, but it is a plain link — no
       third-party script on this page. */
    .upload-button {
      display: inline-block;
      margin: 22px 0 10px;
      padding: 20px 34px;
      border: 1px solid var(--red);
      background: var(--red);
      color: #fff8ec;
      text-decoration: none;
      font: 400 28px/.9 var(--display);
      letter-spacing: .06em;
      text-transform: uppercase;
    }
    .upload-button:hover, .upload-button:focus-visible { background: var(--ink); border-color: var(--ink); }
    .success-panel .hint { margin-top: 6px; }
    .success-panel h3 { margin: 0 0 18px; font: 400 clamp(56px, 8vw, 100px)/.78 var(--display); text-transform: uppercase; }
    .success-panel p { color: var(--ink-soft); }

    .site-footer {
      background: var(--ink);
      color: var(--paper);
      border-bottom: 1px solid var(--ink);
    }
    .footer-signoff, .footer-details {
      display: grid;
      grid-template-columns: var(--ledger-columns);
      border-bottom: 1px solid rgba(233,225,210,.3);
    }
    .footer-index {
      position: relative;
      min-height: 280px;
      padding: clamp(42px, 6vw, 82px) clamp(24px, 4vw, 58px);
      border-right: 1px solid rgba(233,225,210,.3);
      display: flex;
      flex-direction: column;
      justify-content: space-between;
    }
    .footer-index p, .footer-address p, .footer-cell p, .footer-bottom p { margin: 0; }
    .footer-kicker, .footer-label, .footer-bottom {
      font: 700 9px/1.55 var(--mono);
      letter-spacing: .1em;
      text-transform: uppercase;
    }
    .footer-index p { max-width: 290px; color: #a9a092; font-size: 14px; }
    .footer-brand {
      padding: clamp(42px, 6vw, 82px) clamp(24px, 5vw, 76px);
      display: flex;
      flex-direction: column;
      justify-content: space-between;
      gap: 58px;
    }
    .footer-brand h2 {
      margin: 0;
      max-width: 800px;
      font: 400 clamp(72px, 10vw, 150px)/.72 var(--display);
      text-transform: uppercase;
    }
    .footer-start {
      align-self: flex-start;
      padding: 11px 0 8px;
      border-bottom: 1px solid currentColor;
      color: var(--paper);
      text-decoration: none;
      font: 700 11px var(--mono);
      letter-spacing: .1em;
      text-transform: uppercase;
    }
    .footer-start:hover, .footer-start:focus-visible { color: var(--red); outline: none; }
    .footer-address {
      padding: 28px clamp(24px, 4vw, 58px);
      border-right: 1px solid rgba(233,225,210,.3);
      color: #c7beaf;
      font-size: 14px;
    }
    .footer-contact-grid { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); }
    .footer-cell {
      padding: 28px 24px;
      border-right: 1px solid rgba(233,225,210,.22);
    }
    .footer-cell:last-child { border-right: 0; }
    .footer-label { display: block; margin-bottom: 12px; color: var(--red); }
    .footer-cell a { color: var(--paper); text-underline-offset: 4px; }
    .footer-cell p { color: #c7beaf; font-size: 13px; line-height: 1.6; }
    .footer-bottom {
      padding: 16px 28px;
      display: flex;
      justify-content: space-between;
      gap: 24px;
      color: #8f877b;
    }

    /* ── DISPLAY TRACKING — ONE SOURCE OF TRUTH (L2p Task 1) ──────────────
       Bootzy is a tight poster face; at display sizes its pairs collide
       (reference pairs: N–K, T–H, E–E–N, M–B–R). This single rule opens
       every Bootzy usage by the same amount.

       Tune --display-tracking, never an individual selector — per-selector
       letter-spacing is exactly what this rule exists to replace. The
       hero's old -.025em was removed for that reason.

       Placed at the end of the base styles so it is the final word in the
       cascade; no media query sets letter-spacing on a display element.

       DOCUMENTED EXCEPTION (reviewer-ruled): .submit-button is deliberately
       ABSENT from this list. It keeps its intentional .06em UI tracking —
       it reads as a button, not as display type. Do not fold it in.
    */
    .wordmark,
    .hero-title,
    .studio-copy-heading h2,
    .studio-copy-body .craft-line,
    .section-title,
    .proof-name,
    .services-heading h2,
    .service h3,
    .faq-intro h2,
    .faq-question,
    .suppliers-intro h2,
    .supplier-link,
    .live-copy h2,
    .quote-intro h2,
    .success-panel h3,
    .footer-brand h2 {
      letter-spacing: var(--display-tracking);
    }

    @media (max-width: 900px) {
      .masthead { grid-template-columns: 1fr auto; }
      .masthead-meta { display: none; }
      .masthead-nav a:not(.quote-link) { display: none; }
      .hero { grid-template-columns: 1fr; }
      .hero-copy { min-height: 670px; }
      .hero-visual { min-height: 720px; }
      .proofs { grid-template-columns: 1fr; }
      .section-intro { border-right: 0; border-bottom: 1px solid var(--ink); }
      .studio-copy, .quote-section, .faq, .suppliers { grid-template-columns: 1fr; }
      .studio-copy-heading, .quote-intro, .faq-intro, .suppliers-intro { border-right: 0; border-bottom: 1px solid var(--ink); }
      .suppliers-intro { border-bottom-color: rgba(233,225,210,.3); }
      .quote-intro-inner, .faq-intro-inner { position: static; }
      .services-header { grid-template-columns: 1fr; }
      .services-heading {
        border-right: 0;
        border-bottom: 1px solid rgba(233,225,210,.32);
      }
      .services-finish {
        min-height: 0;
        aspect-ratio: 4 / 3;
      }
      .services-list { grid-template-columns: repeat(2, minmax(0, 1fr)); }
      .service:nth-child(n) {
        border-right: 1px solid rgba(233,225,210,.22);
        border-bottom: 1px solid rgba(233,225,210,.22);
      }
      .service:nth-child(2n) { border-right: 0; }
      .service:nth-last-child(-n+2) { border-bottom: 0; }
      .service { grid-template-columns: 46px minmax(150px, .8fr) 1fr; }
      .live { grid-template-columns: 1fr; }
      .live-image { border-right: 0; border-bottom: 1px solid var(--ink); }
      .live-copy { min-height: 620px; }
      .footer-signoff, .footer-details { grid-template-columns: 1fr; }
      .footer-index, .footer-address { border-right: 0; border-bottom: 1px solid rgba(233,225,210,.3); }
      .footer-index { min-height: 220px; }
    }

    @media (max-width: 560px) {
      /* The token, not the masthead alone. Setting it here keeps the bar,
         the hero calculation and the scroll offset on one value at this
         breakpoint — previously the hero subtracted 78px on mobile while
         the bar rendered 64px, so the hero was 14px short of the viewport. */
      :root { --masthead-h: 64px; }

      /* Lightbox: full-screen takeover on mobile (Task 5 §8). A centred
         panel at 390px leaves the photograph too small to be worth opening,
         which is the whole point of the control. */
      .print-lightbox { width: 100vw; max-width: 100vw; height: 100dvh; max-height: 100dvh; margin: 0; border: 0; }
      .print-lightbox-panel { grid-template-columns: 1fr; gap: 20px; padding: 56px 18px 24px; }
      /* The close button sits above the stacked content here, so the desktop
         clearance is not needed and would only indent the title. */
      .print-lightbox-title { padding-right: 0; font-size: 27px; }

      .masthead { min-height: var(--masthead-h); padding: 12px 14px; }
      .masthead-nav { gap: 0; }
      .quote-link { padding: 8px 10px; }
      .hero-copy { min-height: 570px; padding: 56px 20px 32px; }
      .hero-title { font-size: clamp(82px, 30vw, 126px); }
      .hero-intro { grid-template-columns: 36px 1fr; margin-top: 54px; }
      .hero-visual { min-height: 540px; }
      /* MOBILE JOB TICKET — ONE ROW (L2f Addendum D, ruled).
         Was a 2 + 1 layout with the third cell orphaned onto its own row.
         Now three cells in one row, matching the desktop structure.

         The column weights are proportional to the WORST-CASE value in each
         cell across all three instances, not equal thirds: equal thirds cannot
         work here. At 390px the ticket is 372px wide, and the longest value —
         JADEN SMITH X MSFTSREP, 22 characters — needs 132px at 10px mono,
         while an equal third leaves at most ~110px of text space. Weighting
         the columns to their content is what makes one row possible at all.

         Value size is 10px, not the 12px desktop size: 11px would need 145px
         for that same value and does not fit at any workable padding. Padding
         is tightened from 12/14 to 8/7 to buy the remaining width. Label
         letter-spacing is deliberately UNCHANGED — labels are short enough to
         fit, so no exception to the tracking rule was needed. */
      .job-ticket {
        left: 6px; right: 6px; bottom: 6px;
        /* Weights are the per-column WORST CASE measured across ALL THREE
           instances, not equal thirds and not the live data alone:
             col1  ARC'TERYX PICCADILLY (20)  -> 120px at 10px mono
             col2  FOLDED + BAGGED     (15)  ->  90px
             col3  JADEN SMITH X MSFTSREP (22) -> 132px
           An earlier cut sized col2 against live's CITY (10 chars) and the
           hero's FINISH values -- RETAIL FINISH, BRIGHT FINISH, 13 chars --
           wrapped to two lines, breaking the consistent-height requirement on
           four hero slides. Every column must hold its own longest value from
           any instance. */
        grid-template-columns: 1.30fr 1.01fr 1.41fr;
      }
      .job-ticket > div { padding: 8px 5px; }
      .ticket-label { font-size: 8px; margin-bottom: 3px; }
      .ticket-value { font-size: 9.5px; }
      .hero-controls { top: 10px; right: 10px; }
      .hero-control { width: 40px; height: 38px; }
      .hero-count { min-width: 62px; }
      .ink-strip { grid-template-columns: 1fr auto; padding: 12px 14px; }
      .ink-strip > span:last-child { display: none; }
      .colour-chips { justify-content: flex-end; }
      .proof-grid { grid-template-columns: 1fr; }
      .proof { border-left: 0; border-bottom: 1px solid var(--rule) !important; padding: 16px; }
      .proof:last-child { border-bottom: 0 !important; }
      .services-list { grid-template-columns: 1fr; }
      .service {
        grid-template-columns: 34px 1fr;
        gap: 10px 14px;
        min-height: 0;
        padding: 24px 20px;
        border-right: 0 !important;
        border-bottom: 1px solid rgba(233,225,210,.22) !important;
      }
      .service:last-child { border-bottom: 0 !important; }
      .service p { grid-column: 2; }
      .faq-list { padding-inline: 20px; }
      .faq-question { grid-template-columns: 28px 1fr 22px; }
      .faq-answer p { padding-left: 42px; padding-right: 20px; }
      .supplier-list { grid-template-columns: 1fr; }
      .supplier-link { min-height: 88px; border-right: 0; border-bottom: 1px solid rgba(233,225,210,.22) !important; }
      .supplier-link:last-child { border-bottom: 0 !important; }
      .choice-grid, .two-col { grid-template-columns: 1fr; }
      .logo-track img { height: 30px; max-width: 96px; margin-right: 40px; }
      .live-image { min-height: 500px; }
      .live-copy { min-height: 560px; padding: 50px 22px; }
      .live-specs { grid-template-columns: 1fr; }
      .live-specs div {
        min-height: 0;
        padding: 20px;
        border-right: 0;
        border-bottom: 1px solid var(--rule);
      }
      .live-specs div:last-child { border-bottom: 0; }
      .footer-contact-grid { grid-template-columns: 1fr; }
      .footer-cell { border-right: 0; border-bottom: 1px solid rgba(233,225,210,.22); }
      .footer-cell:last-child { border-bottom: 0; }
      .footer-bottom { flex-direction: column; }
    }

    @media (prefers-reduced-motion: reduce) {
      html { scroll-behavior: auto; }
      *, *::before, *::after { transition-duration: .01ms !important; }
      .logo-track { animation: none; }
    }
