/*
 * Franklin and the Duck — design tokens and shop.
 *
 * Tokens are BRAND.md §2. Type is Anton for display, Inter for everything else,
 * both self-hosted WOFF2 subsets (§1) — hotlinking Google Fonts is forbidden by
 * the brand rules and blocked by the CSP.
 *
 * There is no inline style anywhere on this site: the CloudFront CSP sets
 * default-src 'self' with no unsafe-inline, so an inline <style> is blocked in
 * the browser, silently as far as the build is concerned.
 */

/* ---------------------------------------------------------------- type */

@font-face {
  font-family: "Anton";
  src: url("/assets/fonts/anton-400.woff2") format("woff2");
  font-weight: 400;
  font-style: normal;
  font-display: swap;
}
@font-face {
  font-family: "Inter";
  src: url("/assets/fonts/inter-400.woff2") format("woff2");
  font-weight: 400;
  font-style: normal;
  font-display: swap;
}
@font-face {
  font-family: "Inter";
  src: url("/assets/fonts/inter-600.woff2") format("woff2");
  font-weight: 600;
  font-style: normal;
  font-display: swap;
}

/* -------------------------------------------------------------- tokens */

:root {
  --ink: #141414;
  --paper: #f4f1ea;
  --rule: #d5cfc2;
  --muted: #6b6558;
  --sunk: #ebe7dd;

  --franklin-1: #8c1d1d;
  --franklin-2: #c9a227;
  --cookie-2: #e8e4da;
  --abby-1: #e9b44c;
  --daisy-1: #d9a441;
  /* Millie is dusk: indigo and cool grey, blue-toned deliberately so she never
     reads as a Cookie variant despite both being dark — BRAND.md §3. */
  --millie-1: #2e3a4f;
  --millie-2: #8a94a3;

  --ok: #2f6b43;
  --warn: #8c1d1d;

  /* Roster rules — the colourway mark on each character card.
     Three reuse the theme-aware tokens above. Abby's daylight gold (#E9B44C)
     and Daisy's golden hour (#D9A441) do NOT: on the warm paper ground they
     land at 1.68:1 and 1.99:1, which is a 3px rule nobody can see. Abby's is
     darkened enough to clear 3:1 and stay gold; Daisy takes her second colour,
     dusty rose, which passes untouched — and the two reading as one gold was
     already a problem, so splitting them helps twice. Both clear 3:1 on the
     dark ground too, so neither needs an override. */
  --rule-franklin: var(--franklin-1);
  --rule-cookie: var(--ink);
  --rule-abby: #ac8538;
  --rule-daisy: #b5705a;
  --rule-millie: var(--millie-1);

  --measure: 34rem;
  --gutter: 1.25rem;
  --wide: 76rem;

  --display: "Anton", "Arial Narrow", system-ui, sans-serif;
  --body: "Inter", system-ui, -apple-system, "Segoe UI", sans-serif;

  color-scheme: light dark;
}

/* Three states, not two: an explicit choice stamps data-theme, and the default
   "system" setting stamps nothing at all. */
@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) {
    --ink: #ece8df;
    --paper: #16150f;
    --rule: #3b382f;
    --muted: #9c968a;
    --sunk: #201e17;
    --franklin-1: #d96a6a;
    --abby-1: #e0bd4a;
    /* Dusk indigo disappears on a dark ground — lift it, keep it blue. */
    --millie-1: #93a4c0;
    --ok: #6fbf8a;
    --warn: #e58b8b;
  }
}
:root[data-theme="dark"] {
  --ink: #ece8df;
  --paper: #16150f;
  --rule: #3b382f;
  --muted: #9c968a;
  --sunk: #201e17;
  --franklin-1: #d96a6a;
  --abby-1: #e0bd4a;
  --millie-1: #93a4c0;
  --ok: #6fbf8a;
  --warn: #e58b8b;
  /* Tells the BROWSER which way this page went, so the things we do not paint
     — scrollbars, the focus ring's default, form control chrome — follow the
     choice instead of staying on whatever the OS says. */
  color-scheme: dark;
}
/* The light palette is already on bare :root and the dark media query is
   guarded `:not([data-theme="light"])`, so choosing light needs no tokens of
   its own — only the same instruction to the browser chrome. */
:root[data-theme="light"] { color-scheme: light; }

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

body {
  margin: 0;
  background: var(--paper);
  color: var(--ink);
  font-family: var(--body);
  font-size: 1rem;
  line-height: 1.6;
  -webkit-font-smoothing: antialiased;
  display: flex;
  flex-direction: column;
  min-height: 100vh;
}

main { flex: 1; }

a { color: inherit; }

img { max-width: 100%; height: auto; }

:focus-visible {
  outline: 2px solid var(--ink);
  outline-offset: 2px;
}

@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: .01ms !important;
    transition-duration: .01ms !important;
  }
}

/* ------------------------------------------------------------- layout */

/* Available to a screen reader, invisible on screen. Not display:none, which
   removes it from the accessibility tree along with everything else. */
.sr-only {
  position: absolute;
  width: 1px;
  height: 1px;
  margin: -1px;
  padding: 0;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
  border: 0;
}

/* Off-screen until focused. `top` rather than `display:none`, which would take
   it out of the tab order and defeat the point. */
.skip {
  position: absolute;
  left: var(--gutter);
  top: -4rem;
  z-index: 10;
  padding: .6rem 1rem;
  background: var(--ink);
  color: var(--paper);
  font-size: .8125rem;
  font-weight: 600;
  text-decoration: none;
  transition: top .12s ease;
}
.skip:focus-visible { top: .5rem; }


.wrap {
  width: 100%;
  max-width: var(--wide);
  margin-inline: auto;
  padding-inline: var(--gutter);
}

.masthead {
  border-bottom: 1px solid var(--rule);
}
.masthead .wrap {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 1rem;
  padding-block: 1rem;
  flex-wrap: wrap;
}
.brand {
  font-family: var(--display);
  font-size: 1.25rem;
  letter-spacing: .02em;
  text-transform: uppercase;
  text-decoration: none;
}
.brand .amp {
  font-size: .62em;
  letter-spacing: .3em;
  color: var(--franklin-1);
  padding-inline: .35em;
}
.nav {
  display: flex;
  gap: 1.25rem;
  font-size: .75rem;
  font-weight: 600;
  letter-spacing: .22em;
  text-transform: uppercase;
}
.nav a { text-decoration: none; color: var(--muted); }
.nav a:hover, .nav a[aria-current] { color: var(--ink); }

/* Holds the nav and the theme button together, so the masthead's
   space-between still resolves to brand-left / everything-else-right rather
   than spreading three items across the width. */
.masthead-end {
  display: flex;
  align-items: center;
  gap: 1.25rem;
  flex-wrap: wrap;
  /* On a narrow screen the nav fills the row and the button wraps below it.
     Right-aligning the wrapped line keeps it tucked under Cart instead of
     stranding a lone icon against the left margin. */
  justify-content: flex-end;
}

/* ------------------------------------------------------- theme control

   Hidden until theme.js has run. The button cycles system -> light -> dark
   and does nothing at all without JS, and a dead control is worse than no
   control. Because that script is render-blocking in <head>, this resolves
   before the first paint — the button does not flicker into existence. */
.theme-toggle { display: none; }
:root[data-js="on"] .theme-toggle {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 1.9rem;
  height: 1.9rem;
  padding: 0;
  background: none;
  border: 1px solid transparent;
  border-radius: 50%;
  /* Matches the nav links either side of it, including on hover. */
  color: var(--muted);
  cursor: pointer;
}
.theme-toggle:hover { color: var(--ink); border-color: var(--rule); }
.theme-toggle:focus-visible {
  outline: 2px solid var(--ink);
  outline-offset: 2px;
  color: var(--ink);
}

/* One icon per state, selected by the attribute on <html>. Driving this from
   CSS rather than from the script is what lets the correct icon appear in the
   first paint: by the time the button is parsed, the attribute is already set. */
.theme-toggle .i { width: 1rem; height: 1rem; display: none; }
:root:not([data-theme]) .theme-toggle .i-system,
:root[data-theme="light"] .theme-toggle .i-light,
:root[data-theme="dark"] .theme-toggle .i-dark { display: block; }

.foot {
  margin-top: 4rem;
  border-top: 1px solid var(--rule);
  font-size: .75rem;
  color: var(--muted);
}
.foot .wrap { padding-block: 1.5rem; }
.foot p { margin: 0 0 .4rem; }

/* --------------------------------------------------------------- type */

.eyebrow {
  margin: 0 0 .5rem;
  font-size: .6875rem;
  font-weight: 600;
  letter-spacing: .3em;
  text-transform: uppercase;
  color: var(--muted);
}
h1, h2, h3 { text-wrap: balance; }
.display {
  font-family: var(--display);
  font-weight: 400;
  text-transform: uppercase;
  line-height: .95;
  letter-spacing: .01em;
  margin: 0;
}
.lede { max-width: var(--measure); font-size: 1.0625rem; }

