/* Self-hosted brand fonts (latin subsets). Downloaded from Google
   Fonts 2026-08-22; no third-party request remains. Fallbacks are
   metric-tuned (measured, not guessed): Spectral renders at 98.9%
   of Georgia's advance width, Plex Mono at 100.0% of Courier New,
   so the swap moves almost nothing. */
@font-face { font-family: 'IBM Plex Mono'; font-style: normal; font-weight: 400;
  font-display: swap; src: url(assets/fonts/ibm-plex-mono-400.woff2) format('woff2'); }
@font-face { font-family: 'IBM Plex Mono'; font-style: normal; font-weight: 500;
  font-display: swap; src: url(assets/fonts/ibm-plex-mono-500.woff2) format('woff2'); }
@font-face { font-family: 'IBM Plex Mono'; font-style: normal; font-weight: 600;
  font-display: swap; src: url(assets/fonts/ibm-plex-mono-600.woff2) format('woff2'); }
@font-face { font-family: 'Spectral'; font-style: italic; font-weight: 400;
  font-display: swap; src: url(assets/fonts/spectral-400-italic.woff2) format('woff2'); }
@font-face { font-family: 'Spectral'; font-style: italic; font-weight: 600;
  font-display: swap; src: url(assets/fonts/spectral-600-italic.woff2) format('woff2'); }
@font-face { font-family: 'Spectral'; font-style: normal; font-weight: 400;
  font-display: swap; src: url(assets/fonts/spectral-400.woff2) format('woff2'); }
@font-face { font-family: 'Spectral'; font-style: normal; font-weight: 600;
  font-display: swap; src: url(assets/fonts/spectral-600.woff2) format('woff2'); }
@font-face { font-family: 'Spectral'; font-style: normal; font-weight: 800;
  font-display: swap; src: url(assets/fonts/spectral-800.woff2) format('woff2'); }

/* Metric-tuned local fallbacks: shown during font load, sized so the swap
   to the real face barely moves a line. */
@font-face { font-family: 'Spectral-fallback'; src: local('Georgia');
  size-adjust: 98.9%; }
@font-face { font-family: 'PlexMono-fallback'; src: local('Courier New');
  size-adjust: 100%; }

/* ==========================================================================
   Common Specimens - commonspecimens.com
   Palette is brand-locked (DESIGN_PLAYBOOK.md rule 5): navy / brick red / cream.
   Every colour below is one of those three inks, a tint of them, or a blend of
   two of them. Nothing else. Secondary text never lighter than the
   print-contrast floor (rule 8).
   ========================================================================== */

:root {
  /* brand inks - do not substitute */
  --navy:        #1F3A5F;
  --brick:       #B0392A;
  --cream:       #F2E8CF;

  /* derived ground + ink steps, all checked against the cream paper */
  --paper:       #F2E8CF;
  --paper-warm:  #EDE1C2;
  --paper-deep:  #E4D5B0;
  --ink:         #1B2F4A;   /* body text, 11.4:1 on paper */
  --ink-soft:    #3D4F68;   /* secondary, 6.7:1 - above the print-contrast floor */
  --rule:        #C3B48C;
  --rule-strong: #9E8E68;

  --brick-deep:  #8E2C20;   /* for text on cream, 6.3:1 */

  /* Ink steps for the navy field. Solid values, each an exact blend of two
     locked inks, so the palette scan sees no new hue and the contrast gate
     sees a computable colour (an alpha on a dark ground is neither).
       --on-navy      = cream 92% over navy   8.2:1
       --on-navy-soft = cream 78% over navy   6.4:1
       --on-navy-mark = brick 35% + cream 65% 4.9:1 on the about card
     (measured against the card's own 5%-cream-over-navy ground, not bare navy:
      that 5% lift is exactly what put the earlier value at 4.18:1). */
  --on-navy:      #E1DAC6;
  --on-navy-soft: #C4C2B6;
  --on-navy-mark: #DBAB95;

  /* The brick field. Cream on brick measures 4.97:1 -- above the 4.5:1 AA
     floor for normal text, so the band carries body copy, not just labels.
     There is deliberately no "soft" step: anything dimmer than cream on this
     ground drops under the floor. */
  --on-brick:     #F2E8CF;

  /* One easing curve for the whole site. The old transitions were .12-.16s
     with the browser default `ease`, which is the most reliable "built in
     2016" tell there is. */
  --ease: cubic-bezier(.2, .8, .25, 1);
  --t-fast: .18s;
  --t-base: .26s;

  /* One hairline for the navy board. Seven different cream alphas were in use
     (.18 .22 .26 .28 .30 .32 .65), six of them spanning a 30-unit grey range
     doing one job. */
  --rule-on-navy:        rgba(242,232,207,.24);
  --rule-on-navy-strong: var(--rule-on-navy-strong);

  /* The label idiom appeared at 6 sizes and 26 tracking values. Three steps. */
  --label-sm:      10.5px;
  --label:         12px;
  --label-lg:      13px;
  --track-label:   .13em;
  --track-label-wide: .16em;

  --r-round: 50%;

  --shadow-plate: 0 1px 0 rgba(31,58,95,.10), 0 10px 28px -18px rgba(31,58,95,.45);
  --shadow-photo: 0 1px 0 rgba(31,58,95,.10), 0 18px 34px -22px rgba(31,58,95,.55);

  --serif: "Spectral", "Spectral-fallback", Georgia, "Times New Roman", serif;
  --mono:  "IBM Plex Mono", "PlexMono-fallback", "Courier New", ui-monospace, monospace;

  --wrap: 1180px;
  /* Doubled. Every print/editorial reference captured runs 120-200px of
     section padding; this site was capping at 92px, under half. Generous
     vertical space is the cheapest "expensive" signal in web design. */
  --sec-pad: clamp(64px, 9vw, 150px);

  --r-sm: 2px;
  --r-md: 4px;

  /* The page chrome is PINNED, not measured-and-hoped-for: .announce and
     .site-head below carry explicit heights at every breakpoint, and these two
     tokens are the same arithmetic written down. app.js still measures the head
     into --head-h (the pinned chips ride on it), but because the heights cannot
     move, the measurement returns exactly these numbers and re-measuring after
     the webfonts swap changes nothing. That is what makes CLS ~0: the old build
     sized the hero off a measured bar that grew a line when the real sale copy
     arrived, and the whole page stepped down with it. */
  --head-h:   66px;
  --band-h:   44px;    /* .assurances-lead, pinned below */
  --chrome-h: 150px;   /* 66 head + 40 strip + 44 band */
}

/* The strip stacks its two lines below this width (see .announce); the lead
   band goes to two rows at 860 (see .assurances-lead). */
@media (max-width: 860px) {
  :root { --band-h: 72px; --chrome-h: 178px; }   /* 66 + 40 + 72 */
}
@media (max-width: 760px) {
  :root { --chrome-h: 190px; }   /* 66 head + 52 strip + 72 band */
}
@media (max-width: 620px) {
  :root { --head-h: 52px; --chrome-h: 176px; }   /* 52 head + 52 strip + 72 band */
}

*, *::before, *::after { box-sizing: border-box; }

html {
  color-scheme: only light;
  -webkit-text-size-adjust: 100%;
  /* Windows: the modal locks body scroll, the scrollbar vanished, and the
     whole page lurched 15px wider behind the dialog. scrollbar-gutter was
     tried first and Chrome drops the gutter once overflow goes hidden, so
     the root scrollbar is simply always-on: track stays, width never moves. */
  overflow-y: scroll;
  /* Anchor jumps must clear the sticky head + pinned chips, or "Browse the
     plates" lands with its target hidden underneath them. */
  /* Exactly the header: the 26px of "breathing room" that used to be added
     here was always filled by the tail of the section ABOVE the target, so
     every anchor landing showed a half-clipped strip under the header. */
  scroll-padding-top: var(--head-h, 66px);
}
/* No global scroll-behavior: smooth. Over a screen or two it is a courtesy;
   over the 14,000px of catalogue between the header and the About section it
   is a long, disorienting ride that arrives late. app.js decides by distance. */

::selection { background: var(--navy); color: var(--cream); }

body {
  margin: 0;
  background-color: var(--paper);
  color: var(--ink);
  font-family: var(--serif);
  font-size: 17px;
  line-height: 1.62;
  position: relative;
}

/* Faint laid-paper tone. Deliberately on a pseudo-element rather than
   `body { background-image }`: any background-image in an element's ancestor
   chain makes its text contrast mechanically uncomputable, and that alone put
   47 nodes beyond the QA gate's reach. Keeping body a flat colour keeps the
   whole page checkable. */
body::before {
  content: "";
  position: fixed;
  inset: 0;
  z-index: -1;
  pointer-events: none;
  background-image:
    /* Kept faint on purpose. The plate scans carry their own cream ground and
       the texture sits BEHIND them (never over product art), so a heavier tone
       would draw a visible rectangle around every plate on the page. */
    repeating-linear-gradient(0deg, rgba(31,58,95,.012) 0 1px, transparent 1px 3px);
}

img { max-width: 100%; height: auto; display: block; }

a { color: var(--brick-deep); text-decoration-thickness: 1px; text-underline-offset: 2px; }
a:hover { color: var(--brick); }

/* No border-radius here: it mutated the element's own geometry on focus, so
   square things visibly rounded when tabbed to. */
:focus-visible {
  outline: 2px solid var(--brick);
  outline-offset: 3px;
}
/* Brick on navy measures 1.89:1 -- below the 3:1 that WCAG 2.4.11 wants for a
   focus indicator. Cream on navy is 9.42:1. */
.band-navy :focus-visible,
.finder-sec :focus-visible,
.about :focus-visible,
.pp-garment :focus-visible,
.pp-video :focus-visible { outline-color: var(--cream); }
/* Full-bleed and clipped elements cannot show an outset ring. */
.announce:focus-visible,
.plate-open:focus-visible,
.plate-open-link:focus-visible,
.hero-art-link:focus-visible { outline-offset: -3px; }

.wrap { width: min(100% - 2rem, var(--wrap)); margin-inline: auto; }

.skip {
  position: absolute; left: -9999px; top: 0;
  background: var(--navy); color: var(--cream);
  padding: 12px 18px; z-index: 100; text-decoration: none;
}
.skip:focus { left: 0; }

/* ---------- utility strip / sale bar ---------- */

/* ONE strip, TWO states, ONE height.
   Default (no sale running): navy, carrying the shipping line, which is true
   every day of the year. With the sale live app.js adds .announce--sale and it
   repaints brick with the ANNOUNCE copy. Both states are the SAME fixed height
   at every width, so the swap is a repaint and never a reflow, and the page
   never has a gap where a bar used to be. It scrolls away with the page rather
   than sticking: the sticky header already costs 52-66px and a second
   permanent bar costs more than it earns.
   height, not min-height: at 390 the sale line is two lines and the shipping
   line is one, and a min-height would let the taller of the two push the whole
   document down the moment app.js ran. */
.announce {
  display: flex; align-items: center; justify-content: center;
  flex-wrap: wrap; gap: 1px 14px; align-content: center;
  background: var(--navy); color: var(--cream);
  text-decoration: none; text-align: center;
  line-height: 1.35;
  padding: 6px 20px;
  height: 40px; overflow: hidden;
  transition: background .18s ease;
}
.announce--sale { background: var(--brick); }
/* Stacked below the width where the sale line stops fitting on one row. Fixed
   at two lines, so the shipping line and the sale line occupy identical space. */
@media (max-width: 760px) {
  .announce {
    flex-direction: column; flex-wrap: nowrap; justify-content: center;
    height: 52px; padding: 6px 14px;
  }
}
.announce[hidden] { display: none; }
.announce:hover { background: var(--ink); color: var(--cream); }
.announce--sale:hover { background: var(--brick-deep); color: var(--cream); }
/* Each line owns HALF the strip and aligns its text to the middle seam. The
   two boxes therefore never move, however long the copy inside them is: with
   the lines centred as a pair, swapping in the sale copy slid them ~108px
   sideways, and a 108px slide anywhere on the page is scored against the
   largest thing that moved in the same frame -- which was the hero. */
