/* Generated by scripts/fetch-fonts.mjs — do not edit by hand.
   Self-hosted so the site makes no external requests. All three families are
   SIL Open Font License 1.1; see public/fonts/OFL.txt. */

@font-face {
  font-family: 'DM Mono';
  font-style: normal;
  font-weight: 400;
  font-display: swap;
  src: url('../fonts/dm-mono-400-latin-ext.woff2') format('woff2');
  unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}

@font-face {
  font-family: 'DM Mono';
  font-style: normal;
  font-weight: 400;
  font-display: swap;
  src: url('../fonts/dm-mono-400-latin.woff2') format('woff2');
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}

@font-face {
  font-family: 'DM Mono';
  font-style: normal;
  font-weight: 500;
  font-display: swap;
  src: url('../fonts/dm-mono-500-latin-ext.woff2') format('woff2');
  unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}

@font-face {
  font-family: 'DM Mono';
  font-style: normal;
  font-weight: 500;
  font-display: swap;
  src: url('../fonts/dm-mono-500-latin.woff2') format('woff2');
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}

@font-face {
  font-family: 'DM Sans';
  font-style: normal;
  font-weight: 400;
  font-display: swap;
  src: url('../fonts/dm-sans-400-latin-ext.woff2') format('woff2');
  unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}

@font-face {
  font-family: 'DM Sans';
  font-style: normal;
  font-weight: 400;
  font-display: swap;
  src: url('../fonts/dm-sans-400-latin.woff2') format('woff2');
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}

@font-face {
  font-family: 'DM Sans';
  font-style: normal;
  font-weight: 500;
  font-display: swap;
  src: url('../fonts/dm-sans-500-latin-ext.woff2') format('woff2');
  unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}

@font-face {
  font-family: 'DM Sans';
  font-style: normal;
  font-weight: 500;
  font-display: swap;
  src: url('../fonts/dm-sans-500-latin.woff2') format('woff2');
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}

@font-face {
  font-family: 'DM Sans';
  font-style: normal;
  font-weight: 600;
  font-display: swap;
  src: url('../fonts/dm-sans-600-latin-ext.woff2') format('woff2');
  unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}

@font-face {
  font-family: 'DM Sans';
  font-style: normal;
  font-weight: 600;
  font-display: swap;
  src: url('../fonts/dm-sans-600-latin.woff2') format('woff2');
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}

@font-face {
  font-family: 'DM Sans';
  font-style: normal;
  font-weight: 700;
  font-display: swap;
  src: url('../fonts/dm-sans-700-latin-ext.woff2') format('woff2');
  unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}

@font-face {
  font-family: 'DM Sans';
  font-style: normal;
  font-weight: 700;
  font-display: swap;
  src: url('../fonts/dm-sans-700-latin.woff2') format('woff2');
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}

@font-face {
  font-family: 'Manrope';
  font-style: normal;
  font-weight: 400;
  font-display: swap;
  src: url('../fonts/manrope-400-latin-ext.woff2') format('woff2');
  unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}

@font-face {
  font-family: 'Manrope';
  font-style: normal;
  font-weight: 400;
  font-display: swap;
  src: url('../fonts/manrope-400-latin.woff2') format('woff2');
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}

@font-face {
  font-family: 'Manrope';
  font-style: normal;
  font-weight: 500;
  font-display: swap;
  src: url('../fonts/manrope-500-latin-ext.woff2') format('woff2');
  unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}

@font-face {
  font-family: 'Manrope';
  font-style: normal;
  font-weight: 500;
  font-display: swap;
  src: url('../fonts/manrope-500-latin.woff2') format('woff2');
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}

@font-face {
  font-family: 'Manrope';
  font-style: normal;
  font-weight: 600;
  font-display: swap;
  src: url('../fonts/manrope-600-latin-ext.woff2') format('woff2');
  unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}

@font-face {
  font-family: 'Manrope';
  font-style: normal;
  font-weight: 600;
  font-display: swap;
  src: url('../fonts/manrope-600-latin.woff2') format('woff2');
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}

@font-face {
  font-family: 'Manrope';
  font-style: normal;
  font-weight: 700;
  font-display: swap;
  src: url('../fonts/manrope-700-latin-ext.woff2') format('woff2');
  unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}

@font-face {
  font-family: 'Manrope';
  font-style: normal;
  font-weight: 700;
  font-display: swap;
  src: url('../fonts/manrope-700-latin.woff2') format('woff2');
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}

@font-face {
  font-family: 'Manrope';
  font-style: normal;
  font-weight: 800;
  font-display: swap;
  src: url('../fonts/manrope-800-latin-ext.woff2') format('woff2');
  unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}

@font-face {
  font-family: 'Manrope';
  font-style: normal;
  font-weight: 800;
  font-display: swap;
  src: url('../fonts/manrope-800-latin.woff2') format('woff2');
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}

/* ==========================================================================
   GENERATED — do not edit. Source: data/brands.json -> brandColors,
   derived by scripts/lib/color.mjs, emitted by brandTokensCss() in build.mjs.

   Editing this block by hand is pointless: the next `node build.mjs` will
   overwrite it. To change a brand colour, change data/brands.json.

   Measured against --paper #FFFFFF:
     OnaFit           #8A7B66  accent  4.11:1   ink #756957  5.36:1   field #81735F + #FFFFFF  4.61:1
     Karisma Threads  #85C449  accent   2.1:1   ink #4D7626  5.33:1   field #55822A + #FFFFFF  4.55:1
     ScrubAid         #3E90B5  accent  3.58:1   ink #306F8B  5.56:1   field #367E9E + #FFFFFF  4.53:1
     EliteMed         #FFC3B0  accent  1.53:1   ink #BF2E00  5.82:1   field #FFC3B0 + #16181C 11.58:1
     JustScrubs       #C21F68  accent  5.69:1   ink #B91E63  6.12:1   field #C21F68 + #FFFFFF  5.69:1
   ========================================================================== */
:root {
  --brand-onafit: #8A7B66;
  --brand-onafit-ink: #756957;
  --brand-onafit-line: #8A7B66;
  --brand-onafit-field: #81735F;
  --brand-onafit-field-ink: #FFFFFF;
  --brand-onafit-tint: #F7F6F5;
  --wash-onafit: 214 159 81;
  --wash-onafit-panel: 214 159 81;

  --brand-karisma: #85C449;
  --brand-karisma-ink: #4D7626;
  --brand-karisma-line: #69A134;
  --brand-karisma-field: #55822A;
  --brand-karisma-field-ink: #FFFFFF;
  --brand-karisma-tint: #F6FAF2;
  --wash-karisma: 146 214 81;
  --wash-karisma-panel: 146 214 81;
  --brand-karisma-detail: #517B28;

  --brand-scrubaid: #3E90B5;
  --brand-scrubaid-ink: #306F8B;
  --brand-scrubaid-line: #3E90B5;
  --brand-scrubaid-field: #367E9E;
  --brand-scrubaid-field-ink: #FFFFFF;
  --brand-scrubaid-tint: #F2F7FA;
  --wash-scrubaid: 81 173 214;
  --wash-scrubaid-panel: 81 173 214;

  --brand-elitemed: #FFC3B0;
  --brand-elitemed-ink: #BF2E00;
  --brand-elitemed-line: #FF5E2B;
  --brand-elitemed-field: #FFC3B0;
  --brand-elitemed-field-ink: #16181C;
  --brand-elitemed-tint: #FAF4F2;
  --wash-elitemed: 255 92 41;
  --wash-elitemed-panel: 255 92 41;
  --brand-elitemed-detail: #B95B37;

  --brand-justscrubs: #C21F68;
  --brand-justscrubs-ink: #B91E63;
  --brand-justscrubs-line: #C21F68;
  --brand-justscrubs-field: #C21F68;
  --brand-justscrubs-field-ink: #FFFFFF;
  --brand-justscrubs-tint: #FAF2F6;
  --wash-justscrubs: 255 41 149;
  --wash-justscrubs-panel: 225 70 140;
  --brand-justscrubs-support: #FF82C1;
}

[data-accent="onafit"] {
  --accent: var(--brand-onafit);
  --accent-ink: var(--brand-onafit-ink);
  --accent-line: var(--brand-onafit-line);
  --accent-field: var(--brand-onafit-field);
  --accent-field-ink: var(--brand-onafit-field-ink);
  --accent-tint: var(--brand-onafit-tint);
  --wash-brand: var(--wash-onafit-panel);
}
[data-accent="karisma"] {
  --accent: var(--brand-karisma);
  --accent-ink: var(--brand-karisma-ink);
  --accent-line: var(--brand-karisma-line);
  --accent-field: var(--brand-karisma-field);
  --accent-field-ink: var(--brand-karisma-field-ink);
  --accent-tint: var(--brand-karisma-tint);
  --wash-brand: var(--wash-karisma-panel);
  --detail: var(--brand-karisma-detail);
}
[data-accent="scrubaid"] {
  --accent: var(--brand-scrubaid);
  --accent-ink: var(--brand-scrubaid-ink);
  --accent-line: var(--brand-scrubaid-line);
  --accent-field: var(--brand-scrubaid-field);
  --accent-field-ink: var(--brand-scrubaid-field-ink);
  --accent-tint: var(--brand-scrubaid-tint);
  --wash-brand: var(--wash-scrubaid-panel);
}
[data-accent="elitemed"] {
  --accent: var(--brand-elitemed);
  --accent-ink: var(--brand-elitemed-ink);
  --accent-line: var(--brand-elitemed-line);
  --accent-field: var(--brand-elitemed-field);
  --accent-field-ink: var(--brand-elitemed-field-ink);
  --accent-tint: var(--brand-elitemed-tint);
  --wash-brand: var(--wash-elitemed-panel);
  --detail: var(--brand-elitemed-detail);
}
[data-accent="justscrubs"] {
  --accent: var(--brand-justscrubs);
  --accent-ink: var(--brand-justscrubs-ink);
  --accent-line: var(--brand-justscrubs-line);
  --accent-field: var(--brand-justscrubs-field);
  --accent-field-ink: var(--brand-justscrubs-field-ink);
  --accent-tint: var(--brand-justscrubs-tint);
  --wash-brand: var(--wash-justscrubs-panel);
}

/* ==========================================================================
   Denice Inc — wholesale medical scrubs
   src/site.css — theme rebuild (theme-rebuild branch, Aug 2026)

   Two registers, deliberately alternated. The LOOKBOOK register — home, brand
   pages, about, fabrics — runs full-bleed photography, enormous whitespace,
   image-and-text bands and a dark stat band and footer. The SPEC-SHEET
   register — the catalogue grid and the style pages — keeps the CAD-sheet
   language the site already had: technical flats, mono size ratios, circular
   swatch grids, thin red callout rules. The two share one type scale, one
   header and one footer, so the switch between them reads as intentional.

   Sections
     01 tokens          07 buttons          13 catalogue
     02 reset + base    08 photography      14 order / steps / cta
     03 typography      09 figures (flats)  15 motion
     04 motifs          10 cards            16 responsive
     05 layout          11 data blocks      17 focus + print
     06 chrome          12 lookbook bands

   Rules that must not be broken (see CLAUDE.md):
   - Denice red is punctuation only. There is no --danger token on purpose.
   - The BOX owns every image ratio, never the image.
   - Photographs are never scaled beyond their native width. Where a frame is
     wider than the photo allows, the photo's own studio ground is extended as
     a flat bleed and the image sits off-centre against it. See 08.
   - Only transform and opacity are animated. Hidden initial states exist only
     under prefers-reduced-motion: no-preference and only under .js.
   ========================================================================== */


/* == 01  tokens =========================================================== */

:root {
  /* brand */
  --red: #D0112B;
  --red-deep: #A80D22;

  /* ink — the ramp is monotonic AND every step clears WCAG AA (4.5:1) on
     both grounds it is ever set on: --paper #FFFFFF and the pale accent
     tint #ECEFF7. ink-3 was #7A8089 (3.98 on white, 3.46 on the tint) and
     ink-4 was #9BA1A9 (2.60 on white) — both failed, and both carry real
     buying facts, not ornament: ink-3 sets the fabric blend and the footer,
     ink-4 sets "8 colors" and the size-run size labels. The faint tier is
     visibly darker than it was; that is the cost of the floor. Contrast on
     white now: 17.8 / 8.9 / 5.7 / 5.2. */
  --ink: #16181C;
  --ink-2: #454A52;
  --ink-3: #5F646D;
  --ink-4: #686D76;

  /* ---- grounds: three, and every one of them a decision ----
     There were nine (paper, wash, wash-2, bone, bone-2, and four tints) and
     every one sat within a few percent of white, so none of them read as a
     choice — they read as an accident, which is what "unfinished" means here.
     Worse, wash and rule were BLUE-grey (B > R) sitting under warm garment
     photography, and that mismatch is the specific tell.

     Josmo, the reference, uses exactly two grounds — #FFFFFF and a near-black —
     and lets every scrap of colour arrive through full-bleed imagery. That is
     the mechanism being borrowed: not a richer set of tints, but the removal
     of the unresolved middle so the photographs and the fabric fields are the
     only colour on the page.

     --paper   every surface that holds text. It is also, exactly, the studio
               ground of all ten new landscape photographs, which sample
               #ffffff — so a full-bleed hero now meets the page invisibly.
     --shell   the ONE tinted ground, for a band that has to step back from
               paper. Chosen by the owner (19 Aug 2026) from three luminance-
               matched candidates: it is the studio wall itself — #F4F5F7 is
               the single most common non-white ground in data/photos.json,
               with #EAEEF2 and #E6EAED behind it — held at its own cool hue
               and given just enough chroma to read as a surface. A cool
               ground is the foil the rest of the system wants: the Denice
               red advances hardest off it, and none of the five fabric
               fields shares its hue, so it stays out of every fight. The
               warm beige that preceded it (#F2EFE9) sat too close to the
               fields and argued with the red.
     --ink     the near-black anchor: the top bar, the footer, the stat band. */
  --paper: #FFFFFF;
  --shell: #ECEFF7;

  /* rules — tuned to the shell's own cool cast, so a hairline never reads
     warmer than the ground it sits beside */
  --rule: #E3E6EC;      /* hairline on paper */
  --rule-2: #EFF1F6;
  --rule-shell: #DDE1EA; /* hairline ON shell, where --rule would vanish */

  /* dark surfaces — footer, stat band, hero scrims (added Aug 2026) */
  --slate: #0F1114;
  --slate-2: #1B1E23;
  /* The proof chapter's ground: the same graphite as --slate, one step up, so
     the glows have somewhere to fall to and the band is not pure --slate
     against the footer directly below it. Chosen by the owner 20 Aug 2026
     over a warm-neutral and a studio-shell candidate. */
  --slate-field: #13161A;
  --on-slate: #C9CDD4;
  --on-slate-2: #8C929B;
  --slate-rule: #262A31;


  /* the only status colour on the site */
  --ok: #1F7A46;
  --ok-bg: #E8F5ED;

  /* ---- THE GRADIENT WASH IS REMOVED, EXCEPT ON /brands/ (owner, 27 Aug
     2026, amended the same day) ----
     Every light ground on the site is plain --paper white: sections, heroes,
     the spec header, the brand rail, the proof chapter. The dark closing CTA
     and the footer keep their slate field, which was never part of the wash.

     THE ONE EXCEPTION IS THE /brands/ IDENTITY PANEL (.bfield-copy), which
     the owner kept exactly as it was — wash, flip, stacked variant and the
     bleed under the photograph. Its whole mechanism now lives in that one
     block near the bottom of this file rather than in a shared selector list
     up here, so nothing else can pick the ground up by being added to a
     list. The five brand PAGES are not that component and stay white.

     What remains of the machinery here:
       - the --wash-<slug> "r g b" triples are still GENERATED into the built
         stylesheet by brandTokensCss() (data/brands.json is the source);
         .bfield-copy paints from --wash-brand and the .fprops hairlines tint
         from it.
       - scripts/sample-wash-palette.mjs and data/wash-palette.json stay as
         provenance for those hues.
       - scripts/check-wash-contrast.mjs still evaluates the real gradient
         geometry, for the five brand panels that still have one, and checks
         the flat grounds besides. */

  /* brand accents — GENERATED. The five --tier-* tokens that stood here were a
     second copy of data/brands.json, and the map of this stylesheet found they
     had no consumers at all: five brand hexes maintained for nothing, still
     carrying EliteMed's retired #D9601C. Their replacements are --brand-<slug>
     and the [data-accent] scopes, both emitted by build.mjs.

     --accent stays defined here as the FALLBACK for anything outside a brand
     scope. */
  --accent: var(--ink);

  /* type — three faces, each with one job (19 Aug 2026).
     Archivo did the first two and did them neutrally, which is how a site
     laid out correctly still reads as a template. --display is Manrope:
     semi-rounded geometric humanist, warm at large size, and built to take
     the tracking below. --sans is DM Sans, the sibling of the DM Mono this
     project has always self-hosted, so read text and measured text are now
     family rather than coincidence. Both SIL OFL; see scripts/fetch-fonts.mjs. */
  --display: "Manrope", "Segoe UI", "Helvetica Neue", Arial, sans-serif;
  --sans: "DM Sans", "Segoe UI", "Helvetica Neue", Arial, sans-serif;
  --mono: "DM Mono", ui-monospace, "SFMono-Regular", "Cascadia Mono", Menlo, monospace;

  /* Display sizes carry their own line-height and tracking, because the two
     move together: the bigger the type the tighter it has to be set, and a
     single global letter-spacing cannot do that. */
  /* 86px, not 104. Manrope is wider per character than Archivo AND the ceiling
     had gone up, and the two compounded: the home H1 wants 2152px on one line
     against an 855px copy column, which is 2.5 columns, so text-wrap: balance
     settled on FOUR even lines and split the sentence — "scrubs. All of /
     them ours." 86px brings it back to three with the break at the full stop,
     and it still reads larger than Archivo did at 92 because Manrope carries
     a taller x-height. */
  --t-hero: clamp(44px, min(7.2vw, 11svh), 86px);
  --t-hero-lh: .98;
  --t-hero-tr: -.028em;
  --t-d1: clamp(36px, 5vw, 72px);
  --t-d1-lh: 1.02;
  --t-d1-tr: -.025em;
  --t-d2: clamp(27px, 3.2vw, 44px);
  --t-d2-lh: 1.08;
  --t-d2-tr: -.02em;
  --t-d3: clamp(20px, 1.7vw, 27px);
  --t-d3-lh: 1.2;
  --t-d3-tr: -.01em;
  --t-lede: clamp(18px, 1.45vw, 22px);
  /* DM Sans sets a shade smaller on the body than Archivo did at the same
     number, so the body size goes up half a point to hold the same measure. */
  --t-body: 17px;
  --t-data: 13px;
  --t-label: 11px;

  /* space — 4px base */
  --s1: 4px;   --s2: 8px;   --s3: 12px;  --s4: 16px;  --s5: 24px;
  --s6: 32px;  --s7: 48px;  --s8: 64px;  --s9: 96px;  --s10: 128px;

  /* ---- MORE AIR (owner, 20 Aug 2026) ----
     The bands were dense: 176px at the top of the scale sounds generous and
     is not, because it is the ONLY gap in the composition -- the head sat
     48px off its content and two bands inside one section sat 176px apart,
     so the eye met a wall of material and one margin around it.

     Every one of those numbers goes up together, and that is the point. Air
     added at the section boundary alone just pushes dense blocks further
     apart; what makes a page read as calm is the same generosity repeated at
     every level of the nesting -- around the section, under the head, between
     the bands, inside the grid. */
  --sect-y: clamp(112px, 14vw, 224px);
  --sect-y-tight: clamp(72px, 9vw, 140px);
  /* the gap under a section head, and the gap between two bands in one
     section -- both were --s7 (48px) and both are their own decision now */
  --head-gap: clamp(48px, 5vw, 80px);
  --band-gap: clamp(88px, 11vw, 176px);
  --wrap: 1320px;
  --wrap-wide: 1560px;
  --gutter: clamp(20px, 4vw, 56px);
  --head-h: 64px;
  /* The black bar above the header. It is NOT sticky — it scrolls away and the
     masthead pins in its place — but it does occupy the first 40px of the
     document, so anything sized to fill the viewport has to subtract it. */
  --bar-h: 40px;

  /* figures — one token governs every flat's slot on the site */
  --fig-ratio: 4 / 5;

  /* ---- depth ----
     The site was hairlines and nothing else, which is a real part of why it
     read flat and therefore cheap. Three lifts, and two rules taken from the
     reference: the shadow is tinted with the ink (Josmo tints its own
     rgba(15,23,42,…)) because a neutral black shadow on a warm ground reads as
     dirt; and each carries a large NEGATIVE spread, which pulls the shadow in
     under the element so it reads as something lifted off the page rather than
     something with a drop shadow behind it.
     lift-1 rest · lift-2 hover · lift-3 the one genuinely floating thing. */
  --lift-1: 0 1px 2px rgba(20,22,26,.04), 0 8px 24px -16px rgba(20,22,26,.18);
  --lift-2: 0 18px 44px -28px rgba(20,22,26,.28);
  --lift-3: 0 24px 60px -20px rgba(20,22,26,.45);

  /* Radii: three, plus the pill. Was 12/18/20/24/8/2 with no system. */
  --r-sm: 12px;
  --r-md: 20px;
  --r-lg: 28px;
  --r-pill: 999px;

  /* motion — slow, confident, never bouncy */
  --ease: cubic-bezier(.16, 1, .3, 1);
  --ease-io: cubic-bezier(.65, 0, .35, 1);
  --dur: 700ms;
  --dur-x: 900ms;
  --stagger: 80ms;
}


/* == 02  reset + base ===================================================== */

*, *::before, *::after { box-sizing: border-box; }

html {
  -webkit-text-size-adjust: 100%;
  scroll-behavior: smooth;
}
html.menu-open { overflow: hidden; }

body {
  margin: 0;
  font-family: var(--sans);
  font-size: var(--t-body);
  line-height: 1.6;
  color: var(--ink);
  background: var(--paper);
  -webkit-font-smoothing: antialiased;
  text-rendering: optimizeLegibility;
}

img { max-width: 100%; display: block; }
a { color: inherit; }
button { font: inherit; color: inherit; }

/* Nothing jumps state. Every interactive element eases colour, opacity and
   transform; components that need a different curve set their own. */
a, button, input, summary, label, .card, .sw, .chip {
  transition:
    color 300ms var(--ease), background-color 300ms var(--ease),
    border-color 300ms var(--ease), opacity 300ms var(--ease),
    transform 400ms var(--ease);
}

/* The UA rule for [hidden] is display:none, but any class that sets display
   (.card is flex, .grid is grid) outranks it. Without this, filtered-out
   cards stay on screen. */
[hidden] { display: none !important; }

hr {
  border: 0;
  border-top: 1px solid var(--rule);
  margin: var(--s7) 0;
}