/* ----------------------------------------------------------- home */
/*
 * The holding page. `.hold`, `.wordmark`, `.amp` and `.says` had no rules at
 * all, so the one page carrying the five catchphrases was rendering them as a
 * default bulleted list under an unstyled wordmark.
 *
 * The roster is five and the layout has to not care how many — no fixed columns,
 * no 2×2. Millie joined on 2026-10-02 and a sixth would break a grid built for
 * four. See docs/CONTENT-MODEL.md.
 */

.hold {
  max-width: var(--wide);
  margin: 0 auto;
  padding: clamp(3rem, 10vw, 7rem) var(--gutter) clamp(4rem, 12vw, 8rem);
}

.wordmark {
  font-family: var(--display);
  font-weight: 400;
  text-transform: uppercase;
  /* Anton is condensed and very heavy — BRAND.md §1. It wants tight leading. */
  line-height: .88;
  letter-spacing: .005em;
  font-size: clamp(3rem, 15vw, 8.5rem);
  margin: 0 0 1.75rem;
  /* The wordmark reads as one lockup: FRANKLIN / and the / DUCK stacked. */
  display: flex;
  flex-direction: column;
}

/* "and the" is the quiet connector, not a third word of equal weight. */
.wordmark .amp {
  font-family: var(--body);
  font-weight: 600;
  text-transform: lowercase;
  font-size: clamp(.75rem, 2.2vw, 1.0625rem);
  letter-spacing: .34em;
  line-height: 1;
  color: var(--muted);
  padding: .55em 0 .5em .18em;
}

.hold .lede {
  margin: 0 0 clamp(2.5rem, 7vw, 4rem);
  font-size: clamp(1.0625rem, 2.2vw, 1.25rem);
}

/*
 * The five catchphrases, as a roster.
 *
 * The attributions are what turn five unrelated lines into one joke — one
 * exhausted human catchphrase per animal — which is also why the shirt carries
 * them. Each phrase and its attribution stay on one row and wrap together.
 */
.says {
  list-style: none;
  margin: 0;
  padding: 0;
  border-top: 2px solid var(--rule);
  max-width: 44rem;
}

/* The rule stays on the <li>; the layout moved to the <a> inside it.

   Each row links to that character's page, and the link has to BE the flex
   container rather than sit inside one — otherwise the portrait, the phrase
   and the name collapse into a single flex item and the row loses its
   layout. Putting the padding on the link too means the whole band is
   clickable, not just the glyphs. */
.says li { border-bottom: 1px solid var(--rule); }

.says li a {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: .25rem .9rem;
  padding: clamp(.7rem, 2vw, 1rem) 0;
  text-decoration: none;
  color: inherit;
}

/* The phrase is the thing you are pointing at, so it is the thing that
   responds. The name is already muted and shifting it too would make the
   whole row flicker on hover. */
.says li a:hover .said { color: var(--franklin-1); }
.says li a:hover .who { color: var(--ink); }
.says li a:focus-visible {
  outline: 2px solid var(--ink);
  outline-offset: -2px;
}

/*
 * The portrait on each line, about half the size it runs at on /characters/.
 *
 * `align-self: center` rather than changing the row to `align-items: center`:
 * the phrase and the attribution are typographic siblings and their baselines
 * should stay locked to each other. Only the picture opts out.
 *
 * `flex: 0 0 auto` so it never squashes — a flex item with an intrinsic aspect
 * ratio will otherwise shrink to make room for a long phrase, and a squashed
 * circle reads as broken rather than small.
 */
.says-face {
  flex: 0 0 auto;
  align-self: center;
  width: clamp(2.75rem, 9vw, 6rem);
  height: auto;
  aspect-ratio: 1;
  border-radius: 50%;
  /* The same hairline as the roster medallions: the cream ground is baked
     into the file, so on the light theme the disc and the page are the same
     colour and the ring is the only thing defining the shape. */
  border: 1px solid rgba(20, 20, 20, .12);
}

.says .said {
  font-family: var(--display);
  font-weight: 400;
  text-transform: uppercase;
  letter-spacing: .01em;
  line-height: 1.05;
  font-size: clamp(1.25rem, 4.6vw, 2rem);
}

.says .who {
  font-size: .6875rem;
  font-weight: 600;
  letter-spacing: .3em;   /* BRAND.md §1: small caps labels run .26em–.42em */
  text-transform: uppercase;
  color: var(--muted);
  /* Pushed right on a wide row, tucked under the phrase on a narrow one. */
  margin-left: auto;
  padding-left: .3em;     /* letter-spacing trails the last letter */
}

@media (max-width: 26rem) {
  .says .who { margin-left: 0; }
}

/* ---------------------------------------------------- the house (roster) */
/*
 * These are the pages the site exists for, so they get the reading treatment
 * rather than the catalogue one: a real measure, generous leading, and nothing
 * competing with the prose.
 *
 * Nothing here assumes five characters. The roster is a wrapping grid and the
 * relationship list is derived from data, so a sixth animal needs no CSS.
 */

/*
 * Five across, hung in a shallow U.
 *
 * ---------------------------------------------------------------- the orphan
 *
 * `auto-fill, minmax(15rem, 1fr)` fitted FOUR cards at the page's 76rem, which
 * left Millie alone on a second row — and Millie last is the joke (BRAND.md
 * §5: the stack builds through four exasperated lines and lands on the one
 * that isn't). A lone fifth card reads as a layout accident, not a punchline.
 *
 * Five is prime, so the column count has to be chosen rather than fitted:
 * only 5, 3 and 1 divide the roster without stranding anyone. FOUR and TWO
 * are deliberately skipped — both leave exactly one card by itself.
 *
 * ---------------------------------------------------------------- the curve
 *
 * The offsets are margins, not transforms, so the grid row actually grows to
 * hold them; a translated card would hang out of the section and collide with
 * whatever follows.
 *
 * Guarded by `:has(> :nth-child(5):last-child)` — the curve is hand-tuned for
 * exactly five. A sixth character gets the plain grid instead of a lopsided
 * arc, which keeps the promise the template comment makes about adding one.
 */
.roster {
  list-style: none;
  margin: 0 0 3rem;
  padding: 0;
  display: grid;
  grid-template-columns: 1fr;
  gap: 1.75rem 1.5rem;
}

/* Three before five: 3 + 2 still puts Millie beside Daisy, not alone. */
@media (min-width: 44rem) {
  .roster { grid-template-columns: repeat(3, 1fr); }
}

@media (min-width: 68rem) {
  .roster {
    grid-template-columns: repeat(5, 1fr);
    gap: 1.75rem 1.25rem;
    align-items: start;
  }

  .roster:has(> :nth-child(5):last-child) > :nth-child(2),
  .roster:has(> :nth-child(5):last-child) > :nth-child(4) {
    margin-top: 2.75rem;
  }
  .roster:has(> :nth-child(5):last-child) > :nth-child(3) {
    margin-top: 4.5rem;
  }
}

/* Wide but short — a landscape tablet, or a laptop with a lot of browser
   chrome. The curve spends 4.5rem of vertical space on decoration, which is
   the one thing a short viewport has none of, so it flattens back to a row. */
@media (min-width: 68rem) and (max-height: 34rem) {
  .roster:has(> :nth-child(5):last-child) > * { margin-top: 0; }
}

.who-card a {
  display: block;
  text-decoration: none;
  color: inherit;
}
.who-card a:hover h2 { text-decoration: underline; text-underline-offset: .18em; }

/*
 * The colourway rule above each name (BRAND.md §2).
 *
 * It began as a stand-in for the missing portraits. The portraits arrived
 * 2026-10-09 and it earns its place better now than it did then: five ink
 * drawings are tonally very similar at a glance, so the rule is what lets
 * somebody find one card at speed.
 *
 * Still a rule rather than a filled block. Cookie is black and Millie is dusk
 * indigo; five saturated panels would fight both the type and the drawings.
 */
.who-card {
  border-top: 3px solid var(--rule);
  padding-top: 1rem;
}
.who--franklin { border-top-color: var(--rule-franklin); }
.who--cookie   { border-top-color: var(--rule-cookie); }
.who--abby     { border-top-color: var(--rule-abby); }
.who--daisy    { border-top-color: var(--rule-daisy); }
.who--millie   { border-top-color: var(--rule-millie); }

.who-pitch {
  margin: .75rem 0 0;
  font-size: .875rem;
  line-height: 1.55;
  color: var(--muted);
}

.who-card h2 {
  font-family: var(--display);
  font-weight: 400;
  text-transform: uppercase;
  letter-spacing: .01em;
  font-size: 1.5rem;
  line-height: 1;
  margin: .9rem 0 .3rem;
}

.who-epithet {
  margin: 0 0 .5rem;
  font-size: .6875rem;
  font-weight: 600;
  letter-spacing: .24em;
  text-transform: uppercase;
  color: var(--muted);
}
.who-epithet--lg { font-size: .75rem; margin-bottom: 1rem; }

.who-says {
  margin: 0;
  font-size: .9375rem;
  color: var(--muted);
}

/* ------------------------------------------------- the house (one page) */

.who { padding-bottom: 2rem; }

/*
 * Two columns once there is room for them: the text carries the page, the
 * portrait sits beside it. Below 56rem it stacks, portrait first, because on a
 * phone the drawing is the thing worth leading with.
 */