.announce-lead {
  flex: 1 1 0; min-width: 0; text-align: right;
  font-family: var(--mono); font-size: 12px; font-weight: 600;
  letter-spacing: .13em; text-transform: uppercase;
}
.announce-sub {
  flex: 1 1 0; min-width: 0; text-align: left;
  font-family: var(--mono); font-size: 12px;
  letter-spacing: .09em; text-transform: uppercase; color: var(--cream); opacity: .9;
}
@media (max-width: 760px) {
  .announce-lead, .announce-sub { flex: 0 0 auto; width: 100%; text-align: center; }
}

/* ---------- header ---------- */

/* Fixed height, deliberately: --head-h is what the pinned chips sit under and
   what the hero measures itself against, and a header that changes height when
   the webfonts land drags the whole page with it. 66px here, 52px on a phone
   (well under the 64px the phone header is allowed). */
.site-head {
  position: sticky; top: 0; z-index: 40;
  height: 66px;
  background: rgba(242,232,207,.94);
  backdrop-filter: saturate(140%) blur(6px);
  border-bottom: 1px solid var(--rule);
}
.head-inner {
  display: flex; align-items: center; justify-content: space-between;
  gap: 18px; height: 100%;
}
.wordmark {
  text-decoration: none; color: var(--ink);
  display: flex; align-items: center; gap: 11px;
}
/* The hand-lens duo, the same mark that heads the Etsy shop. */
.wordmark-mark {
  width: 44px; height: 44px; flex: 0 0 auto;
  border-radius: 50%;
}
.wordmark-text { display: block; }
.wordmark-main {
  display: block;
  font-weight: 800; font-size: clamp(18px, 2.1vw, 22px);
  letter-spacing: .055em; text-transform: uppercase; line-height: 1.1;
  white-space: nowrap;
}
.wordmark-sub {
  display: block; font-family: var(--mono);
  font-size: 10.5px; letter-spacing: .17em; text-transform: uppercase;
  color: var(--ink-soft); margin-top: 2px;
}
.head-nav { display: flex; align-items: center; gap: clamp(12px, 2.2vw, 26px); }
.head-nav a {
  font-family: var(--mono); font-size: 12.5px; letter-spacing: .1em;
  text-transform: uppercase; text-decoration: none; color: var(--ink);
  padding: 9px 2px; display: inline-block; min-height: 24px;
}
.head-nav a:hover { color: var(--brick-deep); }
.head-nav a.head-cta {
  background: var(--navy); color: var(--cream);
  padding: 10px 16px; border-radius: var(--r-md);
}
.head-nav a.head-cta:hover { background: var(--brick); }

/* ==========================================================================
   HERO - the plate, printed on the page.
   The detail scan's ground is rgb(240,233,205); --paper is rgb(242,232,207).
   That is why the artwork carries no frame, no border, no shadow and no panel:
   any of those would announce "image placed on a website" instead of "plate
   printed on paper", and the plate IS the product.
   ========================================================================== */

.hero {
  min-height: calc(100svh - var(--chrome-h));
  display: flex; align-items: center;
  padding-block: 10px 12px;
  border-bottom: 1px solid var(--rule);
}
.hero-wrap {
  /* DELIBERATELY WIDER than --wrap (1180), which puts the H1 at x=24 while
     the wordmark above it sits at x=130. That 106px break in the left margin
     is real and a design critique flagged it on 2026-09-09. It is kept
     because the alternative is worse and is measured: at --wrap the hero
     artwork falls to 30.3% of the viewport against the 40% floor in
     tools/measure_fixes.py, and the H1 gains a one-word last line at 1600.
     The plate is the offer. Revisit only with a fix that holds both rules. */
  width: min(100% - 48px, 1400px); margin-inline: auto;
  display: grid;
  /* 340px floor: at 1150 (Jason's real window) a 308px column orphaned
     "know.", split "23 / GROUPS" and stranded "SHIPPING" (review 2026-09-07). */
  grid-template-columns: minmax(340px, 30%) minmax(0, 1fr);
  gap: clamp(26px, 3vw, 44px);
  align-items: center;
}

.eyebrow {
  font-family: var(--mono); font-size: 11px; letter-spacing: .16em;
  text-transform: uppercase; color: var(--ink-soft); margin: 0 0 18px;
}

h1 {
  /* Display/body ratio was 2.7x, the flattest of any reference captured
     (Wildsam 5.5x, Kinfolk 4.0x, Taylor Stitch 3.7x). The hero h1 lives in a
     340px column, so it is scaled off the column, not the viewport. */
  font-size: clamp(32px, 3.9vw, 58px);
  line-height: 1.04; letter-spacing: -.021em;
  font-weight: 800; margin: 0 0 .32em; text-wrap: balance;
}

.lede { font-size: clamp(16.5px, 1.35vw, 19px); max-width: 34ch; margin: 0 0 26px; }

.hero-actions {
  display: flex; flex-wrap: wrap; align-items: center; gap: 12px 18px;
  margin-bottom: 26px;
}

.btn {
  display: inline-block; text-decoration: none;
  font-family: var(--mono); font-size: 12.5px; letter-spacing: .1em;
  text-transform: uppercase; padding: 14px 24px; border-radius: var(--r-md);
  border: 1px solid var(--navy);
  transition: background var(--t-fast) var(--ease), color var(--t-fast) var(--ease),
              border-color var(--t-fast) var(--ease);
  min-height: 24px;
}
.btn-primary { background: var(--navy); color: var(--cream); }
.btn-primary:hover { background: var(--brick); border-color: var(--brick); color: var(--cream); }
.btn-ghost { background: transparent; color: var(--ink); }
.btn-ghost:hover { background: var(--navy); color: var(--cream); }

/* The stats, demoted to a quiet data line. They are facts, not a scoreboard. */
.hero-data {
  margin: 0; padding-top: 15px; border-top: 1px solid var(--rule);
  font-family: var(--mono); font-size: 11.5px; letter-spacing: .02em;
  text-transform: uppercase; color: var(--ink-soft); line-height: 1.75;
}
.hero-data s { color: var(--ink-soft); text-decoration-thickness: 1px; }
/* Each fact is one unbreakable item and the separator belongs to the item that
   FOLLOWS it, so a wrapped line can never end in a hanging dot or split
   "23 / groups". Measured before: three-line orphans at 1150 and 390. */
.hero-data .hd-row { display: block; }
.hero-data .hd { white-space: nowrap; }
.nw { white-space: nowrap; }   /* separator + phrase travel together */

/* ---- the artwork ---- */

.hero-art {
  position: relative; margin: 0;
  /* Height-led on desktop: the plate takes the whole first screen it is given,
     capped so it never outgrows its own scan. 4:5 -> width = height x 0.8.
     The 44vw cap keeps the margin notes room to breathe at narrow desktops. */
  --art-h: clamp(320px, calc(100svh - var(--chrome-h) - 48px), 860px);
  width: min(calc(var(--art-h) * 0.8), 44vw);
  justify-self: end;
}
.hero-plate-img {
  width: 100%; height: auto; aspect-ratio: 880 / 1100;   /* box reserved: no CLS */
  display: block;
}

/* One deadpan line, typeset in plate style, pointing at the artwork.
   Never numbered: the printed plate's callout 1 says something else, and a
   numbered marker here would contradict the shirt. */
.hero-callout {
  position: absolute; top: 25%; right: calc(100% + 40px);
  width: clamp(150px, 16vw, 232px);
  text-align: right;
  font-size: clamp(14px, 1.1vw, 17px); line-height: 1.42; color: var(--ink);
}
.hero-callout[hidden] { display: none; }
.hero-callout-lead {
  position: absolute; right: -32px; top: .68em;
  width: 26px; height: 1px; background: var(--rule-strong);
}
.hero-callout-dot {
  position: absolute; right: -44px; top: calc(.68em - 4px);
  width: 9px; height: 9px; border-radius: 50%; background: var(--brick);
}

/* The shirt, framed as a photographic object beside the plate: a real mockup,
   never tinted, on its own studio ground inside a hairline mat. */
.hero-garment {
  position: absolute; bottom: 3%; right: calc(100% + 32px);
  width: clamp(170px, 24vw, 372px);
  display: block; padding: 7px;
  background: var(--paper);
  border: 1px solid var(--rule-strong);
  box-shadow: var(--shadow-photo);
}
/* The crop window. Square, and reserved before either photograph lands, so the
   frame never resizes. Square because the mockups are square: a 4:5 window on a
   1:1 photograph throws away the sleeves before the zoom even starts.
   The scale then brings the garment up to fill the frame. Every mockup Printify
   makes stands on the same grey-taupe studio floor, and that floor is the one
   colour on this page that belongs to no one: measured on the source, the shirt
   occupies 17.5%-82.5% of the frame's width and 18.7%-88% of its height, so the
   window is set to show 16%-84% of both and the studio ground survives only as
   the hairline it should be.
   The photograph itself is untouched -- this is a crop, not a tint. */
.hero-garment-crop {
  display: block; position: relative; overflow: hidden;
  aspect-ratio: 1 / 1; background: var(--paper-warm);
}
.hero-garment img {
  position: absolute; inset: 0;
  width: 100%; height: 100%;
  object-fit: cover; object-position: 50% 50%;
  transform: scale(1.48); transform-origin: 50% 51%;
}
.hero-garment-alt { opacity: 0; transition: opacity .2s ease; }
.hero-garment:hover .hero-garment-alt { opacity: 1; }

.hero-cap {
  display: flex; flex-wrap: wrap; gap: 2px 12px; align-items: baseline;
  margin-top: 10px; padding-top: 9px; border-top: 1px solid var(--rule);
  font-family: var(--mono); font-size: 12px; letter-spacing: .11em;
  line-height: 1.3; text-transform: uppercase;
}
.hero-cap-no { color: var(--brick-deep); }
.hero-cap-fine { color: var(--ink-soft); }

/* Hero entrance: the single orchestrated moment on the page. Transform and
   opacity only, under 300ms, dead under prefers-reduced-motion. */
@keyframes hero-lay {
  from { opacity: 0; transform: translateY(12px); }
  to   { opacity: 1; transform: none; }
}
/* The artwork settles into place WITHOUT fading. Measured 2026-09-09 on Fast
   3G at 390px: with `hero-lay` (which animates opacity from 0) the hero image
   decoded at 2.8s but Largest Contentful Paint was not reported until 4.4s;
   with the animation off it was 2.37s. An element animating up from opacity 0
   does not count as painted, so a 0.3s flourish was costing 2.1 SECONDS of the
   metric Google ranks on, on the page paid traffic lands on. Transform alone
   keeps the movement and costs nothing: LCP does not wait on transform. */
@keyframes hero-settle {
  from { transform: translateY(12px); }
  to   { transform: none; }
}
@media (prefers-reduced-motion: no-preference) {
  .hero-art { animation: hero-settle .3s ease-out both; }
  .hero-say > * { animation: hero-lay .28s ease-out both; }
  .hero-say > *:nth-child(2) { animation-delay: .04s; }
  .hero-say > *:nth-child(3) { animation-delay: .08s; }
  .hero-say > *:nth-child(4) { animation-delay: .12s; }
  .hero-say > *:nth-child(5) { animation-delay: .16s; }
}

/* ---------- sections ---------- */

.section-head { margin-bottom: 28px; }
/* THE RAMP, in one place, so a page title can never render smaller than a
   heading inside it (it did: /about/ shipped its H1 at 40px above an H2 at
   52px, because .page-title was declared twice and the dead rule won).
       hero h1  >  .page-title  >  h2
   Margins are em, not px: a px margin under a vw-clamped heading inverts the
   relationship across widths (12px was 0.43x the heading at 390 and 0.23x at
   1440). */
h2 {
  font-size: clamp(25px, 2.9vw, 40px); font-weight: 800;
  line-height: 1.12; letter-spacing: -.016em; margin: 0 0 .34em;
}
.section-note { color: var(--ink-soft); margin: 0; font-size: 17px; max-width: 52ch; }

/* ==========================================================================
   WHO IS IT FOR - the welcome path
   Every tile is a section/sub-group that already exists in products.json, shown
   with a real plate from that group and its live count. One tap sets exactly
   the state a chip sets, syncs the URL, and scrolls to the results.
   ========================================================================== */

/* The welcome path sits on the navy board: it is the one module that decides
   where a first-time visitor goes, and on cream it read as another paragraph. */