::selection { background: var(--red); color: #fff; }

.skip {
  position: absolute; left: -9999px; top: 0; z-index: 100;
  background: var(--ink); color: #fff; padding: 14px 22px;
  font-family: var(--sans); font-size: var(--t-data); text-decoration: none;
}
.skip:focus { left: 0; }

.vh {
  position: absolute; width: 1px; height: 1px; overflow: hidden;
  clip: rect(0 0 0 0); clip-path: inset(50%); white-space: nowrap;
}


/* == 03  typography ======================================================= */

/* Headings are the display face. Body, ledes, labels and navigation stay in
   --sans: Manrope is drawn to be seen, DM Sans to be read, and a paragraph set
   in a display face is exactly the sort of thing that reads as a template. */
h1, h2, h3, h4 { font-family: var(--display); margin: 0 0 var(--s4); text-wrap: balance; }

h1 {
  font-size: var(--t-d1);
  font-weight: 800;
  letter-spacing: var(--t-d1-tr);
  line-height: var(--t-d1-lh);
}

h2 {
  font-size: var(--t-d2);
  font-weight: 700;
  letter-spacing: var(--t-d2-tr);
  line-height: var(--t-d2-lh);
}

h3 {
  font-size: var(--t-d3);
  font-weight: 700;
  letter-spacing: var(--t-d3-tr);
  line-height: var(--t-d3-lh);
}

h4 {
  font-size: 15px;
  font-weight: 700;
  letter-spacing: -.005em;
}

p { margin: 0 0 var(--s4); max-width: 68ch; }

.lede {
  font-size: var(--t-lede);
  line-height: 1.5;
  color: var(--ink-2);
  max-width: 58ch;
  letter-spacing: -.008em;
}

.prose p + h3 { margin-top: var(--s7); }
.prose p:last-child { margin-bottom: 0; }

.mono { font-family: var(--mono); }

.micro {
  font-family: var(--sans);
  font-size: 12px;
  font-weight: 500;
  letter-spacing: .08em;
  text-transform: uppercase;
  color: var(--ink-3);
}

.dim { color: var(--ink-3); }

/* tabular figures wherever a number is meant to be compared */
.tnum { font-variant-numeric: tabular-nums; }


/* == 04  motifs =========================================================== */

/* ---- the section mark ----
   A 32px tinted plate carrying a 16px line icon, and the label set BESIDE it.
   Replaces the red dot and the 72px hairline that opened every section: that
   was one weight of one colour repeated thirty-one times, so it announced
   that a section had started and said nothing about which one.

   The tint is the whole effect and both numbers were found rather than
   picked. Below about 10% the plate disappears against paper and the icon
   floats unsupported; above about 18% it reads as a filled button, which is
   a control, and none of these is clickable. 12% ground and a 22% ring sit
   in the gap: enough to read as a surface, not enough to read as a target.

   The ring is an inset box-shadow, not a border, so the plate is exactly
   32px whatever the ring does and the label's baseline never shifts.

   --eb is the accent and defaults to the house red. It must always be an
   accentInk() value on a brand page: the LABEL takes the accent too, and at
   11.5px that is small text, which owes 4.5:1 -- exactly what accentInk()
   solves for. */
.eyebrow {
  display: flex;
  align-items: center;
  gap: 10px;
  margin: 0 0 var(--s5);
  font-family: var(--sans);
  font-size: 11.5px;
  font-weight: 600;
  letter-spacing: .12em;
  text-transform: uppercase;
  color: var(--eb, var(--red));
  max-width: none;
}
.eyebrow .eb-t { display: block; }
.eb-i {
  display: grid;
  place-items: center;
  flex: 0 0 32px;
  width: 32px;
  height: 32px;
  border-radius: 10px;
  color: var(--eb, var(--red));
  background: color-mix(in srgb, var(--eb, var(--red)) 12%, transparent);
  box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--eb, var(--red)) 22%, transparent);
}
.eb-i svg { display: block; width: 16px; height: 16px; }

/* On the dark field the label goes white -- the lifted red is legible but it
   is carrying the icon, and a coloured label as well would put two reds in a
   three-word line. The plate keeps the lifted red and its tints come up,
   because 12% of anything over a dark ground is nothing. */
.eyebrow.on-ink { color: #fff; }
.eyebrow.on-ink .eb-i {
  color: var(--hl, #F25F72);
  background: color-mix(in srgb, var(--hl, #F25F72) 18%, transparent);
  box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--hl, #F25F72) 34%, transparent);
}
.eyebrow.quiet { color: var(--ink-3); }
.eyebrow.quiet .eb-i {
  color: var(--ink-3);
  background: color-mix(in srgb, var(--ink-3) 10%, transparent);
  box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--ink-3) 20%, transparent);
}

/* Plate number — 01 / 05 */
.plate {
  font-family: var(--sans);
  font-size: 12px;
  font-weight: 500;
  letter-spacing: .06em;
  color: var(--red);
}
.plate i { font-style: normal; color: var(--ink-4); }

/* Sheet mark — foot of a spec plate */
.sheet-mark {
  font-family: var(--sans);
  font-size: 11.5px;
  letter-spacing: .08em;
  text-transform: uppercase;
  color: var(--ink-4);
  margin: var(--s4) 0 0;
}


/* == 05  layout =========================================================== */

.wrap {
  max-width: var(--wrap);
  margin: 0 auto;
  padding: 0 var(--gutter);
}
.wrap-wide { max-width: var(--wrap-wide); }
.wrap-narrow { max-width: 880px; }

.section {
  padding: var(--sect-y) 0;
}
.section.rule { border-top: 1px solid var(--rule); }
.section.tight { padding: var(--sect-y-tight) 0; }
.section.no-rule { border-top: 0; }


/* ---- plain paper (owner, 27 Aug 2026) ----
   Every section sits on flat --paper. The wash field that used to be painted
   here — three radial glows over every light ground, per-band hue triads and
   intensities, the edge fade — is removed by the owner's instruction. What
   separates sections now is the spacing scale, the eyebrow plates,
   .section.rule hairlines and the dark closing chapter. The .bone step-back
   band went with it: the two sections that carried it are .section.rule now,
   a hairline instead of a tinted base, and --shell-bone is gone. */
.section {
  position: relative;
  background-color: var(--paper);
}

.section-head {
  display: flex;
  align-items: baseline;
  gap: var(--s5);
  flex-wrap: wrap;
  margin-bottom: var(--head-gap);
}
.section-head h2 { margin: 0; }
.section-head .count {
  font-family: var(--sans);
  font-size: 14px;
  color: var(--ink-3);
  margin-left: auto;
}

/* head with a plate number sitting above the rule */
.plate-head {
  display: flex;
  justify-content: space-between;
  align-items: baseline;
  gap: var(--s5);
  padding-bottom: var(--s4);
  margin-bottom: var(--s7);
  border-bottom: 1px solid var(--rule);
  flex-wrap: wrap;
}

/* text-only page opener (spec-sheet register) */
.hero {
  padding: clamp(64px, 9vw, 148px) 0 clamp(56px, 7vw, 108px);
  position: relative;
  background-color: var(--paper);
}
.hero h1 { max-width: 20ch; }
.hero-actions,
.actions {
  display: flex;
  gap: var(--s3);
  flex-wrap: wrap;
  margin: var(--s6) 0 0;
  max-width: none;
}

/* style page head */
.style-head {
  display: grid;
  grid-template-columns: 1fr 440px;
  gap: clamp(32px, 5vw, 80px);
  align-items: start;
  padding: clamp(32px, 4vw, 64px) 0 clamp(28px, 3vw, 48px);
}
/* Number and brand mark on one line, the way a CAD title block sets them. */
.style-title {
  display: flex;
  align-items: center;
  gap: clamp(16px, 2.5vw, 28px);
  flex-wrap: wrap;
  margin: 0 0 var(--s3);
}
.style-brandmark {
  display: inline-block;
  line-height: 0;
  opacity: .92;
  transition: opacity 240ms var(--ease);
  padding-left: clamp(16px, 2.5vw, 28px);
  border-left: 1px solid var(--rule);
}
.style-brandmark:hover { opacity: 1; }
.style-brandmark:focus-visible { outline: 2px solid var(--red); outline-offset: 4px; }
@media (max-width: 520px) {
  .style-brandmark { padding-left: 0; border-left: 0; }
}

.style-no-big {
  font-family: var(--mono);
  font-size: clamp(44px, 7vw, 84px);
  font-weight: 500;
  letter-spacing: -.04em;
  line-height: 1;
  margin: 0;
}

/* The ScrubAid / EliteMed label-exclusivity callout used to live here. It is
   gone (owner, 21 Aug 2026): the two are separate brands and the callout's
   whole argument was that they were one fabric under two labels. Its markup
   was removed from build.mjs, so these rules had no element left to style. */

/* 7/5 editorial split */
.editorial {
  display: grid;
  grid-template-columns: 7fr 5fr;
  gap: clamp(32px, 6vw, 96px);
  align-items: start;
}

/* content beside a data block */
.split {
  display: grid;
  grid-template-columns: 1fr 380px;
  gap: clamp(32px, 5vw, 72px);
  align-items: start;
}

/* generic plate grid on a hairline background */
.plate-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(260px, 1fr));
  gap: var(--s4);
}
.plate-grid > * { border: 1px solid var(--rule); border-radius: var(--r-md); }
.plate-grid > .card.static { padding: clamp(24px, 3vw, 36px); }

.stack > * + * { margin-top: var(--s5); }


/* == 06  chrome =========================================================== */

/* Slim sticky header. Paper at 92% with a blur so photography scrolls under
   it; the shadow class is added by the script once the page has scrolled. */
/* Two states, cross-faded at 300ms. At the top of the page the header is
   transparent and sits over the hero; once the page has scrolled under it,
   a white plate with a soft shadow fades in. The plate is a pseudo-element
   whose OPACITY animates — so the fade is transform/opacity only, and the
   sticky header never changes size. The photo heroes pull up under it by
   the header's height (see .phero). */
/* ---- the black bar ----
   Restored 19 Aug 2026 from `main`, where it lived before the theme rebuild.
   The original also carried the street address; the owner asked for the trade
   statement and the number only, so the address stays in the footer where a
   buyer looks for it. "Wholesale only" is written in sentence case and set in
   caps, rather than typed in caps, so a screen reader says it as a phrase. */
/* --slate, not --ink. --ink is the colour of TEXT; --slate is the colour of a
   dark SURFACE, and the footer and the stat band are already using it. Two
   near-blacks a few units apart doing the same job is precisely the kind of
   undecided value this pass is removing. */
.topbar {
  background: var(--slate);
  color: var(--on-slate);
  font-size: 12.5px;
  letter-spacing: .02em;
}
.topbar .wrap {
  display: flex;
  align-items: center;
  gap: var(--s6);
  min-height: var(--bar-h);
  flex-wrap: wrap;
}
/* Sentence case and bold, as the bar has always been set — the address
   sits beside it and an all-caps label would shout over it. */