.who-head {
  max-width: 48rem;
  padding-block: 2.5rem 1rem;
  display: grid;
  gap: 1.5rem 2.5rem;
  align-items: start;
}

@media (min-width: 56rem) {
  .who-head {
    max-width: 64rem;
    grid-template-columns: minmax(0, 1fr) 20rem;
  }
}

/*
 * The portrait as a medallion.
 *
 * The cream is baked into the file — see scripts/build-characters.mjs. On the
 * light theme it is the page colour, so the ring is what defines the shape;
 * on the dark theme the same file reads as a cream disc. Inverting it instead
 * would turn Cookie from a tuxedo cat into a cream one, which is why the
 * ground does not follow the theme.
 *
 * `border-radius: 50%` does the crop, and the border follows it — so one
 * declaration gives both the circle and its ring, and going back to squares
 * is a one-line revert.
 *
 * The art is inset to 87% of its frame by the build so the crop clips nothing:
 * Cookie's ear tips reach past an inscribed circle and a cropped cat ear reads
 * as a bug. See "the inset" in scripts/build-characters.mjs.
 */
.who-plate,
.who-portrait {
  display: block;
  width: 100%;
  height: auto;
  aspect-ratio: 1;
  border-radius: 50%;
  /* A literal, not a token: there is no --line in this palette, and a var()
     whose fallback always wins is a lie about where the value comes from.
     Slightly stronger than the 8% the square used — a ring is a thinner,
     longer line than a box edge and needs a little more to read at all on
     the light theme, where disc and page are the same cream. */
  border: 1px solid rgba(20, 20, 20, .12);
  /* The drawing already has generous margin inside its own frame, so the
     element needs no padding of its own. */
  background: #f4f1ea;
}

.who-plate { order: -1; }

@media (min-width: 56rem) {
  /* Back beside the text once there are two columns. */
  .who-plate { order: 0; }
}

.who-portrait { margin-bottom: .9rem; }

/* The name sits tight under the portrait; the rule above the card already
   provides the separation that margin was doing. */
.who-card .who-portrait + h2 { margin-top: 0; }

.who-head h1 { font-size: clamp(2.25rem, 8vw, 4rem); margin: 0 0 .35rem; }

/* The family's phrase for them — the most valuable copy in the brand. */
.who-callout {
  margin: 1.5rem 0 0;
  padding: 1rem 1.25rem;
  background: var(--sunk);
  border-left: 3px solid var(--franklin-1);
  display: grid;
  gap: .3rem;
}
.who-callout span {
  font-size: .625rem;
  font-weight: 600;
  letter-spacing: .3em;
  text-transform: uppercase;
  color: var(--muted);
}
.who-callout strong {
  font-family: var(--display);
  font-weight: 400;
  text-transform: uppercase;
  font-size: clamp(1.25rem, 4vw, 1.75rem);
  line-height: 1.05;
}

.who-section {
  padding-block: 2.5rem;
  border-top: 1px solid var(--rule);
}
.who-section > h2 {
  font-family: var(--display);
  font-weight: 400;
  text-transform: uppercase;
  letter-spacing: .02em;
  font-size: clamp(1.25rem, 3.5vw, 1.75rem);
  margin: 0 0 1.25rem;
}

/* The story is the reason the page exists — give it a real measure. */
.who-section p { max-width: var(--measure); }
.who-section > p,
.who-section h3 + p { font-size: 1.0625rem; }
.who-section h3 {
  font-size: 1rem;
  margin: 2rem 0 .5rem;
}
.story-close {
  font-size: 1.125rem;
  margin-bottom: .4rem;
}

@media (min-width: 52rem) {
  .who-section--split { display: grid; grid-template-columns: 1fr 1fr; gap: 3rem; }
  .who-section--split > div > h2 { margin-top: 0; }
}
.who-section--split h2 {
  font-family: var(--display);
  font-weight: 400;
  text-transform: uppercase;
  font-size: 1.25rem;
  margin: 0 0 1rem;
}

.engine dt,
.vitals dt,
.gags dt {
  font-size: .6875rem;
  font-weight: 600;
  letter-spacing: .18em;
  text-transform: uppercase;
  color: var(--muted);
  margin-top: 1rem;
}
.engine dd,
.vitals dd,
.gags dd {
  margin: .25rem 0 0;
  max-width: var(--measure);
}
.engine dd { font-size: 1.0625rem; }
.engine, .vitals, .gags { margin: 0; }

.pillars {
  list-style: none;
  margin: 0;
  padding: 0;
  display: grid;
  gap: 1.5rem;
  counter-reset: pillar;
}
@media (min-width: 52rem) {
  .pillars { grid-template-columns: 1fr 1fr; gap: 1.75rem 3rem; }
}
.pillars li { counter-increment: pillar; }
.pillars h3 {
  margin: 0 0 .35rem;
  font-size: 1rem;
}
/* Numbered because the bibles number them — it is a defined set, not a list
   of examples, and the order is the author's. */
.pillars h3::before {
  content: counter(pillar) ". ";
  color: var(--muted);
  font-variant-numeric: tabular-nums;
}
.pillars p { margin: 0; }

.bonds { display: grid; gap: 1.75rem; }
@media (min-width: 52rem) {
  .bonds { grid-template-columns: 1fr 1fr; gap: 2rem 3rem; }
}
.bond h3 { margin: 0 0 .4rem; font-size: 1rem; }
.bond p { margin: 0; }

.lines {
  list-style: none;
  margin: 0;
  padding: 0;
  columns: 2 16rem;
  column-gap: 2.5rem;
}
.lines li {
  break-inside: avoid;
  padding: .55rem 0;
  border-bottom: 1px solid var(--rule);
  font-size: .9375rem;
}
.lines-lead { font-weight: 600; }

.who-next { border-top: 1px solid var(--rule); padding-top: 2rem; }
.who-next h2 {
  font-size: .6875rem;
  font-weight: 600;
  letter-spacing: .3em;
  text-transform: uppercase;
  color: var(--muted);
  margin: 0 0 1rem;
}
.who-next ul {
  list-style: none;
  margin: 0;
  padding: 0;
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(11rem, 1fr));
  gap: 1rem;
}
.who-next a {
  display: grid;
  gap: .2rem;
  text-decoration: none;
  padding: .75rem 0;
  border-top: 2px solid var(--rule);
}
.who-next a:hover { border-top-color: var(--ink); }
.who-next strong {
  font-family: var(--display);
  font-weight: 400;
  text-transform: uppercase;
  font-size: 1.0625rem;
  line-height: 1.1;
}
.who-next span {
  font-size: .625rem;
  font-weight: 600;
  letter-spacing: .18em;
  text-transform: uppercase;
  color: var(--muted);
}

/* ------------------------------------------------------ the stories */
/*
 * Long-form prose, which wants a different treatment from the catalogue: one
 * column, a real measure, and nothing in the margins. BRAND.md §6 caps the
 * measure at 68 characters — "the case pages live or die on this".
 */

.collection { padding-block: 0 2.5rem; }
.collection-head { border-top: 2px solid var(--rule); padding-top: 1.25rem; }
.collection-head h2 {
  font-family: var(--display);
  font-weight: 400;
  text-transform: uppercase;
  font-size: clamp(1.5rem, 5vw, 2.25rem);
  line-height: 1;
  margin: 0 0 .5rem;
}
.collection-head h2 a { text-decoration: none; }
.collection-head h2 a:hover { text-decoration: underline; text-underline-offset: .15em; }
.collection-blurb { max-width: var(--measure); margin: 0 0 .5rem; }
.collection-count,
.collection-sub { font-size: .8125rem; color: var(--muted); }

/* Each collection carries its noun and its format — "Capers · The Filing" —
   because five collections with five deliberate formats should not read as one
   undifferentiated pile of stories. */
.collection-meta {
  font-size: .6875rem;
  font-weight: 600;
  letter-spacing: .18em;
  text-transform: uppercase;
  color: var(--muted);
  margin: 0 0 .6rem;
}

/* Not started yet. Quieter, but present — a reader who likes the cat should be
   able to see that her collection is coming. */
.collection--planned .collection-head { opacity: .72; }
.collection--planned h2 { color: var(--muted); }
.collection-sub { border-top: 1px solid var(--rule); padding-top: 1.25rem; margin-top: 2rem; }

/* The release promise. Set as a statement, not a note in the margin. */
.cadence {
  max-width: var(--measure);
  margin: 0 0 2.5rem;
  padding: 1rem 1.25rem;
  background: var(--sunk);
  border-left: 3px solid var(--franklin-1);
  font-size: 1.0625rem;
}
.cadence strong { font-weight: 600; }

.story-list { list-style: none; margin: 1.5rem 0 0; padding: 0; }
.story-list li { border-bottom: 1px solid var(--rule); }
.story-list a {
  display: grid;
  grid-template-columns: auto 1fr auto;
  align-items: baseline;
  gap: .4rem 1rem;
  padding: .9rem 0;
  text-decoration: none;
}
.story-list a:hover .story-title { text-decoration: underline; text-underline-offset: .15em; }