.finder-sec { background: var(--navy); color: var(--on-navy); padding-block: var(--sec-pad); }
.finder-sec h2 { color: var(--cream); }
.finder-sec .section-note { color: var(--on-navy); }
.finder {
  display: grid; gap: 0 26px;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  border-top: 1px solid var(--rule-on-navy);
}
.find {
  display: flex; align-items: center; gap: 15px;
  width: 100%; text-align: left; cursor: pointer;
  font: inherit; color: var(--ink);
  background: transparent;
  border: 0; border-bottom: 1px solid var(--rule);
  padding: 11px 10px 11px 2px;
  transition: background .24s var(--ease), padding-left .24s var(--ease);
}
.find:hover { background: var(--paper-warm); padding-left: 8px; }
@media (prefers-reduced-motion: reduce) { .find:hover { padding-left: 2px; } }
/* The thumbnail is the PLATE, not the shelf: the flat mockup cropped hard into
   its own printed area, so nine of these read as artwork rather than as nine
   grey rectangles. Same file the card uses, so it costs nothing extra. */
.find-thumb {
  width: 66px; height: 82px; flex: 0 0 auto; overflow: hidden;
  border: 1px solid var(--rule); border-radius: var(--r-sm);
  background: var(--paper-deep); display: block;
}
.find img {
  width: 100%; height: 100%;
  object-fit: cover;
  transform: scale(2.15); transform-origin: 50% 55%;
}
.find-text { display: block; min-width: 0; }
.find-label { display: block; font-size: 18px; font-weight: 600; line-height: 1.25; }
.find-meta {
  display: block; margin-top: 4px;
  font-family: var(--mono); font-size: 11.5px; letter-spacing: .06em;
  text-transform: uppercase; color: var(--ink-soft);
}

/* ==========================================================================
   HOW TO READ A PLATE - the conventions, shown ON a plate
   The four markers are HTML, positioned in percentages against the render
   engine's fixed geometry (title 7.7%, binomial 12.6%, observation row 31.4%,
   footer rule 86.5%). The image is never touched.
   ========================================================================== */

.rap-sec { padding-block: 0 clamp(44px, 6vw, 78px); }
.rap-head { text-align: center; margin-bottom: 8px; }
.rap-head .section-note { max-width: 46ch; margin-inline: auto; }
.note-no {
  margin: 0 0 8px; font-family: var(--mono); font-size: 12px;
  letter-spacing: .13em; text-transform: uppercase; color: var(--brick-deep);
}
.rap-fig { margin: 0; }
.rap-plate {
  position: relative;
  width: clamp(280px, 32vw, 430px);
  margin: 26px auto 0;
}
.rap-plate img { width: 100%; height: auto; aspect-ratio: 880 / 1100; }

.rap-mark { position: absolute; top: var(--y); width: 0; height: 0; }
.rap-left  { left: 0; }
.rap-right { left: 100%; }
.rap-mark i {
  position: absolute; left: 0; top: 0; transform: translate(-50%, -50%);
  width: 21px; height: 21px; border-radius: 50%;
  background: var(--brick); color: var(--cream);
  font: 600 10px/1 var(--mono); font-style: normal;
  display: grid; place-items: center; letter-spacing: 0;
}
.rap-lead { position: absolute; top: 0; height: 1px; background: var(--rule-strong); }
.rap-left  .rap-lead { right: 11px; width: 42px; }
.rap-right .rap-lead { left: 11px; width: 42px; }
.rap-label {
  position: absolute; top: 0; transform: translateY(-50%);
  width: min(196px, 19vw);
  font-size: 14.5px; line-height: 1.36; color: var(--ink);
}
.rap-left  .rap-label { right: 58px; text-align: right; }
.rap-right .rap-label { left: 58px; }

.rap-cap {
  margin: 16px auto 0; text-align: center;
  font-family: var(--mono); font-size: 12px; letter-spacing: .12em;
  text-transform: uppercase; color: var(--ink-soft);
}
.rap-legend { display: none; }

/* ---------- assurance strip ---------- */

/* Four facts between the welcome and the catalogue. Quiet on purpose: a band of
   loud badges would fight the field-guide type and read as a different shop. */
/* The four facts a cold visitor most needs, on the brick board. Was a
   paper-warm strip -- a 3% luminance step off the page ground, invisible at a
   glance. 14px, not 12: this is read, not decoration. */
.assurances {
  background: var(--brick); color: var(--on-brick);
  padding: clamp(18px, 2.2vw, 26px) 0;
}
/* At the TOP of the page the band is chrome, so its height is pinned like the
   head and the strip (see --band-h): one 44px row on desktop, two rows of 72px
   from 860 down. A pinned height is what lets --chrome-h be right before
   app.js measures anything. */
.assurances-lead {
  padding: 0;
  min-height: var(--band-h); height: var(--band-h);
  /* grid + align-content, not flex: the .wrap inside keeps its own gutter
     and max-width, so the four facts line up with the wordmark above. */
  display: grid; align-content: center;
  border-bottom: 1px solid rgba(20, 30, 48, .18);
}
.assurances-lead .assurance-list { gap: 6px 28px; }
.assurances-lead .assurance-list li { font-size: 13px; line-height: 1.3; }
.assurances-lead .a-mark { font-size: 12px; }
/* Four columns get tight below ~1200: the longest item ("30-day replacement
   or refund") wrapped to two lines at 1150, Jason's real window width. */
@media (max-width: 1200px) {
  .assurances-lead .assurance-list { gap: 6px 20px; }
  .assurances-lead .assurance-list li { font-size: 12px; letter-spacing: .03em; }
}
@media (max-width: 860px) {
  .assurances-lead .assurance-list { gap: 6px 18px; }
  .assurances-lead .assurance-list li { font-size: 12.5px; }
}
@media (max-width: 420px) {
  /* Two columns still. The numerals are decorative (aria-hidden) and cost four
     characters each; without them "30-day replacement or refund" takes two
     lines instead of three at 360px, and the pinned 72px holds. */
  .assurances-lead .assurance-list { grid-template-columns: repeat(2, 1fr); gap: 4px 12px; }
  .assurances-lead .assurance-list li { font-size: 11.5px; letter-spacing: .02em; line-height: 1.25; }
  .assurances-lead .a-mark { display: none; }
}
.assurance-list {
  list-style: none; margin: 0; padding: 0;
  display: grid; gap: 10px 34px;
  grid-template-columns: repeat(4, 1fr);
}
.assurance-list li {
  display: flex; align-items: baseline; gap: 9px;
  font-family: var(--mono); font-size: 14px; letter-spacing: .04em;
  text-transform: uppercase; color: var(--on-brick);
  line-height: 1.4;
}
.a-mark { color: var(--on-brick); opacity: .72; font-size: 13px; letter-spacing: .1em; flex: 0 0 auto; }

@media (max-width: 860px) {
  .assurance-list { grid-template-columns: repeat(2, 1fr); gap: 10px 22px; }
}
@media (max-width: 420px) {
  .assurance-list { grid-template-columns: 1fr; }
}

/* ==========================================================================
   THE BINDING - full-bleed ink grounds
   A field guide is cream plate pages between dark boards. These are the boards.
   Both grounds are locked inks; the ink steps on them are pre-computed solids
   (never alpha over a dark field, which is not mechanically checkable).
   ========================================================================== */

.band-navy {
  background: var(--navy); color: var(--on-navy);
  padding-block: var(--sec-pad);
}
.band-navy h2 { color: var(--cream); }
.band-navy .section-note, .band-navy p { color: var(--on-navy); }

/* ---------- controls ---------- */

.catalog { padding-block: var(--sec-pad); }

.controls {
  display: flex; flex-direction: column; gap: 20px;
  padding-bottom: 18px; border-bottom: 1px solid var(--rule); margin-bottom: 8px;
}

/* The collection chips stay pinned while the grid scrolls, so switching
   sections never means scrolling back up. Solid paper ground: the cards
   sliding underneath must not show through. Sub-chips scroll away normally --
   pinning both rows would eat a third of a phone screen. */
.filters {
  display: flex; flex-wrap: wrap; gap: 8px;
  /* Below the site head, which is itself sticky. The head's real height is
     measured into --head-h by app.js -- a hardcoded value pinned the chips
     UNDER the mobile head. */
  position: sticky; top: var(--head-h, 66px); z-index: 30;
  background: var(--paper);
  padding: 12px 0;
  margin: -12px 0;
  border-bottom: 1px solid transparent;
}
/* Applied by JS while the row is actually pinned. The hairline + soft shadow
   make content sliding underneath read as layered, not clipped. */
.filters.stuck {
  border-bottom-color: var(--rule);
  box-shadow: 0 7px 14px -10px rgba(31,58,95,.28);
}
.chip {
  font-family: var(--mono); letter-spacing: .09em;
  text-transform: uppercase; cursor: pointer;
  background: transparent; color: var(--ink);
  /* Squared to 2px: nothing else in this system is round except the crest and
     the plate's own observation dots, and those are the artwork's geometry,
     not the interface's. */
  border: 1px solid var(--rule-strong); border-radius: var(--r-sm);
  padding: 10px 16px; min-height: 38px; font-size: 12px;
  transition: background .22s var(--ease), color .22s var(--ease), border-color .22s var(--ease);
}
.chip:hover { border-color: var(--navy); }
.chip[aria-pressed="true"] { background: var(--navy); border-color: var(--navy); color: var(--cream); }
.chip .n { opacity: .72; margin-left: 6px; font-size: 10.5px; }

/* Scene sub-filters, shown only inside a section that declares them */
.subfilters {
  display: flex; flex-wrap: wrap; gap: 8px; align-items: center;
  padding: 16px 0 2px; margin-top: 4px;
  border-top: 1px dashed var(--rule);
}
.subfilters[hidden] { display: none; }
.subfilters-label {
  flex-basis: 100%; margin: 0 0 4px;
  font-family: var(--mono); font-size: 11px; letter-spacing: .12em;
  text-transform: uppercase; color: var(--ink-soft);
}
.subfilters-note {
  flex-basis: 100%; margin: -2px 0 6px;
  font-family: var(--serif); font-size: 14px; color: var(--ink-soft);
}
.subfilters .chip { border-style: dashed; }
.subfilters .chip[aria-pressed="true"] { border-style: solid; }

.result-count {
  font-family: var(--mono); font-size: 11.5px; letter-spacing: .12em;
  text-transform: uppercase; color: var(--ink-soft); margin: 18px 0 26px;
}

.show-more-wrap { text-align: center; margin: 34px 0 6px; }
.show-more { min-width: 240px; }
/* `.btn { display: inline-block }` is an author rule and outranks the UA's
   `[hidden] { display: none }`, so `more.hidden = true` left a live-looking
   240x46 button painting under a fully loaded grid - reading a stale count
   from the previous view, and reachable by Tab while absent from the
   accessibility tree. The same patch already exists for .announce,
   .hero-callout, .subfilters, .modal, .lightbox and .related. */
.show-more[hidden] { display: none; }

/* ==========================================================================
   PLATE GRID - a page of plates, not a shelf of boxes.
   The tile IS the artwork. Every mockup Printify makes stands on the same
   grey-taupe studio ground, so a grid of mockups is a grid of identical grey
   rectangles on a cream page no matter how the card around it is styled -- the
   exact failure this brand cannot afford, since the plate is the thing being
   sold. The plate scans, by contrast, are printed on rgb(241,232,205) and the
   paper under them is rgb(242,232,207): the artwork simply lands on the page,
   with no box, no border, no shadow and no ground of its own.
   The garment is not lost, it is one hover away (and, where nothing hovers, it
   is the colour dots and the Tee / Crewneck buttons under every card).
   ========================================================================== */

/* Three across on a desktop, not four. A plate is a composition -- a title, a
   binomial, an animal and four callouts -- and at 280px those callouts are
   grey dust. At 360px they are legible, and the page reads as a guide someone
   set rather than a shelf of thumbnails. */
.grid {
  display: grid;
  gap: clamp(30px, 3.4vw, 56px) clamp(22px, 2.6vw, 40px);
  grid-template-columns: repeat(auto-fill, minmax(292px, 1fr));
}

.plate {
  background: none; border: 0; box-shadow: none;
  display: flex; flex-direction: column;
}