.tb-label { color: #fff; font-weight: 600; }
.tb-addr { white-space: nowrap; }
.topbar .spacer { margin-left: auto; }
.topbar a { color: inherit; text-decoration: none; transition: color 300ms var(--ease); }
.topbar a:hover { color: #fff; }

.masthead {
  position: sticky;
  top: 0;
  z-index: 40;
  height: var(--head-h);
  background: transparent;
}
.masthead::before {
  content: "";
  position: absolute;
  inset: 0;
  /* .86, not .96 — enough translucency that content scrolling beneath reads
     as frost through the blur instead of vanishing behind a white plate */
  background: rgba(255,255,255,.86);
  -webkit-backdrop-filter: saturate(180%) blur(16px);
  backdrop-filter: saturate(180%) blur(16px);
  box-shadow: 0 1px 0 rgba(22,24,28,.06), 0 8px 28px rgba(22,24,28,.06);
  opacity: 0;
  transition: opacity 300ms var(--ease-io);
  pointer-events: none;
}
.masthead.scrolled::before { opacity: 1; }
/* pages that do not open on a photograph get the plate straight away */
.masthead.solid::before { opacity: 1; }
.masthead > .wrap {
  position: relative;
  height: 100%;
  display: flex;
  align-items: center;
  gap: var(--s6);
}

.logo {
  display: inline-flex;
  align-items: baseline;
  gap: 6px;
  font-size: 19px;
  font-weight: 800;
  letter-spacing: -.02em;
  text-decoration: none;
  line-height: 1;
  white-space: nowrap;
}
.logo span { color: var(--red); }
/* The parent company's mark, in the masthead and the footer. The <a> is a
   baseline flex row built for type; an image has no baseline worth aligning
   to, so the mark centres instead and the tagline beside it keeps its own
   baseline via the small's own alignment. */
.logo:has(.housemark) { align-items: center; }
/* No `width: auto` here. The delivered SVG carries a viewBox and no width or
   height of its own, so with `width: auto` the browser sizes it by the
   default-sizing algorithm and ignores the width/height attributes entirely —
   which put a 197px mark in a 64px masthead. The attributes size it;
   max-width lets it shrink; height: auto keeps the ratio while it does. */
.housemark { display: block; max-width: 100%; height: auto; }
/* The typed lockup, kept as the fallback for a missing logo file. */
.logo-type span { color: var(--red); }

nav.main {
  margin-left: auto;
  display: flex;
  gap: clamp(18px, 2.2vw, 30px);
  align-items: center;
}
nav.main a {
  position: relative;
  text-decoration: none;
  font-size: 13.5px;
  font-weight: 500;
  letter-spacing: -.005em;
  color: var(--ink-2);
  padding: 6px 0;
  transition: color 300ms var(--ease);
}
nav.main a::after {
  content: "";
  position: absolute;
  left: 0; right: 0; bottom: 0;
  height: 1.5px;
  background: var(--red);
  transform: scaleX(0);
  transform-origin: left;
  transition: transform 400ms var(--ease);
}
nav.main a:hover { color: var(--ink); }
nav.main a:hover::after,
nav.main a[aria-current]::after { transform: scaleX(1); }
nav.main a[aria-current] { color: var(--ink); }

.masthead-cta { flex: none; }

/* Mobile menu — a <details>, so it opens without JS. The script only adds
   the scroll lock. Hidden on desktop; the desktop nav is hidden on mobile, so
   only one set of links is in the accessibility tree at a time. */
.menu { display: none; margin-left: auto; }
.menu > summary {
  list-style: none;
  cursor: pointer;
  width: 40px; height: 40px;
  display: grid; place-items: center;
  margin-right: -8px;
}
.menu > summary::-webkit-details-marker { display: none; }
.menu .burger,
.menu .burger::before,
.menu .burger::after {
  display: block;
  width: 22px; height: 1.5px;
  background: var(--ink);
  transition: transform 400ms var(--ease), opacity 300ms var(--ease);
}
.menu .burger { position: relative; }
.menu .burger::before,
.menu .burger::after { content: ""; position: absolute; left: 0; }
.menu .burger::before { top: -7px; }
.menu .burger::after { top: 7px; }
.menu[open] .burger { background: transparent; }
.menu[open] .burger::before { transform: translateY(7px) rotate(45deg); }
.menu[open] .burger::after { transform: translateY(-7px) rotate(-45deg); }
@media (prefers-reduced-motion: no-preference) {
  .menu[open] .menu-panel { animation: drawer-in 360ms var(--ease); }
  @keyframes drawer-in {
    from { opacity: 0; transform: translateY(-10px); }
  }
}
/* The drawer starts at the masthead's REAL bottom edge. --head-h is the
   masthead alone (64px); the topbar sits above it and is not sticky, so at
   scroll-top the header actually ends at --bar-h + --head-h and a panel
   pinned to --head-h opened straight across the middle of the logo. The
   script writes the measured edge into --menu-top when the panel opens,
   which is exact at any scroll position; the fallback is the scroll-top
   figure, so a no-JS <details> — which is how this menu is built to open —
   still clears the logo. */
.menu-panel {
  position: fixed;
  left: 0; right: 0;
  top: var(--menu-top, calc(var(--bar-h) + var(--head-h)));
  bottom: 0;
  background: var(--paper);
}
/* the drawer frosts where the platform can afford it; paper where it cannot */
@supports ((-webkit-backdrop-filter: blur(1px)) or (backdrop-filter: blur(1px))) {
  .menu-panel {
    background: rgba(255,255,255,.86);
    -webkit-backdrop-filter: saturate(180%) blur(24px);
    backdrop-filter: saturate(180%) blur(24px);
  }
}
.menu-panel {
  padding: var(--s6) var(--gutter) var(--s9);
  overflow-y: auto;
  display: flex;
  flex-direction: column;
  gap: var(--s2);
}
/* `> a:not(.btn)`, not `a`. The old selector was plain `.menu-panel a`, so
   it also matched the Order wholesale <a class="btn">, and at 0,1,1 it beat
   .btn's own 0,1,0 — handing the button 28px display type and `padding:
   12px 0` with NO horizontal padding. That is why it read as spanning the
   panel: it was a display-sized line of text, not a button. The foot links
   sit inside <p class="menu-foot">, so the child combinator leaves them to
   their own rule.

   19px at 375 and 390, not 28: five nav items at display scale is a
   shout, and the panel is a list of five places to go, not a headline.
   14px of vertical padding keeps each row at ~51px, clear of the 44px
   touch target. */
.menu-panel > a:not(.btn) {
  text-decoration: none;
  font-size: clamp(19px, 5vw, 23px);
  font-weight: 600;
  letter-spacing: -.02em;
  line-height: 1.2;
  padding: 14px 0;
  border-bottom: 1px solid var(--rule);
}
.menu-panel a[aria-current] { color: var(--red); }
.menu-panel .menu-foot {
  margin-top: auto;
  padding-top: var(--s6);
  font-size: 14px;
  color: var(--ink-3);
}
.menu-panel .menu-foot a { font-size: 14px; font-weight: 500; border: 0; padding: 0; color: var(--ink); }
/* Shrink-wrapped and pill-shaped again, a touch larger than the base 14px
   so it still reads as the one action in the panel. */
.menu-panel .btn {
  margin-top: var(--s5);
  align-self: flex-start;
  font-size: 15px;
  padding: 16px 26px;
}

.crumb {
  font-family: var(--sans);
  font-size: 13px;
  color: var(--ink-3);
  padding: var(--s5) 0 0;
  margin: 0;
  max-width: none;
}
.crumb a { text-decoration: none; }
.crumb a:hover { color: var(--red); }

/* Footer — slate, three columns, brand marks in a row beneath. */
footer.site {
  background-color: var(--slate);
  background-image: linear-gradient(180deg, #121419, #0B0D10);
  color: var(--on-slate);
  font-size: 14.5px;
  padding: clamp(64px, 8vw, 112px) 0 var(--s7);
}
footer.site .foot-grid {
  display: grid;
  grid-template-columns: 1.4fr 1fr 1fr 1.2fr;
  gap: var(--s7) var(--s6);
  padding-bottom: clamp(48px, 6vw, 88px);
  border-bottom: 1px solid var(--slate-rule);
}
footer.site .foot-brand .logo { color: #fff; font-size: 22px; }
/* The footer is `site light` — bone, not slate — so this is the colour cut,
   the same one as the masthead. */
footer.site .foot-brand .housemark { margin-bottom: var(--s2); }
/* About: the company signing the section that is about the company. */
.house-sig { margin-bottom: var(--s5); }
footer.site .foot-brand p { color: var(--on-slate-2); font-size: 14px; max-width: 30ch; margin: var(--s4) 0 0; }
footer.site .fhead {
  font-family: var(--sans);
  font-size: 12px;
  font-weight: 600;
  letter-spacing: .08em;
  text-transform: uppercase;
  color: var(--on-slate-2);
  margin: 0 0 var(--s4);
}
footer.site ul { list-style: none; margin: 0; padding: 0; }
footer.site li { margin: 0 0 var(--s2); }
footer.site a {
  color: var(--on-slate);
  text-decoration: none;
  transition: color 300ms var(--ease);
}
footer.site a:hover { color: #fff; }
footer.site .nolink { color: var(--on-slate-2); }
footer.site address { font-style: normal; line-height: 1.7; }
.foot-logos {
  display: flex;
  align-items: center;
  gap: clamp(28px, 4vw, 64px);
  flex-wrap: wrap;
  padding-top: clamp(32px, 4vw, 56px);
  padding-bottom: clamp(32px, 4vw, 56px);
  border-bottom: 1px solid var(--slate-rule);
}
.foot-logos a { line-height: 0; opacity: .78; transition: opacity 300ms var(--ease); }
.foot-logos a:hover { opacity: 1; }
/* EliteMed has no white lockup yet (see the open-items list); its mark on
   dark ground is a wordmark set in bone, matched to the weight of the
   others. Applied everywhere a brand row sits on slate. */
.wordmark {
  display: inline-block;
  font-family: var(--sans);
  font-weight: 400;
  font-size: 22px;
  letter-spacing: -.01em;
  color: var(--shell);
  line-height: 1;
}
.wordmark small {
  display: block;
  font-size: 9.5px;
  letter-spacing: .3em;
  text-transform: uppercase;
  margin-top: 4px;
  color: var(--on-slate-2);
  font-weight: 500;
}
.foot-legal {
  display: flex;
  justify-content: space-between;
  gap: var(--s4);
  flex-wrap: wrap;
  padding-top: var(--s5);
  font-family: var(--sans);
  font-size: 12.5px;
  color: var(--on-slate-2);
  letter-spacing: 0;
}


/* == 07  buttons ========================================================== */

.btn {
  display: inline-flex;
  align-items: center;
  gap: var(--s3);
  background: var(--red);
  color: #fff;
  text-decoration: none;
  font-weight: 600;
  font-size: 14px;
  letter-spacing: 0;
  padding: 16px 28px;
  border: 0;
  cursor: pointer;
  border-radius: var(--r-pill);
  /* touch-action kills the legacy double-tap-zoom wait on the tap path.
     The transform duration here is the RELEASE; the press itself overrides it
     to 100ms below, because an expo-out over 400ms reads as lag when it is the
     thing answering your finger. */
  touch-action: manipulation;
  /* tonal, one hue: the same red lit from above. background-color stays the
     fallback for anything that cannot draw the gradient. */
  background-image: linear-gradient(180deg, #DA1F38, #C30E27);
  box-shadow: 0 1px 2px rgba(120, 8, 22, .22), 0 8px 20px -10px rgba(120, 8, 22, .5);
  transition: background 300ms var(--ease), background-image 300ms var(--ease),
              box-shadow 300ms var(--ease), transform 260ms var(--ease);
}
.btn:hover {
  background: var(--red-deep);
  background-image: linear-gradient(180deg, #C51229, #A80D22);
  box-shadow: 0 1px 2px rgba(120, 8, 22, .25), 0 12px 26px -10px rgba(120, 8, 22, .55);
  transform: translateY(-1px);
}
.btn .arw { transition: transform 400ms var(--ease); }
.btn.ghost, .btn.on-ink, .btn.ghost.on-ink { background-image: none; box-shadow: none; }
.btn:hover .arw { transform: translateX(4px); }

.btn.ghost {
  background: transparent;
  color: var(--ink);
  box-shadow: inset 0 0 0 1px var(--ink);
}
.btn.ghost:hover { background: var(--ink); color: #fff; }

.btn.on-ink { background: #fff; color: var(--ink); }
.btn.on-ink:hover { background: var(--shell); }
.btn.ghost.on-ink { background: transparent; color: #fff; box-shadow: inset 0 0 0 1px rgba(255,255,255,.55); }
.btn.ghost.on-ink:hover { background: #fff; color: var(--ink); }

.btn.lg { padding: 20px 36px; font-size: 15px; }
.btn.sm { padding: 10px 18px; font-size: 13px; }

/* quiet text link with a rule under it */
.tlink {
  display: inline-flex;
  align-items: center;
  gap: var(--s2);
  font-family: var(--sans);
  font-weight: 500;
  font-size: 14px;
  text-decoration: none;
  border-bottom: 1px solid var(--rule);
  padding-bottom: 4px;
  transition: color 300ms var(--ease), border-color 300ms var(--ease);
}
.tlink:hover { border-color: var(--red); color: var(--red); }
.tlink .arw { transition: transform 400ms var(--ease); }
.tlink:hover .arw { transform: translateX(3px); }
.tlink.on-ink { color: #fff; border-color: var(--slate-rule); }
.tlink.on-ink:hover { color: #fff; border-color: #fff; }


/* ---- the press state ----
   Hover is a desktop-only conversation. Everything above answered the mouse and
   nothing answered a finger, so on a phone the whole order funnel had only the
   browser's default grey tap flash to show for a press — and `.btn` and `.card`
   were each declaring a transform transition they never used, promising an
   animation that did not exist.

   The rule (Apple, Designing Fluid Interfaces): feedback belongs on pointer
   DOWN, not on release, and it has to be immediate. So the press is 100ms in
   and eases back out over the longer duration each element already declares —
   fast to acknowledge, unhurried to let go.

   Scale is kept small deliberately. These are wide surfaces; 3% on a button
   reads as a press, and the same 3% on a catalog card would read as the card
   flinching, so the card gets 0.5% and leans on its background shift instead. */
.btn:active,
.filter-toggle:active,
.thumb:active,
.slide-dots button:active {
  transform: scale(.97);
  transition-duration: 100ms;
}
.btn.lg:active { transform: scale(.98); }   /* bigger target, same apparent travel */
.card:active {
  transform: scale(.995);
  background: var(--shell);
  transition-duration: 100ms;
}
.card.static:active { transform: none; }    /* not a link — nothing to press */
.tlink:active { color: var(--red); border-color: var(--red); transition-duration: 100ms; }
.tlink.on-ink:active { color: #fff; border-color: #fff; }
.style-brandmark:active,
.foot-logos a:active,
.logo-row a:active { opacity: 1; transform: scale(.97); transition-duration: 100ms; }
/* Filter rows are labels, so the press has to be felt on the row, not the box. */
.opt:active, .sw-opt:active { transform: scale(.99); transition-duration: 100ms; }
/* the two card-anchors that had a hover lift but no press (found by the
   opportunity sweep, 19 Aug 2026) — same values as .card:active above */
.brand-card:active { transform: scale(.985); transition-duration: 100ms; }
.bplate:active { transform: scale(.995); transition-duration: 100ms; }

/* Reduced motion: keep the acknowledgement, drop the movement. The global
   reduce block already flattens every duration, so what is left to do here is
   remove the scale and let the colour shift carry the press on its own. */
@media (prefers-reduced-motion: reduce) {
  .btn:active,
  .btn.lg:active,
  .filter-toggle:active,
  .thumb:active,
  .slide-dots button:active,
  .card:active,
  .style-brandmark:active,
  .foot-logos a:active,
  .logo-row a:active,
  .opt:active,
  .sw-opt:active { transform: none; }
  .btn:active { background: var(--red-deep); }
  .filter-toggle:active { opacity: .82; }
}


/* == 08  photography ======================================================
   Model and fabric photography, WebP, from public/photos/. Every <img> here
   carries width/height and a srcset; the box owns the ratio.

   THE SHARPNESS RULE. The largest model file is 848 x 1264. A photograph is
   never displayed wider than its native width: the media column of a hero is
   min(50%, native px), rail cards are capped at 430px, band figures live in
   the wrap. Where a frame is wider than the photo, the frame is filled with
   the photo's own studio ground (sampled at import, --hp-bg) as a flat bleed,
   the photo sits off-centre against it, and its inner edge is feathered into
   the ground so the wall runs on and the floor fades rather than stopping at
   a hard line. Sharp and letterboxed beats full-bleed and blurry. */

.ph { width: 100%; height: 100%; object-fit: cover; display: block; }

/* ---- photo hero ----
   Full height: 100svh on desktop, 88svh on mobile. The header is transparent
   over it and the hero pulls up under the header by its height, so the
   photograph runs from the very top of the page.

   Desktop: two columns on the flat ground — copy left, photograph right. The
   media column is min(50%, native width, the width the photo would have at
   full height): a full-length shot therefore stands whole, feet in, and a
   three-quarter shot is cropped no lower than the hip. It is never scaled
   past its native pixels; the frame beyond it is the photo's own studio
   ground, and the photo's left edge is feathered a long way into it so
   there is no visible seam.

   Mobile (≤ 900px): the photograph fills the whole 88svh box (cover, no
   upscale — a 375-wide slot needs far fewer pixels than we have) with the
   copy set over its lower third on a scrim, Josmo-style.

   THE FACE RULE (owner, 17 Aug 2026). Every model is photographed with the
   head near the top of the frame, so the photograph starts one header-height
   DOWN and the strip above it is the photo's own studio ground. The
   transparent header therefore sits on quiet wall rather than on a face, and
   the media column is sized against (100svh - header) so the frame still
   matches the photograph's own aspect exactly and crops nothing. This
   replaces the 160px ground wash that used to be painted over the top of the
   picture: at 90% opacity across 22% of the frame it was rubbing out the
   very faces it was drawn to protect. */
.phero {
  position: relative;
  background: var(--hp-bg, var(--paper));
  color: var(--ink);
  display: grid;
  grid-template-columns: 1fr min(50%, calc(var(--hp-w, 848) * 1px), calc((100svh - var(--bar-h) - var(--head-h)) * var(--hp-w, 848) / var(--hp-h, 1264)));
  height: calc(100svh - var(--bar-h));
  min-height: 560px;
  margin-top: calc(-1 * var(--head-h));
  overflow: hidden;
}
/* (the compact .phero.short variant is gone — the catalog and how-to-order
   pages open with .spec-head now, and nothing else used it) */
.phero-copy {
  align-self: center;
  padding: calc(var(--head-h) + var(--s7)) var(--gutter) var(--s7);
  padding-left: max(var(--gutter), calc((100vw - var(--wrap-wide)) / 2 + var(--gutter)));
  max-width: 100%;
  min-width: 0;
}
.hl-s { display: block; }
/* One phrase of a headline in the house red (20 Aug 2026). The brand accents
   were tried here and are muted at display size — see hl() in build.mjs. */
/* ---- the coloured phrase in a headline ----
   One phrase per heading, never a whole line, and the colour is INHERITED
   rather than passed: --hl defaults to the house red, and a scope that owns
   a colour sets it once. A brand page sets --hl to its own accentInk on the
   wrapper that already carries --accent, so every heading inside it reads in
   that brand without a single call site changing.

   --hl-ink is the lifted red for the dark field. Denice red on #13161A is
   3.1:1, which large text technically clears and which reads as a dull
   maroon against a lit ground. This is the same red -- hue 352, saturation
   .85 -- with only its lightness raised to .66, the same operation
   accentInk() runs for white paper. It measures 5.8:1 on the field. */
.hl-c { color: var(--hl, var(--red)); }
.darkfield, .cta-close { --hl: #F25F72; }
.phero-copy h1 {
  font-size: var(--t-hero);
  font-weight: 800;
  letter-spacing: var(--t-hero-tr);
  line-height: var(--t-hero-lh);
  max-width: 15ch;
  margin-bottom: var(--s5);
  /* never a single word on a line: balance the rag, keep two words minimum
     per line via the width and size above; text-wrap: pretty as the fallback
     that avoids an orphan on the last line */
  text-wrap: balance;
}
.phero-copy .lede { margin-bottom: var(--s2); max-width: 44ch; color: var(--ink-2); }
.phero-copy .brand-h1 { line-height: 0; margin: 0 0 var(--s5); }
.phero-copy .brand-h1 img { max-width: min(100%, 420px); height: auto; }
.phero-label {
  position: absolute;
  top: calc(var(--head-h) + var(--s5));
  left: max(var(--gutter), calc((100vw - var(--wrap-wide)) / 2 + var(--gutter)));
  font-family: var(--sans);
  font-size: 12px;
  font-weight: 600;
  letter-spacing: .08em;
  text-transform: uppercase;
  color: var(--ink-3);
  z-index: 2;
  margin: 0;
}
.phero-media {
  position: relative;
  overflow: hidden;
  height: 100%;
}
.phero-media img {
  position: absolute;
  top: var(--head-h);
  left: 0;
  width: 100%;
  height: calc(100% - var(--head-h));
  object-fit: cover;
  object-position: var(--hp-pos, 50% 0%);
  -webkit-mask-image: linear-gradient(to right, transparent, #000 40%);
          mask-image: linear-gradient(to right, transparent, #000 40%);
}

/* the mobile scrim — only painted in the stacked layout (see 16) */
.phero-media::after {
  content: "";
  position: absolute;
  inset: 0;
  background: linear-gradient(to top, rgba(15,17,20,.72) 0%, rgba(15,17,20,.28) 42%, rgba(15,17,20,0) 68%);
  opacity: 0;
  pointer-events: none;
}

/* ---- the spec header — the title block on /catalog/ and /order/ ----
   The spec-sheet register speaking for itself: the thin red callout rule
   drawn under the H1, and the page's hard facts set as a mono title block the
   way a CAD sheet carries them, with a red registration tick on its corner.

   On PAPER, not slate (owner, 20 Aug 2026: the near-black field was heavy
   and matched nothing else). The owner offered white or the homepage brand
   sections' treatment — which are themselves on paper, so the two options
   converge. White is also where this register already lives: the style pages
   set the same red rules and mono blocks on white, and an actual CAD sheet
   is a white document. A bottom hairline ends the header, because sections
   below do not draw their own top rule. */
.spec-head {
  position: relative;
  overflow: hidden;
  /* The spec title block is on paper (owner, 20 Aug 2026) and stays there. */
  background-color: var(--paper);
  color: var(--ink);
  padding: clamp(56px, 8vw, 116px) 0 clamp(48px, 6vw, 92px);
  border-bottom: 1px solid var(--rule);
}
.spec-head .wrap {
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto;
  gap: clamp(32px, 6vw, 96px);
  /* CENTERED against the heading block (owner, 20 Aug 2026) — align-items:
     end hung the title block off the baseline of whatever the copy column
     happened to end with, which read as a panel that had slipped. */
  align-items: center;
}
.spec-head h1 {
  font-size: var(--t-d1);
  font-weight: 800;
  letter-spacing: var(--t-d1-tr);
  line-height: var(--t-d1-lh);
  color: var(--ink);
  max-width: 18ch;
  text-wrap: balance;
}
/* the register's signature: a thin red callout rule under the title. It
   DRAWS itself once on load — the page's one authored moment, transform-only
   and visible by default, so no-JS and reduced-motion readers just see it. */
.spec-head h1::after {
  content: "";
  display: block;
  width: 64px;
  height: 3px;
  background: var(--red);
  margin-top: clamp(16px, 2vw, 26px);
  transform-origin: 0 50%;
}
@media (prefers-reduced-motion: no-preference) {
  .js .spec-head h1::after { transform: scaleX(0); }
  .js.ready .spec-head h1::after {
    transform: scaleX(1);
    transition: transform 700ms var(--ease) 350ms;
  }
}
.spec-head .lede { color: var(--ink-2); max-width: 54ch; margin-top: var(--s5); }
.spec-head .hero-actions { display: flex; flex-wrap: wrap; gap: var(--s3); margin-top: var(--s6); }
/* The title block on paper: the shell — the studio-wall tint, the site's one
   tinted ground — gives the panel a surface without a heavy frame, and the
   hairlines inside are the shell's own rule weight. The red registration
   tick carries over unchanged; red on white is this register's native ink. */
.spec-head-meta {
  position: relative;
  margin: 0;
  min-width: min(300px, 100%);
  padding: clamp(18px, 2vw, 26px) clamp(20px, 2.2vw, 30px);
  border: 1px solid var(--rule-shell);
  border-radius: var(--r-sm);
  background: var(--shell);
}
/* the registration tick */
.spec-head-meta::before {
  content: "";
  position: absolute;
  top: -1px;
  left: -1px;
  width: 14px;
  height: 14px;
  border-top: 2px solid var(--red);
  border-left: 2px solid var(--red);
  border-top-left-radius: var(--r-sm);
}
.spec-head-meta div {
  display: flex;
  justify-content: space-between;
  align-items: baseline;
  gap: var(--s6);
  padding: 10px 0;
  border-top: 1px solid var(--rule-shell);
}
.spec-head-meta div:first-child { border-top: 0; padding-top: 0; }
.spec-head-meta div:last-child { padding-bottom: 0; }
.spec-head-meta dt {
  font-family: var(--sans);
  font-size: 11px;
  font-weight: 500;
  letter-spacing: .08em;
  text-transform: uppercase;
  color: var(--ink-3);
}
.spec-head-meta dd { margin: 0; font-family: var(--mono); font-size: 13px; color: var(--ink); white-space: nowrap; }
@media (max-width: 900px) {
  .spec-head .wrap { grid-template-columns: 1fr; align-items: start; }
  .spec-head-meta { justify-self: start; min-width: min(340px, 100%); }
}

/* ---- image-and-text bands ----
   Josmo's rhythm: image seven columns, text five, sides swapping down the
   page. The image is a framed plate at 4:5, sized by the grid, so it is
   always narrower than the wrap and never wider than the photo. */
.band {
  display: grid;
  grid-template-columns: 7fr 5fr;
  gap: clamp(48px, 8vw, 144px);
  align-items: center;
}
.band.flip { grid-template-columns: 5fr 7fr; }
.band.flip > .band-copy { order: -1; }
.band-media {
  position: relative;
  aspect-ratio: var(--band-ratio, 4 / 5);
  overflow: hidden;
  border-radius: var(--r-md);
  background: var(--hp-bg, var(--paper));
}
.band-media img { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover; object-position: var(--hp-pos, 50% 0%); }
.band-media.wide { --band-ratio: 3 / 2; }
.band-copy h2 { font-size: var(--t-d2); margin-bottom: var(--s5); }
.band-copy .lede { margin-bottom: var(--s5); }
.band-copy .prose { color: var(--ink-2); }
.band + .band { margin-top: var(--band-gap); }

/* ---- the brand rail ----
   Replaces the pinned brand ladder (owner, 19 Aug 2026). One normal section:
   a horizontal scroll-snap track of five photograph cards, paddles and a
   01/05 index in the section head. The track is native scroll — it browses
   with no JS and under reduced motion — and the next card peeks in from the
   right edge, which is what invites the swipe. Nothing pins any more;
   NOTHING on the site pins.

   Each card carries exactly one accent: the 2px seam between photograph and
   copy, repeated by the fabric name's ink and the hover border. The photo
   box is 4:5 of a card never wider than 430px, so the 848px files are always
   sampled below native width, and the crop anchor stays 50% 0% — cover takes
   its crop off the bottom, never off a head. */
.brands-rail {
  padding: var(--sect-y) 0 var(--sect-y-tight);
  position: relative;
  background-color: var(--paper);
}
.rail-head {
  max-width: var(--wrap-wide);
  margin: 0 auto clamp(28px, 4vw, 52px);
  padding: 0 var(--gutter);
  display: flex;
  align-items: flex-end;
  justify-content: space-between;
  gap: var(--s5);
}
.rail-head .section-head { margin: 0; }
.rail-nav { display: flex; align-items: center; gap: var(--s2); }
.rail-index {
  font-family: var(--sans);
  font-size: 12px;
  letter-spacing: .06em;
  color: var(--ink-4);
  margin: 0 var(--s3) 0 0;
  font-variant-numeric: tabular-nums;
}
.rail-index b { font-weight: 600; color: var(--ink-3); }
.rail-btn {
  width: 46px;
  height: 46px;
  display: grid;
  place-items: center;
  border: 1px solid var(--rule);
  border-radius: var(--r-pill);
  background: var(--paper);
  color: var(--ink);
  font-size: 17px;
  line-height: 1;
  cursor: pointer;
  box-shadow: var(--lift-1);
  /* the press must be instant — the transform gets its own fast track while
     the colors keep the house pace */
  transition: background 300ms var(--ease), color 300ms var(--ease),
              opacity 300ms var(--ease), transform 160ms var(--ease);
}
@media (hover: hover) and (pointer: fine) {
  .rail-btn:hover { background: var(--ink); color: #fff; }
}
.rail-btn:active { transform: scale(.96); }
.rail-btn[disabled] { opacity: .3; pointer-events: none; }
/* the paddles and the index are wired by the script; without it the track
   still scrolls, so only the dead chrome is hidden */
html:not(.js) .rail-nav { display: none; }
.rail-track {
  display: flex;
  gap: clamp(16px, 2vw, 28px);
  overflow-x: auto;
  /* the leading pad aligns card one with the wrap; the same pad trails so
     card five can snap clear of the right edge. 12px top / 28px bottom keep
     the lift shadows and the hover rise out of the clip. */
  padding: 12px max(var(--gutter), calc((100vw - var(--wrap-wide)) / 2 + var(--gutter))) 28px;
  scroll-snap-type: x mandatory;
  scroll-padding-inline: max(var(--gutter), calc((100vw - var(--wrap-wide)) / 2 + var(--gutter)));
  scrollbar-width: none;
}
.rail-track::-webkit-scrollbar { display: none; }
.rail-track:focus-visible { outline: 2px solid var(--red); outline-offset: -2px; }
.rail-card {
  flex: none;
  width: clamp(290px, 26vw, 430px);
  scroll-snap-align: start;
  display: flex;
  flex-direction: column;
  background: var(--paper);
  border: 1px solid var(--rule);
  border-radius: var(--r-lg);
  overflow: hidden;
  text-decoration: none;
  color: inherit;
  box-shadow: var(--lift-1);
  transition: transform 500ms var(--ease), box-shadow 500ms var(--ease),
              border-color 500ms var(--ease);
}
@media (hover: hover) and (pointer: fine) {
  .rail-card:hover { transform: translateY(-6px); border-color: var(--accent); box-shadow: var(--lift-2); }
  .rail-card:hover .rail-media img { transform: scale(1.04); }
  .rail-card:hover .tlink .arw { transform: translateX(4px); }
}
.rail-media {
  position: relative;
  aspect-ratio: 4 / 5;
  overflow: hidden;
  background: var(--hp-bg, var(--shell));
  /* the one accent: a seam between the photograph and the copy */
  border-bottom: 2px solid var(--accent);
}
.rail-media img {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  object-position: var(--hp-pos, 50% 0%);
  /* growth goes down into the garment, never up into a head */
  transform-origin: 50% 0%;
  transition: transform 900ms var(--ease);
}
.rail-body {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  flex: 1;
  padding: clamp(20px, 2.2vw, 30px);
}
.rail-body .brandmark { max-width: min(100%, 210px); height: auto; margin-bottom: var(--s4); }
.rail-body .line {
  font-family: var(--display);
  font-size: var(--t-d3);
  font-weight: 700;
  letter-spacing: var(--t-d3-tr);
  line-height: var(--t-d3-lh);
  margin: 0 0 var(--s3);
  color: var(--ink);
}
.rail-body .fabric {
  font-size: 14px;
  line-height: 1.5;
  color: var(--ink-3);
  margin: 0 0 var(--s5);
}
.rail-body .fabric b { font-weight: 600; color: var(--accent-ink, var(--ink)); }
.rail-body .tlink { margin-top: auto; }
.rail-body .tlink .arw { display: inline-block; transition: transform 400ms var(--ease); }
@media (max-width: 720px) {
  /* the head wraps: heading on its own line, index and paddles beneath it */
  .rail-head { flex-wrap: wrap; }
  .rail-nav { margin-left: auto; }
  .rail-btn { width: 42px; height: 42px; }
}

/* ---- the dark field ----
   The ground the glass actually needs. Frosted glass over flat black has
   nothing behind it to blur, so it reads as a bordered dark card — the
   failure that shipped twice. What is under the glass:

     1. three very large overlapping radial glows on slate, each falling off
        to zero alpha, so brightness varies across the whole width rather
        than sitting flat;
     2. two larger soft glows that drift, so the ground under the glass is
        not even static.

   All CSS. The obvious ground was a fabric macro, and the smallest usable
   file is 277KB of decoration on the home page; light carries the same idea
   for nothing.

   Two things were tried here and removed, both by the owner on 20 Aug 2026:
   a deep blue-green base (it belonged to nothing else on the site — slate,
   the site's own dark surface, replaced it), and a 45-degree twill over the
   whole band (it read as a pattern printed on a surface; the band is meant
   to read as depth). Do not reinstate either. */
.darkfield {
  position: relative;
  overflow: hidden;
  isolation: isolate;
  color: #fff;
  /* SLATE (owner, 20 Aug 2026, chosen from three candidates). The field is the
     site's own dark surface — the same graphite as the top bar and the footer
     — rather than a hue that belonged to nothing else on the page. Because it
     is near-neutral, all five brand accents are legible against it, which the
     blue field could not manage: it swallowed ScrubAid's blue entirely.

     ATMOSPHERE, NOT TEXTURE (owner, same date). The 45-degree twill that used
     to sit over this is gone. What replaces it is light: three very large,
     very soft radial glows that fall off to nothing, so the band reads as
     depth rather than as a surface with a pattern printed on it. */
  background-color: var(--slate-field, #13161A);
  /* Three notes on why these are written the way they are.

     1. Each ramp ends at the SAME colour with zero alpha, never at the keyword
        `transparent`. It is the constant-hue trick: a ramp that only changes
        opacity has no chroma shift to quantise, so the steps it can produce
        are both fewer and less visible than a ramp that also travels in hue.
     2. There is no linear-gradient across the band any more. A single linear
        ramp over 900px of a near-neutral was the one layer most likely to
        band, because its steps are long, straight and parallel — exactly what
        the eye picks out. Three overlapping radials put their steps at
        different angles and different spacings, so no two coincide.
     3. The radii are deliberately larger than the band. A glow that finishes
        inside the frame draws an edge; these are cut off by the frame instead,
        so there is no visible boundary anywhere. */
  background-image:
    radial-gradient(120% 96% at 22% 6%,  rgba(149, 161, 181, .22), rgba(149, 161, 181, 0) 66%),
    radial-gradient(104% 84% at 88% 46%, rgba(101, 112, 131, .18), rgba(101, 112, 131, 0) 64%),
    radial-gradient(150% 108% at 46% 118%, rgba(74, 83, 98, .26),  rgba(74, 83, 98, 0) 68%);
}
.df-bg { position: absolute; inset: 0; z-index: 0; pointer-events: none; }
.df-bg .orb {
  position: absolute;
  border-radius: 50%;
  filter: blur(80px);
  will-change: transform;
}
/* The two drifting glows, on slate. Wider than before and lower in alpha, so
   they read as air moving rather than as two lamps. The blur is doing double
   duty: it is what makes them atmosphere, and a blurred layer is also the
   cheapest dither on the page — it smears any step in its own ramp across
   80px, and it smears the ramps UNDERNEATH it too where they overlap. */
.df-bg .orb.a {
  width: clamp(460px, 58vw, 940px);
  height: clamp(460px, 58vw, 940px);
  left: 4%;
  top: -12%;
  background: radial-gradient(circle, rgba(166, 176, 194, .26), rgba(166, 176, 194, 0) 70%);
}
.df-bg .orb.b {
  width: clamp(380px, 46vw, 760px);
  height: clamp(380px, 46vw, 760px);
  right: -6%;
  bottom: -16%;
  background: radial-gradient(circle, rgba(208, 213, 222, .14), rgba(208, 213, 222, 0) 70%);
}
@media (prefers-reduced-motion: no-preference) {
  .js .df-bg .orb.a { animation: orb-drift 26s var(--ease-io) infinite alternate; }
  .js .df-bg .orb.b { animation: orb-drift 34s var(--ease-io) infinite alternate-reverse; }
}
@keyframes orb-drift {
  from { transform: translate3d(0, 0, 0); }
  to   { transform: translate3d(9%, 14%, 0); }
}
.darkfield > .wrap { position: relative; z-index: 1; }
.lightfield > .wrap { position: relative; z-index: 1; }

/* ================= the light proof chapter ==============================
   THE DARK BAND GOES PALE (owner, 20 Aug 2026), AND THE WASH UNDER IT GOES
   WHITE (owner, 27 Aug 2026). The proof chapter sits on plain paper like
   every other light band now. The glass went with the wash, and had to: the
   standing rule was that the ground comes before the glass, and a flat white
   ground gives a pane nothing to blur — a backdrop-filter over white is an
   invisible white box. So the reduced-transparency fallback is the ONLY
   state: a solid near-white gradient pane, a hairline, the lit top edge and
   the cast shadow. It reads as a pane by being a step denser than the paper
   around it, which was always what the fallback was for. The drifting orbs
   are gone with the ground they lit.

   Still one slab, not four cards; the gradient-cut numerals (one hue, two
   luminances, owner-requested) and the accentInk() per-figure underlines are
   unchanged. Retailer plates keep their hairline and the ink-tinted --lift
   shadows — a neutral black shadow on a warm ground reads as dirt. */
.lightfield {
  position: relative;
  overflow: hidden;
  isolation: isolate;
  color: var(--ink);
  background-color: var(--paper);
}
.lightfield.proof { padding: clamp(96px, 12vw, 180px) 0 clamp(80px, 10vw, 148px); }

/* ---- the pane ----
   Solid, not glass (owner, 27 Aug 2026) — the former prefers-reduced-
   transparency fallback, promoted to the only state when the wash under the
   band went white. Mixed a step BELOW the ground around it: on a light field
   a pane reads as a pane by being slightly denser than what it sits on. */
.lightfield .stats-panel {
  background: linear-gradient(164deg, #FFFFFF 0%, #F7F9FC 58%, #FFFFFF 100%);
  /* The base .stats-panel (the slate glass, kept for the dark variant) still
     declares a backdrop-filter; behind this opaque pane it can paint nothing,
     but it is switched off rather than left to compositing luck. */
  -webkit-backdrop-filter: none;
          backdrop-filter: none;
  border: 1px solid rgba(20, 22, 26, .10);
  box-shadow:
    0 44px 92px -48px rgba(20, 22, 26, .40),
    0 2px 6px rgba(20, 22, 26, .04),
    inset 0 1px 0 rgba(255,255,255,.95);   /* the lit top edge, kept */
}
.lightfield .stat-cell { border-left: 1px solid rgba(20, 22, 26, .10); }
.lightfield .stat-cell:first-child { border-left: 0; }
/* One hue, two luminances -- in ink now.
   background-IMAGE, not the background shorthand. The shorthand resets
   background-clip to border-box, which un-clips the gradient from the glyphs
   and paints it as a solid rectangle over each figure. */
.lightfield .stat-cell .v {
  background-image: linear-gradient(174deg, var(--ink) 26%, #5B626C);
}
.lightfield .stat-cell .k { color: var(--ink); }
.lightfield .stat-cell .sub { color: var(--ink-3); }

/* ---- the credentials row ---- */
.lightfield .creds .creds-head h2 { color: var(--ink); }
.lightfield .creds .creds-head .micro { color: var(--ink-3); }
.lightfield .creds p { color: var(--ink-3); }
.lightfield .cred-more { color: var(--ink-4); }
.lightfield .cred-more::before,
.lightfield .cred-more::after { background: var(--rule); }
/* THE MARKS STAY IN FULL COLOUR ON WHITE PLATES -- unchanged, and it is a
   standing rule: they ship as delivered and are never re-cut or recoloured,
   and a multi-coloured lockup needs a light plate under it. What changes is
   only how the plate is held off the ground. On slate the plate WAS the
   contrast and needed no border; white on a pale wash needs a hairline to
   have an edge at all, and the shadow goes from a near-black cast to the
   site's own ink-tinted lift -- a neutral black shadow on a warm ground
   reads as dirt, which is why those tokens exist. */
.lightfield .cred-card {
  box-shadow: inset 0 0 0 1px var(--rule), var(--lift-1);
}
.lightfield .cred-card::after { box-shadow: inset 0 0 0 1px var(--rule-2), var(--lift-2); }

/* Selection inside the light chapter goes back to the house pair; the dark
   chapters keep their own, set further down. */
.lightfield ::selection { background: var(--red); color: #fff; }

.darkfield.proof { padding: clamp(80px, 10vw, 148px) 0 clamp(64px, 8vw, 116px); }

/* ---- the stat panel ----
   ONE slab of glass, not four cards. You can see the field continue past all
   four of its edges and shift in luminance behind it, which is what makes a
   pane read as a pane; four separate cards each had ground on every side and
   read as four dark tiles. The figures are cells inside it, divided by
   hairlines.

   On slate the saturate() came down from 185% to 108%. It was there to pull a
   blue field up through the glass, and a near-neutral ground has almost no
   chroma to pull — at 185% it was amplifying the little that remained and
   tinting the pane against the band around it. Brightness does the work
   instead, which is what separates glass from its ground on a grey field. */
.stats-panel {
  display: grid;
  grid-template-columns: repeat(4, 1fr);
  border-radius: var(--r-lg);
  background: linear-gradient(164deg, rgba(255,255,255,.15), rgba(255,255,255,.045) 58%, rgba(255,255,255,.13));
  -webkit-backdrop-filter: blur(26px) saturate(108%) brightness(1.14);
          backdrop-filter: blur(26px) saturate(108%) brightness(1.14);
  border: 1px solid rgba(255,255,255,.155);
  box-shadow:
    0 54px 104px -44px rgba(0, 12, 22, .9),
    inset 0 1px 0 rgba(255,255,255,.34);   /* the lit top edge */
  overflow: hidden;
}
.stat-cell {
  padding: clamp(28px, 3vw, 46px) clamp(22px, 2.4vw, 36px);
  border-left: 1px solid rgba(255,255,255,.13);
}
.stat-cell:first-child { border-left: 0; }
.stat-cell .v {
  display: block;
  font-family: var(--display);
  font-size: clamp(42px, 5.4vw, 84px);
  font-weight: 800;
  letter-spacing: -.03em;
  line-height: .92;
  font-variant-numeric: tabular-nums;
  /* one hue, two luminances — a light gradient, not a color gradient */
  background: linear-gradient(174deg, #FFFFFF 26%, rgba(255,255,255,.52));
  -webkit-background-clip: text;
          background-clip: text;
  color: transparent;
}
/* the per-figure accent underline — one accent per figure */
.stat-cell .v::after {
  content: "";
  display: block;
  width: 40px;
  height: 3px;
  border-radius: 2px;
  margin: clamp(14px, 1.6vw, 22px) 0 var(--s4);
  background: var(--stat-a, var(--red));
}
.stat-cell .k {
  font-family: var(--sans);
  font-size: 12px;
  font-weight: 500;
  letter-spacing: .08em;
  text-transform: uppercase;
  color: #E7E9ED;
}
.stat-cell .sub { display: block; font-size: 14px; line-height: 1.5; color: #C3C8D0; margin-top: var(--s2); }
/* ---- reduced transparency ----
   The platform asked for less see-through, so the pane goes solid and the
   blur is dropped. It does NOT go neutral charcoal, which is what the first
   version did: on a lit blue field a grey slab is the dark rectangle this
   section exists to stop being. The solid is the field's own blue, mixed to
   sit a step ABOVE the ground behind it, and it keeps the lit top edge and
   the border — so the composition still reads as a lit pane on a lit field,
   just without the see-through. The drifting glows stop, the static field
   stays: colour is not the thing being reduced here. */
@media (prefers-reduced-transparency: reduce) {
  .df-bg { display: none; }
  .stats-panel {
    /* The solid is the field's own graphite, mixed a step ABOVE the ground
       behind it, so the pane still reads as a lit pane on a lit field even
       with the see-through gone. */
    background: linear-gradient(164deg, #33383F 0%, #23272D 58%, #2E333A 100%);
    -webkit-backdrop-filter: none;
            backdrop-filter: none;
    border-color: rgba(255,255,255,.20);
  }
}

/* ---- credentials — the chains we supply, their marks on the dark field ----
   The lower half of the proof chapter. It paints no ground of its own: it
   sits inside .darkfield, on the one continuous field that also carries the
   stat panel (20 Aug 2026). Two grounds meeting used to restart the gradient
   at the seam, which is exactly the join a reader notices.

   The marks stay on WHITE plates. A retailer's mark ships as delivered and is
   never re-cut or recoloured, and these are multi-coloured lockups — they
   need a light plate to be legible. So the plates are the brightest thing in
   the chapter and the colour comes through the field around them. */
.creds {
  margin-top: clamp(48px, 5vw, 76px);
}

/* ---- the retailer marquee ----
   Two identical tracks side by side. The pair translates left by exactly one
   track plus one gap, at which point the second track stands where the first
   started and the animation restarts on a pixel-identical frame. That is what
   makes it seamless: the distance comes from the content, not from a number
   somebody tuned by eye. */
.cred-marquee {
  display: flex;
  gap: var(--s5);
  overflow: hidden;
  -webkit-mask-image: linear-gradient(to right, transparent, #000 6%, #000 94%, transparent);
          mask-image: linear-gradient(to right, transparent, #000 6%, #000 94%, transparent);
}
.cred-track {
  display: flex;
  flex: none;
  gap: var(--s5);
  margin: 0;
  padding: 0;
  list-style: none;
}
.cred-track > li { flex: none; width: clamp(150px, 15vw, 210px); }
@keyframes cred-scroll {
  from { transform: translateX(0); }
  to   { transform: translateX(calc(-100% - var(--s5))); }
}
@media (prefers-reduced-motion: no-preference) {
  .js .cred-track { animation: cred-scroll 64s linear infinite; will-change: transform; }
  .cred-marquee:hover .cred-track,
  .cred-marquee:focus-within .cred-track { animation-play-state: paused; }
}
/* reduced motion, or no JS: the same cards, wrapped and still */
@media (prefers-reduced-motion: reduce) {
  .cred-marquee { flex-wrap: wrap; overflow: visible; -webkit-mask-image: none; mask-image: none; }
  .cred-marquee [aria-hidden="true"] { display: none; }
  .cred-track { flex-wrap: wrap; }
}
.creds .creds-head {
  display: flex;
  align-items: baseline;
  gap: var(--s5);
  flex-wrap: wrap;
  margin-bottom: var(--s6);
}
.creds .creds-head h2 { margin: 0; font-size: var(--t-d3); font-weight: 500; color: #fff; }
.creds .creds-head .micro { margin-left: auto; color: #9FB2C2; }
/* The retailers as their own marks, in the brand grid's card: white, hairline
   border, 24px radius, one logo centred with room around it. FIVE columns,
   fixed (owner, 19 Aug 2026): auto-fit made six-and-four at common widths,
   and ten marks read as two clean rows of five or not at all. The closing
   "and many more" strip spans the full row, so the count stays even. */
.cred-grid {
  list-style: none;
  margin: 0; padding: 0;
  display: grid;
  grid-template-columns: repeat(5, 1fr);
  gap: var(--s4);
}
.cred-more {
  grid-column: 1 / -1;
  display: flex;
  align-items: center;
  gap: var(--s4);
  margin-top: var(--s2);
  color: var(--on-slate-2);
  font-family: var(--sans);
  font-size: 13px;
  font-weight: 500;
  letter-spacing: .08em;
  text-transform: uppercase;
}
/* hairlines either side, so it closes the grid rather than floating in it */
.cred-more::before,
.cred-more::after {
  content: "";
  flex: 1;
  height: 1px;
  background: var(--slate-rule);
}
.cred-card {
  position: relative;
  display: grid;
  place-items: center;
  min-height: clamp(100px, 9.5vw, 128px);
  padding: clamp(20px, 2.4vw, 32px);
  background: var(--paper);
  /* on the dark field the border reads as a halo, not a hairline — the plate
     itself is the contrast */
  border: 0;
  border-radius: var(--r-sm);
  /* the plate sits ON the field: a tinted cast shadow, not a black one */
  box-shadow: 0 22px 44px -26px rgba(0, 6, 14, .78);
  transition: transform 500ms var(--ease), box-shadow 500ms var(--ease);
}
.cred-card::after {
  content: "";
  position: absolute; inset: 0;
  border-radius: var(--r-sm);
  box-shadow: 0 30px 58px -22px rgba(0, 6, 14, .85);
  opacity: 0;
  transition: opacity 500ms var(--ease);
  pointer-events: none;
}
/* Hover is gated: on a touch screen :hover fires on tap and sticks, which
   leaves one card lifted after the finger has gone. */
@media (hover: hover) and (pointer: fine) {
  .cred-card:hover { transform: translateY(-4px); }
  .cred-card:hover::after { opacity: 1; }
}
/* The mark is capped on both axes, and the two caps do two different jobs
   (owner, 22 Aug 2026 correction). --logo-k on max-height is the real one —
   it is the measured optical scale from LOGO_K in build.mjs, sized so every
   mark carries roughly the same ink. max-width is a SAFETY NET, not a second
   sizing rule: it exists only to stop a mark from spilling past the card's
   inner edge, and it is set loose enough that height governs for every mark
   whose ink-derived width actually fits.

   It used to be 74%, chosen for crowding at a 44-58px cap that no longer
   applies, and at that value it bound BEFORE height did for seven of the ten
   marks — every wide wordmark hit the same ~122px ceiling and rendered at
   whatever height its own aspect ratio produced, which is the 4.3x spread
   this page used to have.

   96% does not remove the constraint, and could not: the card's own content
   box is ~165px at a typical desktop width, and Fashion Nova's ink-derived
   height alone wants a ~292px-wide box at 8:1. No percentage under 177%
   would let it clear. What 96% does is stop punishing the marks that DO fit —
   at measured render, five of ten (Gabriel Brothers, Burlington, Big Lots,
   Shoppers World, Fashion Nova) are still width-bound, the other five size by
   --logo-k exactly as intended, and the spread measured 4.3x -> 3.41x. Going
   tighter than 96% would pull marks that now fit back onto the width cap for
   no reason; going to the card's true 100% buys back only a few more px and
   was not worth losing the small edge margin for. The remaining spread is the
   card's fixed width against Fashion Nova's aspect ratio, not this rule —
   fixing it further means widening the card, which this change does not do. */
.cred-card img {
  max-width: 96%;
  max-height: calc(clamp(30px, 3vw, 40px) * var(--logo-k, 1));
  width: auto; height: auto;
  object-fit: contain;
}
/* A retailer with no delivered mark, set as type in the same card. Weighted
   and tracked to sit at the same visual density as the logos beside it, not
   to imitate any of them. */
.cred-word {
  font-family: var(--display);
  font-size: clamp(19px, 1.8vw, 24px);
  font-weight: 700;
  letter-spacing: -.02em;
  color: var(--ink);
}
.creds p { color: #9FB2C2; font-size: 14px; margin: var(--s6) 0 0; }

/* ---- brand grid — white cards, one per brand, closing on a dark tile ---- */
.brand-grid {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: var(--s4);
}
.brand-card {
  position: relative;
  display: grid;
  place-items: center;
  min-height: clamp(180px, 20vw, 260px);
  padding: clamp(28px, 4vw, 56px);
  background: var(--paper);
  border: 1px solid var(--rule);
  border-radius: var(--r-lg);
  box-shadow: var(--lift-1);
  text-decoration: none;
  color: inherit;
  transition: transform 500ms var(--ease), box-shadow 500ms var(--ease);
}
.brand-card::after {
  content: "";
  position: absolute; inset: 0;
  border-radius: var(--r-lg);
  box-shadow: var(--lift-2);
  opacity: 0;
  transition: opacity 500ms var(--ease);
  pointer-events: none;
}
@media (hover: hover) and (pointer: fine) {
  .brand-card:hover { transform: translateY(-6px); }
  .brand-card:hover::after { opacity: 1; }
}
.brand-card .brandmark { max-width: 100%; height: auto; }
.brand-card.dark {
  background: var(--slate);
  color: #fff;
  border-color: var(--slate);
  text-align: center;
}
.brand-card.dark span { font-family: var(--display); font-size: var(--t-d3); font-weight: 700; letter-spacing: var(--t-d3-tr); }
.brand-card.dark .arw { display: block; margin-top: var(--s3); font-size: 22px; transition: transform 400ms var(--ease); }
.brand-card.dark:hover .arw { transform: translateX(6px); }

/* ---- brand logo row on paper (home, brands landing) ---- */
.logo-row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--s6);
  flex-wrap: wrap;
  padding: var(--s6) 0;
  border-top: 1px solid var(--rule);
  border-bottom: 1px solid var(--rule);
}
.logo-row a { line-height: 0; opacity: .85; transition: opacity 300ms var(--ease), transform 500ms var(--ease); }
.logo-row a:hover { opacity: 1; transform: translateY(-2px); }

/* ---- fabric plates (fabrics page) ---- */
.fab {
  display: grid;
  grid-template-columns: 7fr 5fr;
  gap: clamp(32px, 6vw, 104px);
  align-items: center;
  padding: clamp(48px, 6vw, 96px) 0;
  border-top: 1px solid var(--rule);
}
.fab:first-of-type { border-top: 0; }
.fab.flip { grid-template-columns: 5fr 7fr; }
.fab.flip > .fab-media { order: 2; }
.fab-media {
  position: relative;
  aspect-ratio: 3 / 2;
  overflow: hidden;
  border-radius: var(--r-md);
  background: var(--shell);
}
.fab-media img { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover; }
.fab-media.plate {
  background: var(--accent);
  display: grid;
  place-items: center;
  padding: var(--s7);
}
.fab-media.plate img { position: static; width: min(64%, 300px); height: auto; object-fit: contain; }
.fab-media.plate .plate-line { position: absolute; left: var(--s6); right: var(--s6); bottom: var(--s6); color: var(--shell); font-family: var(--sans); font-size: 12px; letter-spacing: .08em; text-transform: uppercase; margin: 0; max-width: none; }
.fab-copy .names { display: flex; align-items: center; gap: var(--s4); flex-wrap: wrap; margin: 0 0 var(--s5); }
.fab-copy .names em { font-style: normal; color: var(--ink-4); font-weight: 400; font-size: 20px; line-height: 1; }
.fab-copy .fabric-name {
  font-family: var(--sans);
  font-weight: 500;
  font-size: 14px;
  color: var(--accent);
  letter-spacing: 0;
  margin: 0 0 var(--s4);
}
.fab-copy .fabric-name span { color: var(--ink-4); }
.fab-copy .benefit { font-family: var(--display); font-size: var(--t-d2); font-weight: 700; letter-spacing: var(--t-d2-tr); line-height: var(--t-d2-lh); margin: 0 0 var(--s5); max-width: 18ch; }
.fab-copy .detail { color: var(--ink-2); max-width: 48ch; }
.fab-copy .links { display: flex; gap: var(--s5); flex-wrap: wrap; margin: var(--s5) 0 0; }


/* == 09  figures — technical flats ======================================== */
/* The BOX owns the aspect ratio, never the image. A placeholder and a real
   sketch occupy pixel-identical space, so dropping files into
   public/sketches/<style>/ cannot shift or break any layout. */

.fig {
  position: relative;
  aspect-ratio: var(--fig-ratio);
  background: var(--paper);
  border: 1px solid var(--rule);
  border-radius: var(--r-sm);
  overflow: hidden;
  display: grid;
  place-items: center;
}

/* Brand logos. Vector, fitted into a bounding box at build time and carrying
   exact width/height, so the only job left here is to stop a narrow column
   squashing one. */
.brandmark {
  display: inline-block;
  max-width: 100%;
  height: auto;
  vertical-align: middle;
}
.brand-h1 { line-height: 0; margin-bottom: var(--s4); }

/* A fabric blend is one fact, so it is one line. Without this it breaks at
   the slash and "spandex" drops onto a line of its own — on the accent
   fields of /brands/ that second line collided with the copy under it
   (owner, 17 Aug 2026). Nowrap moves the break to the separator BEFORE the
   blend instead; at 21 characters the blend still fits on one line inside a
   360px screen, so no width needs a second rule. */
.blend { white-space: nowrap; }

/* No registration ticks on the frames (owner, Aug 2026) — the grid needs
   calm. The hairline and the paper ground are the whole frame. */

.fig img {
  width: 100%;
  height: 100%;
  object-fit: contain;
  padding: var(--s5);
  transition: transform 700ms var(--ease);
}

.fig-ph {
  font-family: var(--mono);
  font-size: 11px;
  line-height: 1.7;
  color: var(--ink-4);
  text-align: center;
  padding: var(--s5);
  margin: 0;
  max-width: none;
  word-break: break-word;
}
.fig-ph b {
  display: block;
  color: var(--ink-3);
  font-weight: 500;
  letter-spacing: .1em;
  text-transform: uppercase;
  font-size: 9.5px;
  margin-bottom: var(--s2);
}

.fig.wash { background: var(--shell); }
.fig.tall { --fig-ratio: 3 / 4; }

/* Magnifier over the main sketch. A background layer rather than a transform
   on the img, so it is clipped by .fig and can never overflow the page. */
.fig-zoom {
  position: absolute;
  inset: 0;
  z-index: 1;
  opacity: 0;
  background-repeat: no-repeat;
  background-size: 250%;
  background-position: var(--zx, 50%) var(--zy, 50%);
  background-color: var(--paper);
  transition: opacity 240ms var(--ease);
  pointer-events: none;
}
.fig[data-zoomed] .fig-zoom { opacity: 1; }
.js .figmain[data-zoom-root] { cursor: zoom-in; }
.js .figmain[data-zoomed] { cursor: zoom-out; }
/* Only while magnified does the sketch claim the gesture, so an ordinary page
   scroll that happens to start on the sketch still scrolls. This replaces a
   non-passive touchmove listener calling preventDefault: with Pointer Events
   the browser needs to know BEFORE the gesture starts who owns it, and
   touch-action is how it is told. Declaring it up front also means the pan
   never fights a scroll that has already begun. */
.js .figmain[data-zoomed] { touch-action: none; }

/* == pack options =========================================================
   How a style can be bought, and the size run that comes with each way. */
.packs { border-top: 1px solid var(--rule); }
.packs-head {
  font-family: var(--sans);
  font-size: 12px;
  letter-spacing: .06em;
  text-transform: uppercase;
  color: var(--ink-3);
  font-weight: 500;
  margin: var(--s4) 0 var(--s3);
}
.pack-opt { padding: var(--s4) 0; border-top: 1px solid var(--rule-2); }
.pack-opt:first-of-type { border-top: 0; }
.pack-opt-head {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--s3);
}
/* Heading LEVEL is document structure; heading SIZE is design. These two
   are set apart on purpose: the style page's sections were h3/h4 under an
   h1, which skipped h2 entirely on 117 pages, while a later peer section
   was already h2. The levels are now h2 > h3, and these rules hold the
   type exactly where it was — promoting the tag alone would have inflated
   every style page from the d3 scale to the d2 scale. */
.pack-opt-head h3.pack-opt-title {
  font-size: 15px;
  font-weight: 700;
  letter-spacing: -.005em;
  line-height: normal;
  margin: 0;
}
/* A section heading that is an h2 by structure but sits on the d3 scale. */
.head-sm {
  font-size: var(--t-d3);
  font-weight: 700;
  letter-spacing: var(--t-d3-tr);
  line-height: var(--t-d3-lh);
}
.pack-size {
  font-family: var(--mono);
  font-size: var(--t-data);
  color: var(--ink);
  white-space: nowrap;
}
.pack-note { margin: 2px 0 var(--s3); font-size: 13.5px; color: var(--ink-3); }
.pack-scale { padding: var(--s3) 0 0; }
.pack-range {
  margin: 0 0 var(--s2);
  font-family: var(--sans);
  font-size: 12px;
  letter-spacing: .02em;
  color: var(--ink-3);
}

/* ---- the size run ----
   What is actually in the case, per size. The QUANTITY is the headline and the
   size is its caption; the ratio underneath is the proportion it was worked
   out from, kept so the figure stays checkable against the CAD sheet. */
.sizerun-list {
  list-style: none;
  margin: 0;
  padding: 0;
  display: grid;
  grid-auto-flow: column;
  grid-auto-columns: minmax(0, 1fr);
  gap: 1px;
  background: var(--rule);
  border: 1px solid var(--rule);
}
.sizerun-list li {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 2px;
  padding: var(--s3) 4px;
  background: var(--paper);
}
.sizerun-list b {
  font-family: var(--mono);
  font-size: clamp(17px, 2vw, 21px);
  font-weight: 500;
  letter-spacing: -.02em;
  line-height: 1;
}
.sizerun-list span {
  font-family: var(--mono);
  font-size: var(--t-label);
  letter-spacing: .08em;
  color: var(--ink-4);
}
.sizerun-total {
  margin: var(--s3) 0 0;
  font-family: var(--mono);
  font-size: var(--t-data);
  color: var(--ink-2);
}
.sizerun-total b { font-weight: 500; color: var(--ink); }
.sizerun-ratio {
  margin: 2px 0 0;
  font-family: var(--mono);
  font-size: var(--t-label);
  letter-spacing: .04em;
  color: var(--ink-4);
}
.sizerun-ratio span { color: var(--accent, var(--ink-3)); }
.card .sizerun { margin-top: var(--s3); }
@media (max-width: 520px) {
  .sizerun-list li { padding: var(--s3) 2px; }
  .sizerun-list b { font-size: 16px; }
  /* 11px, not 10px: this span is the SIZE in "8 S · 8 M · 16 L" — which
     size the quantity belongs to is the whole point of the breakdown, so
     it does not go under the legibility floor to save 1px of width. */
  .sizerun-list span { font-size: 11px; letter-spacing: .04em; }
}
.pack-colours {
  margin: var(--s3) 0 0;
  font-size: 13px;
  color: var(--ink-2);
  line-height: 1.65;
  max-width: none;
}
.pack-colours.dim { color: var(--ink-4); font-style: italic; }

@media (min-width: 720px) {
  .packs { display: grid; grid-template-columns: 1fr 1fr; gap: 0 var(--s6); }
  .packs-head { grid-column: 1 / -1; }
  .pack-opt:nth-of-type(2) { border-top: 0; }
}

/* Every view delivered for the style, not a selection. */
.fig-strip {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(62px, 1fr));
  gap: var(--s1);
  margin-top: var(--s2);
}
.thumb {
  position: relative;
  aspect-ratio: 1 / 1;
  padding: 0;
  border: 1px solid var(--rule);
  border-radius: var(--r-sm);
  background: var(--paper);
  cursor: pointer;
  overflow: hidden;
  transition: border-color 300ms var(--ease);
}
.thumb img {
  width: 100%;
  height: 100%;
  object-fit: contain;
  padding: 5px;
  display: block;
}
.thumb:hover { border-color: var(--ink-3); }
.thumb[aria-pressed="true"] {
  border-color: var(--ink);
  box-shadow: inset 0 0 0 2px var(--ink);
}
.thumb:focus-visible {
  outline: 2px solid var(--red);
  outline-offset: 2px;
}


/* == 10  cards ============================================================ */

/* The catalogue grid stays a hairline table -- that is the spec register and
   it is correct there. On the LOOKBOOK pages the same grid was the densest
   thing on the page: eight cards edge to edge with a 1px rule between them,
   which reads as a spreadsheet dropped into a lookbook. There it opens up
   into separate cards with real space around them. */
.grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(268px, 1fr));
  gap: 1px;
  background: var(--rule);
  border: 1px solid var(--rule);
}
body[data-register="lookbook"] .grid {
  gap: clamp(16px, 1.6vw, 24px);
  background: none;
  border: 0;
}
body[data-register="lookbook"] .grid .card {
  border-radius: var(--r-md);
  box-shadow: inset 0 0 0 1px var(--rule), var(--lift-1);
  overflow: hidden;
}

.card {
  position: relative;
  background: var(--paper);
  padding: var(--s5);
  text-decoration: none;
  color: inherit;
  display: flex;
  flex-direction: column;
  gap: var(--s3);
  touch-action: manipulation;
  transition: background 500ms var(--ease), transform 320ms var(--ease);
}
.card:hover { background: var(--shell); }
.card:hover .fig img { transform: scale(1.03); }
/* The style number goes to the brand's colour on hover. --detail first where
   a brand has one: the card is WHITE, and an accent light enough to work on a
   brand panel can be invisible here. EliteMed's #FFC3B0 measures 1.3:1 on a
   white card; its detail #CC7452 measures 3.28:1. Karisma's accent measures
   2.09:1; its detail #517B28 measures 4.98:1. Brands without a detail keep the
   derived ink, which is already solved for legibility. */
.card:hover .style-no { color: var(--detail, var(--accent-ink, var(--accent))); }

.card .fig { margin-bottom: var(--s2); }

.card-top {
  display: flex;
  justify-content: space-between;
  align-items: flex-start;
  gap: var(--s3);
}
.card .style-no {
  font-family: var(--mono);
  font-size: 21px;
  font-weight: 500;
  letter-spacing: -.02em;
  line-height: 1.1;
  transition: color 400ms var(--ease);
}
.card .brand {
  font-family: var(--sans);
  font-size: 11px;
  font-weight: 600;
  letter-spacing: .06em;
  text-transform: uppercase;
  color: var(--accent-ink, var(--accent));
  margin-top: 5px;
}
.card .garment {
  font-size: 14.5px;
  color: var(--ink-2);
  letter-spacing: -.005em;
}
.card .ratio {
  font-family: var(--mono);
  font-size: 11.5px;
  color: var(--ink-4);
  letter-spacing: .02em;
}
.card .more {
  font-family: var(--mono);
  font-size: 11px;
  color: var(--ink-4);
  align-self: center;
}
.card .foot {
  margin-top: auto;
  padding-top: var(--s3);
  display: flex;
  align-items: center;
  gap: var(--s3);
  flex-wrap: wrap;
}

/* soft shadow that fades in on hover — an opacity change on a layer, so the
   card itself only ever moves by transform */
.card::before {
  content: "";
  position: absolute;
  inset: 0;
  box-shadow: var(--lift-2);
  opacity: 0;
  transition: opacity 500ms var(--ease);
  pointer-events: none;
}
.card:hover { z-index: 2; }
.card:hover::before { opacity: 1; }

/* accent hairline that draws in on hover */
.card::after {
  content: "";
  position: absolute;
  left: 0; right: 0; top: 0;
  height: 2px;
  background: var(--accent);
  transform: scaleX(0);
  transform-origin: left;
  transition: transform 500ms var(--ease);
}
.card:hover::after { transform: scaleX(1); }

/* plain content card, no link affordance */
.card.static { cursor: default; }
.card.static:hover { background: var(--paper); }
.card.static::after { display: none; }
.card.static h3 { margin-bottom: var(--s2); }
.card.static p { font-size: 15px; color: var(--ink-2); margin: 0; }


/* == 11  data blocks ====================================================== */

.spec-block, .pack {
  border-top: 2px solid var(--ink);
  margin: 0;
}
.spec-row, .pack-row {
  display: flex;
  justify-content: space-between;
  align-items: baseline;
  gap: var(--s4);
  padding: 13px 0;
  border-bottom: 1px solid var(--rule);
}
.spec-row dt, .pack-row .k {
  font-family: var(--sans);
  font-size: 12px;
  letter-spacing: .06em;
  text-transform: uppercase;
  color: var(--ink-3);
  flex: none;
}
.spec-row dd, .pack-row .v {
  margin: 0;
  font-weight: 600;
  text-align: right;
  font-size: 14.5px;
}
.spec-row dd { font-family: var(--sans); font-weight: 600; font-size: 15px; }
.pack-row .v .mono {
  display: block;
  font-family: var(--sans);
  font-weight: 400;
  font-size: 12.5px;
  color: var(--ink-4);
  letter-spacing: .02em;
  margin-top: 3px;
}
/* on slate */
.spec-block.on-ink { border-top-color: #fff; }
.spec-block.on-ink .spec-row { border-bottom-color: var(--slate-rule); }
.spec-block.on-ink dt { color: var(--on-slate-2); }
.spec-block.on-ink dd { color: #fff; }

/* measured ratio line — a dimension callout */
.ratio-strip {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: var(--s3);
  font-family: var(--mono);
  font-size: clamp(17px, 2.2vw, 26px);
  font-weight: 500;
  letter-spacing: -.01em;
  padding: var(--s5) 0;
  border-top: 1px solid var(--ink);
  border-bottom: 1px solid var(--rule);
  margin: 0 0 var(--s3);
}
.ratio-strip b { font-weight: 500; }
.ratio-strip .seg { display: inline-flex; align-items: center; gap: var(--s3); }
.ratio-strip .seg::after {
  content: "";
  width: 18px; height: 1px;
  background: var(--rule);
}
.ratio-strip .seg:last-child::after { display: none; }
.ratio-strip .sz { color: var(--ink-4); }
.ratio-note {
  font-family: var(--mono);
  font-size: 11.5px;
  color: var(--ink-4);
  margin: 0;
  letter-spacing: .02em;
}

/* swatches. The hover grows a RING drawn by a pseudo-element and scaled with
   transform — never a box-shadow transition — so the dot itself never moves
   and only transform is animated. */
.swatches { display: flex; flex-wrap: wrap; gap: 6px; align-items: center; }
.sw {
  position: relative;
  width: 18px; height: 18px;
  border-radius: 50%;
  box-shadow: inset 0 0 0 1px rgba(0,0,0,.16);
}
.sw::after {
  content: "";
  position: absolute;
  inset: -3px;
  border-radius: 50%;
  border: 1px solid var(--ink-3);
  opacity: 0;
  transform: scale(.7);
  transition: transform 400ms var(--ease), opacity 400ms var(--ease);
}
.sw:hover::after { opacity: 1; transform: none; }
.sw.lg { width: 26px; height: 26px; }

.colorgrid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(168px, 1fr));
  gap: var(--s5) var(--s4);
}
.color { display: flex; gap: var(--s3); align-items: flex-start; }
.color.solo { align-items: center; }
.color .chip {
  position: relative;
  width: 42px; height: 42px;
  border-radius: 50%;
  flex: none;
  box-shadow: inset 0 0 0 1px rgba(0,0,0,.16);
}
.color .chip::after {
  content: "";
  position: absolute; inset: -4px;
  border-radius: 50%;
  border: 1px solid var(--ink-3);
  opacity: 0; transform: scale(.8);
  transition: transform 400ms var(--ease), opacity 400ms var(--ease);
}
.color:hover .chip::after { opacity: 1; transform: none; }
.color .cname {
  font-size: 14.5px;
  font-weight: 600;
  line-height: 1.3;
  letter-spacing: -.008em;
}
.color .cav {
  font-family: var(--sans);
  font-size: 12px;
  color: var(--ink-4);
  letter-spacing: 0;
}
.color .cav.in { color: var(--ok); }

/* == colour comparison ==================================================== */
.colour-compare {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: var(--s6) clamp(28px, 5vw, 80px);
  margin-top: var(--s6);
  align-items: start;
}
.cc-col { border-top: 2px solid var(--ink); padding-top: var(--s4); }
.cc-head {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--s3);
  flex-wrap: wrap;
  margin-bottom: var(--s4);
}
.cc-head h3 { margin: 0; }
.cc-n {
  font-family: var(--sans);
  font-size: 12px;
  letter-spacing: 0;
  color: var(--ink-3);
  white-space: nowrap;
}
.cc-note { margin: 0 0 var(--s4); font-size: 14px; color: var(--ink-3); max-width: none; }
.cc-list {
  list-style: none;
  margin: 0;
  padding: 0;
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: var(--s4) var(--s4);
}
.cc-list.asst {
  grid-template-rows: repeat(4, auto);
  grid-auto-flow: column;
}
.cc-item { display: flex; gap: var(--s3); align-items: flex-start; }
.cc-item.solo { align-items: center; }
.cc-item .chip {
  width: 38px; height: 38px;
  border-radius: 50%;
  flex: none;
  box-shadow: inset 0 0 0 1px rgba(0,0,0,.16);
}
.cc-text { display: flex; flex-direction: column; gap: 2px; min-width: 0; }
.cc-item .cname { font-size: 14.5px; font-weight: 600; line-height: 1.3; letter-spacing: -.008em; }
.cc-item .cav { font-family: var(--sans); font-size: 12px; color: var(--ink-4); letter-spacing: 0; }
.cc-item .cav.in { color: var(--ok); }
.cc-mark {
  font-family: var(--sans);
  font-size: 11.5px;
  letter-spacing: 0;
  color: var(--ink-3);
  padding-left: 8px;
  border-left: 2px solid var(--accent, var(--ink));
  line-height: 1.4;
}
.cc-empty { margin: 0; font-size: 14.5px; color: var(--ink-3); max-width: 38ch; }
.cc-foot {
  margin: var(--s5) 0 0;
  font-family: var(--sans);
  font-size: 12.5px;
  color: var(--ink-3);
  max-width: none;
}
@media (max-width: 760px) {
  .colour-compare { grid-template-columns: 1fr; }
}
@media (max-width: 430px) {
  .cc-list { grid-template-columns: 1fr; }
  .cc-list.asst { grid-template-rows: none; grid-auto-flow: row; }
}

/* parent <-> variant link in the style head */
.variant-link { margin: -8px 0 16px; }
.variant-link a.mono {
  color: var(--ink);
  font-weight: 500;
  text-decoration: none;
  border-bottom: 1px solid var(--red);
  padding-bottom: 1px;
}
.variant-link a.mono:hover { color: var(--red); }

/* colour family group */
.fam { margin-bottom: var(--s7); }
.fam:last-child { margin-bottom: 0; }
.fam-head {
  display: flex;
  align-items: baseline;
  gap: var(--s3);
  padding-bottom: var(--s3);
  margin-bottom: var(--s4);
  border-bottom: 1px solid var(--rule);
}
.fam-head h3 {
  font-family: var(--sans);
  font-size: 12px;
  font-weight: 600;
  letter-spacing: .06em;
  text-transform: uppercase;
  color: var(--ink-2);
  margin: 0;
}
.fam-head .n {
  font-family: var(--sans);
  font-size: 12px;
  color: var(--ink-4);
  margin-left: auto;
}

/* the only status indicator on the site */
.badge {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  background: var(--ok-bg);
  color: var(--ok);
  font-family: var(--sans);
  font-size: 11px;
  font-weight: 600;
  letter-spacing: .06em;
  text-transform: uppercase;
  padding: 5px 10px;
  border-radius: var(--r-pill);
  white-space: nowrap;
}
.badge .dot { width: 6px; height: 6px; border-radius: 50%; background: var(--ok); }

.tag {
  display: inline-flex;
  align-items: center;
  font-family: var(--sans);
  font-weight: 500;
  font-size: 11px;
  letter-spacing: .06em;
  text-transform: uppercase;
  color: var(--ink-3);
  padding: 5px 10px;
  border-radius: var(--r-pill);
  box-shadow: inset 0 0 0 1px var(--rule);
}

/* Full bleed. No 100vw maths — sections are already full width, so the bleed
   is simply a wrap that stops constraining. */
.bleed > .wrap {
  max-width: none;
  padding-left: 0;
  padding-right: 0;
}
.bleed .bleed-head { padding: 0 var(--gutter); }

/* Palette wall — the colours as an edge-to-edge band. */
.wall {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(116px, 1fr));
}
.wall .c {
  position: relative;
  aspect-ratio: 1;
  display: flex;
  align-items: flex-end;
  padding: 12px;
  transition: transform 500ms var(--ease);
}
@media (hover: hover) and (pointer: fine) {
  .wall .c:hover { transform: scale(1.06); z-index: 2; }
}
.wall .c .nm {
  font-family: var(--sans);
  font-size: 11px;
  line-height: 1.3;
  letter-spacing: 0;
}
.wall .c.on-light .nm { color: rgba(0,0,0,.72); }
.wall .c.on-dark .nm { color: rgba(255,255,255,.86); }
.wall .c .new {
  position: absolute;
  top: 10px; right: 10px;
  width: 5px; height: 5px;
  border-radius: 50%;
  background: var(--red);
}
@media (max-width: 720px) {
  /* 88px cells, not 84: the colour NAME is the content here — a buyer
     reading the wall is reading which colour is which — so the label holds
     the 11px floor and the cell widens to carry it rather than the label
     shrinking to 9px to fit the cell. */
  .wall { grid-template-columns: repeat(auto-fill, minmax(88px, 1fr)); }
  .wall .c { padding: 8px; }
  .wall .c .nm { font-size: 11px; }
}


/* == 12  lookbook bands — brand plates on the home page ================== */

.brand-plates {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
  gap: var(--s4);
}
.bplate { border: 1px solid var(--rule); border-radius: var(--r-md); overflow: hidden; }
.bplate {
  position: relative;
  background: var(--paper);
  padding: clamp(24px, 3vw, 40px);
  text-decoration: none;
  color: inherit;
  display: flex;
  flex-direction: column;
  gap: var(--s4);
  transition: background 500ms var(--ease);
}
.bplate:hover { background: var(--shell); }
.bplate::before {
  content: "";
  position: absolute;
  left: 0; top: 0; bottom: 0;
  width: 6px;
  background: var(--accent);
}
.bplate .bnum { font-family: var(--sans); font-size: 12px; letter-spacing: .06em; color: var(--ink-4); }
.bplate .bname { display: flex; align-items: center; gap: var(--s3); flex-wrap: wrap; margin: 0; line-height: 0; }
.bplate .bname em { font-style: normal; color: var(--ink-4); font-weight: 400; font-size: 18px; line-height: 1; }
.bplate .bfabric { font-family: var(--sans);
  font-weight: 500; font-size: 13.5px; color: var(--accent-ink, var(--accent)); letter-spacing: .02em; }
.bplate .bfabric span { display: block; color: var(--ink-4); margin-top: 3px; }
.bplate .bline { font-size: 15px; color: var(--ink-2); margin: 0; max-width: 42ch; }
.bplate .bline b { color: var(--ink); font-weight: 700; }
.bplate .bfoot {
  margin-top: auto;
  padding-top: var(--s4);
  border-top: 1px solid var(--rule);
  display: flex;
  align-items: center;
  gap: var(--s3);
  flex-wrap: wrap;
}
.bplate .bcount { font-family: var(--sans); font-size: 13px; color: var(--ink-3); margin-left: auto; }


/* == 13  catalogue ======================================================== */

.catalog {
  display: grid;
  /* minmax(0, 1fr), not 1fr. A 1fr track carries an automatic MIN-CONTENT
     floor, and the card grid inside reports a min-content of two 190px
     tracks plus their gap — 415px. Given a column narrower than that the
     track refused to shrink and pushed the whole page sideways. */
  grid-template-columns: 272px minmax(0, 1fr);
  gap: clamp(28px, 4vw, 64px);
  align-items: start;
}

.side {
  position: sticky;
  top: calc(var(--head-h) + var(--s5));
  max-height: calc(100vh - var(--head-h) - var(--s7));
  overflow-y: auto;
  overscroll-behavior: contain;
  padding-right: var(--s3);
}
.side::-webkit-scrollbar { width: 4px; }
.side::-webkit-scrollbar-thumb { background: var(--rule); }

.fgroup { border-bottom: 1px solid var(--rule); }
.fgroup > summary {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--s3);
  padding: var(--s4) 0;
  cursor: pointer;
  list-style: none;
  font-family: var(--sans);
  font-size: 12.5px;
  font-weight: 600;
  letter-spacing: .06em;
  text-transform: uppercase;
  color: var(--ink);
}
.fgroup > summary::-webkit-details-marker { display: none; }
.fgroup > summary::after {
  content: "";
  width: 9px; height: 9px;
  border-right: 1px solid var(--ink-3);
  border-bottom: 1px solid var(--ink-3);
  transform: rotate(45deg) translate(-2px, -2px);
  transition: transform 400ms var(--ease);
  flex: none;
}
.fgroup[open] > summary::after { transform: rotate(225deg) translate(-2px, -2px); }
.fgroup > summary:hover { color: var(--red); }
.fgroup .fbody { padding: 0 0 var(--s5); }

.opt {
  display: flex;
  align-items: center;
  gap: var(--s3);
  padding: 7px 0;
  cursor: pointer;
  font-size: 14px;
  color: var(--ink-2);
  line-height: 1.35;
  min-height: 32px;
}
.opt:hover { color: var(--ink); }
.opt input { position: absolute; opacity: 0; width: 1px; height: 1px; }
.opt .box {
  width: 15px; height: 15px;
  flex: none;
  box-shadow: inset 0 0 0 1px var(--ink-3);
  background: var(--paper);
  position: relative;
  transition: background 240ms var(--ease), box-shadow 240ms var(--ease);
}
.opt .box::after {
  content: "";
  position: absolute;
  left: 50%; top: 47%;
  width: 4px; height: 8px;
  margin: -5px 0 0 -2px;
  border-right: 1.5px solid #fff;
  border-bottom: 1.5px solid #fff;
  transform: rotate(45deg) scale(0);
  transform-origin: center;
  transition: transform 240ms var(--ease);
}
.opt input:checked ~ .box { background: var(--ink); box-shadow: inset 0 0 0 1px var(--ink); }
.opt input:checked ~ .box::after { transform: rotate(45deg) scale(1); }
.opt input:focus-visible ~ .box { outline: 2px solid var(--red); outline-offset: 2px; }
.opt .n { margin-left: auto; font-family: var(--sans); font-size: 12px; color: var(--ink-4); }
.opt input:checked ~ .lbl { color: var(--ink); font-weight: 600; }
/* a brand option carries the brand's logo rather than its name in our type */
/* The logo is sized explicitly. An <img> with max-width:100% inside a
   shrink-to-fit inline-flex label resolves cyclically and collapses to 0px —
   which is exactly how the five brand filters shipped with no labels. */
.opt .lbl.logo-lbl { flex: none; line-height: 0; display: inline-flex; align-items: center; min-height: 22px; }
.opt .lbl.logo-lbl img { height: 22px; width: auto; max-width: none; }
.opt input:checked ~ .lbl.logo-lbl { font-weight: inherit; }

.opt.radio .box { border-radius: 50%; }
.opt.radio .box::after {
  left: 50%; top: 50%;
  width: 7px; height: 7px;
  margin: 0;
  border: 0;
  border-radius: 50%;
  background: #fff;
  transform: translate(-50%, -50%) scale(0);
}
.opt.radio input:checked ~ .box::after { transform: translate(-50%, -50%) scale(1); }

/* colour swatch options */
.swatch-opts {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(34px, 1fr));
  gap: var(--s2);
}
.sw-opt {
  position: relative;
  display: grid;
  place-items: center;
  cursor: pointer;
  padding: 3px;
}
.sw-opt input { position: absolute; opacity: 0; width: 1px; height: 1px; }
.sw-opt .dot {
  position: relative;
  width: 24px; height: 24px;
  border-radius: 50%;
  background: var(--c);
  box-shadow: inset 0 0 0 1px rgba(0,0,0,.18);
}
.sw-opt .dot::after {
  content: "";
  position: absolute; inset: -4px;
  border-radius: 50%;
  border: 1.5px solid var(--ink);
  opacity: 0; transform: scale(.7);
  transition: transform 400ms var(--ease), opacity 400ms var(--ease);
}
.sw-opt:hover .dot::after { opacity: .5; transform: none; }
.sw-opt input:checked ~ .dot::after { opacity: 1; transform: none; }
.sw-opt input:focus-visible ~ .dot::after { opacity: 1; transform: none; border-color: var(--red); }

/* search — first control in the sidebar */
.search { position: relative; margin-bottom: var(--s4); }
.search input {
  width: 100%;
  font-family: var(--sans);
  font-size: 15px;
  color: var(--ink);
  background: var(--paper);
  border: 1px solid var(--rule);
  border-radius: var(--r-sm);
  padding: 13px 38px 13px 16px;
  -webkit-appearance: none;
  appearance: none;
  transition: border-color 300ms var(--ease);
}
.search input::placeholder { font-family: var(--sans); font-size: 14px; color: var(--ink-4); letter-spacing: 0; }
.search input:hover { border-color: var(--ink-4); }
.search input:focus { border-color: var(--ink); outline: none; }
.search input:focus-visible { outline: 2px solid var(--red); outline-offset: 2px; }
.search input::-webkit-search-cancel-button { display: none; }
.search-clear {
  position: absolute;
  top: 50%; right: 6px;
  transform: translateY(-50%);
  width: 28px; height: 28px;
  display: grid; place-items: center;
  background: none; border: 0; cursor: pointer;
  font-size: 19px; line-height: 1;
  color: var(--ink-3);
}
.search-clear:hover { color: var(--red); }

/* the trade gate */
.gate { max-width: 62ch; }
.gate h2 { margin-bottom: var(--s4); }
.gate .gate-note {
  font-size: 15px;
  color: var(--ink-2);
  padding-left: var(--s5);
  border-left: 2px solid var(--red);
  margin: var(--s5) 0 0;
}
.gate-actions { display: flex; gap: var(--s3); flex-wrap: wrap; margin: var(--s7) 0 var(--s7); }
.gate-spec { max-width: 420px; }

/* ---- the trade application form (/signup/) ----
   The controls take the search field's language — same border, radius,
   padding and focus behaviour — so the one form on the site reads as part of
   the same instrument set as the catalog's search. Labels are the spec
   register's micro-caps. */
.form { max-width: 560px; }
.form .field { margin-bottom: var(--s5); }
.form-row { display: grid; grid-template-columns: 1fr 1fr; gap: var(--s4); }
@media (max-width: 480px) { .form-row { grid-template-columns: 1fr; } }
.form label {
  display: block;
  font-family: var(--sans);
  font-size: 12.5px;
  letter-spacing: .06em;
  text-transform: uppercase;
  font-weight: 600;
  color: var(--ink);
  margin-bottom: 8px;
}
.form label .opt {
  text-transform: none;
  letter-spacing: 0;
  font-weight: 400;
  color: var(--ink-3);
  margin-left: 6px;
}
.form input,
.form select {
  width: 100%;
  font-family: var(--sans);
  font-size: 15px;
  color: var(--ink);
  background: var(--paper);
  border: 1px solid var(--rule);
  border-radius: var(--r-sm);
  padding: 13px 16px;
  -webkit-appearance: none;
  appearance: none;
  transition: border-color 300ms var(--ease);
}
.form select {
  padding-right: 40px;
  /* The chevron is two 6px gradient squares meeting in a caret — not an SVG
     data URI, because the SVG namespace URL contains a four-digit year that
     is also a real style number, and the gated-value scan reads CSS strings
     (and comments — do not write the number here either). A drawing with no
     string cannot trip a scan that reads strings. */
  background-image:
    linear-gradient(45deg, transparent 50%, var(--ink-4) 50%),
    linear-gradient(135deg, var(--ink-4) 50%, transparent 50%);
  background-position: calc(100% - 21px) calc(50% - 1px), calc(100% - 15px) calc(50% - 1px);
  background-size: 6px 6px, 6px 6px;
  background-repeat: no-repeat;
}
.form input:hover, .form select:hover { border-color: var(--ink-4); }
.form input:focus, .form select:focus { border-color: var(--ink); outline: none; }
.form input:focus-visible, .form select:focus-visible { outline: 2px solid var(--red); outline-offset: 2px; }
/* The region field: three of them exist, one per region country. Without JS
   all three render, each labelled with its country, and the server reads only
   the one matching the chosen country. With JS only the match is shown — the
   hidden ones are also DISABLED by the script, so nothing hidden ever
   submits. Under .js only, so a no-JS visitor never faces a hidden control. */
.js .region-field { display: none; }
.js .region-field.on { display: block; }
.form-alert {
  font-size: 15px;
  color: var(--ink-2);
  padding-left: var(--s5);
  border-left: 2px solid var(--red);
  margin: 0 0 var(--s5);
}
.form .btn { margin-top: var(--s2); }
.form-note { font-size: 13.5px; color: var(--ink-3); margin-top: var(--s4); }
.form-done { max-width: 56ch; }
.form-done:focus { outline: none; }
.form-done h3 { margin-bottom: var(--s3); }
.form-done p { font-size: 16px; color: var(--ink-2); }

.empty-actions { display: flex; gap: var(--s3); justify-content: center; flex-wrap: wrap; }

/* ---- /account/ ----
   Four blocks down one measure, separated by the same hairline the spec rows
   use. Everything here is the existing vocabulary — .form, .spec-block,
   .section-head, the eyebrow plates — so the page inherits the site rather
   than describing itself. What is genuinely new is only the state handling:
   a page that cannot know its own content until a request comes back.

   AIR AT EVERY LEVEL, not just around the section: --band-gap between two
   blocks inside one section is the rule the rebuild set, and four stacked
   blocks is exactly the case it was written for. */
.acct-block {
  max-width: 640px;
  padding-bottom: var(--band-gap);
  margin-bottom: var(--band-gap);
  border-bottom: 1px solid var(--rule);
}
.acct-block-last { padding-bottom: 0; margin-bottom: 0; border-bottom: 0; }
.acct-block .section-head { margin-top: var(--s3); }
.acct-block .lede { margin-top: var(--s4); max-width: 52ch; }

/* The loading line exists only where script does. Without it the <noscript>
   block is the whole page, and a "Loading…" that will never finish would be
   the cruellest thing to leave on screen. */
.acct-wait { display: none; }
.js .acct-wait { display: block; color: var(--ink-3); }
.acct-guard, .acct-fail { max-width: 56ch; }
.acct-guard p, .acct-fail p { color: var(--ink-2); margin-top: var(--s4); }
.acct-guard .btn { margin-top: var(--s5); }

/* A confirmation, not an error — .form-alert's geometry with the rule in ink.
   Red is punctuation on this site and never a status colour, and "your details
   are saved" is the one sentence here that is unambiguously good news. */
.acct-note { border-left-color: var(--ink); }

.mono-v { font-family: var(--mono); font-weight: 400; font-size: 14px; }

.side-actions {
  display: flex;
  align-items: center;
  gap: var(--s4);
  padding: var(--s4) 0;
  border-bottom: 2px solid var(--ink);
  margin-bottom: var(--s2);
}
.side-actions .ttl {
  font-family: var(--sans);
  font-size: 12.5px;
  letter-spacing: .06em;
  text-transform: uppercase;
  color: var(--ink);
  font-weight: 600;
}
.clear {
  margin-left: auto;
  background: none;
  border: 0;
  padding: 4px 0;
  cursor: pointer;
  font-family: var(--sans);
  font-size: 12.5px;
  letter-spacing: 0;
  color: var(--ink-3);
  border-bottom: 1px solid var(--rule);
}
.clear:hover { color: var(--red); border-color: var(--red); }

.resultbar {
  display: flex;
  align-items: center;
  gap: var(--s4);
  flex-wrap: wrap;
  padding-bottom: var(--s4);
  margin-bottom: var(--s5);
  border-bottom: 1px solid var(--rule);
}
.resultbar .rcount { font-family: var(--sans); font-size: 14px; color: var(--ink); letter-spacing: 0; }
.resultbar .rcount b { font-weight: 500; color: var(--red); }

.active-filters { display: flex; gap: var(--s2); flex-wrap: wrap; }
.afchip {
  display: inline-flex;
  align-items: center;
  gap: var(--s2);
  background: var(--ink);
  color: #fff;
  border: 0;
  cursor: pointer;
  font-family: var(--sans);
  font-size: 12px;
  letter-spacing: 0;
  padding: 6px 10px;
  border-radius: var(--r-pill);
}
.afchip:hover { background: var(--red); }
.afchip::after { content: "\00d7"; font-size: 13px; line-height: 1; }

.filter-toggle { display: none; }

.empty {
  padding: var(--s9) var(--s5);
  text-align: center;
  border: 1px solid var(--rule);
  border-radius: var(--r-md);
  background: var(--shell);
  min-height: 60vh;
  display: flex;
  flex-direction: column;
  justify-content: center;
}
.empty p { margin: 0 auto var(--s4); color: var(--ink-3); max-width: 42ch; }


/* == 14  order / steps / cta ============================================= */

.step {
  display: flex;
  gap: var(--s4);
  padding: var(--s4) 0;
  border-bottom: 1px solid var(--slate-rule);
}
.step:first-child { padding-top: 0; }
.step .n {
  font-family: var(--sans);
  font-weight: 600;
  font-size: 12px;
  letter-spacing: .06em;
  color: var(--red);
  flex: none;
  padding-top: 4px;
  min-width: 34px;
}
.step p { margin: 0; color: var(--on-slate); font-size: 15px; }
.step b { color: #fff; font-weight: 600; }
.step.light { border-color: var(--rule); }
.step.light p { color: var(--ink-2); }
.step.light b { color: var(--ink); }

/* ---- the closing CTA ----
   Cut down hard (owner, 20 Aug 2026). It was a full screen of centred text on
   flat black — 176px of padding top and bottom, an 80px heading and a 52ch
   paragraph, all to say four words — and it appears at the foot of every
   page, so the cost was paid seven times over.

   Now it is a BAND, not a screen: half the padding, the heading down a step,
   and the copy and the actions laid across one row on desktop so the height
   is set by the taller of two columns instead of by four stacked blocks. It
   carries the same dark field as the proof chapter (it is .darkfield too), so
   the page closes on the same material it opened its dark chapter with
   rather than on a black rectangle. */
.cta-close {
  color: #fff;
}
.cta-close .wrap {
  position: relative;
  z-index: 1;
  padding-top: clamp(52px, 6vw, 92px);
  padding-bottom: clamp(52px, 6vw, 92px);
  display: grid;
  grid-template-columns: minmax(0, 1.15fr) auto;
  gap: clamp(28px, 5vw, 88px);
  align-items: center;
}
.cta-close h2 {
  color: #fff;
  font-size: clamp(30px, 3.6vw, 54px);
  font-weight: 700;
  letter-spacing: -.03em;
  margin: var(--s3) 0 var(--s3);
  max-width: 18ch;
}
.cta-close p { color: var(--on-slate-2); margin: 0; max-width: 46ch; font-size: 15px; }
.cta-close .eyebrow { display: inline-flex; text-align: left; }
/* the actions become the second column — stacked, so two full-width buttons
   read as one decisive block rather than a centred pair */
.cta-close .actions {
  display: flex;
  flex-direction: column;
  align-items: stretch;
  gap: var(--s3);
  margin: 0;
  min-width: clamp(220px, 22vw, 280px);
}
.cta-close .actions .btn { justify-content: center; }
@media (max-width: 860px) {
  .cta-close .wrap { grid-template-columns: 1fr; gap: var(--s6); }
  .cta-close .actions { min-width: 0; }
}


/* address block on the contact page */
.contact-grid {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: clamp(32px, 6vw, 96px);
  align-items: start;
}


/* == 14b  color system ==================================================
   Color comes from the garments, the fabric photos, the swatches and ONE
   brand accent at a time — never several blended together (owner, Aug 2026;
   the multi-stop spectrum was tried and removed). What carries color:
   rotating section grounds, the per-accent fields on /brands/, per-brand
   hover borders, per-figure stat underlines, Denice red on the primary
   button and the eyebrow rule. The data pages (body[data-register="spec"])
   stay calm. */

.section.slate { background: var(--slate); color: #fff; }
.section.slate h2, .section.slate h3 { color: #fff; }
.section.slate .lede, .section.slate p { color: var(--on-slate); }

/* the "5 Brands" heading — solid ink, the numeral in Denice red */
h2 .num { color: var(--red); }

/* plain hairline dividers between homepage bands */
hr.divider {
  border: 0;
  height: 1px;
  margin: 0;
  background: var(--rule);
}

/* (the per-figure stat underline now lives with the stat band itself —
   see .stat-cell .v::after in section 12) */

/* brand grid: the sixth tile is solid slate; a card's hover border picks up
   THAT brand's own accent (set as --accent on the card) */
.brand-card.dark { background: var(--slate); border-color: var(--slate); }
.brand-card:not(.dark):hover { border-color: var(--accent, var(--ink)); }

/* larger swatches — the most colorful thing we own */
.sw { width: 28px; height: 28px; }
.sw.lg { width: 36px; height: 36px; }
.swatches { gap: 8px; }
.color .chip { width: 56px; height: 56px; }
.cc-item .chip { width: 52px; height: 52px; }
.sw-opt .dot { width: 30px; height: 30px; }
.swatch-opts { grid-template-columns: repeat(auto-fill, minmax(40px, 1fr)); }
.wall { grid-template-columns: repeat(auto-fill, minmax(140px, 1fr)); }
.colorgrid { grid-template-columns: repeat(auto-fill, minmax(190px, 1fr)); }

/* ---- /brands/ — one section per brand, a bold field of its accent ----
   Full-bleed row: the fabric photograph large on one side, the accent field
   on the other carrying the white lockup, fabric name, blend and the benefit
   line in white. The one short paragraph sits BELOW on light ground — body
   text stays black on light, always. */
.bfield {
  display: grid;
  grid-template-columns: 1fr 1fr;
  min-height: clamp(420px, 56vh, 640px);
}
.bfield.flip .bfield-media { order: 2; }
.bfield {
  /* The colour the panel's wash is at where it meets the photograph, which is
     what the bleed under the photograph's inner edge has to end at. It cannot
     be read off the radial, so it is mixed to the same recipe: the brand hue
     at roughly the wash's leading alpha, over paper. */
  --bfield-edge: color-mix(in srgb, rgb(var(--wash-brand)) 22%, var(--paper));
}
.bfield-media {
  position: relative;
  overflow: hidden;
  background: var(--shell);
}
/* The field bleeds into the cloth.
   The panel and the photograph used to meet at a hard 50/50 seam, which reads
   as two rectangles side by side. Carrying the field colour a little way UNDER
   the photograph's inner edge, on a soft gradient, makes the colour look like
   it is coming out of the weave — which is the entire claim this page makes,
   since the colour is literally sampled from that weave.

   It sits on the media rather than the copy so it is clipped by the
   photograph's own overflow, and it follows the flip: the gradient always runs
   from transparent at the outside to solid at the edge the copy is on. */
.bfield-media::after {
  content: "";
  position: absolute;
  top: 0; bottom: 0; right: 0;
  width: clamp(40px, 5vw, 80px);
  background: linear-gradient(to right, transparent, var(--bfield-edge));
  pointer-events: none;
}
.bfield.flip .bfield-media::after {
  right: auto; left: 0;
  background: linear-gradient(to left, transparent, var(--bfield-edge));
}
.bfield-media img { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover; }
.bfield-media.two { display: grid; grid-template-rows: 1fr 1fr; gap: 4px; }
.bfield-media.two > div { position: relative; overflow: hidden; }
.bfield-media.two img { position: absolute; }
/* ---- the brand panel: a wash, not a block (owner, 20 Aug 2026) ----
   This was five solid --accent-field slabs with white copy on them, and
   against the light direction it was the loudest thing on the site: five
   full-saturation rectangles alternating with five fabric macros, which is
   the opposite of calm. The instruction was that nothing is a flat block of
   colour, and this was the largest flat block there was.

   THE BRAND'S COLOUR DOES NOT LEAVE THE PAGE, it just stops being the
   panel. The fabric macro is still full-bleed in the other half of the
   section at full saturation -- and that is the standing preference anyway:
   colour comes from the garments, the fabric photos and the swatches. The
   cloth was always making this page's argument; the panel was repeating it
   at ten times the area.

   The wash is the same brand hue the macro was sampled for -- --wash-brand,
   set per section from data/wash-palette.json -- run deeper here than
   anywhere else on the site, because this panel is half a section and has to
   read as a surface rather than as a tint.

   /brands/ WAS EXPLICITLY EXCLUDED FROM THE 27 AUG 2026 WASH REMOVAL
   (owner). Every other light ground on the site is plain white; this panel
   keeps its wash, its flip, its stacked variant and its bleed exactly as
   they were. That makes .bfield-copy the ONLY washed surface left, so the
   whole mechanism lives here rather than in a shared selector list at the
   top of the file -- the geometry, the edge fade and the ramps are declared
   on this element and its variants and nowhere else. Nothing can inherit
   this ground by being added to a list, which is how it used to spread.

   THE .bfield* COMPONENTS ARE USED BY /brands/ AND BY NOTHING ELSE. The five
   brand pages (/brand/<slug>/) are built from .section, .hero and band(),
   all of which are white -- so this block cannot drag them back, and the
   build asserts exactly that (see the bfield-scope check in build.mjs).

   Copy goes to ink. --accent-field, the darkened cut that existed ONLY so
   white body copy could clear 4.5:1 on the solid, is no longer used here --
   it stays defined because .bfield-media::after still bleeds toward it. */
.bfield-copy {
  position: relative;
  background-color: var(--paper);
  /* The house geometry, declared HERE because this is now the only element
     that paints a wash. Same three ellipses, same stops, same alphas as the
     panel has always had. */
  --wash-geo-a: 128% 112% at 6% -14%;
  --wash-geo-b: 106% 96% at 98% 2%;
  --wash-geo-c: 118% 106% at 56% 116%;
  --wash-fade: clamp(56px, 8%, 152px);
  /* The edge fade: the panel arrives at paper at its own top and bottom, so
     it meets the white sections above and below it with no seam. Painted as
     two linear ramps at the TOP of the stack, over the radials, rather than
     as a mask -- a mask would need its own pseudo-element and a stacking
     context to go with it.

     Safe as a custom property containing var() ONLY because --paper and
     --wash-fade are constants at this point: the trap (var() in a custom
     property resolves where the property is DECLARED) bit the old
     --wash-stack token, which carried the per-element hues. The hues below
     are written straight into background-image for that reason. */
  --wash-edge:
    linear-gradient(to bottom, var(--paper), rgb(255 255 255 / 0) var(--wash-fade)),
    linear-gradient(to top,    var(--paper), rgb(255 255 255 / 0) var(--wash-fade));
  --wash-a: var(--wash-brand); --wash-b: var(--wash-brand); --wash-c: var(--wash-brand);
  --w-a: .26; --w-b: .14; --w-c: .19;
  background-image:
    var(--wash-edge),
    radial-gradient(var(--wash-geo-a), rgb(var(--wash-a) / var(--w-a)), rgb(var(--wash-a) / 0) 64%),
    radial-gradient(var(--wash-geo-b), rgb(var(--wash-b) / var(--w-b)), rgb(var(--wash-b) / 0) 62%),
    radial-gradient(var(--wash-geo-c), rgb(var(--wash-c) / var(--w-c)), rgb(var(--wash-c) / 0) 66%);
  color: var(--ink);
  padding: clamp(48px, 7vw, 112px) clamp(32px, 5.5vw, 96px);
  display: flex;
  flex-direction: column;
  justify-content: center;
}
/* The plate number, which used to head the white block underneath. */
/* --ink-2, not --ink-4, and that is a contrast decision rather than a
   styling one. The four ink tiers were solved against the two grounds they
   were ever set on -- #FFFFFF and the pale tint #ECEFF7 -- where they measure
   17.8 / 8.9 / 5.7 / 5.2. This panel is deliberately DEEPER than #ECEFF7: at
   its darkest point it reaches rgb(244 214 214) under JustScrubs, and --ink-4
   measures 3.83 there, under the 4.5 floor for small text. --ink-3 is 4.38 on
   the same ground and also fails.

   The right answer is a darker tier on a darker ground, not a thinner ground.
   Flattening the wash until --ink-4 cleared would have taken most of the
   colour out of the panel to protect one plate number. --ink-2 measures 6.57
   at the same worst point. */
.bfield-copy .bfield-idx {
  display: block;
  color: var(--ink-2);
  margin-bottom: var(--s5);
}
.bfield-copy .brandmark { max-width: min(100%, 320px); height: auto; margin-bottom: var(--s6); }
.bfield-copy .benefit {
  font-family: var(--display);
  font-size: var(--t-d2);
  font-weight: 700;
  letter-spacing: var(--t-d2-tr);
  line-height: var(--t-d2-lh);
  margin: 0 0 var(--s4);
  max-width: 16ch;
  color: var(--ink);
}
/* 34ch was narrower than the longest fabric name set beside its blend, and
   broke the blend across two lines; with .blend held on one line the
   measure only has to be wide enough for the fabric name (owner, 17 Aug
   2026). An explicit line-height stops the two lines touching where the
   field is narrow. */
.bfield-copy .fabric-name { font-size: 15px; line-height: 1.5; color: var(--ink-2); margin: 0 0 var(--s5); max-width: 48ch; }
.bfield-copy .fabric-name b { color: var(--ink); font-weight: 600; }

/* ---- fabric properties — the hangtag facts, under the blend line ----
   Two columns where the field is wide enough for the longest label
   ("Soil-release finish") to stay on one line, one column where it is not.
   A hairline above and below sets the list apart from the paragraph beneath
   without adding a box: this panel is one surface and should stay one. */
.fprops {
  list-style: none;
  margin: 0 0 var(--s5);
  padding: var(--s4) 0;
  /* A hairline mixed from the brand hue rather than a flat grey, so the rule
     belongs to the panel it is drawn on.

     --detail overrides it where a brand sets one (owner, 20 Aug 2026). A wash
     tint is right when the accent is mid-toned, and invisible when the accent
     is a pale peach — so EliteMed pins these to its own darker cut instead.
     Brands without a `detail` keep the tint exactly as before. */
  border-top: 1px solid var(--detail, rgb(var(--wash-brand) / .35));
  border-bottom: 1px solid var(--detail, rgb(var(--wash-brand) / .35));
  display: grid;
  /* min(170px, 100%), not a bare 170px. Under min-content sizing auto-fit
     lays one track per item, so a hard 170px floor made this list report
     3 x 170 + gaps = 558px of min-content. That floored the 1fr column of
     the .bfield it sits in and put 18px of sideways scroll on /brands/ and
     /fabrics/ at 600px. Wrapped in min() the track may fall below 170 when
     the container is narrower, which is exactly what a narrow phone needs. */
  grid-template-columns: repeat(auto-fit, minmax(min(170px, 100%), 1fr));
  gap: var(--s3) var(--s5);
  max-width: 48ch;
}
.fprops li {
  display: flex;
  align-items: center;
  gap: 10px;
  font-size: 14px;
  font-weight: 500;
  line-height: 1.25;
  letter-spacing: -.005em;
  color: var(--ink);
}
/* The icon takes its colour from the text, which is the whole point: one set
   of marks, every accent ground. `flex: none` so a two-word label wrapping
   never squeezes the glyph out of square. */
/* The property glyphs take the BRAND accent -- one accent per panel, and the
   only colour in the copy column besides the headline phrase. --eb is set on
   the section to accentInk(), which is the value that clears 4.5:1. */
.fprops .picon {
  flex: none;
  width: 19px; height: 19px;
  /* --detail first, then the derived eyebrow ink, then a neutral. A brand
     that sets no --detail is unchanged. */
  color: var(--detail, var(--eb, var(--ink-3)));
}
/* The paragraph that used to sit in a white block below the panel. */
.bfield-copy .bfield-detail {
  font-size: 15px;
  line-height: 1.65;
  color: var(--ink-2);
  margin: 0 0 var(--s6);
  max-width: 52ch;
}
.bfield-copy .btn { align-self: flex-start; }
/* Mirrored, so the deep end of the wash always meets the photograph rather
   than always sitting on the left. Same three glows, X flipped. */
.bfield.flip .bfield-copy {
  --wash-geo-a: 128% 112% at 94% -14%;
  --wash-geo-b: 106% 96% at 2% 2%;
  --wash-geo-c: 118% 106% at 44% 116%;
}
@media (max-width: 900px) {
  /* minmax(0, 1fr) so no single child's min-content can widen the stacked
     column past the viewport, the way .fprops did. */
  .bfield, .bfield.flip { grid-template-columns: minmax(0, 1fr); min-height: 0; }
  .bfield.flip .bfield-media { order: 0; }
  .bfield-media { aspect-ratio: 3 / 2; }
  .bfield-media.two { aspect-ratio: 3 / 2; grid-template-rows: none; grid-template-columns: 1fr 1fr; }
  /* Stacked, so the copy is underneath and the bleed turns with it. */
  .bfield-media::after,
  .bfield.flip .bfield-media::after {
    top: auto; left: 0; right: 0; bottom: 0;
    width: auto;
    height: clamp(36px, 9vw, 72px);
    background: linear-gradient(to bottom, transparent, var(--bfield-edge));
  }
  /* Stacked, the copy is underneath, so the deep end of the wash moves to the
     TOP edge -- the seam with the photograph -- rather than to a side that no
     longer touches it. */
  .bfield-copy, .bfield.flip .bfield-copy {
    --wash-geo-a: 150% 90% at 50% -18%;
    --wash-geo-b: 120% 80% at 6% 40%;
    --wash-geo-c: 130% 90% at 88% 116%;
    --w-a: .26; --w-b: .13; --w-c: .16;
    /* BOTTOM EDGE ONLY. Stacked, this panel's top edge is not a boundary with
       another ground — it is the seam with its own photograph, and the photo
       bleeds DOWN into it via .bfield-media::after, which ramps to
       --bfield-edge. Fading the panel to paper there put a white line between
       the two: the bleed arrived at #FEEBCE and the panel started at #FFFFFF,
       a 49/255 step, which was the single worst seam left on the site after
       the edge fade went in. Left un-faded the two meet within 9/255. */
    --wash-edge: linear-gradient(to top, var(--paper), rgb(255 255 255 / 0) var(--wash-fade));
  }
}
@media (max-width: 420px) {
  .fprops { grid-template-columns: 1fr; }
}

/* ---- the home slideshow hero ----
   The photo hero with several layers. Each layer carries its own ground and
   photograph and cross-fades at 1200ms; the copy sits on top. Dots control
   it; hovering or focusing the hero pauses the timer; reduced motion shows
   the first slide only (see 15). */
.phero.slides { grid-template-columns: 1fr; }
.phero.slides .phero-copy { grid-area: 1 / 1; position: relative; z-index: 3; width: 60%; }
.phero.slides .phero-media { grid-area: 1 / 1; height: 100%; }
.phero.slides .slide {
  position: absolute; inset: 0;
  background: var(--hp-bg, var(--paper));
  opacity: 0;
  transition: opacity 1200ms var(--ease-io);
}
.phero.slides .slide.on { opacity: 1; }
.phero.slides .slide img {
  position: absolute; top: var(--head-h); right: 0; bottom: 0;
  height: calc(100% - var(--head-h));
  width: min(50%, calc(var(--hp-w, 848) * 1px), calc((100svh - var(--head-h)) * var(--hp-w, 848) / var(--hp-h, 1264)));
  left: auto;
  object-fit: cover;
  object-position: var(--hp-pos, 50% 0%);
  -webkit-mask-image: linear-gradient(to right, transparent, #000 40%);
          mask-image: linear-gradient(to right, transparent, #000 40%);
}
.slide-dots {
  display: flex;
  gap: 10px;
  margin: var(--s6) 0 0;
  padding: 0;
  list-style: none;
}
.slide-dots button {
  width: 40px; height: 28px;
  border: 0; padding: 0;
  background: transparent;
  cursor: pointer;
  position: relative;
}
.slide-dots button::before {
  content: "";
  position: absolute;
  left: 3px; right: 3px; top: 50%;
  height: 4px;
  transform: translateY(-50%);
  border-radius: 2px;
  background: rgba(22,24,28,.18);
  transition: background 300ms var(--ease), transform 300ms var(--ease);
}
.slide-dots button[aria-current="true"]::before { background: var(--ink); }
.slide-dots button:hover::before { transform: translateY(-50%) scaleY(1.6); }
.slide-dots button:focus-visible { outline: 2px solid var(--red); outline-offset: 2px; border-radius: 6px; }
@media (max-width: 900px) {
  /* The slideshow joins the side-by-side hero like every other. These
     selectors carry .slides, so they outrank the generic mobile hero rules
     further down and have to restate the same geometry rather than leave
     the old overlay in place: one cell for the copy, one for the picture.
     The scrim that used to be painted over each slide goes with it — there
     is no copy on the photograph here to make legible. */
  .phero.slides { grid-template-columns: minmax(0, 1fr) minmax(0, var(--hero-photo-w, 43%)); }
  .phero.slides .phero-copy {
    grid-area: auto;
    grid-column: 1;
    grid-row: 1;
    position: static;
    width: auto;
  }
  .phero.slides .phero-media {
    grid-area: auto;
    grid-column: 2;
    grid-row: 1;
    position: relative;
    height: auto;
  }
  .phero.slides .slide img { width: 100%; -webkit-mask-image: none; mask-image: none; }
  .phero.slides .slide::after,
  .phero.slides .phero-media::after { display: none; }
  .slide-dots button::before { background: rgba(22,24,28,.18); }
  .slide-dots button[aria-current="true"]::before { background: var(--ink); }
}
@media (prefers-reduced-motion: reduce) {
  .phero.slides .slide { transition: none; }
}

/* ---- the full-bleed landscape hero (19 Aug 2026) ----
   One 16:9 photograph edge to edge, the copy laid into the studio void beside
   the model. It replaces the half-column device, which existed only because
   every model file used to be 848px wide and could not be asked to fill a
   screen; the August delivery is 3200px on the long edge, so at 1600 CSS px the frame is
   still sampling below native.

   Which side the copy sits on comes from PHOTO_SIDE in build.mjs, opposite
   the model, and the build refuses a slideshow whose frames disagree. */
@media (min-width: 901px) {
  .phero.wide {
    display: block;
    grid-template-columns: none;
    /* no negative top margin: the hero starts BELOW the chrome */
    margin-top: 0;
    /* ALWAYS full-bleed. aspect-ratio + max-height was wrong here: when the
       screen is shorter than 16:9 (1920x1080 is), aspect-ratio transfers the
       height clamp into WIDTH and the hero shrinks to 1735px with dead paper
       on the right, squeezing the photograph toward the copy. Clamping the
       HEIGHT and letting cover crop the bottom keeps the box at 100vw at
       every size — and keeps every horizontal fraction mapping 1:1, which is
       what the copy-stop arithmetic assumes. A bottom crop can only ever
       take feet: the vertical anchor is 0%. */
    height: min(calc(100vw * 9 / 16), calc(100svh - var(--bar-h) - var(--head-h)));
    min-height: 480px;
  }
  .phero.wide .phero-media { position: absolute; inset: 0; height: 100%; }
  .phero.wide .slide { inset: 0; }
  .phero.wide .phero-media img,
  .phero.wide .slide img {
    position: absolute;
    top: 0; left: 0; right: auto; bottom: 0;
    width: 100%;
    height: 100%;
    object-fit: cover;
    object-position: var(--hp-pos, 50% 0%);
    /* the soft left edge belonged to the half-column; full bleed has no edge */
    -webkit-mask-image: none;
            mask-image: none;
  }
  .phero.wide .phero-copy {
    position: absolute;
    top: 50%;
    transform: translateY(-50%);
    z-index: 3;
    /* Anchored at the gutter, STOPPED at --copy-stop — the per-hero line
       build.mjs draws just clear of the nearest subject edge across every
       frame, at the cinematic hold's worst case. Both edges scale with the
       viewport, so the clearance holds at every width, not just the ones
       that were checked. max-width keeps the measure honest on big screens. */
    width: auto;
    max-width: 780px;
    padding: 0;
    left: max(var(--gutter), calc((100vw - var(--wrap-wide)) / 2 + var(--gutter)));
    right: calc(100% - var(--copy-stop, 58%));
  }
  .phero.wide[data-copy="right"] .phero-copy {
    left: var(--copy-start, 42%);
    right: max(var(--gutter), calc((100vw - var(--wrap-wide)) / 2 + var(--gutter)));
    /* an absolutely positioned box resolves its spare width into an auto
       margin, so this anchors it against the right gutter */
    margin-left: auto;
  }
  .phero.wide[data-copy="right"] .phero-label {
    left: auto;
    right: max(var(--gutter), calc((100vw - var(--wrap-wide)) / 2 + var(--gutter)));
  }
  .phero.wide .phero-label { top: var(--s5); }
  /* 15ch was measured against the half-column; the void is wider than that */
  .phero.wide .phero-copy h1 { max-width: 18ch; }
}

/* ---- the cinematic hold ----
   A slow push from 1.00 to 1.06 over 28s on a brand hero, and a 7.2s settle
   from 1.03 on each home slide as it arrives — 7.2s being the slide's 6s hold
   plus the 1.2s crossfade, so the arc is always cut off by the next frame and
   never lands.

   THE TRANSFORM ORIGIN IS PINNED TO THE MODEL'S OWN EDGE, and that is the
   whole trick. A centred scale(1.06) crops 2.83% off BOTH sides, and these
   models stand out to 96-100% of the frame — it would shave an arm on every
   brand page and a face on the widest Karisma hero. Anchored at the model's edge,
   every pixel of the growth is taken out of the empty studio ground instead.
   The vertical origin is 0% for the same reason it is everywhere else on this
   site: growth may only ever go downward, never up into a head. That is also
   why there is no translate — with the origin pinned, a pan either eats the
   model or opens a gap at the opposite edge. */
/* data-copy names where the COPY sits, so the model is on the other side —
   and it is the MODEL's edge the growth has to be pinned to. */
.phero.wide .phero-media img,
.phero.wide .slide img { transform-origin: 100% 0%; }
.phero.wide[data-copy="right"] .phero-media img,
.phero.wide[data-copy="right"] .slide img { transform-origin: 0% 0%; }

@media (prefers-reduced-motion: no-preference) {
  /* the generic hero reveal scales the photograph in; the hold does its own
     scaling, so here the reveal is opacity only */
  .js .phero.wide .phero-media img { transform: none; }
  .js.ready .phero.wide:not(.slides) .phero-media img {
    will-change: transform;
    animation: cine-hold 28s linear infinite alternate;
  }
  .js .phero.wide.slides .slide.on img {
    will-change: transform;
    animation: cine-arc 7200ms var(--ease-io) forwards;
  }
}
@keyframes cine-hold {
  from { transform: scale(1); }
  to   { transform: scale(1.06); }
}
@keyframes cine-arc {
  from { transform: scale(1.03); }
  to   { transform: scale(1); }
}
@media (prefers-reduced-motion: reduce) {
  .phero.wide .phero-media img,
  .phero.wide .slide img { animation: none !important; transform: none !important; }
}

/* compact brand block, high on the home page */
.section.brandrow { padding: clamp(40px, 5vw, 64px) 0 clamp(48px, 6vw, 80px); }
.brand-grid.compact { grid-template-columns: repeat(6, 1fr); gap: var(--s3); }
.brand-grid.compact.five { grid-template-columns: repeat(5, 1fr); }
.brand-grid.compact .brand-card { min-height: 112px; padding: clamp(16px, 2vw, 28px); border-radius: var(--r-md); }
.brand-grid.compact .brand-card.dark span { font-size: 15px; }
.brand-grid.compact .brand-card.dark .arw { font-size: 18px; margin-top: 6px; }
@media (max-width: 1100px) {
  .brand-grid.compact { grid-template-columns: repeat(3, 1fr); }
  /* five cards in threes is three-and-two; in twos the fifth spans the row */
  .brand-grid.compact.five { grid-template-columns: 1fr 1fr; }
  .brand-grid.compact.five > :nth-child(5) { grid-column: 1 / -1; }
}
@media (max-width: 560px) { .brand-grid.compact { grid-template-columns: 1fr 1fr; } }

/* light footer — bone, so the color lockups sit properly */
footer.site.light { background: var(--shell); color: var(--ink-2); }
footer.site.light .foot-grid { border-bottom-color: var(--rule-shell); }
footer.site.light .foot-brand .logo { color: var(--ink); }
footer.site.light .foot-brand p, footer.site.light .fhead, footer.site.light .nolink, footer.site.light .foot-legal { color: var(--ink-3); }
footer.site.light a { color: var(--ink-2); }
footer.site.light a:hover { color: var(--red); }
footer.site.light .foot-logos { border-bottom-color: var(--rule-shell); }
footer.site.light .foot-logos a { opacity: 1; }
@media (hover: hover) and (pointer: fine) {
  footer.site.light .foot-logos a:hover { transform: translateY(-2px); }
}


/* == 15  motion =========================================================== */
/* Two safety rails:
   1. Hidden initial states live ONLY inside prefers-reduced-motion:
      no-preference, so reduced-motion users get a plain visible page.
   2. Everything is gated on .js, set inline in <head>. If JS fails, no
      element is ever hidden and the brand rail simply scrolls. */

@media (prefers-reduced-motion: no-preference) {

  .js .reveal {
    opacity: 0;
    transform: translateY(20px);
  }
  .js .reveal.in {
    opacity: 1;
    transform: none;
    transition:
      opacity var(--dur) var(--ease),
      transform var(--dur) var(--ease);
    transition-delay: calc(var(--i, 0) * var(--stagger));
  }

  /* hero load ladder — runs once, no observer */
  .js .hero-step {
    opacity: 0;
    transform: translateY(20px);
  }
  .js.ready .hero-step {
    opacity: 1;
    transform: none;
    transition:
      opacity var(--dur-x) var(--ease),
      transform var(--dur-x) var(--ease);
    transition-delay: calc(var(--i, 0) * 110ms);
  }
  /* the hero photograph settles in a touch behind the type */
  .js .phero-media img { opacity: 0; transform: scale(1.03); }
  .js.ready .phero-media img {
    opacity: 1; transform: none;
    transition: opacity 1200ms var(--ease-io), transform 1600ms var(--ease);
  }

  .card { will-change: transform; }
  .card:hover { transform: translateY(-4px); }
}

@media (prefers-reduced-motion: reduce) {
  html { scroll-behavior: auto; }
  *, *::before, *::after {
    animation-duration: .001ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: .001ms !important;
    scroll-behavior: auto !important;
  }
}


/* == 16  responsive ======================================================= */

@media (max-width: 1080px) {
  .split { grid-template-columns: 1fr 320px; }
  footer.site .foot-grid { grid-template-columns: 1fr 1fr; }
}

@media (max-width: 960px) {
  /* minmax(0, 1fr): see the note on .catalog. Collapsed to one column this
     is what was putting a horizontal scrollbar on /catalog/ at 390 — the
     track sized to the card grid's 415px min-content inside a 350px
     container, which then fitted two cards where one belongs. */
  .catalog { grid-template-columns: minmax(0, 1fr); }
  /* ---- the filter drawer ----
     This used to be display:none -> display:block, which cannot be
     transitioned at all: the main browsing tool on a phone appeared and
     vanished with no path in and no path out. It is a sheet, so it now rises
     from the bottom edge and dismisses back down to the same edge — if
     something disappears one way we expect it to come back from there.

     visibility, NOT display, is what does the hiding. It still takes the drawer
     out of the tab order and out of the accessibility tree exactly as display
     did, but unlike display it can be transitioned — held visible for the whole
     slide out, then flipped at the end. opacity alone would have left a
     keyboard trap behind.

     The global prefers-reduced-motion rule flattens every duration on the page,
     so a reduced-motion visitor gets the drawer instantly with no slide and no
     extra rule needed here. */
  .side {
    position: fixed;
    inset: 0;
    z-index: 60;
    max-height: none;
    background: var(--paper);
    padding: var(--s5) var(--gutter) var(--s9);
    overflow-y: auto;
    overscroll-behavior: contain;   /* scrolling the drawer must not scroll the grid under it */
    visibility: hidden;
    opacity: 0;
    transform: translateY(100%);
    transition:
      transform 340ms var(--ease-io),
      opacity 200ms var(--ease-io),
      visibility 0s linear 340ms;
  }
  .side[data-open] {
    visibility: visible;
    opacity: 1;
    transform: none;
    transition:
      transform 380ms var(--ease),
      opacity 160ms var(--ease),
      visibility 0s;
  }
  .filter-toggle {
    display: inline-flex;
    align-items: center;
    gap: var(--s3);
    background: var(--ink);
    color: #fff;
    border: 0;
    cursor: pointer;
    font-family: var(--sans);
    font-size: 13px;
    font-weight: 600;
    letter-spacing: 0;
    padding: 13px 20px;
    border-radius: var(--r-pill);
  }
  .side .filter-toggle { width: 100%; justify-content: center; margin-top: var(--s5); }
  .side-actions { position: sticky; top: 0; background: var(--paper); z-index: 2; }
  .stats-panel { grid-template-columns: 1fr 1fr; }
  /* the hairline that divided columns now has to divide rows too */
  .stat-cell:nth-child(3) { border-left: 0; }
  .stat-cell:nth-child(n+3) { border-top: 1px solid rgba(255,255,255,.13); }
}

@media (max-width: 900px) {
  .editorial, .split, .style-head, .contact-grid {
    grid-template-columns: 1fr;
    gap: var(--s7);
  }
  .band, .band.flip, .fab { grid-template-columns: 1fr; gap: var(--s6); }
  .band.flip > .band-copy { order: 0; }
  .fab.flip > .fab-media { order: 0; }
  .band-media { max-width: 560px; }
  .side { top: 0; }
}

@media (max-width: 900px) {
  /* ---- every hero on a phone: copy and photograph SIDE BY SIDE ----
     Owner's call, 19 Aug 2026, replacing the stacked treatment. The whole
     hero — headline, lede, both buttons and the picture — has to land on
     the first screen with nothing scrolled, so the type comes down a step,
     the leading tightens and the buttons shrink to pay for it.

     The photo column is the delicate half. These are 16:9 landscapes in a
     slot narrower than they are, so the crop runs SIDEWAYS and the taller
     the box the less of the width survives. --hp-ar is the tallest this
     box may be per unit of its own width before the visible window becomes
     narrower than the model (build.mjs, heroAspect) — 1.5 to 2.0 for the
     landscapes, and a portrait or square file's own aspect otherwise,
     which crops nothing. --hp-pos-n anchors the crop to the model's own
     side, so what survives is her and not the far wall.

     No copy is over the photograph at all here — they are separate grid
     columns — so there is no legibility question and no scrim, exactly as
     when they were stacked. */
  .phero, .phero.wide {
    display: grid;
    grid-template-columns: minmax(0, 1fr) minmax(0, var(--hero-photo-w, 43%));
    align-items: center;
    column-gap: var(--s4);
    height: auto;
    min-height: 0;
    max-height: none;
    aspect-ratio: auto;
    margin-top: 0;
    padding: var(--s5) var(--gutter);
    background: var(--paper);
    overflow: hidden;
  }
  .phero-copy {
    grid-column: 1;
    grid-row: 1;
    position: static;
    z-index: auto;
    width: auto;
    padding: 0;
    color: var(--ink);
    background: none;
  }
  .phero-media {
    grid-column: 2;
    grid-row: 1;
    position: relative;
    inset: auto;
    height: auto;
    width: 100%;
    /* never wider than the file itself, whatever the viewport does */
    max-width: calc(var(--hp-w, 848) * 1px);
    margin-inline: auto;
    aspect-ratio: 1 / var(--hp-ar, 1.49);
    overflow: hidden;
    background: var(--hp-bg, var(--paper));
  }
  .phero-media img,
  .phero.wide .phero-media img,
  .phero .slide img {
    position: absolute;
    inset: 0;
    top: 0;
    width: 100%;
    height: 100%;
    object-fit: cover;
    object-position: var(--hp-pos-n, var(--hp-pos, 50% 0%));
    -webkit-mask-image: none; mask-image: none;
    transform: none;
  }
  .phero .slide { position: absolute; inset: 0; }
  /* the scrim, and everything it was compensating for, gone */
  .phero-media::after,
  .phero .slide::after { opacity: 0; display: none; }

  /* Scaled to fit half a phone. */
  .phero .phero-copy h1 {
    font-size: clamp(20px, 5.4vw, 27px);
    line-height: 1.1;
    letter-spacing: -.02em;
    color: var(--ink);
    margin: 0 0 var(--s3);
  }
  /* Not 12px. The first pass shrank until it fitted and then kept going —
     everything cleared the fold at 360x640 with 121px to spare, which is
     slack bought at the cost of a lede a buyer has to squint at. Given back
     to the two elements that were paying for it. */
  .phero-copy .lede {
    font-size: clamp(12.5px, 3.5vw, 15px);
    line-height: 1.4;
    color: var(--ink-2);
    margin: 0 0 var(--s3);
    max-width: none;
  }
  .phero-copy .hero-actions {
    display: flex;
    flex-direction: column;
    align-items: stretch;
    gap: var(--s2);
  }
  .phero-copy .btn,
  .phero-copy .btn.lg {
    justify-content: center;
    padding: 11px 16px;
    font-size: 13px;
    gap: var(--s2);
  }
  .phero-copy .btn.ghost { color: var(--ink); box-shadow: inset 0 0 0 1px var(--rule-strong, rgba(22,24,28,.22)); }
  .phero-copy .btn.ghost:hover { background: var(--ink); color: #fff; }
  .phero-copy .brand-h1 img { max-width: min(100%, 200px); height: auto; }
  .phero-label { display: none; }
  /* no scrim under it any more, so the logo needs no plate to sit on */
  .phero-copy .brand-h1 img { background: none; padding: 0; }

  /* The stacked "photograph, then copy" treatment that used to live here
     is gone: every hero, wide or not, is now the side-by-side grid above.
     What remains is the dot colouring, because the dots sit on paper here
     rather than on the photograph. */
  .phero.wide .slide-dots button::before { background: rgba(22,24,28,.18); }
  .phero.wide .slide-dots button[aria-current="true"]::before { background: var(--ink); }
}

@media (max-width: 860px) {
  nav.main, .masthead-cta { display: none; }
  .menu { display: block; }
}

@media (max-width: 720px) {
  /* the bar needs 619px for all three parts; the address is in the footer of
     every page, so it is the one that goes rather than wrapping the bar */
  .tb-addr { display: none; }
  .topbar .wrap { gap: var(--s4); font-size: 12px; }
  /* 214px, not 190. auto-fill only lays a track it can fit, but it fits
     against the DECLARED minimum — so a declared 190 while a catalog card's
     real min-content is 206 let it lay two tracks that the card then burst
     out of. Declared at the true figure plus a little headroom, the
     arithmetic and the content agree and the count is right at every width
     without a hand-set breakpoint per page. */
  .grid { grid-template-columns: repeat(auto-fill, minmax(214px, 1fr)); }
  .colorgrid { grid-template-columns: repeat(auto-fill, minmax(150px, 1fr)); }
  footer.site .foot-grid { grid-template-columns: 1fr; }
  .cred-grid { grid-template-columns: 1fr 1fr; }
  .brand-grid { grid-template-columns: 1fr 1fr; }
}

@media (max-width: 560px) {
  .stats-panel { grid-template-columns: 1fr; }
  .stat-cell { border-left: 0; }
  .stat-cell + .stat-cell { border-top: 1px solid rgba(255,255,255,.13); }
}

@media (max-width: 480px) {
  :root { --t-body: 16px; }
  /* `.grid { 1fr 1fr }` used to sit here and was the bug: a 1fr track IS
     floored by min-content, so it forced two columns at widths where two
     cards could not fit — 31px of sideways page scroll at 390, and worse on
     /catalog/, whose cards carry the ratio line and run 206px rather than
     190. The auto-fill rule above already picks the honest count. */
  .card { padding: var(--s4); }
  .card .style-no { font-size: 18px; }
  .fig-strip { grid-template-columns: repeat(3, 1fr); }
  .btn { padding: 14px 22px; font-size: 13px; }
  .btn.lg { padding: 16px 26px; font-size: 14px; }
  .plate-grid { grid-template-columns: 1fr; }
  .brand-grid { grid-template-columns: 1fr; }
}

/* 360px floor — nothing may overflow horizontally below this */
@media (max-width: 380px) {
  :root { --gutter: 16px; }
  .grid { grid-template-columns: 1fr; }
  .colorgrid { grid-template-columns: 1fr 1fr; }
  .ratio-strip { font-size: 15px; gap: var(--s2); }
  .ratio-strip .seg::after { width: 10px; }
}


/* == 17  focus + print ==================================================== */

:focus-visible {
  outline: 2px solid var(--red);
  outline-offset: 3px;
}
.card:focus-visible { outline-offset: -2px; }

/* The burger/X is a <summary>, which keeps :focus-visible matched after a
   POINTER activation in Chromium and WebKit — so a tap to open the menu
   left a 2px red ring, offset 3px around a 40x40 control, drawn as a box
   around the X for the whole time the panel was open. Suppressed only when
   the script has recorded that the last input was a pointer; keyboard focus
   still paints the ring, which the accessibility floor requires. Scoped to
   this one control so focus behaviour is unchanged everywhere else. */
html[data-input="pointer"] .menu > summary:focus-visible { outline: none; }

/* == 18  quality of life (19 Aug 2026) ==================================== */

/* a jump target must land clear of the sticky masthead, not under it */
[id] { scroll-margin-top: calc(var(--head-h) + 24px); }

/* the scrollbar appearing and disappearing between short and long pages was
   shifting every centred layout sideways by half its width */
html { scrollbar-gutter: stable; }

/* selection carries the house ink; on dark bands it inverts */
::selection { background: var(--ink); color: #fff; }
.darkfield ::selection, .cta-close ::selection, .bfield-copy ::selection,
footer.site:not(.light) ::selection { background: #fff; color: var(--ink); }

/* any native control that ever slips through the custom chrome */
input, select { accent-color: var(--red); }

/* paragraph rag control — headings already balance */
p { text-wrap: pretty; }

/* scroll progress: a 2px ink line under the masthead, lookbook pages only —
   the spec pages stay calm — and only once the page is long enough that
   position feedback earns its pixels. Driven by --scroll-p from the script. */
[data-register="lookbook"] .masthead.progress::after {
  content: "";
  position: absolute;
  left: 0; right: 0; bottom: -2px;
  height: 2px;
  background: var(--ink);
  transform-origin: 0 50%;
  transform: scaleX(var(--scroll-p, 0));
}

/* photographs arrive, they do not pop — the hidden state exists only under
   .js and only when motion is welcome, per the standing rule */
@media (prefers-reduced-motion: no-preference) {
  .js .band-media img.ph, .js .fig img.ph,
  .js .bfield-media img.ph, .js .rail-media img.ph {
    opacity: 0;
    transition: opacity 600ms var(--ease);
  }
  .js .band-media img.ph.loaded, .js .fig img.ph.loaded,
  .js .bfield-media img.ph.loaded, .js .rail-media img.ph.loaded {
    opacity: 1;
  }
}

@media print {
  nav.main, .menu, .masthead-cta, .side, .filter-toggle, .cta-close, .darkfield { display: none; }
  .masthead { position: static; }
  body { font-size: 11pt; }
  .fig { break-inside: avoid; }
  .card { break-inside: avoid; }
}


/* Frost is honoured as a PREFERENCE (apple-design): when the platform asks
   for reduced transparency, both glass surfaces go solid. Placed after the
   @supports frost rules so it wins by order at equal specificity. */
@media (prefers-reduced-transparency: reduce) {
  .masthead::before {
    background: rgba(255, 255, 255, .98);
    -webkit-backdrop-filter: none;
    backdrop-filter: none;
  }
  .menu-panel {
    background: var(--paper);
    -webkit-backdrop-filter: none;
    backdrop-filter: none;
  }
}

/* == 19  saved styles ===================================================== */
/* The star, the counter in the chrome, and /saved/. All of it is driven by
   src/saved-store.js through four calls; none of it knows where the list is
   kept. Kept in one block for the same reason the store is one file — when
   saving moves to accounts this is the surface that has to still look right,
   and a surface scattered through eighteen sections is one nobody re-checks. */

/* ---- the card is a container now ----
   It used to be the <a> itself. A save control is a button, and a button
   inside a link is invalid and unreachable by keyboard, so the link became a
   stretched overlay and the star a sibling above it. The click target is
   unchanged: ::after covers the whole card. */
.card-link {
  position: static;
  text-decoration: none;
  color: inherit;
}
.card-link::after {
  content: "";
  position: absolute;
  inset: 0;
  z-index: 1;
}
/* The card carried a pointer cursor for free while it was an anchor. */
.card:not(.static) { cursor: pointer; }

/* The focus ring belongs to the whole card, not to a zero-size anchor in the
   corner of it — so it is drawn on the overlay, which is card-sized. */
.card-link:focus-visible { outline: none; }
.card-link:focus-visible::after {
  outline: 2px solid var(--red);
  outline-offset: -2px;
}

/* ---- the star ----
   No JS, no star: it is inert without the store, and an inert control is
   worse than an absent one. Every other progressive control here is gated the
   same way.

   AND NO SESSION, NO STAR (owner, 27 Aug 2026). The shortlist lives on the
   account now, so a signed-out visitor has nowhere to save to —
   /api/account/saved answers 401, and a star that appears to work and
   silently does not is the worst failure available here. `.is-in` is the
   class the masthead's auth controls already hang off, so this is one extra
   token on a selector that was already doing this job rather than any new
   machinery. Before the session answer lands neither class is set, so the
   star is absent rather than briefly wrong.

   It only matters while GATING is off. With the gate on a signed-out visitor
   cannot reach a style page at all, so the rule stops applying and does no
   harm — which is why it is a selector and not a second state in the
   template to design and maintain. */
.star { display: none; }
.js.is-in .star {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: var(--s2);
  border: 0;
  background: none;
  color: var(--ink-4);
  cursor: pointer;
  font-family: var(--sans);
  touch-action: manipulation;
  transition: color 300ms var(--ease), background 300ms var(--ease),
              transform 260ms var(--ease), box-shadow 300ms var(--ease);
}
.star-i {
  display: block;
  line-height: 0;
  flex: none;
}
.star-i svg { display: block; width: 100%; height: 100%; }
/* One closed path, so the whole state change is a fill. */
.star[aria-pressed="true"] { color: var(--red); }
.star[aria-pressed="true"] .star-i svg { fill: currentColor; }
.star:active { transform: scale(.92); }

/* on a card: a small plate in the top corner of the sketch, above the
   stretched link so the two are genuinely separate controls */
.js .star-icon {
  position: absolute;
  top: var(--s5);
  right: var(--s5);
  z-index: 2;
  width: 34px;
  height: 34px;
  padding: 0;
  border-radius: var(--r-pill);
  /* the sketches are shot on white, so the glyph needs its own ground to sit
     on rather than dissolving into the garment */
  background: rgba(255, 255, 255, .82);
  box-shadow: inset 0 0 0 1px var(--rule), var(--lift-1);
}
.star-icon .star-i { width: 17px; height: 17px; }
.star-icon:hover { color: var(--red); background: var(--paper); }
.star-icon[aria-pressed="true"] { background: var(--paper); }

/* beside "Order this style": a visible button, sized to the ghost buttons it
   stands next to */
.js .star-button {
  padding: 15px 24px;
  border-radius: var(--r-pill);
  font-size: 14px;
  font-weight: 600;
  color: var(--ink);
  box-shadow: inset 0 0 0 1px var(--rule);
  background: var(--paper);
}
.star-button .star-i { width: 18px; height: 18px; }
.star-button:hover { color: var(--red); box-shadow: inset 0 0 0 1px var(--red); }
.star-button[aria-pressed="true"] { box-shadow: inset 0 0 0 1px var(--red); }
.star:focus-visible { outline: 2px solid var(--red); outline-offset: 2px; }

@media (prefers-reduced-motion: reduce) {
  .js .star { transition: none; }
  .star:active { transform: none; }
}

/* ---- the counter in the chrome ----
   Rendered hidden by the build and revealed by the script only when there is
   something to count, so a first visit never sees a control holding nothing. */
.saved-link {
  display: inline-flex;
  align-items: center;
  gap: var(--s2);
  text-decoration: none;
  color: var(--ink-2);
  flex: none;
  transition: color 300ms var(--ease);
}
.saved-link:hover { color: var(--ink); }
.saved-link .star-i { width: 18px; height: 18px; }
.saved-n {
  font-family: var(--mono);
  font-size: 11px;
  font-weight: 500;
  line-height: 1;
  min-width: 18px;
  padding: 4px 5px;
  border-radius: var(--r-pill);
  background: var(--red);
  color: #fff;
  text-align: center;
}
.saved-link[aria-current] { color: var(--ink); }

/* in the slide-out menu the link is set as text, like the other menu rows */
.sl-menu {
  font-size: 17px;
  font-weight: 500;
  color: var(--ink);
  padding: 10px 0;
}
.sl-menu .star-i { width: 20px; height: 20px; }
.saved-link:focus-visible { outline: 2px solid var(--red); outline-offset: 3px; }

/* ---------------------------------------------------------------------------
   THE MOBILE MASTHEAD (owner, 27 Aug 2026)

   What was here put `margin-left: auto` on the SAVED COUNTER and took it off
   the burger. That is one rule too clever by half, and it broke in two
   different directions depending on a state it had no business depending on:

     - the counter is HIDDEN AT ZERO — [hidden] is display:none !important, so
       a hidden flex item is not a flex item. With nothing saved (the common
       case, and every signed-out visitor) the bar lost its only auto margin,
       and since the burger's had been removed there was none left at all.
       Everything packed to the LEFT and left 71px of dead space against the
       right edge, measured at 375px.
     - with something saved, the auto margin came back and the whole
       right-hand group was pushed over — so the bar changed shape depending
       on whether the buyer had starred anything, which nobody asked for.

   So it was two mistakes compounding, not one: the spacer was put on a
   conditionally-hidden element, AND the reliable spacer was deleted in the
   same rule. Either alone would have been survivable.

   THE SPACER NOW LIVES ON THE LOGO, which is the one item in this bar that is
   never conditionally anything. Whatever else is present, the logo holds the
   left edge and everything after it sits in a group against the right.

   A third thing was wrong and was not a layout bug: nothing here met the 44px
   tap floor. The counter measured 19px tall, the account control 34, the
   burger 40. Those are set below, and the 64px bar has room for all of them.
   --------------------------------------------------------------------------- */
@media (max-width: 860px) {
  /* nav.main and the CTA are gone at this width (see the rule above), so the
     bar is: logo | ...gap... | counter, account, menu. */
  .masthead > .wrap { gap: var(--s4); }
  .masthead .logo { margin-right: auto; }
  /* Both of these are now inert and are stated rather than left to inherit,
     because the previous version's whole failure was an auto margin nobody
     could see. */
  .sl-bar { margin-left: 0; }
  .masthead .menu { margin-left: 0; }

  /* ---- 44px, the floor this stylesheet already keeps elsewhere ----
     The menu panel's own rows document it: "14px of vertical padding keeps
     each row at ~51px, clear of the 44px". These are the three controls that
     were under it. */
  .saved-link.sl-bar {
    min-width: 44px;
    min-height: 44px;
    justify-content: center;
    gap: var(--s1);
  }
  /* Scoped to .masthead so these beat the base rules on specificity rather
     than on source order. The sign-in/account block is written further down
     this file, so an unscoped .acctlink-bar here would set 44px and then be
     overruled by the 34px twenty rules later — which is exactly what happened
     the first time, and is invisible unless you measure. */
  .masthead .acctlink-bar { width: 44px; height: 44px; }
  /* min-WIDTH as well as height: "Sign in" sets 41px, so the link met the
     floor vertically and missed it horizontally. Centred inside its own box so
     widening the target does not move the word. */
  .masthead .authlink-bar { min-height: 44px; min-width: 44px; justify-content: center; }
  .menu > summary {
    width: 44px;
    height: 44px;
    /* The burger is a 22px glyph in a 44px box, so its ink sits 11px inside
       the box on each side. Pulling the box 10px past the content edge puts
       the GLYPH about where the logo's left edge is mirrored, which is what
       the eye lines up against — the old -8px did the same job for a 40px
       box. The target keeps its full 44px; only the box hangs over. */
    margin-right: -10px;
  }

  /* THE ACCOUNT CONTROL AND THE MENU ARE TWO CONTROLS, and at 44px each they
     have to read as two. The slot's own right-aligned content puts its 28px of
     slack on the LEFT, so the flex gap alone would separate the pair by less
     than it separates them from the counter. This is what makes the gap on
     the right side of the account control the wider of the two. */
  .authslot { margin-right: var(--s2); }
}

/* The "sign out" rules that were here are gone with the link (owner, 27 Aug
   2026). It left the masthead for /account/, where it is a button on the page
   the profile icon leads to. See the note over authControls() in build.mjs.

   Worth recording, because it was the argument for the shape it had: the
   mobile bar dropped that link entirely and left it to the slide-out, since
   at 360px a logo, a counter, a control and a burger is one item more than
   the row can carry without the counter losing its air. The bar is back to
   three items and does not need the exception. */

/* ---- sign in / your account ----
   Two controls, one slot, exactly one of them ever visible. The build renders
   both and neither is the default — see authControls() in build.mjs for why
   "render one and swap it" was not good enough. The rules below are the whole
   mechanism and their order matters:

     1. the account link is hidden outright, so it never appears to anyone we
        have not been told about — including a visitor with no JavaScript,
        where nothing will ever tell us;
     2. under .js the sign-in link hides too, so between parse and the
        session answer the slot is empty rather than wrong;
     3. the answer arrives as .is-in or .is-out on <html> and reveals one.

   Without .js only rule 1 applies and Sign in renders, which is the correct
   no-script fallback: a real link to a real page in both gating modes.

   The two variants take different display values — the bar is inline-flex in
   a flex row, the slide-out is a full-width flex row of its own — so each
   states its own rather than sharing one and fighting over it. */
.acctlink { display: none; }
.js .authlink[data-signin-link] { display: none; }
.is-out .authlink-bar[data-signin-link] { display: inline-flex; }
.is-out .authlink-menu[data-signin-link] { display: flex; }
.is-in .acctlink-bar { display: inline-flex; }
.is-in .acctlink-menu { display: flex; }

/* THE SLOT KEEPS ITS WIDTH WHATEVER LANDS IN IT. The two states are a word
   and a 34px icon, so without a floor here the nav beside them would jump
   sideways the moment the fetch resolved — the layout shift version of the
   flash the markup went to such trouble to avoid. Right-aligned so the
   control still sits against the CTA. */
.authslot {
  display: inline-flex;
  align-items: center;
  justify-content: flex-end;
  min-width: 72px;
  flex: none;
}

.authlink-bar {
  display: inline-flex;
  align-items: center;
}
.authlink {
  font-size: 13px;
  font-weight: 500;
  letter-spacing: 0.01em;
  text-decoration: none;
  color: var(--ink-2);
  white-space: nowrap;
  transition: color 300ms var(--ease);
}
.authlink:hover { color: var(--ink); }
.authlink:focus-visible { outline: 2px solid var(--red); outline-offset: 3px; }

/* The signed-in control is an icon in a ring — the one place in the masthead
   that stands for a person rather than a destination, so it takes a shape
   instead of a word. Not a plate and not a button: the masthead already
   carries one button and it is the one the whole site funnels to. */
.acctlink-bar {
  align-items: center;
  justify-content: center;
  width: 34px;
  height: 34px;
  border-radius: 50%;
  color: var(--ink-2);
  box-shadow: inset 0 0 0 1px var(--rule);
  transition: color 300ms var(--ease), box-shadow 300ms var(--ease);
}
.acctlink-bar:hover { color: var(--ink); box-shadow: inset 0 0 0 1px var(--ink-4); }
.acctlink-bar[aria-current] { color: var(--ink); box-shadow: inset 0 0 0 1px var(--ink); }
.acctlink:focus-visible { outline: 2px solid var(--red); outline-offset: 3px; }
.acct-i { width: 18px; height: 18px; display: block; }
.acct-i svg { display: block; width: 100%; height: 100%; }

/* in the slide-out both are set as text, like the other menu rows */
.authlink-menu { display: flex; }
.authlink-menu,
.acctlink-menu {
  align-items: center;
  gap: var(--s2);
  font-size: 17px;
  font-weight: 500;
  color: var(--ink);
  padding: 10px 0;
}
.acctlink-menu .acct-i { width: 20px; height: 20px; }

/* ---- /saved/ ---- */
.saved-bar {
  display: flex;
  align-items: center;
  gap: var(--s4);
  margin-bottom: var(--s5);
}
.saved-bar .rcount { margin-right: auto; }
/* .afchip's × means "remove this filter"; this one empties a list and asks
   before it does, so it carries neither the glyph nor the promise. */
.saved-clear::after { content: none; }
.saved-clear.armed { background: var(--red); }
.saved-empty { margin-top: var(--s5); }