/* Tabular so the numbers line up in a column, which is the point of numbering. */
.story-no {
  font-size: .625rem;
  font-weight: 600;
  letter-spacing: .22em;
  text-transform: uppercase;
  color: var(--muted);
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
}
.story-title {
  font-family: var(--display);
  font-weight: 400;
  text-transform: uppercase;
  letter-spacing: .01em;
  line-height: 1.05;
  font-size: clamp(1.0625rem, 3.2vw, 1.375rem);
}
.story-meta { font-size: .75rem; color: var(--muted); white-space: nowrap; }

@media (max-width: 34rem) {
  /* Three columns do not fit a phone; stack and drop the reading time, which
     is the least useful of the three when space is tight. */
  .story-list a { grid-template-columns: 1fr; gap: .2rem; }
  .story-list .story-meta { display: none; }
}

/*
 * Stories listed under their collection, on /stories/.
 *
 * The whole point of this variant is that a story must not read like a
 * collection. On the index the two sit directly against each other, and when
 * both were Anton uppercase the only difference was size — so the longer line
 * won, and a story title looked like a heading with sub-items. Reported by the
 * owner on launch day, 2026-10-10.
 *
 * So the page now has two voices rather than two sizes of one:
 *
 *   collection   Anton, uppercase, large      — the display face shouts
 *   story        Inter, sentence case, 600    — the body face speaks
 *
 * The label ("Case 001") does the rest of the work. Nothing says "member of a
 * numbered series" like a number, and it was already in the data.
 */
.story-list--index a {
  grid-template-columns: auto 1fr auto;
  align-items: baseline;
}
.story-list--index .story-title {
  font-family: var(--body);
  font-weight: 600;
  text-transform: none;
  letter-spacing: 0;
  line-height: 1.4;
  font-size: 1.0625rem;
  color: var(--ink);
}
/* The label is the anchor of the row, so it keeps a fixed column and tabular
   figures — "Case 001" and "Case 015" must start and end in the same place. */
.story-list--index .story-no {
  /* REM, not em. In em this resolves against .story-no's own 0.625rem, giving
     75px — wide enough for "Case 001" and not for "Field Note 301", so that
     one collection's titles started further right than every other's and the
     column stopped looking like a column. Sized in rem to fit the longest
     label the five collections can produce. */
  min-width: 7.5rem;
  color: var(--muted);
}
.story-list--index a:hover .story-no { color: var(--ink); }

@media (max-width: 34rem) {
  /* Stack on a phone, label above title. The reading time is dropped by the
     shared rule above; the label is NOT — it is the thing that says this is a
     story rather than another collection, which matters more when the two are
     stacked and there is no column to separate them. */
  .story-list--index a { grid-template-columns: 1fr; gap: .15rem; }
  .story-list--index .story-no { min-width: 0; }
}

.upcoming { border-top: 1px solid var(--rule); padding-top: 2rem; margin-top: 2.5rem; }
.upcoming h2 {
  font-size: .6875rem;
  font-weight: 600;
  letter-spacing: .3em;
  text-transform: uppercase;
  color: var(--muted);
  margin: 0 0 1rem;
}
.upcoming-list {
  list-style: none;
  margin: 0;
  padding: 0;
  columns: 2 18rem;
  column-gap: 2.5rem;
  color: var(--muted);
}
.upcoming-list li {
  break-inside: avoid;
  padding: .4rem 0;
  font-size: .9375rem;
}
.upcoming-list .story-no { margin-right: .5rem; }

/* ---- one case ---- */

.story-head {
  padding-block: 3rem 1.5rem;
  max-width: 44rem;
}
.story-head h1 {
  font-size: clamp(2rem, 7vw, 3.5rem);
  margin: .25rem 0 .75rem;
}
.story-head .eyebrow a { color: inherit; }
.story-byline { margin: 0; font-size: .8125rem; color: var(--muted); }

/*
 * The reading column. 34rem is roughly 68 characters at this size — BRAND.md §6.
 * Slightly larger and looser than the rest of the site: these are stories, and
 * the whole proposition is that they are pleasant to read on a phone.
 */
.story-body {
  max-width: 34rem;
  margin-inline: auto;
  padding-inline: var(--gutter);
  padding-bottom: 2rem;
  font-size: 1.125rem;
  line-height: 1.7;
}
.story-body p { margin: 0 0 1.25rem; }

/* The sound effects and the moments of realisation are set in italic and bold
   in the prose; let them have a little more presence than the default. */
.story-body em { font-style: italic; }
.story-body strong { font-weight: 600; }

/* Section heads inside a story — "FRANKLIN", "COOKIE". The Whole House runs
   five accounts of one afternoon and heads each with whose it is. Set small and
   letterspaced rather than large: they are a change of speaker, not a chapter,
   and a heavy heading would interrupt the reading more than the switch does. */
.story-body h2,
.story-body h3 {
  font-size: .6875rem;
  font-weight: 600;
  letter-spacing: .26em;
  text-transform: uppercase;
  color: var(--muted);
  margin: 2.5rem 0 1rem;
}

/* The rule between accounts. Short and centred — a breath, not a divider. */
.story-body hr {
  border: 0;
  border-top: 1px solid var(--rule);
  width: 4rem;
  margin: 2.25rem auto;
}

/* A rule immediately before a section head would double the separation. */
.story-body hr + h2,
.story-body hr + h3 { margin-top: 1.5rem; }

.story-foot {
  max-width: 44rem;
  border-top: 1px solid var(--rule);
  padding-block: 2rem;
}
.story-next h2 {
  font-size: .6875rem;
  font-weight: 600;
  letter-spacing: .3em;
  text-transform: uppercase;
  color: var(--muted);
  margin: 0 0 1rem;
}
.story-next ul { list-style: none; margin: 0; padding: 0; }
.story-next li { border-bottom: 1px solid var(--rule); }
.story-next a {
  display: grid;
  gap: .15rem;
  padding: .75rem 0;
  text-decoration: none;
}
.story-next a:hover .story-title { text-decoration: underline; text-underline-offset: .15em; }
.story-sub { font-size: .8125rem; color: var(--muted); margin: 1.5rem 0 0; }

/* --------------------------------------------------------------- shop */

.page-head { padding-block: 3rem 2rem; }
.page-head h1 { font-size: clamp(2rem, 7vw, 3.25rem); }

.grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(15rem, 1fr));
  gap: 2rem;
  padding-bottom: 3rem;
  list-style: none;
  margin: 0;
  padding-inline: 0;
}

/* ----------------------------------------------------- shop categories

   The shop sells two unrelated kinds of thing, and one undifferentiated grid
   made a postcard pack read as a garment that had lost its sizes.

   The heading is set in the display face at a size BELOW the page title and
   above a product name, so the page reads h1 -> group -> product rather than
   two competing titles. A rule carries the eye across to the blurb, which is
   the quietest thing in the block. */
.shop-group + .shop-group { padding-top: 1rem; }
.shop-group-head {
  display: flex;
  align-items: baseline;
  gap: 1rem;
  flex-wrap: wrap;
  border-bottom: 1px solid var(--rule);
  padding-bottom: .6rem;
  margin-bottom: 1.75rem;
}
.shop-group-name {
  font-family: var(--display);
  font-size: clamp(1.35rem, 3.5vw, 1.9rem);
  letter-spacing: .02em;
  text-transform: uppercase;
  margin: 0;
}
.shop-group-blurb {
  margin: 0;
  color: var(--muted);
  font-size: .8125rem;
  /* Pushed to the far end on a wide row, directly under the heading when the
     row wraps on a phone. */
  margin-left: auto;
}
/* The grid's own bottom padding separated the groups by far too much once
   there were two of them. */
.shop-group .grid { padding-bottom: 2rem; }

/* ------------------------------------------------------------- shop tabs
   Added by shop-tabs.js on top of the stacked sections. The strip ships
   `hidden` and is revealed only when the module runs, so a page without
   JavaScript keeps the stack rather than showing buttons that do nothing. */

/* `display: flex` BEATS the user agent's `[hidden] { display: none }`, which
   is a bare attribute selector — so the strip ships `hidden` and rendered
   anyway, giving a reader without JavaScript a row of buttons that do
   nothing. The same mistake had already been made on `.options`. */
.shop-tabs[hidden] { display: none; }
.shop-tabs {
  display: flex;
  flex-wrap: wrap;
  gap: .4rem;
  margin: 0 0 1.75rem;
  border-bottom: 1px solid var(--rule);
  padding-bottom: .75rem;
}
.shop-tabs button {
  display: inline-flex;
  align-items: baseline;
  gap: .45rem;
  cursor: pointer;
  font-family: var(--display);
  font-size: 1.05rem;
  letter-spacing: .05em;
  text-transform: uppercase;
  padding: .5rem .9rem;
  border: 1px solid var(--rule);
  background: none;
  color: var(--muted);
}
.shop-tabs button:hover { color: var(--ink); border-color: var(--ink); }
.shop-tabs button[aria-selected="true"] {
  background: var(--ink);
  border-color: var(--ink);
  color: var(--paper);
}
.shop-tabs button:focus-visible { outline: 2px solid var(--ink); outline-offset: 2px; }