.plate-figure {
  position: relative; margin: 0;
  /* Slightly shorter than the scan's 880:1100, which trims ~90px of the
     plate's own blank margin (all of it outside the printed rules) and brings
     the caption up under the artwork. Nothing printed is ever cropped. */
  aspect-ratio: 880 / 1010; overflow: hidden;
  /* paper-warm, not paper: on a 3G-class connection the slot is visible for a
     beat before the plate lands, and plate cream on page cream made an
     unloaded card indistinguishable from empty shelving. The image covers
     the whole slot once it arrives. */
  background: var(--paper-warm);
}
.plate-figure img {
  width: 100%; height: 100%; object-fit: cover;
  position: absolute; inset: 0;
}
/* The plate: no zoom, no pan, no hover crop. Cropping the artwork to make a
   tile livelier would be cropping the product. */
.plate-art { object-position: 50% 25%; }

/* The product photograph, crossfaded over the plate: the same shirt, in the
   dark colourway, cropped into the garment so the studio ground is a hairline
   at the corners instead of a third of the tile. The scale is measured, not
   guessed: the shirt occupies 18.7%-88% of the square mockup's height, and a
   tall tile showing the whole square would band the top and bottom of the
   hover with exactly the grey this grid was rebuilt to get rid of. */
/* The plate LIFTS off the page on hover. It does not become something else:
   the previous crossfade to a cropped garment photo showed the product smaller
   than the artwork it replaced, with hard studio-grey bands at the corners. */
.plate:hover .plate-figure,
.plate:focus-within .plate-figure {
  transform: translateY(-4px);
  box-shadow: var(--shadow-plate);
}
.plate-figure { transition: transform .26s var(--ease), box-shadow .26s var(--ease); }
@media (prefers-reduced-motion: reduce) {
  .plate:hover .plate-figure, .plate:focus-within .plate-figure { transform: none; }
}

/* The frame belongs to the PHOTOGRAPH, not to the artwork: it fades in with
   the mockup so the hover reads as a print sample laid on the page, and the
   resting grid stays frameless. */
.plate-figure::after {
  content: ""; position: absolute; inset: 0; pointer-events: none;
  border: 1px solid var(--rule-strong);
  opacity: 0; transition: opacity .18s ease;
}
.plate:hover .plate-figure::after,
.plate:focus-within .plate-figure::after { opacity: 1; }

.plate-open {
  position: absolute; inset: 0; width: 100%; height: 100%;
  /* An <a> now, so it needs these; it was a <button>. */
  display: block; text-decoration: none; color: inherit;
  /* pointer, not zoom-in: this click SELECTS the item (opens its product
     view); the cursor should promise selection, not magnification */
  background: transparent; border: 0; cursor: pointer; padding: 0;
}
.plate-open:focus-visible { outline-offset: -3px; }

.plate-body {
  padding: 12px 1px 0; margin-top: 10px;
  border-top: 1px solid var(--rule);
  display: flex; flex-direction: column; flex: 1;
}
/* Plate line left, shortlist heart right: the caption row of a guide entry. */

/* ---------- garment colour swatches ---------- */

/* The BUTTON is 24x24 -- the WCAG 2.2 SC 2.5.8 minimum -- and the colour is
   drawn as an 18px disc inside it. Relying on the spacing exception instead
   (18px targets on a 24px pitch) is arguably conformant and was rejected: the
   gate cannot see the exception, tangent circles are a hair's breadth from
   failing, and a bigger tap target on a phone is worth more than the argument. */
.swatches {
  display: flex; flex-wrap: wrap; align-items: center; gap: 2px;
  margin: 2px 0 13px;
}
/* 30px, not 24. These are CONTROLS (Jason 2026-09-08: "those color bubbles, i
   want you to be able to click them and it shows you the shirt color") and 24px
   discs shoulder-to-shoulder were a precision-tap problem on a phone. */
.sw {
  width: 30px; height: 30px; flex: 0 0 auto; padding: 0;
  display: grid; place-items: center; position: relative;
  background: transparent; border: 0; cursor: pointer;
}
.sw::before {
  content: ""; width: 22px; height: 22px; border-radius: 50%;
  background: var(--sw, var(--paper));
  border: 1px solid var(--rule-strong);
  transition: box-shadow .2s var(--ease), border-color .2s var(--ease);
}
.sw:hover::before { border-color: var(--navy); }
.sw[aria-pressed="true"]::before {
  box-shadow: 0 0 0 2px var(--paper), 0 0 0 3.5px var(--navy);
}
.sw:focus-visible { outline: 2px solid var(--navy); outline-offset: 1px; }

/* Returns the view to the plate artwork. A slash, not a colour: it is not one
   of the garments and must not read as a tenth swatch. */
.sw-reset::before { background: var(--paper); }
.sw-reset::after {
  content: ""; position: absolute; width: 1.5px; height: 20px;
  background: var(--rule-strong); transform: rotate(45deg);
}

/* Marks the six-up group plate so it is not mistaken for one of the singles
   cut from it. In the caption, not on the artwork. */
/* Inline on the plate line. As a block it added ~30px to its own card and
   broke the horizontal bands its row-mates share. */
.plate-badge {
  display: inline-block; white-space: nowrap;
  font-family: var(--mono); font-size: 11px; letter-spacing: .1em;
  text-transform: uppercase; color: var(--brick-deep);
  border: 1px solid currentColor; border-radius: var(--r-sm);
  padding: 1px 6px; margin-left: 4px; vertical-align: baseline;
}

/* Distinguishes the eight -her twins from their base versions on the plate
   line. Brick, not a new colour, and no border: it must read as part of the
   catalogue reference, not as a promotional badge. */
/* The -her twins carry the SAME name, binomial and description as their base
   plate, so in the grid two adjacent cards looked like a duplication bug and
   the only differentiator was this line of small brick text. It now uses the
   same outlined chip as "Six-plate group", which is a component the card
   already owns and which reads at a glance. */
.for-her {
  display: inline-block; white-space: nowrap;
  font-family: var(--mono); font-size: 11px; letter-spacing: .1em;
  text-transform: uppercase; color: var(--brick-deep);
  border: 1px solid currentColor; border-radius: var(--r-sm);
  padding: 1px 6px; margin-left: 4px; vertical-align: baseline;
}

.plate-no {
  font-family: var(--mono); font-size: 12px; letter-spacing: .13em;
  text-transform: uppercase; color: var(--ink-soft); margin: 0 0 7px;
}
/* These render UPPERCASE content in rules that were tuned for lowercase
   display type, so they inherited negative tracking. Caps need opening, not
   closing. And only Spectral 400/600/800 are installed: `700` resolved UP to
   800, so the card title was painting at the same weight as the page h1. */
.plate-name, #modalName { letter-spacing: .015em; text-wrap: balance; }
.plate-name {
  font-size: 19px; font-weight: 600;
  line-height: 1.22; margin: 0 0 4px;
}
.binomial {
  font-style: italic; font-size: 15px; color: var(--brick-deep); margin: 0 0 9px;
}
.quip {
  font-size: 14.5px; color: var(--ink-soft); margin: 0 0 16px; line-height: 1.5;
}

.plate-buys { margin-top: auto; display: flex; gap: 8px; }
.buy {
  flex: 1; text-align: center; text-decoration: none;
  font-family: var(--mono); font-size: var(--label); letter-spacing: .07em;
  text-transform: uppercase; padding: 12px 8px; border-radius: var(--r-sm);
  border: 1px solid var(--navy); color: var(--ink); min-height: 44px;
}
.buy:hover { background: var(--navy); color: var(--cream); }
.buy-primary { background: var(--navy); color: var(--cream); }
.buy-primary:hover { background: var(--brick); border-color: var(--brick); }

.empty {
  font-size: 18px; color: var(--ink-soft); text-align: center;
  padding: 64px 20px; font-style: italic;
}


/* ---------- editorial field-note tiles ---------- */

.note-link {
  margin-top: auto; align-self: flex-start;
  font-family: var(--mono); font-size: 12px; letter-spacing: .08em;
  text-transform: uppercase; color: var(--navy);
  text-decoration: none; border-bottom: 1px solid var(--rule-strong);
  /* 24px minimum target: the underline look stays, the hit area grows */
  padding: 6px 2px 5px 0;
}
.note-link:hover { border-color: var(--navy); color: var(--navy); }

/* ---------- about ---------- */

.about { background: var(--navy); color: var(--cream); padding-block: var(--sec-pad); }
.about-inner {
  display: grid; gap: clamp(30px, 5vw, 60px);
  grid-template-columns: minmax(0, 1.55fr) minmax(0, 1fr); align-items: start;
}
.about-text p { max-width: 60ch; color: var(--on-navy); }
.about-card {
  align-self: center;
  border: 1px solid var(--rule-on-navy); border-radius: var(--r-md);
  padding: 24px 24px 8px; background: rgba(242,232,207,.05);
}
.about-duo {
  width: 96px; height: auto; margin: 0 0 16px;
}
.about-card-label {
  font-family: var(--mono); font-size: 10.5px; letter-spacing: .18em;
  text-transform: uppercase; color: var(--on-navy-soft); margin: 0 0 16px;
}
.conventions { list-style: none; margin: 0; padding: 0; }
.conventions li {
  display: flex; gap: 12px; padding: 11px 0;
  border-top: 1px solid var(--rule-on-navy); font-size: 15.5px; color: var(--on-navy);
}
.conventions li:first-child { border-top: 0; }
.conventions span { font-family: var(--mono); font-size: 12px; color: var(--on-navy-mark); min-width: 26px; }

/* ---------- footer ---------- */

.site-foot { padding-top: clamp(44px, 6vw, 72px); }
.foot-inner {
  display: flex; flex-wrap: wrap; gap: 26px 40px; justify-content: space-between;
  align-items: flex-start; padding-bottom: 28px;
}
.foot-brand { display: flex; align-items: center; gap: 13px; }
.foot-mark-img { width: 52px; height: 52px; flex: 0 0 auto; border-radius: 50%; }
.foot-mark {
  font-weight: 800; text-transform: uppercase; letter-spacing: .055em;
  font-size: 17px; margin: 0 0 4px;
}
.foot-note {
  font-family: var(--mono); font-size: 11px; letter-spacing: .1em;
  text-transform: uppercase; color: var(--ink-soft); margin: 0;
}
.foot-nav { display: flex; flex-wrap: wrap; gap: 8px 22px; align-content: flex-start; }
.foot-nav a {
  font-family: var(--mono); font-size: 12px; letter-spacing: .08em;
  text-transform: uppercase; text-decoration: none; color: var(--ink);
  padding: 6px 0; min-height: 24px; display: inline-block;
}
.foot-nav a:hover { color: var(--brick-deep); }
.foot-facts {
  flex-basis: 100%; margin: 0; max-width: 62ch;
  font-size: 14.5px; color: var(--ink-soft);
}
.foot-privacy {
  margin: 4px 0 0;
  font-family: var(--mono); font-size: 11px; letter-spacing: .1em;
  text-transform: uppercase; color: var(--ink-soft);
}

.foot-legal {
  border-top: 1px solid var(--rule); padding-block: 18px 32px;
  display: flex; flex-wrap: wrap; gap: 4px 24px; justify-content: space-between;
}
.foot-legal p {
  font-family: var(--mono); font-size: 11px; letter-spacing: .1em;
  color: var(--ink-soft); margin: 0;
}

/* ---------- modal ---------- */

.modal { position: fixed; inset: 0; z-index: 60; display: grid; place-items: center; padding: 20px; }
.modal[hidden] { display: none; }
.modal-backdrop { position: absolute; inset: 0; background: rgba(27,47,74,.72); border: 0; }
.modal-panel {
  position: relative; background: var(--paper); border: 1px solid var(--rule-strong);
  border-radius: var(--r-md); max-width: 940px; width: 100%;
  max-height: 90vh; overflow-y: auto; overflow-x: hidden;
  box-shadow: 0 30px 70px -30px rgba(27,47,74,.7);
}
.modal-bar { display: contents; }   /* desktop: the bar is invisible to layout */
.modal-close {
  position: absolute; top: 10px; right: 12px; z-index: 2;
  background: var(--paper-warm); border: 1px solid var(--rule-strong);
  color: var(--ink); font-size: 22px; line-height: 1;
  width: 40px; height: 40px; border-radius: 999px; cursor: pointer;
}
.modal-close:hover { background: var(--navy); color: var(--cream); }
/* These three set a size but never a family, so the x and the arrows were the
   only system-font glyphs anywhere on the site. */
.modal-close, .modal-nav, .lightbox-close { font-family: var(--mono); }
.modal-body { display: grid; grid-template-columns: minmax(0, 1.1fr) minmax(0, 1fr); align-items: stretch; }
/* contain, not cover. The detail image is a zoomed crop of the plate, so
   cover-cropping it a second time inside the panel cut the specimen in half.
   Its ground is the paper, so the panel behind it is the paper too. */
.modal-body img {
  width: 100%; height: 100%; max-height: 78vh;
  object-fit: contain; background: var(--paper);
  border-right: 1px solid var(--rule);
}
.modal-meta { padding: clamp(24px, 4vw, 40px); }
.modal-meta h2 { font-size: clamp(24px, 3.4vw, 34px); font-weight: 800; margin: 0 0 6px; line-height: 1.16; }
.modal-meta .binomial { font-size: 18px; margin-bottom: 14px; }
.modal-meta .quip { font-size: 17px; color: var(--ink); margin-bottom: 26px; }
.modal-buys { display: flex; flex-direction: column; gap: 10px; margin-bottom: 16px; }
.modal-fine {
  font-family: var(--mono); font-size: 10.5px; letter-spacing: .1em;
  text-transform: uppercase; color: var(--ink-soft); margin: 0;
}

.modal-figure { position: relative; margin: 0; }
.modal-figure img { cursor: zoom-in; display: block; }

/* The affordance the image alone lacked: nothing said the plate could be
   opened bigger. Sits on the artwork's cream, so paper on paper needs the
   border to separate it. */
.modal-figure { position: relative; }
.modal-enlarge {
  /* STICKY, not absolute. The panel is the scroll container and the figure is
     taller than it (885 vs 855 at 1440x900), so an absolutely positioned
     "bottom: 12px" resolved against the scrolled CONTENT and put the control
     18px below the fold at 1440x900 and 76px past the panel entirely at
     1150x800 - off-screen on both common desktop widths, on the button that
     opens the plate. Sticky pins it to the visible bottom of the scrollport
     instead, so it is on screen at every height. The negative margin keeps it
     out of the layout, exactly as the absolute version was. */
  position: sticky; float: right; clear: both;
  bottom: 12px; margin: 12px 12px 0 0; z-index: 3;
  font-family: var(--mono); font-size: 11.5px; letter-spacing: .1em;
  text-transform: uppercase; cursor: zoom-in;
  color: var(--navy); background: var(--paper);
  border: 1px solid var(--navy); border-radius: 3px;
  padding: 7px 11px;
}
.modal-enlarge:hover { background: var(--navy); color: var(--cream); }

/* Prev/next: step through the current filter without closing the plate.
   Pinned to the panel edges, vertically centred; markers sized like the
   close button so the three controls read as one family. */
.modal-nav {
  position: absolute; top: 50%; transform: translateY(-50%);
  z-index: 2;
  width: 44px; height: 44px; border-radius: 50%;
  font-size: 26px; line-height: 1; cursor: pointer;
  color: var(--navy); background: var(--paper);
  border: 1px solid var(--rule-strong);
}
.modal-nav:hover { border-color: var(--navy); background: var(--paper-warm); }
/* Inside the panel at every size. Half-outside arrows looked right on a
   desktop mock but the panel is a SCROLL CONTAINER (overflow:auto): anything
   positioned past its edge becomes horizontal overflow. */
.modal-prev { left: 10px; }
.modal-next { right: 10px; }

/* Desktop only: step the arrows off the plate and into the backdrop gutter.
   `fixed` rather than a negative offset because the panel is a scroll
   container - the note above is right that anything positioned past its edge
   becomes horizontal overflow, but a fixed element is out of that flow
   entirely, so it costs nothing. The gutter is 105px at 1150 and 250px at
   1440; the control is 44px. */
/* The panel is 940px wide and centred, so a 44px arrow only clears the plate
   once the backdrop gutter is wider than the control: (viewport - 940) / 2 >= 56
   means viewport >= 1052. Below that a centred arrow sits ON the artwork - an
   adversarial sweep of widths 840-1200 found the prev arrow covering printed
   callout text from 880 all the way to 1024. Above the threshold, the gutter. */
@media (min-width: 1060px) {
  .modal-nav { position: fixed; top: 50%; z-index: 82; }
  .modal-prev { left: calc((100vw - var(--modal-w, 940px)) / 2 - 56px); }
  .modal-next { right: calc((100vw - var(--modal-w, 940px)) / 2 - 56px); }
}

/* 861-1059: no gutter to put them in, so use the treatment the phone layout
   already proved - an opaque toolbar stuck to the top of the panel, which its
   own note describes as "always reachable, never over content". */
@media (min-width: 861px) and (max-width: 1059px) {
  .modal-bar {
    display: flex; align-items: center; gap: 8px;
    position: sticky; top: 0; z-index: 3;
    padding: 8px 10px; background: var(--paper);
    border-bottom: 1px solid var(--rule);
  }
  .modal-nav, .modal-close {
    position: static; top: auto; right: auto; left: auto; bottom: auto;
    transform: none; box-shadow: none;
  }
  .modal-close { margin-left: auto; }
}

/* Struck list price beside a computed sale price. Soft ink, thin strike:
   the sale number is the message, the old number is the context. */
.buy s {
  color: rgba(242,232,207,.72); text-decoration-thickness: 1px;
  font-weight: 400; margin-right: 1px;
}
.buy:not(.buy-primary) s { color: var(--ink-soft); }

.modal-sale {
  font-family: var(--mono); font-size: 12px; letter-spacing: .1em;
  text-transform: uppercase; color: var(--brick-deep);
  margin: 0 0 10px;
}

.share-btn {
  display: inline-block; margin-left: 10px;
  font-family: var(--mono); font-size: 11.5px; letter-spacing: .08em;
  text-transform: uppercase; cursor: pointer;
  color: var(--navy); background: transparent;
  border: 0; border-bottom: 1px solid var(--rule-strong);
  padding: 4px 1px;
}
.share-btn:hover { border-color: var(--navy); }

.chip-random { border-style: dashed; }

.modal-swatches { margin: 4px 0 18px; }
/* A GRID, not a flex row. The label is `.swatches`' first flex child, so when
   the ninth tee disc did not fit (338.7px available against 340.0px needed,
   over by 1.3px) it wrapped to the row's left edge and landed underneath the
   word TEE - out of line with the eight above it, reading as a stray control
   rather than the ninth colour. Measured wrapping at 2560, 1440, 1150, 1024,
   900, 390 and 360; only 768 escaped, because the modal is single-column
   there. The narrow-screen compensation below did not save it either: its
   comment does the arithmetic for 24px discs, and these became 30px controls
   when Jason asked for them to be clickable.

   Taking the label into its own grid column leaves the discs a track of their
   own: nine 30px controls need 270px against 286px, and if they ever do wrap
   they wrap under each other. */
.modal-swatch-row {
  margin: 0 0 8px;
  display: grid;
  grid-template-columns: auto minmax(0, 1fr);
  align-items: start;
  column-gap: 4px;
}
.modal-swatch-row .sw-dots {
  display: flex; flex-wrap: wrap; align-items: center; gap: 2px;
}
.modal-swatch-row .sw-name { grid-column: 2; }
.sw-garment {
  font-family: var(--mono); font-size: 12px; letter-spacing: .1em;
  text-transform: uppercase; color: var(--ink-soft);
  /* 44px, not 52. Nine 30px discs plus eight 2px gaps need 286px; a 52px
     label left 283px, so the row missed fitting by THREE PIXELS and the ninth
     tee colour wrapped. "CREWNECK" is the longest label and sets its own
     width anyway, so the floor only ever mattered for the short ones. */
  min-width: 44px;
}
/* Text echo of the tapped colour, for eyes that cannot split navy from black
   on an 18px dot. The height is reserved whether or not a name is showing, so
   selecting a colour moves nothing. */
.sw-name {
  flex-basis: 100%; display: block; min-height: 16px; line-height: 16px;
  font-family: var(--mono); font-size: 12px; letter-spacing: .08em;
  text-transform: uppercase; color: var(--ink-soft);
  margin: 1px 0 0 3px;
}
@media (max-width: 700px) {
  /* The label floor is 44px everywhere now (see .sw-garment). On a phone the
     row can still lose ~15px to a classic scrollbar, so the discs close up. */
  .modal-swatch-row { column-gap: 2px; }
  .modal-swatch-row .sw-dots { gap: 1px; }
}

/* ---------- second-stage zoom (lightbox) ---------- */

.lightbox {
  position: fixed; inset: 0; z-index: 80;
  background: rgba(20, 30, 48, .93);
}
.lightbox[hidden] { display: none; }
.lightbox-scroll {
  position: absolute; inset: 0; overflow: auto;
  display: grid; place-items: center;   /* centres while FIT; pan when magnified */
  -webkit-overflow-scrolling: touch;
}
/* fit-to-viewport by default; .magnified releases the cap for full-res pan */
.lightbox-scroll img {
  max-width: min(92vw, 1200px); max-height: 92vh;
  width: auto; height: auto; display: block;
  cursor: zoom-in;   /* fit: a click magnifies; .magnified switches to grab */
  box-shadow: 0 24px 80px -24px rgba(20,30,48,.7);
}
/* Opens FIT; a click on the plate magnifies (app.js setLbScale). */
.lightbox-scroll.magnified { display: block; }
.lightbox-scroll.magnified img {
  max-width: none; max-height: none; height: auto;
  margin: 40px auto; cursor: grab;
}
.lightbox-scroll.dragging img { cursor: grabbing; }
.lightbox-scroll.dragging { user-select: none; }
/* The zoom controls. One pill, fixed to the corner, ABOVE the plate at every
   zoom level - a magnified plate covers the whole viewport, so a control that
   the picture can cover is a trap. */
.lightbox-tools {
  position: fixed; top: 14px; right: 18px; z-index: 82;
  display: flex; align-items: center; gap: 2px;
  padding: 4px; border-radius: 26px;
  background: rgba(20,30,48,.88);
  border: 1px solid var(--rule-on-navy-strong);
}
.lb-tool {
  width: 44px; height: 44px; border-radius: 50%;
  display: grid; place-items: center;
  font-family: var(--mono); font-size: 22px; line-height: 1;
  cursor: pointer; color: var(--cream);
  background: transparent; border: 0; padding: 0;
}
.lb-tool:hover:not(:disabled) { background: rgba(242,232,207,.18); }
.lb-tool:focus-visible { outline: 2px solid var(--cream); outline-offset: -3px; }
/* Greyed, not hidden: the end of the range should be visible before it is hit. */
.lb-tool:disabled { opacity: .32; cursor: default; }
.lb-pct {
  min-width: 52px; text-align: center;
  font-family: var(--mono); font-size: 12px; letter-spacing: .06em;
  color: rgba(242,232,207,.92);
}
.lightbox-close {
  margin-left: 4px;
  border-left: 1px solid var(--rule-on-navy-strong);
  border-radius: 0 22px 22px 0;
  font-size: 26px;
}
@media (max-width: 600px) {
  .lightbox-tools { top: 10px; right: 10px; left: 10px; justify-content: center; border-radius: 26px; }
}
/* The hint sits OVER a magnified plate, and the plates are cream, so pale
   text on nothing vanished at full zoom. It gets its own backing instead. */
.lightbox-hint {
  position: fixed; left: 50%; bottom: 12px; z-index: 81;
  transform: translateX(-50%);
  max-width: calc(100vw - 24px);
  margin: 0; text-align: center; pointer-events: none;
  padding: 7px 14px; border-radius: 20px;
  background: rgba(20,30,48,.88);
  border: 1px solid var(--rule-on-navy-strong);
  font-family: var(--mono); font-size: 12px; letter-spacing: .1em;
  text-transform: uppercase; color: rgba(242,232,207,.95);
}
@media (max-width: 600px) {
  .lightbox-hint { font-size: 11px; letter-spacing: .06em; padding: 6px 10px; }
}

/* ---------- related plates (grid <-> members) ---------- */