/* How many things are in there, so a tab is a choice rather than a guess. */
.shop-tab-count {
  font-family: var(--body);
  font-size: .6875rem;
  font-weight: 600;
  letter-spacing: .08em;
  color: var(--muted);
}
.shop-tabs button[aria-selected="true"] .shop-tab-count { color: var(--rule); }

/* The panel is focusable so the keyboard lands on the products, but it is not
   a control and should not be ringed like one on a mouse click. */
.shop-group:focus { outline: none; }
.shop-group:focus-visible { outline: 2px solid var(--ink); outline-offset: 4px; }

/* With the tabs up, the section heading repeats the tab that is already
   selected. Hidden from sight, kept for anyone navigating by heading — and
   the blurb beside it stays, because it says something the tab does not. */
.has-shop-tabs .shop-group-name {
  position: absolute;
  width: 1px; height: 1px;
  margin: -1px; padding: 0;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
}
/*
 * With the heading gone the section head is just the blurb, so it should not
 * keep the heading's furniture: the blurb was pushed to the far right by the
 * `margin-left: auto` that balanced it against the heading, leaving it
 * stranded over empty space, and its underline sat a few pixels below the tab
 * strip's — two rules doing one rule's job.
 */
.has-shop-tabs .shop-group-head {
  justify-content: flex-start;
  border-bottom: 0;
  padding-bottom: 0;
  margin-bottom: 1.25rem;
}
.has-shop-tabs .shop-group-blurb { margin-left: 0; }
.card a {
  text-decoration: none;
  display: grid;
  gap: .6rem;
}
.card .shot {
  background: var(--sunk);
  aspect-ratio: 1;
  display: block;
  width: 100%;
  object-fit: cover;
}
.card h2 {
  font-family: var(--display);
  font-size: 1.15rem;
  text-transform: uppercase;
  margin: 0;
  letter-spacing: .01em;
}
.card .meta { font-size: .8125rem; color: var(--muted); margin: 0; }
.swatches { display: flex; gap: .3rem; margin: 0; padding: 0; list-style: none; }

/* `.swatch` marks anything carrying a fabric colour, so it appears twice: as
   these read-only dots on a grid card, and as the selectable circles in the
   product picker. The SIZE therefore belongs to the context, not to the class —
   scoped here so changing a card dot cannot resize the picker. */
.swatches .swatch {
  width: .9rem;
  height: .9rem;
  border-radius: 50%;
  border: 1px solid var(--rule);
  display: block;
}

/* ------------------------------------------------------------ product */

.product {
  display: grid;
  gap: 2.5rem;
  padding-block: 2rem 4rem;
  align-items: start;
}
@media (min-width: 52rem) {
  .product { grid-template-columns: 1fr 1fr; gap: 3.5rem; }
  .product .gallery { position: sticky; top: 2rem; }
}
.gallery img {
  width: 100%;
  aspect-ratio: 1;
  object-fit: cover;
  background: var(--sunk);
}
.product h1 { font-size: clamp(1.75rem, 6vw, 2.75rem); margin-bottom: .5rem; }
.price {
  font-family: var(--display);
  font-size: 1.75rem;
  letter-spacing: .01em;
  margin: 0 0 1.25rem;
}

fieldset { border: 0; padding: 0; margin: 0 0 1.5rem; }
legend {
  padding: 0 0 .6rem;
  font-size: .6875rem;
  font-weight: 600;
  letter-spacing: .26em;
  text-transform: uppercase;
  color: var(--muted);
}
legend .chosen { color: var(--ink); letter-spacing: .1em; }

.options { display: flex; flex-wrap: wrap; gap: .5rem; }
/* `display: flex` BEATS the user agent's `[hidden] { display: none }`, which
   is a bare attribute selector. So hiding a group of options by setting
   `hidden` did nothing at all and the phone case rendered all five
   characters' phrases at once — 59 of them, stacked. */
.options[hidden] { display: none; }

/* The radio itself stays in the DOM for keyboard and screen readers; only its
   default box is hidden, so focus and checked state still work natively. */
.options input {
  position: absolute;
  opacity: 0;
  width: 1px;
  height: 1px;
}
.options label {
  cursor: pointer;
  border: 1px solid var(--rule);
  padding: .5rem .85rem;
  font-size: .8125rem;
  font-weight: 600;
  min-width: 3rem;
  text-align: center;
}
/* A selected TEXT chip (a size) inverts to ink-on-paper.

   A colour swatch must not: its whole job is to show the fabric colour, so it
   is excluded here and indicated by a ring instead. Without the `:not`, this
   rule painted a selected swatch solid ink — and the `background: none` that
   tried to undo it also removed the `.sw-*` colour, since those classes are
   only (0,1,0) against this rule's (0,2,2). The result was a TRANSPARENT
   circle: the page showing through, which reads as black on the dark theme and
   as nothing at all on the light one. */
.options input:checked + label:not(.swatch) {
  border-color: var(--ink);
  background: var(--ink);
  color: var(--paper);
}
.options input:focus-visible + label {
  outline: 2px solid var(--ink);
  outline-offset: 2px;
}

.colours label {
  width: 2.5rem;
  height: 2.5rem;
  /* `.options label` sets `min-width: 3rem` for text chips, and min-width
     CLAMPS width rather than losing to it — so without this reset a swatch was
     48x40, i.e. `border-radius: 50%` drew an ellipse, not a circle.
     `flex: none` keeps it one at every viewport width. */
  min-width: 0;
  flex: none;
  padding: 0;
  border-radius: 50%;
  position: relative;
  overflow: hidden;
  text-indent: -999em;
}
/* Selection is the ring and nothing else, so the swatch keeps the colour that
   colours.css gives it. The inner paper gap is what separates the ring from a
   dark swatch (Black, Pepper) and the outer ink ring is what makes it visible
   on a pale one (Ivory, Butter) — both themes, no per-colour special cases.

   Deliberately no `background` here: declaring one at this specificity is what
   broke the picker. */
.colours input:checked + label {
  box-shadow: 0 0 0 2px var(--paper), 0 0 0 4px var(--ink);
}

/* --------------------------------------------------- the case preview stage
   The picture has to change SHAPE, not just content: 34 phone models, 16
   distinct print canvases, 0.5640 to 0.6053 wide-to-tall. */

.case-stage {
  display: grid;
  place-items: center;
  /* The same ground the generated shots use, so the page and the shop card
     sit in the same world. */
  background: #d5cfc2;
  padding: clamp(1.25rem, 4vw, 2.5rem);
}

.case-view {
  /* Sized by HEIGHT, with the width following from the model's ratio. Sizing
     by width instead would make a 0.56-ratio case nearly twice as tall as the
     column is wide on desktop. */
  height: min(30rem, 58vh);
  aspect-ratio: var(--case-ratio, .564);
  /* So `cqw` below resolves against this box rather than the page. */
  container-type: inline-size;
  padding: 0;
  border: 0;
  background: none;
  cursor: zoom-in;
  display: block;
  /* The shape change is the whole point, so let it be seen happening. */
  transition: aspect-ratio .18s ease;
}
@media (prefers-reduced-motion: reduce) {
  .case-view { transition: none; }
}

.case-view img {
  display: block;
  width: 100%;
  height: 100%;
  /* EXPLICIT, because `.gallery img` sets `aspect-ratio: 1` at the same
     specificity and this rule only wins on source order. With both width and
     height definite the square ratio is ignored anyway — but "it happens to be
     ignored" is not a thing to rely on for the one picture on the site whose
     shape is the point. Say it. */
  aspect-ratio: auto;
  /* THE CROP IS THE PRODUCT. Printify's order sends `scale: 1`, which matches
     the print's width to the canvas and centres it — so a wider phone
     overflows vertically and is trimmed top and bottom. `cover` on a box of
     that ratio is the same operation, which makes this preview honest rather
     than decorative. */
  object-fit: cover;
  object-position: center;
  /* A percentage radius would be elliptical on a non-square box: 11.5% of a
     tall box's height is nothing like 11.5% of its width. `cqw` is 1% of the
     container's INLINE size, so the corners stay round at every ratio. */
  border-radius: 11.5cqw;
  box-shadow: 0 1px 2px rgba(20, 20, 20, .18), 0 10px 24px rgba(20, 20, 20, .14);
}
.case-view:focus-visible { outline: 2px solid var(--ink); outline-offset: 4px; }

.case-caption {
  margin: .7rem 0 0;
  font-size: .8125rem;
  color: var(--muted);
}
.case-caption #case-model { color: var(--ink); font-weight: 600; }

/* ------------------------------------------- a configured product's pickers
   Three of them — who, what they say, which phone — instead of a colour and a
   size. The chips are the same control as above; what changes is that one of
   the lists holds sentences rather than words. */

/* Character names are words, not 3rem-wide size chips. */
.pick-who .options label { min-width: 0; }