.related { margin-top: 26px; padding-top: 20px; border-top: 1px solid var(--rule); }
.related[hidden] { display: none; }
.related-label {
  font-family: var(--mono); font-size: 11px; letter-spacing: .13em;
  text-transform: uppercase; color: var(--ink-soft); margin: 0 0 14px;
}
.related-row { display: flex; flex-wrap: wrap; gap: 12px; }
.rel {
  display: flex; flex-direction: column; gap: 7px; width: 104px;
  background: none; border: 0; padding: 0; cursor: pointer; text-align: left;
  font: inherit; color: var(--ink);
}
/* Cropped into the printed area of the mockup, measured on the source: the
   plate occupies roughly 26%-75% of the square photograph, so a 2.05x zoom
   about its centre fills the thumbnail with artwork and leaves the studio
   ground outside the frame. The photograph is never altered, only framed. */
.rel-thumb {
  display: block; width: 104px; height: 104px; overflow: hidden;
  border: 1px solid var(--rule); border-radius: var(--r-sm);
  background: var(--paper); transition: border-color .15s;
}
.rel img {
  width: 100%; height: 100%; object-fit: cover;
  transform: scale(2.05); transform-origin: 50% 51%;
}
.rel-name {
  font-family: var(--mono); font-size: 10.5px; letter-spacing: .06em;
  line-height: 1.35; color: var(--ink-soft);
}
.rel:hover .rel-thumb { border-color: var(--navy); }
.rel:hover .rel-name { color: var(--brick-deep); }

/* ==========================================================================
   RESPONSIVE
   ========================================================================== */

/* Read-a-plate: below this the margin labels have nowhere to live, so the
   markers keep their numerals on the image and the labels become a legend. */
@media (max-width: 939px) {
  .rap-label, .rap-lead { display: none; }
  .rap-plate { width: min(340px, 100%); }
  .rap-legend {
    display: block; list-style: none; margin: 22px auto 0; padding: 0;
    max-width: 460px;
  }
  .rap-legend li {
    display: flex; gap: 12px; align-items: baseline;
    padding: 9px 0; border-top: 1px solid var(--rule);
    font-size: 15px; color: var(--ink-soft);
  }
  .rap-legend li:first-child { border-top: 0; }
  .rap-legend li > span:first-child {
    font-family: var(--mono); font-size: 12px; color: var(--brick-deep);
    min-width: 26px; flex: 0 0 auto;
  }
}

/* The hero folds to one column: type, then the plate, then the shirt and the
   callout side by side under it. The plate is still the biggest thing on the
   screen, and the primary action is still above it.

   The condition is width OR shape, not width alone. A 1024x768 tablet held
   landscape is wider than it is tall: stacked, the type eats the top third and
   a 4:5 plate is left with 250px, which is a picture of a plate rather than the
   plate. Any window from 900px up that is wider than 5:4 keeps the two-column
   hero above, where the artwork has the full height of the screen to grow into. */
@media (max-width: 899px),
       (max-width: 1099px) and (max-aspect-ratio: 5/4) {
  .hero {
    min-height: 0; align-items: flex-start;
    padding-block: 12px 26px;
  }
  .hero-wrap {
    width: min(100% - 2rem, 620px);
    grid-template-columns: minmax(0, 1fr);
    gap: 0;
  }
  /* display:contents lets the data line move below the artwork on a phone
     without moving it in the document. */
  .hero-say { display: contents; }
  .eyebrow      { order: 1; }
  h1            { order: 2; }
  .lede         { order: 3; max-width: none; }
  .hero-actions { order: 4; }
  .hero-art     { order: 5; }
  .hero-data    { order: 6; margin-top: 12px; }

  .hero-art {
    /* Left, not centred: the plate lines up with the headline and the button
       above it, so the stacked hero reads as one column instead of a centred
       image that has drifted off the type. */
    justify-self: start;
    /* Width-led here, but still shortened against the real viewport height so
       the shirt photograph and the callout under the plate stay inside the
       first screen instead of hanging just below it. */
    width: min(100%, 520px,
               max(250px, calc((100svh - var(--chrome-h) - 356px) * 0.8)));
    display: grid;
    grid-template-columns: auto minmax(0, 1fr);
    grid-template-areas: "plate plate" "garment callout" "garment cap";
    column-gap: 14px;
    margin-top: 18px;
  }
  .hero-plate-img { grid-area: plate; }
  .hero-garment {
    /* inset:auto matters: these two keep position, so the desktop rule's
       right/bottom offsets would shove a relatively-positioned box off the
       left edge of the phone. */
    grid-area: garment; position: relative; inset: auto;
    width: clamp(96px, 13vw, 148px); margin-top: 12px; padding: 5px;
    align-self: start;
  }
  .hero-callout {
    grid-area: callout; position: relative; inset: auto;
    width: auto; text-align: left; align-self: start;
    margin-top: 13px; padding-left: 15px; font-size: 15px;
  }
  .hero-callout-lead { display: none; }
  .hero-callout-dot {
    right: auto; left: 0; top: .55em;
    width: 8px; height: 8px;
  }
  .hero-cap {
    grid-area: cap; align-self: start;
    margin-top: 8px; padding-top: 0; border-top: 0;
  }
}

/* ---- tablets and short laptops: the margin notes move into a rail ----
   Between a phone and a laptop the stacked hero was spending ~150px of the
   first screen on the shirt-and-callout row UNDER the artwork, and 240px of
   the width on nothing at all. Both go into a rail beside the plate instead:
   the plate grows to the full height the screen can give it, and the callout,
   the shirt photograph and the plate line all stay above the fold with it.
   Measured at 768x1024: plate art + shirt = 41% of the first viewport, against
   33% stacked, with the whole plate, its footer line and the caption visible. */
@media (min-width: 621px) and (max-width: 899px),
       (min-width: 621px) and (max-width: 1099px) and (max-aspect-ratio: 5/4) {
  /* The column is sized FROM the composition: the plate is as tall as the
     screen allows (4:5, so width = height x 0.8, less the type above it and its
     own caption below), the rail is a fixed reading width, and the wrap is
     exactly the two of them side by side. */
  .hero-wrap {
    --art-w: clamp(250px, calc((100svh - var(--chrome-h) - 308px) * 0.8), 560px);
    --rail-w: clamp(148px, 22vw, 208px);
    width: min(100% - 2rem, calc(var(--art-w) + 24px + var(--rail-w)));
  }
  /* The wrap is now as wide as the artwork, which is wider than a line of text
     wants to be. The measure is held here rather than by the column. */
  .lede { max-width: 52ch; }

  .hero-art {
    display: block; position: relative;
    width: min(var(--art-w), calc(100% - var(--rail-w) - 24px));
    margin-top: 16px;
  }
  .hero-callout {
    position: absolute; left: calc(100% + 24px); right: auto; top: 11%;
    width: var(--rail-w); text-align: left;
    margin-top: 0; padding-left: 16px; font-size: 15.5px;
  }
  .hero-callout-dot { left: 0; right: auto; top: .55em; width: 8px; height: 8px; }
  /* Foot of the photograph on the foot of the plate: the print sample laid
     beside the page it was printed from. */
  .hero-garment {
    position: absolute; left: calc(100% + 24px); right: auto;
    /* Under the callout, not on the fold: at 768x1024 the bottom-anchored
       photograph sat 29px past the fold with 361px of empty rail above it. */
    top: calc(11% + 84px); bottom: auto;
    width: min(var(--rail-w), 190px); margin: 0; padding: 6px;
  }
  /* Back under the artwork, where a plate caption belongs, and where its
     height is predictable enough to size the plate against. */
  .hero-cap {
    display: flex; margin-top: 10px; padding-top: 9px;
    border-top: 1px solid var(--rule);
  }
}

@media (max-width: 939px) {
  .finder { grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 0 20px; }
}

@media (max-width: 860px) {
  .about-inner { grid-template-columns: 1fr; }
  .modal-body { grid-template-columns: 1fr; }
  .modal-body img {
    aspect-ratio: 1 / 1; height: auto; border-right: 0; border-bottom: 1px solid var(--rule);
  }
  /* Stacked, the panel is a tall scroller. The controls used to float fixed
     over the screen's bottom corners, where they sat on the crewneck button and
     the panel edge sliced it (9px at 390, 38px at 768); shortening the panel
     only moved the cut to the tee button. Now they are an opaque toolbar that
     sticks to the top of the sheet: always reachable, never over content, and
     the panel can use the whole screen. */
  .modal-bar {
    display: flex; align-items: center; gap: 8px;
    position: sticky; top: 0; z-index: 3;
    padding: 8px 10px; background: var(--paper);
    border-bottom: 1px solid var(--rule);
  }
  .modal-nav, .modal-close {
    position: static; top: auto; right: auto; left: auto; bottom: auto;
    transform: none; box-shadow: none;
  }
  .modal-nav { width: 40px; height: 40px; font-size: 24px; }
  .modal-close { margin-left: auto; }
  .modal { padding: 16px; }
  .modal-panel { max-height: calc(100svh - 32px); }
  .modal-meta { padding-bottom: 28px; }
  /* A shorter plate on phones brings the price and buy buttons closer to the
     first view of the sheet. */
  .modal-body img { max-height: 46vh; }
  /* ENLARGE sits on a cream band below the artwork, 14px clear of the plate's
     footer line (it was touching it at 390). */
  .modal-figure { padding-bottom: 58px; }
  .modal-enlarge { bottom: 10px; }
}

/* Desktop modal: the next-arrow (44px, 10px from the edge) overlapped the
   corner of the primary buy button by 14x24px at 1440x900. The column keeps
   its content clear of the arrow zone. */
@media (min-width: 861px) {
  .modal-meta { padding-right: calc(clamp(24px, 4vw, 40px) + 28px); }
}