.phrases {
  /* `.options` is a wrapping row, which is right for sizes and wrong for
     sentences: "Somebody in this room is the best dog ever and I'd love to
     meet her." does not belong in a chip beside "I'm good."

     `flex-wrap: nowrap` matters as much as the column direction — a wrapping
     column inside a capped height does not scroll, it breaks into columns. */
  flex-direction: column;
  flex-wrap: nowrap;
  align-items: stretch;
  gap: .25rem;
  /* Thirteen phrases at full width is taller than everything else on the page
     put together, and on a phone it buries "Your phone" and Add to cart under
     several screens of scrolling. Capped and scrolled, with a frame so it is
     obvious there is more below. */
  max-height: 16rem;
  overflow-y: auto;
  /* A visible track, not an overlay one. The cap cuts the list at a row
     boundary as often as not, so there is no ragged edge to suggest more
     below — and a list that looks complete but is not is worse than a long
     one. */
  scrollbar-width: thin;
  scrollbar-color: var(--rule) transparent;
  padding: .3rem;
  border: 1px solid var(--rule);
}
.phrases label.phrase {
  min-width: 0;
  text-align: left;
  font-weight: 500;
  line-height: 1.35;
  /* Transparent rather than absent: a border that appears on hover would move
     every row below it by two pixels. */
  border-color: transparent;
}
.phrases label.phrase:hover { border-color: var(--rule); }
.phrases .tag {
  margin-left: .45rem;
  font-size: .625rem;
  font-weight: 600;
  letter-spacing: .14em;
  text-transform: uppercase;
  color: var(--muted);
}
/* On the selected row the chip is ink, so the tag has to come with it. */
.phrases input:checked + label .tag { color: var(--rule); }

.pick-model select {
  font: inherit;
  font-size: .9375rem;
  padding: .55rem .7rem;
  border: 1px solid var(--rule);
  background: var(--paper);
  color: var(--ink);
  width: 100%;
  max-width: 18rem;
}
.pick-model select:focus-visible { outline: 2px solid var(--ink); outline-offset: 2px; }
/* The placeholder reads as a prompt rather than as a chosen value, so nobody
   mistakes "Choose your phone model" for a phone called that. */
.pick-model select:invalid { color: var(--muted); }
.pick-model .hint {
  margin: .5rem 0 0;
  font-size: .75rem;
  color: var(--muted);
  max-width: 24rem;
}

.buy {
  font-family: var(--display);
  font-size: 1.1rem;
  letter-spacing: .06em;
  text-transform: uppercase;
  background: var(--ink);
  color: var(--paper);
  border: 1px solid var(--ink);
  padding: .9rem 1.5rem;
  width: 100%;
  cursor: pointer;
}
.buy:hover { background: transparent; color: var(--ink); }
.buy[disabled] { opacity: .45; cursor: not-allowed; }
.buy[disabled]:hover { background: var(--ink); color: var(--paper); }

.notes {
  margin-top: 1.5rem;
  border-top: 1px solid var(--rule);
  padding-top: 1.25rem;
  font-size: .8125rem;
  color: var(--muted);
}
.notes p { margin: 0 0 .5rem; }
.notes strong { color: var(--ink); }

/* ----------------------------------------------- dialogs: sizing and zoom */
/*
 * A native <dialog> for both. showModal() gives the focus trap, the Escape key
 * and an inert background — the three things a hand-rolled modal gets subtly
 * wrong, and exactly what drawer.js had to build by hand for the checkout.
 */

.sheet {
  border: 0;
  padding: 0;
  max-width: min(42rem, calc(100vw - 2rem));
  max-height: calc(100vh - 3rem);
  background: var(--paper);
  color: var(--ink);
  border-radius: 3px;
  box-shadow: 0 1.5rem 3rem rgba(0, 0, 0, .3);
}
.sheet::backdrop { background: rgba(0, 0, 0, .55); }

.sheet-head {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 1rem;
  padding: 1.1rem 1.25rem .9rem;
  border-bottom: 1px solid var(--rule);
}
.sheet-head h2 {
  margin: 0;
  font-family: var(--display);
  font-weight: 400;
  text-transform: uppercase;
  font-size: 1.125rem;
  letter-spacing: .01em;
}
.sheet-close {
  flex: none;
  border: 0;
  background: transparent;
  color: var(--muted);
  font: inherit;
  font-size: 1rem;
  line-height: 1;
  padding: .35rem .5rem;
  cursor: pointer;
}
.sheet-close:hover { color: var(--ink); }

.sheet-body { padding: 1.1rem 1.25rem 1.5rem; }
.sheet-body > p { max-width: var(--measure); font-size: .9375rem; }
.sheet-note { font-size: .8125rem; color: var(--muted); margin: 1rem 1.25rem 1.25rem; }

/* A measurement table must stay readable on a phone; let it scroll rather than
   crush the columns. BRAND.md §6: the body never scrolls sideways, so the
   overflow belongs to the table's own container. */
.table-scroll { overflow-x: auto; margin: 1rem 0; }
.sizes { border-collapse: collapse; width: 100%; font-variant-numeric: tabular-nums; }
.sizes th, .sizes td {
  padding: .5rem .9rem .5rem 0;
  text-align: left;
  border-bottom: 1px solid var(--rule);
  white-space: nowrap;
}
.sizes thead th {
  font-size: .625rem;
  font-weight: 600;
  letter-spacing: .16em;
  text-transform: uppercase;
  color: var(--muted);
}
.sizes tbody th { font-weight: 600; }

/*
 * Beside the sizes, not after them. Right-aligned so it sits across from the
 * "Size — L" legend rather than starting a new column of text, and in --ink
 * rather than --muted: it is a control somebody is actively looking for, and
 * muted grey is how it stayed invisible.
 */
.size-link {
  margin: -.2rem 0 .7rem;
  text-align: right;
}
.size-link .linkish { color: var(--ink); }
.linkish {
  border: 0;
  background: transparent;
  padding: 0;
  font: inherit;
  font-size: .875rem;
  color: var(--muted);
  text-decoration: underline;
  text-underline-offset: .18em;
  cursor: pointer;
}
.linkish:hover { color: var(--ink); }

/* ---- zoom ---- */

/* The shot is a control, so it looks like one on hover and focus. */
.shot-zoom {
  display: block;
  width: 100%;
  padding: 0;
  border: 0;
  background: transparent;
  cursor: zoom-in;
  position: relative;
}
.shot-zoom img { display: block; width: 100%; }

/*
 * A GARMENT's picture is TWO LAYERS: a photograph of the blank in the chosen
 * colour, and the artwork over it at the print area's measured rectangle.
 *
 * The rectangle is set inline, as percentages of this box — by the server for
 * the first frame, then by product.js — so one set of numbers holds at every
 * size the picture is drawn at, the zoom included.
 *
 * This replaced a flat colour behind a transparent print. That was honest
 * about the ink and silent about the thing a customer actually wants to know:
 * how big the badge is, and where on the shirt it sits.
 */
.garment-stage { position: relative; display: block; }
.garment-stage .garment-blank { display: block; width: 100%; }
.garment-stage .garment-print {
  position: absolute;
  display: block;
  /* `fill`, not `cover`: the overlay IS the print area, so it stretches to the
     rectangle exactly as Printify fits it. `cover` would crop the edges off a
     badge that is already small. */
  object-fit: fill;
  /* The whole stage is one zoom button; the overlay must not eat its clicks. */
  pointer-events: none;
  /* Beat `.gallery img`, which paints a ground behind every picture — behind a
     transparent overlay that ground would be a grey box around the badge. */
  background: none;
  aspect-ratio: auto;
}

/* The zoom is the same two layers, larger. 2400px: the chest badge lands near
   250px, which is a badge you can actually read, and the garment is carrying
   only a 17% upscale from its 2048px source to get there. */
.zoom-body .garment-stage {
  width: 2400px;
  max-width: none;
  cursor: grab;
}
.zoom-body .garment-stage .garment-blank { width: 100%; }

/* ---- which side ---- */

.garment-views {
  display: flex;
  gap: .4rem;
  justify-content: center;
  margin-top: .7rem;
}
.garment-views button {
  font: inherit;
  font-size: .6875rem;
  font-weight: 600;
  letter-spacing: .12em;
  text-transform: uppercase;
  padding: .4rem .95rem;
  border: 1px solid var(--rule);
  border-radius: 2px;
  background: transparent;
  color: var(--muted);
  cursor: pointer;
}
.garment-views button[aria-pressed="true"] {
  background: var(--ink);
  border-color: var(--ink);
  color: var(--paper);
}
.shot-hint {
  position: absolute;
  right: .6rem;
  bottom: .6rem;
  background: var(--ink);
  color: var(--paper);
  font-size: .625rem;
  font-weight: 600;
  letter-spacing: .14em;
  text-transform: uppercase;
  padding: .3rem .55rem;
  border-radius: 2px;
  opacity: 0;
  transition: opacity .12s ease;
}
.shot-zoom:hover .shot-hint,
.shot-zoom:focus-visible .shot-hint { opacity: 1; }

.sheet--zoom { max-width: min(56rem, calc(100vw - 2rem)); }

/* Scrolls rather than scaling to fit: on a phone a zoom that shrinks the image
   back to the viewport is the same picture again. Panning by scroll also means
   no drag handlers and no pointer-event edge cases. */
.zoom-body {
  overflow: auto;
  max-height: min(70vh, 40rem);
  background: var(--sunk);
  overscroll-behavior: contain;
}
.zoom-body img {
  display: block;
  width: 1200px;
  max-width: none;   /* beat the global img{max-width:100%} — the point is to overflow */
  height: auto;
  cursor: grab;
}

@media (max-width: 34rem) {
  .sheet-head { padding: .9rem 1rem .75rem; }
  .sheet-body { padding: .9rem 1rem 1.25rem; }
}

/* --------------------------------------------------------------- cart */

.cart-count {
  font-variant-numeric: tabular-nums;
  background: var(--ink);
  color: var(--paper);
  border-radius: 1em;
  padding: .05em .5em;
  font-size: .7rem;
  margin-left: .35em;
}
.cart-count:empty { display: none; }

.cart-list { list-style: none; margin: 0; padding: 0; }
.cart-line {
  display: grid;
  grid-template-columns: 4.5rem 1fr auto;
  gap: 1rem;
  align-items: center;
  padding: 1rem 0;
  border-bottom: 1px solid var(--rule);
}
.cart-line img { width: 4.5rem; aspect-ratio: 1; object-fit: cover; background: var(--sunk); }
.cart-line h3 { margin: 0; font-size: .95rem; font-weight: 600; }
.cart-line .variant { margin: 0; font-size: .8125rem; color: var(--muted); }
.cart-line .amount { font-variant-numeric: tabular-nums; font-weight: 600; }
.qty { display: flex; align-items: center; gap: .4rem; margin-top: .35rem; }
.qty button {
  border: 1px solid var(--rule);
  background: none;
  color: inherit;
  width: 1.75rem;
  height: 1.75rem;
  cursor: pointer;
  font: inherit;
  line-height: 1;
}
.qty output { font-variant-numeric: tabular-nums; min-width: 1.5rem; text-align: center; }
.remove {
  background: none;
  border: 0;
  color: var(--muted);
  font-size: .75rem;
  text-decoration: underline;
  cursor: pointer;
  padding: 0;
  margin-top: .35rem;
}

.totals {
  display: flex;
  justify-content: space-between;
  align-items: baseline;
  padding-block: 1.25rem;
  font-family: var(--display);
  font-size: 1.35rem;
  text-transform: uppercase;
}
.totals .sum { font-variant-numeric: tabular-nums; }
.tax-note { font-size: .8125rem; color: var(--muted); margin: 0 0 1.5rem; font-family: var(--body); text-transform: none; }

.empty { padding-block: 3rem; color: var(--muted); }

.error {
  border-left: 3px solid var(--warn);
  padding: .75rem 1rem;
  background: var(--sunk);
  font-size: .875rem;
  margin-bottom: 1rem;
}
.error:empty { display: none; }

/* -------------------------------------------------------------- order */

.order-status { padding-block: 3rem 4rem; max-width: var(--measure); }
.order-status h1 { font-size: clamp(1.75rem, 6vw, 2.75rem); margin-bottom: 1rem; }
.order-status .ref {
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
  font-size: .8125rem;
  word-break: break-all;
  color: var(--muted);
}
.steps { list-style: none; margin: 2rem 0; padding: 0; }
.steps li {
  padding: .75rem 0 .75rem 1.75rem;
  border-bottom: 1px solid var(--rule);
  position: relative;
}
.steps li::before {
  content: "";
  position: absolute;
  left: 0;
  top: 1.15rem;
  width: .6rem;
  height: .6rem;
  border-radius: 50%;
  background: var(--rule);
}
.steps li[data-done="true"]::before { background: var(--ok); }
.steps strong { display: block; font-size: .9375rem; }
.steps span { font-size: .8125rem; color: var(--muted); }

/* ----------------------------------------------------------- checkout */

.checkout-mount {
  min-height: 28rem;
  padding-bottom: 3rem;
}
.checkout-mount .empty { padding-block: 4rem; text-align: center; }

/* --------------------------------------------------- checkout (elements) */

.pay-form {
  display: grid;
  gap: 2rem;
  max-width: 34rem;
  padding-bottom: 4rem;
}
.pay-form section { display: grid; gap: .6rem; }
.pay-form .eyebrow { margin: 0; }

.pay-summary {
  border-top: 1px solid var(--rule);
  padding-top: 1rem;
  display: grid;
  gap: .4rem;
}
.pay-summary .row {
  display: flex;
  justify-content: space-between;
  font-size: .9375rem;
  color: var(--muted);
}
.pay-summary .row.total {
  border-top: 1px solid var(--rule);
  margin-top: .4rem;
  padding-top: .7rem;
  font-family: var(--display);
  font-size: 1.35rem;
  text-transform: uppercase;
  color: var(--ink);
}
.pay-summary .sum { font-variant-numeric: tabular-nums; }

/* -------------------------------------------------------------- drawer */

body.drawer-open {
  position: fixed;
  width: 100%;
  overflow: hidden;
}

.drawer { position: fixed; inset: 0; z-index: 50; }
.drawer[hidden] { display: none; }

.drawer-scrim {
  position: absolute;
  inset: 0;
  background: color-mix(in srgb, var(--ink) 55%, transparent);
}

.drawer-panel {
  position: absolute;
  inset-block: 0;
  inset-inline-end: 0;
  width: min(32rem, 100%);
  background: var(--paper);
  border-inline-start: 1px solid var(--rule);
  display: flex;
  flex-direction: column;
  overflow-y: auto;
  overscroll-behavior: contain;
  animation: drawer-in .22s ease-out;
}
@keyframes drawer-in {
  from { transform: translateX(1.5rem); opacity: .6; }
  to   { transform: none; opacity: 1; }
}
@media (prefers-reduced-motion: reduce) {
  .drawer-panel { animation: none; }
}

.drawer-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 1rem;
  padding: 1.25rem var(--gutter);
  border-bottom: 1px solid var(--rule);
  position: sticky;
  top: 0;
  background: var(--paper);
  z-index: 1;
}
.drawer-head .eyebrow { margin: 0; }

.drawer-close {
  background: none;
  border: 1px solid var(--rule);
  color: inherit;
  font: inherit;
  line-height: 1;
  padding: .4rem .6rem;
  cursor: pointer;
}
.drawer-close:hover { border-color: var(--ink); }

.drawer-body { padding: 1.5rem var(--gutter) 3rem; }
.drawer-body .pay-form { max-width: none; padding-bottom: 0; }
.drawer-body .checkout-mount { min-height: 20rem; }

/* ---------------------------------------------------------------- quiz */

/*
 * "What's Your Pet?" — one question on screen at a time.
 *
 * The questions and all five results ship in the HTML and are shown and hidden
 * with the `hidden` attribute, which means every rule in this block that sets
 * `display` has to be beaten by `[hidden]`. It is not, by default: an author
 * `display: grid` outranks the UA stylesheet's `[hidden] { display: none }`,
 * so the element stays visible and the page shows all six questions and all
 * five answers stacked on top of each other. That exact bug shipped on another
 * site in this account and was reported as "it's showing all the different
 * sections on the same page".
 *
 * Scoped to the quiz rather than made global so it cannot change the behaviour
 * of anything already on the site — the drawer has its own equivalent rule.
 */
.quiz [hidden],
.quiz[hidden] { display: none !important; }

.quiz { padding-bottom: 4rem; }

.quiz-noscript {
  max-width: var(--measure);
  border-inline-start: 3px solid var(--rule);
  padding-inline-start: 1rem;
  font-size: .9375rem;
  color: var(--muted);
}

.quiz-progress {
  margin: 0 0 1.25rem;
  font-size: .6875rem;
  font-weight: 600;
  letter-spacing: .24em;
  text-transform: uppercase;
  color: var(--muted);
}

.quiz-ask {
  font-family: var(--display);
  font-weight: 400;
  text-transform: uppercase;
  letter-spacing: .01em;
  /* Questions are a sentence or two, so they want to be smaller than a page
     headline and still clearly the loudest thing in this block. */
  font-size: clamp(1.4rem, 4.5vw, 2.1rem);
  line-height: 1.05;
  max-width: 26rem;
  margin: 0 0 1.5rem;
  text-wrap: balance;
}
/* The heading only takes focus programmatically, on advance. Showing a ring
   there is right for the keyboard reader and wrong for the mouse user who has
   just clicked an answer — :focus-visible draws that distinction for us. */
.quiz-ask:focus { outline: none; }
.quiz-ask:focus-visible { outline: 2px solid var(--ink); outline-offset: .35rem; }

.quiz-options {
  list-style: none;
  margin: 0;
  padding: 0;
  display: grid;
  gap: .6rem;
  max-width: 34rem;
}

/*
 * An answer is a full-width target, not a chip.
 *
 * The options are whole sentences of differing lengths — Cookie's is one word
 * ("Noted.") and Daisy's runs to eleven. Laid out as inline chips they wrap
 * into a ragged block where the short answers read as less important than the
 * long ones, which is a thumb on the scale. One per row, same width, same
 * weight.
 */
.quiz-pick {
  display: block;
  width: 100%;
  text-align: left;
  font: inherit;
  font-size: .9375rem;
  line-height: 1.45;
  color: inherit;
  background: var(--paper);
  border: 1px solid var(--rule);
  padding: .85rem 1rem;
  cursor: pointer;
}
.quiz-pick:hover { border-color: var(--ink); background: var(--sunk); }
.quiz-pick:focus-visible { outline: 2px solid var(--ink); outline-offset: 2px; }
/* The chosen answer, seen only on the way back through with Back. */
.quiz-pick[aria-pressed="true"] {
  border-color: var(--ink);
  background: var(--ink);
  color: var(--paper);
}