@media (max-width: 620px) {
  body { font-size: 16.5px; }

  .site-head { height: 52px; }
  .head-inner { gap: 10px; }
  .wordmark { gap: 9px; }
  .wordmark-mark { width: 36px; height: 36px; }
  .wordmark-main { font-size: 15.5px; letter-spacing: .04em; }
  .wordmark-sub { display: none; }   /* the mark carries the lockup here */
  .head-nav { gap: 12px; }
  .head-nav a { padding: 8px 1px; }
  .nav-long { display: none; }
  .head-nav a.nav-about { display: none; }   /* lives in the footer on a phone */
  .head-nav a.head-cta { padding: 9px 13px; }

  h1 { font-size: 27px; margin-bottom: 12px; }
  .eyebrow { display: none; }
  .lede { margin-bottom: 14px; font-size: 16.5px; }
  .hero-actions { gap: 8px 16px; margin-bottom: 0; }
  .btn { padding: 13px 18px; }
  /* One row, always: the secondary action becomes a rule-underlined link so a
     wrapped second button cannot push the plate out of the first screen. */
  .hero-actions .btn-ghost {
    border: 0; border-bottom: 1px solid var(--rule-strong); border-radius: 0;
    background: none; padding: 12px 2px 8px;
  }
  .hero-actions .btn-ghost:hover { background: none; color: var(--brick-deep); }
  .hero-art { margin-top: 14px; width: min(100%, 380px,
               max(250px, calc((100svh - var(--chrome-h) - 340px) * 0.8))); }
  .hero-garment { width: 112px; }
  .hero-data { line-height: 1.7; }

  .finder-sec { padding-block: 28px 24px; }
  .finder-sec .section-head { margin-bottom: 14px; }
  .finder { grid-template-columns: minmax(0, 1fr); gap: 0; }
  .find { padding: 5px 6px 5px 2px; gap: 12px; }
  .find-thumb { width: 42px; height: 52px; }
  .find-label { font-size: 16.5px; }
  .find-meta { margin-top: 2px; }

  /* ONE card per row on a phone. At 2-up the plate is ~172px wide and its four
     printed callouts -- which ARE the joke, and the entire reason to buy -- are
     illegible. A card that cannot be read sells nothing, so the page gets
     longer and the product stays legible. */
  .grid { grid-template-columns: minmax(0, 1fr); gap: 30px; }
  /* Nine dots plus the count do not fit a 158px column: give the count its own
     line rather than letting it break mid-phrase. */
  .plate-name { font-size: 17px; }
  .plate-body { padding-top: 11px; margin-top: 10px; }
  .quip { font-size: 14px; }

  .foot-inner { flex-direction: column; gap: 22px; }
  .foot-legal { flex-direction: column; }

  /* ---- THE 12px FLOOR -------------------------------------------------
     Nothing under 12px on a phone. Every mono label in this stylesheet is
     set 10.5-11.5px in its own rule for the field-guide look: correct at
     desktop reading distance, genuinely hard to read on a handset. Those
     base values are DESKTOP values, and this block is the single place the
     handset floor is enforced -- so a 10.5px declaration further up the file
     is not a violation, it is a rule this one overrides below 620px.
     Verified by measurement, not by reading: a computed-style scan of every
     visible text-owning element at 390x844 returns zero nodes under 12px in
     every state -- boot, all 125 cards loaded, sub-chips, shortlist chip,
     plate modal, group modal, lightbox, sale bar present, sale bar expired,
     and 404. Tracking is eased alongside each size so nothing that grew a
     point and a half grew wider than the box it sits in. */
  .announce-lead,
  .announce-sub,
  .wordmark-sub,
  .head-nav a,
  .eyebrow,
  .hero-data,
  .hero-cap,
  .rap-cap,
  .note-no,
  .find-meta,
  .chip,
  .result-count,
  .plate-no,
  .buy,
  .about-card-label,
  .subfilters-label,
  .plate-badge,
  .sw-garment,
  .related-label,
  .rel-name,
  .conventions span,
  .foot-note,
  .foot-nav a,
  .foot-legal p,
  .foot-privacy,
  .modal-fine,
  /* Second pass over the same rule. The list above caught the labels that
     were obviously text; these were missed because they read as ornament in
     the mock -- the tally inside a chip, the colour count under a card, the
     two utilities under the buy buttons, the numerals in the field-note list
     and on the assurance strip. On a handset they are all functional text,
     so the floor applies to them exactly as it does to a paragraph. */
  .chip .n,
  .modal-enlarge,
  .share-btn,
  .a-mark { font-size: 12px; }

  .eyebrow, .result-count, .foot-note,
  .foot-legal p, .foot-privacy, .modal-fine, .about-card-label { letter-spacing: .12em; }
  .plate-no { letter-spacing: .13em; }
  .hero-cap { letter-spacing: .09em; }
  .chip { letter-spacing: .07em; padding: 9px 13px; }
  /* .04em, not .07em: with a live sale the tee button reads
     "Shop the tee on Etsy · $34 $22.10" (34 chars) and at .07em it wrapped
     the sale price onto its own line at 390, which read as two prices. */
  .buy { letter-spacing: .04em; padding: 12px 6px; }
  .announce-lead { letter-spacing: .1em; }
  .announce-sub { letter-spacing: .07em; }
  /* Tracking eased with the size so nothing that just grew a point and a half
     grows wider than the box it sits in: the chip row still scrolls one row,
     the colour count still holds one line under a 158px card. */
  .chip .n { letter-spacing: .03em; margin-left: 5px; }
  .a-mark { letter-spacing: .07em; }
  .share-btn { letter-spacing: .05em; }
  .modal-enlarge { letter-spacing: .06em; padding: 7px 10px; }
  /* The disc grows with the numeral inside it: III set at 12px in the mono
     face measures ~21.6px, which would burst the 21px circle it sits in. */
  .rap-mark i { width: 26px; height: 26px; font-size: 12px; }
}

/* Narrow phones (320-400): the one-row header must never overflow. The
   wordmark gives up a point and its tracking, and the nav keeps only the
   Etsy action - the hero's "Browse the plates" sits directly below, so the
   PLATES link is redundant at this width. Measured before this block: the
   header ran 66px past a 320px viewport and 26px past 360px. */
@media (max-width: 400px) {
  .head-inner { gap: 8px; }
  .wordmark { gap: 7px; min-width: 0; }
  .wordmark-main { font-size: 14px; letter-spacing: .02em; white-space: nowrap; }
  .head-nav { gap: 8px; flex: 0 0 auto; }
  .head-nav a:not(.head-cta) { display: none; }
  .head-nav a.head-cta { padding: 8px 11px; }
}


/* Pinned chips on a phone: one scrollable row, not a wrapped block -- the head
   plus a wrapped chip row ate a quarter of the screen. */
@media (max-width: 700px) {
  .filters {
    flex-wrap: nowrap; overflow-x: auto;
    -webkit-overflow-scrolling: touch;
    scrollbar-width: none;
  }
  .filters::-webkit-scrollbar { display: none; }
  .filters .chip { flex: 0 0 auto; }

  /* Scene chips get the same treatment: sixteen wrapped chips were a 407px
     wall before any product. */
  .subfilters { flex-wrap: nowrap; overflow-x: auto;
    -webkit-overflow-scrolling: touch; scrollbar-width: none; }
  .subfilters::-webkit-scrollbar { display: none; }
  .subfilters .chip { flex: 0 0 auto; }
  .subfilters-label { flex-basis: auto; white-space: nowrap; margin: 0; }
  .subfilters-note { display: none; }
}

/* Right-edge fade on chip rows that overflow horizontally (set by JS).
   A chip sliced mid-word reads as a bug; a fade reads as "keep scrolling". */