.quiz-back-row { margin: 1.5rem 0 0; }

/* ------------------------------------------------------------ the result */

.quiz-result {
  border-top: 3px solid var(--rule);
  padding-top: 1.25rem;
  max-width: 32rem;
}
/* Same colourway marks as the roster cards, from the same tokens, so the
   answer is recognisably the card the reader will meet again on /characters/. */
.quiz-result.who--franklin { border-top-color: var(--rule-franklin); }
.quiz-result.who--cookie   { border-top-color: var(--rule-cookie); }
.quiz-result.who--abby     { border-top-color: var(--rule-abby); }
.quiz-result.who--daisy    { border-top-color: var(--rule-daisy); }
.quiz-result.who--millie   { border-top-color: var(--rule-millie); }

.quiz-face {
  display: block;
  width: clamp(8rem, 42vw, 14rem);
  height: auto;
  aspect-ratio: 1;
  border-radius: 50%;
  /* Same hairline and same baked-in cream ground as every other portrait on
     the site — scripts/build-characters.mjs explains why the ground does not
     follow the theme. */
  border: 1px solid rgba(20, 20, 20, .12);
  background: #f4f1ea;
  margin: .25rem 0 1rem;
}

.quiz-result h2 {
  font-size: clamp(1.75rem, 6vw, 2.5rem);
  line-height: .95;
  margin: 0 0 .4rem;
}

.quiz-epithet {
  margin: 0 0 1rem;
  font-size: .6875rem;
  font-weight: 600;
  letter-spacing: .24em;
  text-transform: uppercase;
  color: var(--muted);
}

.quiz-verdict {
  margin: 0;
  font-size: 1.0625rem;
  line-height: 1.55;
}

.quiz-says {
  margin: 1.1rem 0 0;
  font-family: var(--display);
  font-weight: 400;
  text-transform: uppercase;
  letter-spacing: .02em;
  font-size: 1.25rem;
  line-height: 1.1;
}

.quiz-actions {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: .9rem 1.25rem;
  margin: 1.75rem 0 0;
}
/* `.buy` is `width: 100%` because on a product page it is the one thing to do.
   Used as a link in a row of other things, that swallows the row. Shared with
   the roster's call to action rather than overridden twice. */
.buy--inline {
  width: auto;
  text-decoration: none;
  text-align: center;
}

.quiz-again {
  margin: 2.5rem 0 0;
  padding-top: 1.25rem;
  border-top: 1px solid var(--rule);
  font-size: .875rem;
  color: var(--muted);
}
.quiz-again a { color: var(--muted); }

/* The quiz invitation under the roster. Centred because the roster above it is
   a symmetrical five-up curve, and a left-aligned button under the middle of
   that reads as a stray. */
.roster-cta {
  margin: 2.75rem 0 0;
  padding-top: 1.75rem;
  border-top: 1px solid var(--rule);
  text-align: center;
}

/* --------------------------------------------------------------- legal */

/*
 * Terms, Privacy, Returns, Shipping.
 *
 * Set for READING UNDER STRESS. Somebody on this page is usually mid-problem —
 * a parcel that has not come, a charge they do not recognise — so it is a
 * single narrow column, generous leading, and headings that are findable by
 * scanning rather than by reading. No columns, no cards, no cleverness.
 */
.legal {
  max-width: var(--measure);
  padding-bottom: 4rem;
}

.legal-date {
  margin: 1.25rem 0 0;
  font-size: .8125rem;
  color: var(--muted);
}

.legal h2 {
  font-family: var(--display);
  font-weight: 400;
  text-transform: uppercase;
  letter-spacing: .01em;
  font-size: 1.375rem;
  line-height: 1.1;
  /* The space above a heading is what makes a long policy scannable — it is
     doing more work here than the type size is. */
  margin: 2.75rem 0 .75rem;
  padding-top: 1.25rem;
  border-top: 1px solid var(--rule);
}

.legal h3 {
  font-size: .9375rem;
  font-weight: 600;
  letter-spacing: .02em;
  margin: 1.75rem 0 .5rem;
}

.legal p,
.legal li { font-size: .9375rem; line-height: 1.65; }
.legal ul { padding-left: 1.1rem; }
.legal li { margin-bottom: .5rem; }
.legal a { color: var(--ink); text-underline-offset: .18em; }

/* Who else sees your data, and why. A definition list because that is what it
   is — a term and its explanation — and it reads better than a table at phone
   width, where a two-column table has to scroll. */
.legal-dl { margin: 1.25rem 0 0; }
.legal-dl dt {
  font-weight: 600;
  font-size: .8125rem;
  letter-spacing: .12em;
  text-transform: uppercase;
  margin-top: 1.25rem;
}
.legal-dl dd {
  margin: .35rem 0 0;
  padding-left: 1rem;
  border-left: 2px solid var(--rule);
  font-size: .9375rem;
  line-height: 1.6;
}

.legal-contact {
  margin-top: 3rem;
  padding: 1.5rem;
  background: var(--sunk);
}
.legal-contact h2 {
  margin-top: 0;
  padding-top: 0;
  border-top: 0;
  font-size: 1.125rem;
}
.legal-entity { color: var(--muted); font-size: .8125rem; margin-bottom: 0; }

.legal-nav {
  display: flex;
  flex-wrap: wrap;
  gap: .5rem 1.25rem;
  margin-top: 2rem;
  padding-top: 1.25rem;
  border-top: 1px solid var(--rule);
  font-size: .75rem;
  font-weight: 600;
  letter-spacing: .14em;
  text-transform: uppercase;
}
.legal-nav a { color: var(--muted); text-decoration: none; }
.legal-nav a:hover { color: var(--ink); text-decoration: underline; }

/* The same links in the global footer. Sits above the copyright line. */
.foot-legal {
  display: flex;
  flex-wrap: wrap;
  gap: .4rem 1rem;
  margin-bottom: .6rem;
}
.foot-legal a { color: var(--muted); text-decoration: none; }
.foot-legal a:hover { color: var(--ink); text-decoration: underline; }

/* The statement descriptor warning on the checkout page. Deliberately not
   muted like .tax-note beside it — this is the one line on the page that
   prevents a dispute, and a customer skimming before they pay has to actually
   see it. Bordered rather than coloured: a red box next to a card form reads
   as an error and stops people paying. */
.statement-note {
  max-width: var(--measure);
  margin: 1.25rem 0 0;
  padding: .85rem 1rem;
  border: 1px solid var(--rule);
  background: var(--sunk);
  font-size: .875rem;
  line-height: 1.55;
}

/* Line items in the checkout summary. The customer should see WHAT they are
   buying next to the price, not just a bare total appearing out of nowhere. */
.pay-summary .row.item {
  color: var(--ink);
  align-items: flex-start;
  gap: 1rem;
}
.pay-summary .row.item small {
  color: var(--muted);
  font-size: .8125rem;
}
.pay-summary .row.item + .row.subtotal {
  border-top: 1px solid var(--rule);
  margin-top: .5rem;
  padding-top: .7rem;
}
/* Tax that cannot be computed until an address exists. Stated in words rather
   than as $0.00, which would be a promise broken on the statement. */
.pay-summary .row.tax-pending .sum {
  font-style: italic;
  font-size: .8125rem;
}

/* ---- discount code on the checkout ---- */
/* Collapsed behind a link on purpose: a visible empty "discount code" box
   tells every customer WITHOUT a code that there is one going, and some of
   them leave to look for it. */
.pay-promo { margin-top: 1.25rem; }
.promo-row { display: flex; gap: .5rem; margin-top: .6rem; max-width: 22rem; }
.promo-row input {
  flex: 1; min-width: 0; font: inherit; font-size: .9375rem;
  padding: .55rem .7rem; border: 1px solid var(--rule);
  background: var(--paper); color: var(--ink);
}
.promo-row input:focus-visible { outline: 2px solid var(--ink); outline-offset: 1px; }
.promo-apply {
  font: inherit; font-size: .8125rem; font-weight: 600; letter-spacing: .08em;
  text-transform: uppercase; cursor: pointer; padding: .55rem 1rem;
  border: 1px solid var(--ink); background: var(--ink); color: var(--paper);
}
.promo-apply[disabled] { opacity: .5; cursor: wait; }
.promo-msg { margin: .5rem 0 0; font-size: .875rem; min-height: 1.2em; }
.promo-msg.bad { color: var(--warn); }
.promo-msg.applied { color: var(--ok); }
.promo-msg .linkish { margin-left: .35rem; }

/* The contact line on the order page. Muted, but present — it is the only
   route to a human on the page somebody lands on after paying. */
.order-contact {
  max-width: var(--measure);
  font-size: .875rem;
  color: var(--muted);
  border-top: 1px solid var(--rule);
  padding-top: 1rem;
  margin-top: 1.5rem;
}
.order-contact a { color: var(--ink); }