@media (max-width: 700px) {
  .filters.scrollable, .subfilters.scrollable {
    -webkit-mask-image: linear-gradient(90deg, #000 90%, transparent 99%);
    mask-image: linear-gradient(90deg, #000 90%, transparent 99%);
  }
}

/* ==========================================================================
   Motion layer. Every animation is transform/opacity only (no layout, no CLS)
   and every one dies under prefers-reduced-motion. The register is deliberately
   quiet: this is a field guide, not an app store page.
   ========================================================================== */

/* Modal opens with a settle, like a plate being laid on the desk. */
@keyframes modal-settle {
  from { opacity: 0; transform: scale(.975) translateY(6px); }
  to   { opacity: 1; transform: none; }
}
.modal-panel { animation: modal-settle .22s ease-out; }

/* Turning to another plate (prev/next/related): the incoming image rises in.
   JS re-triggers this by toggling .turning on the figure. */
@keyframes plate-turn {
  from { opacity: 0; transform: translateY(10px); }
  to   { opacity: 1; transform: none; }
}
.modal-figure.turning img { animation: plate-turn .24s ease-out; }

/* A tapped swatch pops once - confirmation you can feel at 18px. */
@keyframes sw-pop {
  0% { transform: scale(1); } 55% { transform: scale(1.28); } 100% { transform: scale(1); }
}
.sw[aria-pressed="true"]::before { animation: sw-pop .2s ease-out; }

/* Newly rendered cards rise into place. Uniform, not staggered: a 48-card
   stagger reads as loading, not craft.

   Transform only, never opacity. An element animating up from opacity 0 is not
   counted as painted, and the first card's art is this page's Largest
   Contentful Paint: measured 2026-09-09 on Fast 3G at 390px, /plates/ reported
   LCP at 5436ms with the fade and 2.4s without it. A 0.26s flourish was
   costing three seconds of the metric Google ranks on. */
@keyframes card-rise {
  from { transform: translateY(10px); }
  to   { transform: none; }
}
.plate { animation: card-rise .26s ease-out both; }

@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after { transition-duration: .01ms !important; animation-duration: .01ms !important; }
  .modal-panel, .modal-figure.turning img,
  .sw[aria-pressed="true"]::before, .plate,
  .hero-art, .hero-say > * { animation: none; }
}

/* Windows default scrollbars are a grey slab across the modal and lightbox.
   Thin + brand-toned keeps them functional without breaking the paper. */
.modal-panel, .lightbox-scroll {
  scrollbar-width: thin;
  scrollbar-color: var(--rule-strong) transparent;
}
.modal-panel::-webkit-scrollbar, .lightbox-scroll::-webkit-scrollbar {
  width: 10px; height: 10px;
}
.modal-panel::-webkit-scrollbar-thumb, .lightbox-scroll::-webkit-scrollbar-thumb {
  background: var(--rule-strong); border-radius: 5px;
  border: 2px solid transparent; background-clip: content-box;
}
.modal-panel::-webkit-scrollbar-track, .lightbox-scroll::-webkit-scrollbar-track {
  background: transparent;
}


/* ---- desktop hero geometry (the complement of the stacked/rail blocks) ----
   The shirt photograph hangs to the LEFT of the artwork, in the space between
   the text column and the plate. Its width and the artwork's width are tied:
   the art may never be wider than its column minus the photograph minus 64px,
   so the photograph always clears the type. Measured before this rule: a 2px
   gutter at 1440x900 and a 60-120px OVERLAP at 1920x1080. */
@media (min-width: 900px) and (min-aspect-ratio: 5/4), (min-width: 1100px) {
  .hero-art { --garment-w: clamp(150px, 19vw, 280px);
              max-width: calc(100% - var(--garment-w) - 64px); }
  .hero-garment { width: var(--garment-w); }
  /* The leader and dot pointed at the plate's blank margin, not at the figure
     (review + verifier, 2026-09-07). Without them the line reads as a margin
     note beside the plate, which is what it is. */
  .hero-callout-lead { display: none; }
  /* The quip becomes the caption ABOVE the shirt photograph, aligned to its
     left edge, with the same brick bullet the phone layout uses. Floating at
     25% of the art with a leader into blank margin it read as a stranded
     fragment (review 2026-09-07). */
  .hero-callout {
    top: auto; bottom: calc(3% + var(--garment-w) + 14px + 18px);
    width: var(--garment-w); right: calc(100% + 32px);
    text-align: left; padding-left: 16px;
  }
  .hero-callout-dot { right: auto; left: 0; top: .5em; width: 8px; height: 8px; }
  .h1-br { display: none; }   /* let text-wrap: balance set the three lines */
}
@media (min-width: 1700px) {
  .hero-wrap { width: min(100% - 48px, 1560px); }
}

/* JS-off / data-failed: an honest message where the plate would be, never an
   empty 742px box with a caption pointing at it. */
.hero-noscript { font-size: 16px; line-height: 1.5; max-width: 40ch; margin: 0 0 12px; }

/* ---------- multi-page (2026-09-07): home / plates / about ---------- */

.page-title {
  /* between the hero h1 and h2; see the ramp note above */
  font-size: clamp(30px, 3.5vw, 50px); line-height: 1.04; letter-spacing: -.018em;
}

/* Home: two doors under the finder, not the whole house. */
.teaser { padding-block: clamp(40px, 6vw, 72px); border-top: 1px solid var(--rule); }
.teaser-inner {
  display: grid; grid-template-columns: 1fr 1fr; gap: clamp(26px, 5vw, 72px);
  align-items: start;
}
.teaser h2 { font-size: clamp(22px, 2.4vw, 28px); margin: 0 0 8px; }
.teaser p { max-width: 46ch; margin: 0 0 14px; }
@media (max-width: 700px) { .teaser-inner { grid-template-columns: 1fr; } }

/* Finder tiles are links now (they lead to /plates/?c=...). */
a.find { text-decoration: none; color: inherit; }

/* Card colour dots are controls again: the tapped colour's render replaces the
   plate on the card. Printify renders sit on a white ground, so the tile goes
   white while a colour is pinned, and the hover swap stands down. */
.card-swatches { margin: 2px 0 12px; }
.card-swatches .sw-name { flex-basis: auto; margin-left: 8px; min-height: 0; }
.plate-figure.pinned { background: #FFFFFF; }
.plate-figure.pinned .plate-art { object-position: 50% 50%; }

/* /plates/ renders its chips and cards after products.json arrives. Reserve the
   room up front so the footer is not in the first screen for a frame and then
   shoved down (CLS 0.55 on a phone before this). */
.filters:empty { min-height: 58px; }
.result-count:empty { min-height: 1.75em; }
.grid:empty { min-height: 120vh; }

/* One line under the card's buy pair: the funnel audit found four different
   labels for one action and no statement anywhere on a card that the button
   leaves the site. */
.buy-fine {
  margin: 8px 0 0; font-family: var(--mono); font-size: 11px;
  letter-spacing: .07em; text-transform: uppercase; color: var(--ink-soft);
}

/* The hero artwork is a link. It sits over the plate only -- the callout, the
   garment photo and the caption keep their own behaviour. */
.hero-art-link {
  position: absolute; inset: 0; z-index: 1;
  border-radius: var(--r-sm);
}
.hero-art-link:focus-visible { outline: 2px solid var(--brick); outline-offset: 4px; }
/* z-index ONLY. An earlier version of this rule also set position:relative,
   which overrode the absolute positioning the tablet rail and the desktop hero
   depend on and pushed the callout 992px past the right edge at 768. */
.hero-garment, .hero-callout, .hero-cap { z-index: 2; }

/* Closing bands: neither the catalogue nor the field notes may dead-end. */
.about-cta, .grid-end { text-align: center; }
.about-cta .section-note, .grid-end .section-note { margin-inline: auto; }
.about-cta .btn, .grid-end .btn { margin-top: 22px; }
.band-navy .btn-primary {
  background: var(--cream); color: var(--navy); border-color: var(--cream);
}
.band-navy .btn-primary:hover { background: var(--brick); border-color: var(--brick); color: var(--cream); }

/* The finder moved onto the navy board; its inks were still the paper set,
   which put the meta line at 1.38:1 (measured by the gate, mobile+desktop). */
.finder-sec .find { color: var(--on-navy); }
.finder-sec .find-meta { color: var(--on-navy-soft); }
.finder-sec .find { border-top-color: var(--rule-on-navy); }
.finder-sec .find:hover { background: rgba(242,232,207,.07); }
.finder-sec .find-thumb { border-color: var(--rule-on-navy); background: var(--paper-deep); }
.finder-sec .find:hover .find-label { color: var(--cream); }

/* text-wrap: pretty is baseline in every current browser and is exactly the
   tool for this: the typography audit counted 73 paragraph widows and 130
   lines ending on a stopword. Browsers without it simply ignore it. */
p, li, dd, figcaption, .lede, .section-note, blockquote { text-wrap: pretty; }
h2, .page-title, .plate-name, .pp-name, #modalName { text-wrap: balance; }

/* The label idiom, collapsed onto the three tokens above. */
.eyebrow, .about-card-label { font-size: var(--label-sm); letter-spacing: var(--track-label-wide); }
.related-label, .note-no, .result-count, .plate-no { font-size: var(--label); letter-spacing: var(--track-label); }

/* Measure. The lede was capped in `ch`, which measures Spectral's zero and
   under-counts real lowercase; 34ch rendered at 38 characters per line. */
.lede { max-width: 46ch; }
.about-text p { max-width: 66ch; }


/* ==========================================================================
   FORCED COLORS (Windows High Contrast)
   The garment swatches and the card colour previews are the one place where
   colour IS the content, not decoration: forced-colors was replacing sixteen
   different shirts with sixteen identical empty circles. Everything else keeps
   the system colours, which is the point of the mode.
   ========================================================================== */
@media (forced-colors: active) {
  .sw::before, .pdot { forced-color-adjust: none; }
  .sw-reset::after { forced-color-adjust: none; background: ButtonText; }
  /* Selected state survives only as a background here, which the mode drops. */
  .chip[aria-pressed="true"],
  .sw[aria-pressed="true"]::before { outline: 2px solid ButtonText; outline-offset: 1px; }
  /* Primary and secondary actions were rendering identically. */
  .btn-primary, .buy-primary { border: 2px solid ButtonText; }
  .modal-panel, .pp-sticky { border: 1px solid ButtonText; }
  .modal-backdrop { background: Canvas; }
}

/* ==========================================================================
   PRINT
   A person printing a plate page is a real, small case. A person printing the
   catalogue is not. Default browser settings drop backgrounds, which turned
   the primary CTA into cream text on white and stamped the fixed buy bar onto
   all five sheets.
   ========================================================================== */
@media print {
  .announce, .site-head, .pp-sticky, .filters, .controls, .show-more-wrap,
  .pp-turn, .pp-guide-line, .ld-more, .site-foot nav, .modal, .lightbox,
  .pp-video, .finder-sec, .teaser, .grid-end, .about-cta { display: none !important; }

  html, body { background: #fff; color: #000; }
  .band-navy, .about, .pp-garment, .assurances,
  .band-navy h2, .about h2, .pp-band-h { background: #fff !important; color: #000 !important; }
  .buy, .btn, .buy-primary, .btn-primary {
    color: #000 !important; background: #fff !important; border: 1px solid #000 !important;
  }
  a[href^="http"]::after { content: " (" attr(href) ")"; font-size: 10px; word-break: break-all; }
  .plate-figure, .pp-plate { break-inside: avoid; }
  img { max-width: 100% !important; }
}

/* Hover effects belong to devices that can hover. Without this every :hover
   sticks after a tap on a touch screen. */
@media (hover: none) {
  .plate:hover .plate-figure, .plate:focus-within .plate-figure { transform: none; box-shadow: none; }
  .find:hover { background: none; padding-left: 0; }
}

/* A 125-card catalogue is ~380 tab stops; give a keyboard user a way past it. */
.skip-grid {
  position: absolute; left: -9999px; top: auto;
  background: var(--navy); color: var(--cream); padding: 12px 18px; z-index: 100;
  font-family: var(--mono); font-size: var(--label); text-decoration: none;
}
.skip-grid:focus { left: 0; position: static; display: inline-block; margin: 8px 0; }

/* A browser cannot show which of 125 plates has already been opened without
   a visited style, and these are all real links. */
.plate-name a:visited, .rel:visited span, .pp-rel:visited .pp-rel-name { color: var(--brick-deep); }
/* The finder tiles are EXEMPT from visited-red. On the navy band a visited
   tile went brick while its neighbours stayed cream, which read as two random
   tiles being "on sale" or broken (Jason, 2026-09-13). Visited memory still
   applies to plate names and related plates, where it sits on paper. */

/* ==========================================================================
   THE PHONE FLOOR, RE-ASSERTED LAST.
   Nothing informational under 12px on a handset. This block has to sit after
   the label tokens above: a media query adds no specificity, so a later rule
   with the same weight wins regardless of the query, and the tokens were
   quietly dropping .about-card-label to 10.5 and .buy-fine to 11 on mobile.
   ========================================================================== */
@media (max-width: 620px) {
  .eyebrow, .about-card-label, .related-label, .note-no, .result-count,
  .plate-no, .buy-fine, .rel-name, .sw-garment, .modal-fine, .foot-note,
  .foot-legal p, .foot-privacy, .plate-badge, .subfilters-label,
  .conventions span, .lightbox-hint, .hero-cap, .chip { font-size: 12px; }
}

/* The filter row held 622px of chips in 358px of phone. Three of the six were
   cut or invisible, including GROUPS - the largest section at 57 plates - and
   SURPRISE ME. Wrapping shows the whole taxonomy at once, which is what an
   index is for. */
@media (max-width: 620px) {
  .filters, .subfilters { flex-wrap: wrap; overflow-x: visible; }
  .filters.scrollable, .subfilters.scrollable {
    -webkit-mask-image: none; mask-image: none;
  }
}


/* ---------- plate page button, and the plate/film frame (2026-09-12) ----------
   The plate window is a fast browsing surface; the plate PAGE is the selling
   surface, and the route between them was an 11px word. */

.modal-platepage {
  display: block; width: 100%; margin: 10px 0 0;
  text-align: center;
}

/* The film occupies the SAME box as the plate, so switching moves nothing. */
.modal-film-el {
  display: block; width: 100%; height: 100%; max-height: 78vh;
  object-fit: contain; background: var(--navy);
  border-right: 1px solid var(--rule);
}
.modal-film-el[hidden], .modal-figure img[hidden] { display: none !important; }

/* Plate / Film toggle, bottom-left of the frame, opposite ENLARGE. */
.modal-gal {
  position: sticky; float: left; clear: both;
  bottom: 12px; margin: 12px 0 0 12px; z-index: 3;
  display: inline-flex; gap: 2px; padding: 3px;
  border-radius: 20px;
  background: rgba(242, 232, 207, .92);
  border: 1px solid var(--rule);
}
.modal-gal-btn {
  min-height: 32px; padding: 6px 14px; border: 0; border-radius: 18px;
  cursor: pointer; background: transparent; color: var(--ink-soft);
  font-family: var(--mono); font-size: 11.5px; letter-spacing: .1em;
  text-transform: uppercase;
}
.modal-gal-btn.is-on { background: var(--navy); color: var(--cream); }
/* The play affordance, on the chip rather than over the specimen. */
.modal-gal-btn { display: inline-flex; align-items: center; gap: 7px; }
.modal-gal-tri {
  width: 0; height: 0;
  border-top: 5px solid transparent;
  border-bottom: 5px solid transparent;
  border-left: 8px solid currentColor;
}
.modal-gal-btn:focus-visible { outline: 2px solid var(--brick); outline-offset: 1px; }

@media (max-width: 760px) {
  /* On a phone the frame is short; keep the control inside the tap-safe area
     and let the two chips stay 44px tall. */
  .modal-gal-btn { min-height: 40px; padding: 10px 16px; }
}


/* ---------- the plate-page route, on the tile (2026-09-12) ----------
   Quiet next to the two buy buttons on purpose: the buy is still the loud
   control, and this is the "tell me more" beside it. Full width so it reads as
   a control rather than a stray word, and a 44px tap target on a phone. */
.plate-page-link {
  display: block;
  margin: 8px 0 0;
  padding: 11px 10px;
  min-height: 44px;
  border: 1px solid var(--rule);
  border-radius: 2px;
  text-align: center;
  text-decoration: none;
  font-family: var(--mono);
  font-size: 12px;
  letter-spacing: .1em;
  text-transform: uppercase;
  color: var(--ink-soft);
  background: transparent;
}
.plate-page-link:hover {
  border-color: var(--navy);
  color: var(--navy);
  background: var(--paper-warm);
}
.plate-page-link:focus-visible {
  outline: 2px solid var(--brick);
  outline-offset: 2px;
}


/* ---------- Film chip on a test tile (2026-09-13) ----------
   Only tiles in app.js DIRECT_TO_PAGE carry it. Lands on /p/<slug>/#film,
   where plate-film.js opens the film on arrival. */
/* Shares a row with the plate-page link so a test tile stays the same height
   as its neighbours (an extra row pushed its buy buttons out of line). */
.plate-page-row { display: flex; gap: 8px; margin: 8px 0 0; }
.plate-page-row .plate-page-link { flex: 1 1 auto; margin: 0; }
/* The film in the tile's own frame. Absolute over the figure's aspect-ratio
   box, so swapping it in changes nothing about the tile's size. */
.plate-film-el {
  position: absolute; inset: 0; width: 100%; height: 100%;
  display: block; object-fit: contain; background: var(--navy); z-index: 2;
}
.plate-film-el[hidden], .plate-art[hidden], .plate-open[hidden] { display: none !important; }
.plate-film-link {
  display: inline-flex; align-items: center; justify-content: center; gap: 7px;
  flex: 0 0 auto; margin: 0; padding: 11px 16px; min-height: 44px;
  appearance: none; cursor: pointer;
  border: 1px solid var(--rule); border-radius: 2px;
  font-family: var(--mono); font-size: 11.5px; letter-spacing: .1em;
  text-transform: uppercase; color: var(--ink-soft); text-decoration: none;
  background: transparent;
}
.plate-film-link:hover { border-color: var(--navy); color: var(--navy); background: var(--paper-warm); }
.plate-film-link[aria-pressed="true"] { border-color: var(--navy); color: var(--cream); background: var(--navy); }
/* Pressed = the film is playing and the chip now offers the plate back, so
   the play triangle becomes a stop square. */
.plate-film-link[aria-pressed="true"] .plate-film-tri { border: 0; width: 9px; height: 9px; background: currentColor; }
.plate-film-link:focus-visible { outline: 2px solid var(--brick); outline-offset: 2px; }
.plate-film-tri {
  width: 0; height: 0;
  border-top: 5px solid transparent; border-bottom: 5px solid transparent;
  border-left: 8px solid currentColor;
}
@media (max-width: 760px) { .plate-film-link { min-height: 44px; } }


/* ---------- catalogue after the plate window (2026-09-13) ---------- */
.plate-film-link[hidden] { display: none !important; }

/* /plates/#<slug> marks the plate it scrolled to: a brick outline that
   fades, so the eye finds the tile without it staying decorated. */
.plate.plate-flash .plate-figure { animation: plate-flash 2.2s ease-out; }
@keyframes plate-flash {
  0%, 35% { box-shadow: 0 0 0 3px var(--brick); }
  100%    { box-shadow: 0 0 0 3px rgba(176, 57, 42, 0); }
}
@media (prefers-reduced-motion: reduce) {
  .plate.plate-flash .plate-figure { animation: none; box-shadow: 0 0 0 3px var(--brick); }
}
