/* ==========================================================================
   DESIGN TOKENS — this is the file to edit
   ==========================================================================

   Change a number, commit, done. Nothing here needs a build step to understand.

   Colours are hardcoded, deliberately. The palette is the brand, not a
   per-store preference, so there is no admin colour picker and no runtime
   injection of these values.

   STATUS: signed off 2026-08-03.
   The design export used 57 distinct font sizes, 71 distinct paddings and 6
   border widths. This collapses them to 8 / 8 / 3. Expect a slight shift from
   the export; that is the intended tradeoff.

   When the next design revision lands, change the VALUES here. Do not add steps.
   If a revision genuinely needs a ninth size, that is a conversation, not a
   quiet append — the whole point of a scale is that it stays small.
   ========================================================================== */

:root {
  /* --- Palette. Eight values, the whole system. -------------------------- */
  --paper:        #EFE4CE;   /* page base                                    */
  --ink:          #14110F;   /* type, borders, inverted bands                */
  --accent:       #C6371B;   /* fills, rules, active states, display type    */
  --accent-text:  #A82A12;   /* the same red, darkened for small text        */
  --panel:        #E7D9BC;   /* second surface                               */
  --ink-dim:      #5A5148;   /* secondary text                               */
  --rule:         #C9BA9B;   /* hairlines, inactive borders                  */
  --rule-dark:    #3A342C;   /* rules on black                               */

  /* The paper a print is printed ON, and it is deliberately NOT redefined for the dark theme.
     The catalogue's artwork carries an alpha channel — the cream of the sheet is transparent, so
     whatever sits behind the image becomes the paper. With `--paper` behind it, dark mode turned
     every cream letterform near-black and the prints read as inverted. A sheet of paper is a
     property of the object, not of the interface looking at it. */
  --print-paper:  #EFE4CE;

  /* Two reds, because one cannot do both jobs. --accent on --paper measures
     4.20:1, under the 4.5:1 AA needs once type drops below the large-text cutoff
     of 24px, or 18.66px bold. So links, eyebrows, "Sold out" and the accent
     button's fill take --accent-text: 5.54:1 on paper, 5.01:1 on panel.
     --accent keeps the fills, hairlines and display type, where the bar is 3:1
     and 4.20 clears it, and it keeps every accent sitting on --ink, where
     darkening would take 3.55:1 down to 2.69:1. */

  --link:         var(--accent-text);
  --btn-bg:       var(--accent-text);
  --btn-fg:       var(--paper);
  --btn-bg-hover: var(--ink);

  /* --- Type scale. 8 steps, from 57 ad-hoc sizes in the export. ----------- */
  --step--1: 0.72rem;   /* 11.5px — labels, nav, captions, fine print       */
  --step-0:  0.8rem;    /* 12.8px — UI default, buttons, form fields        */
  --step-1:  1rem;      /* 16px   — body copy small                        */
  --step-2:  1.19rem;   /* 19px   — body copy                              */
  --step-3:  clamp(1.375rem, 2.6vw, 1.875rem);  /* section subheads        */
  --step-4:  clamp(1.875rem, 3.6vw, 2.875rem);  /* section heads           */
  --step-5:  clamp(2.375rem, 5vw, 3.875rem);    /* page titles             */
  --step-6:  clamp(2rem, 8.4vw, 6rem);          /* hero only               */

  /* --- Letter spacing. 3 values, from 18 in the export. ----------------- */
  --track-tight: -0.01em;
  --track-label: 0.14em;   /* uppercase mono labels */
  --track-wide:  0.22em;   /* uppercase mono eyebrows */

  /* --- Line height ------------------------------------------------------ */
  --leading-display: 0.94;
  --leading-tight:   1.15;
  --leading-body:    1.6;
  --leading-mono:    1.75;

  /* --- Spacing scale. 8 steps, from 71 paddings and 33 gaps. ------------ */
  --space-1: 0.5rem;    /*  8px */
  --space-2: 0.75rem;   /* 12px */
  --space-3: 1.125rem;  /* 18px */
  --space-4: 1.625rem;  /* 26px */
  --space-5: 2rem;      /* 32px */
  --space-6: 2.75rem;   /* 44px */
  --space-7: 4rem;      /* 64px */
  --space-8: 4.5rem;    /* 72px */

  /* --- Borders. 3 widths, down from 6 in the export. -------------------- */
  --rule-hair:  2px;   /* hairlines, inactive borders, table lines */
  --rule-frame: 4px;   /* image frames, content blocks, controls   */
  --rule-major: 6px;   /* between major sections                   */

  /* --- Layout ----------------------------------------------------------- */
  --measure:  1280px;
  --gutter:   var(--space-5);
  --gutter-sm: 1.25rem;   /* below 700px */

  /* The sticky header's rendered height, measured in Chrome at 1440px: 72.47px,
     rounded up so nothing shows through. The shop's filter bar sticks at this
     offset and focus targets scroll clear of it. base.css overrides it at 980px
     and 640px, where the header grows the MENU drawer and the wordmark wraps. */
  --header-h: 73px;

  /* --- Grain. Structural signature, do not remove. ----------------------
     Opacity is the one knob worth tuning. 0.34 to 0.38 is the design's range. */
  --grain-opacity: 0.38;
  --grain-blend: multiply;

  /* --- Type families. Both faces are self-hosted; see fonts.css. --------- */
  --font-display: 'Bowlby One SC', Impact, sans-serif;
  --font-text: 'Space Mono', ui-monospace, monospace;
}

/* Breakpoints, for reference. CSS cannot tokenize these; they are literals in
   the media queries. Keep this list and the queries in sync.
     1120px  two-column layouts collapse
      980px  nav collapses into the MENU drawer
      700px  gutters shrink to --gutter-sm
      640px  wordmark wraps, cart control shrinks                            */

/* ---------- The dark theme -------------------------------------------------
   THREE STATES, NOT TWO. A visitor either chose a theme or did not, and the
   default is "did not" — so the bare :root above is the complete light palette,
   the media query below covers a visitor whose device asks for dark, and the
   [data-theme] block covers the Worker having been told which to serve.

   PRECEDENCE, and it is deliberate: `data-theme` WINS. Base.astro stamps it only
   when the THEME variable is set on the Worker, which makes that variable a
   lever the shop can pull — flip it, redeploy, and every visitor sees the same
   thing regardless of their device. Leave it unset and the device decides.

   The roles swap rather than the colours inverting. --paper is the page base in
   both themes, so here it holds the dark value; --ink is the type in both, so
   here it holds the light one. Nothing in base.css has to know which theme it is
   in, which is the whole reason this is eight token values and not a second
   stylesheet.

   Every value here is held to the same 23 contrast pairs as the light palette,
   by scripts/check-design.mjs, against design-system.json's darkPalette. The two
   accents are brightened because the light theme's are tuned for cream and are
   too dark to carry type on near-black.
   -------------------------------------------------------------------------- */

@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) {
    --paper: #14110F;
    --ink: #EFE4CE;
    --panel: #221D19;
    --accent: #E0491F;
    --accent-text: #F2765A;
    --ink-dim: #A2957F;
    --rule: #3A342C;
    --rule-dark: #C9BA9B;

    /* Multiply is invisible on a dark ground: it can only darken, and there is
       almost nothing left to darken. Screen lifts the same noise instead, so the
       paper texture survives the theme rather than silently vanishing. */
    --grain-blend: screen;
    --grain-opacity: 0.20;
  }
}

:root[data-theme="dark"] {
  --paper: #14110F;
  --ink: #EFE4CE;
  --panel: #221D19;
  --accent: #E0491F;
  --accent-text: #F2765A;
  --ink-dim: #A2957F;
  --rule: #3A342C;
  --rule-dark: #C9BA9B;
  --grain-blend: screen;
  --grain-opacity: 0.20;
}

/*!
 * Bowlby One SC — Copyright (c) 2011 by vernon adams (vern@newtypography.co.uk),
 *   with Reserved Font Names "Bowlby" "Bowlby One" and "Bowlby One SC".
 * Space Mono — Copyright 2016 The Space Mono Project Authors
 *   (https://github.com/googlefonts/spacemono).
 * Both licensed under the SIL Open Font License 1.1: http://scripts.sil.org/OFL
 * Full license texts are in ../fonts/OFL-*.txt, alongside the font files.
 *
 * The OFL requires this notice to travel with the fonts. It rides in the
 * stylesheet because the stylesheet is the one artifact guaranteed to reach the
 * served site. Keep the OFL-*.txt files in the repo. Do not delete this block.
 */

/* Self-hosted webfonts, referenced by ABSOLUTE /fonts/ URLs on purpose.
   The font files live in this package under ../fonts/ and are copied into
   apps/storefront/public/fonts/, which Astro serves at /fonts/<filename>.
   Keeping the URLs absolute means the stylesheet contains no bundler-specific
   asset import (no `~/`, `$lib/`, `@/assets`), so it stays portable to anything
   that can serve a directory of files. If the copies drift, the site falls back
   to Impact and ui-monospace, which is visible immediately.

   Glyph coverage VERIFIED against the actual TTFs:
     BowlbyOneSC-Regular  401 glyphs
     SpaceMono            624 glyphs
   Both cover the full Spanish and French set used in product titles
   (¿ ñ á é í ó ú ü ç à è ê œ, accented capitals, middot, em dash, curly quote).
   The fallback risk flagged during design review is closed.

   font-display: swap on the display face is a deliberate tradeoff — a flash of
   Impact beats invisible headings. Revisit only if the flash tests badly.

   woff2 FIRST, ttf SECOND, both listed. woff2 is the same outlines under a better
   compressor — 60% smaller across these three faces — and every browser that
   matters has supported it since 2016. The TTF stays as the second source rather
   than being deleted, because the cost of keeping it is one line and the cost of
   being wrong about that is a page in Impact. `npm run fonts` generates the woff2
   from the ttf and `npm run check:fonts` fails if they have drifted apart.

   NOT SUBSETTED. Subsetting would save more and is exactly the operation that
   silently drops a glyph — and the coverage above was verified deliberately, after a
   design review flagged the risk, for languages nobody on this project reads first.
   Two thirds of the saving for none of that risk. */

@font-face {
  font-family: 'Bowlby One SC';
  src: url('/fonts/BowlbyOneSC-Regular.woff2') format('woff2'),
       url('/fonts/BowlbyOneSC-Regular.ttf') format('truetype');
  font-weight: 400;
  font-style: normal;
  font-display: swap;
}

@font-face {
  font-family: 'Space Mono';
  src: url('/fonts/SpaceMono-Regular.woff2') format('woff2'),
       url('/fonts/SpaceMono-Regular.ttf') format('truetype');
  font-weight: 400;
  font-style: normal;
  font-display: swap;
}

@font-face {
  font-family: 'Space Mono';
  src: url('/fonts/SpaceMono-Bold.woff2') format('woff2'),
       url('/fonts/SpaceMono-Bold.ttf') format('truetype');
  font-weight: 700;
  font-style: normal;
  font-display: swap;
}

/* Base + layout. Built against the 2026-08-05 design revision.
   Structural signatures first, then page composition.

   Values come from tokens.css. If you find yourself typing a hex or a raw px here,
   the token is missing — add it there instead. */

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

body {
  margin: 0;
  background: var(--paper);
  color: var(--ink);
  font-family: var(--font-text);
  font-size: var(--step-1);
  line-height: var(--leading-body);
  -webkit-font-smoothing: antialiased;
}

/* Square corners everywhere. A rule, not a habit. The !important also catches
   anything a third-party embed drops into the page. */
* { border-radius: 0 !important; }

h1, h2, h3, .wordmark, .display {
  font-family: var(--font-display);
  text-transform: uppercase;
  line-height: var(--leading-display);
  margin: 0;
}

a { color: var(--link); text-decoration: none; }
a:hover { color: var(--ink); text-decoration: underline; text-underline-offset: 3px; }
::selection { background: var(--accent-text); color: var(--paper); }
img { max-width: 100%; }

/* ---------- Focus ---------------------------------------------------------- */
/* The UA ring cannot be relied on here. On the ink header and footer a dark
   default ring is invisible, and .grain multiplies down whatever is painted
   under it. Measured through the grain at its darkest pixel (see the note on
   .grain below): an ink ring on paper holds 9.18:1 and a paper ring on ink the
   same, where an accent ring would fall to 2.59:1 on paper and 2.50:1 on ink —
   under the 3:1 that WCAG 1.4.11 asks of a focus indicator. So the ring is ink,
   flipped to paper on the inverted surfaces.
   The 3px offset is load-bearing: it leaves a gap of page background between the
   ring and any filled control, which is what keeps the ring legible on one.
   Gap-to-fill measures 9.18:1 over an ink fill and 3.41:1 over --accent-text.
   Ceiling: the list is explicit, so a new kind of focusable element has to be
   added to it. A bare :focus-visible would also ring the #content skip target,
   which is a wrapper rather than a control. */
a:focus-visible,
button:focus-visible,
summary:focus-visible,
input:focus-visible,
select:focus-visible,
textarea:focus-visible,
[tabindex]:not([tabindex="-1"]):focus-visible {
  outline: 3px solid var(--ink);
  outline-offset: 3px;
}
.site-header :focus-visible,
.site-footer :focus-visible,
.section--ink :focus-visible { outline-color: var(--paper); }

/* Focus targets clear the sticky header instead of landing underneath it
   (WCAG 2.4.11). --header-h tracks the header per breakpoint.
   Ceiling: on /shop the filter bar sticks below the header and can still cover
   part of a focused card. Clearing that too would mean a 384px offset at 390px
   (a measured 154px header plus a 229px filter bar, whose chips wrap onto four
   rows), which buries the card it is meant to reveal. Partial overlap is what
   2.4.11 permits; total obscuring is what it forbids. */
a, button, summary, input, select, textarea, [tabindex] {
  scroll-margin-top: calc(var(--header-h) + var(--space-3));
}

/* Monospace label: the design's third voice, after display and body. */
.label, .eyebrow, .site-nav, .cart-link, .filters, .fine-print, .meta {
  font-family: var(--font-text);
  text-transform: uppercase;
  letter-spacing: var(--track-label);
}
.eyebrow {
  font-size: var(--step--1);
  letter-spacing: var(--track-wide);
  color: var(--accent-text);
  font-weight: 700;
}

/* ---------- THE LAYERING LADDER, in one place ------------------------------
   Every z-index in this file is on this ladder and nothing is off it. It is written down here
   because it was NOT, and that produced a shipped bug: artwork was lifted to 61 to escape the
   grain, which also lifted it over the sticky header at 50, so scrolling a product page slid the
   print up over the site header.

     (auto)  ordinary content
       60    .grain — fixed, full-viewport, blended
       61    artwork: .primary-image and .pcard-media > a
       70    .filters, the sticky chip row
       80    .site-header
       90    .sr-only--focusable:focus, the skip link

   Two rules hold it together:

     1. ARTWORK IS ABOVE THE GRAIN and cannot be otherwise. The prints are transparent where the
        sheet shows, and a fixed blend layer over them is a filter nobody asked for — multiplying
        in one theme and screening in the other. `mix-blend-mode` blends with whatever is painted
        below it, so going above it is the only escape; `isolation` cannot help, because isolating
        the grain's own parent would leave it blending with nothing.

     2. CHROME IS ABOVE ARTWORK, which is what was wrong. Anything that stays put while the page
        moves has to win against anything that moves, or it stops being chrome. That puts the
        header and the filter bar above 61 and therefore above the grain too — they lose the paper
        texture, which on solid ink is invisible and on the panel bar is a seam worth less than
        content sliding over the navigation.

   Paper grain. Fixed multiply layer above the page. One known cost: compositing on scroll over a
   fixed layer. */
.grain {
  position: fixed;
  inset: 0;
  z-index: 60;
  pointer-events: none;
  mix-blend-mode: var(--grain-blend);
  opacity: var(--grain-opacity);
  background-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='220' height='220'><filter id='n'><feTurbulence type='fractalNoise' baseFrequency='0.85' numOctaves='4' stitchTiles='stitch'/><feColorMatrix type='saturate' values='0'/></filter><rect width='220' height='220' filter='url(%23n)' opacity='0.55'/></svg>");
}

/* ---------- Shared shell ---------------------------------------------------- */

.wrap { max-width: var(--measure); margin: 0 auto; padding-left: var(--gutter); padding-right: var(--gutter); }

/* Major section breaks. The 6px rule is a structural signature, not decoration. */
.section { border-bottom: var(--rule-major) solid var(--ink); }
.section--panel { background: var(--panel); }
.section--ink { background: var(--ink); color: var(--paper); }
.section--ink a { color: var(--paper); }

/* ---------- Header --------------------------------------------------------- */

.site-header {
  /* 80: above artwork at 61. At 50 the print on a product page scrolled over the header, which is
     the bug the layering ladder exists to prevent recurring. */
  position: sticky; top: 0; z-index: 80;
  background: var(--ink); color: var(--paper);
  border-bottom: var(--rule-frame) solid var(--ink);
}
.site-header > .wrap {
  display: flex; align-items: center; justify-content: space-between;
  gap: 1.75rem;
  padding-top: 0.875rem; padding-bottom: 0.875rem;
}
.site-header a { color: var(--paper); }

.wordmark {
  min-width: 0; white-space: nowrap;
  font-size: 1.1875rem; letter-spacing: 0.04em; line-height: 1;
  display: flex; align-items: center; gap: 0.625rem;
}
/* The rotated star and the boxed last word are the wordmark's whole identity.
   Do not straighten them. */
.wordmark .star {
  display: inline-flex; align-items: center; justify-content: center;
  width: 26px; height: 26px;
  background: var(--accent); color: var(--paper);
  font-size: 15px; transform: rotate(-6deg);
}
.wordmark .boxed {
  display: inline-block;
  background: var(--accent); color: var(--paper);
  padding: 1px 8px 3px;
  transform: rotate(-3.5deg);
}

.site-nav { display: flex; align-items: center; gap: 2.125rem; white-space: nowrap; font-size: var(--step-0); }
/* Every link reserves the underline; only the current one colours it in. Applying the border to
   `[aria-current]` alone made that link 5px taller, and `align-items: center` then pushed its
   text ~2.5px above every other item in the row. */
.site-nav a { border-bottom: 3px solid transparent; padding-bottom: 2px; }
.site-nav a[aria-current] { border-bottom-color: var(--accent); }

.header-end { flex-shrink: 0; display: flex; align-items: center; gap: 1rem; font-size: var(--step-0); }
.cart-link { border: var(--rule-hair) solid var(--paper); padding: 0.5rem 0.875rem; }

.mobile-menu { display: none; border-top: var(--rule-hair) solid var(--rule-dark); }
.mobile-menu summary {
  list-style: none; cursor: pointer;
  padding: 0.8rem var(--gutter-sm);
  font-size: var(--step-0); letter-spacing: 0.18em; text-transform: uppercase;
  display: flex; align-items: center; justify-content: space-between;
}
.mobile-menu summary::-webkit-details-marker { display: none; }
.mobile-menu .bars { display: flex; flex-direction: column; gap: 3.5px; width: 19px; }
.mobile-menu .bars span { display: block; height: 2.5px; background: var(--accent); }
.mobile-menu nav {
  display: flex; flex-direction: column;
  padding: 4px var(--gutter-sm) var(--gutter-sm);
  font-size: 0.875rem; letter-spacing: 0.12em; text-transform: uppercase;
}
.mobile-menu nav a { padding: 0.75rem 0; border-top: 1px solid var(--rule-dark); }

/* ---------- Buttons ------------------------------------------------------- */

/* No bare `button[type="submit"]` here. It reads (0,1,1) and so beat `.btn--accent` at
   (0,1,0) regardless of source order, which meant every primary CTA on the site — Checkout,
   Add to cart, and the card's hover Add — rendered ink instead of the vermilion that
   design/DESIGN-SYSTEM.md names as the accent's job. Every submit button in the storefront
   carries `.btn` or `.pcard-add`, so the attribute selector was buying nothing.
   Ceiling: a new submit button with neither class gets no styling at all, which is loud
   enough to notice. */
.btn, .cta {
  display: inline-block;
  font-family: var(--font-text); font-weight: 700;
  font-size: var(--step-0); letter-spacing: 0.18em; text-transform: uppercase;
  padding: var(--space-3) var(--space-6);
  border: 3px solid var(--ink);
  background: var(--ink); color: var(--paper);
  cursor: pointer; text-align: center;
}
/* Hover fills with --accent-text, not --accent: the label is --paper at 12.8px,
   so the hover state has the same 4.5:1 obligation as the resting one. */
.btn:hover, .cta:hover {
  background: var(--accent-text); border-color: var(--accent-text); color: var(--paper);
  text-decoration: none;
}
.btn--accent { background: var(--btn-bg); border-color: var(--btn-bg); color: var(--btn-fg); }
.btn--accent:hover { background: var(--btn-bg-hover); border-color: var(--btn-bg-hover); }
.btn[disabled] { background: var(--rule); border-color: var(--ink-dim); color: var(--ink-dim); cursor: not-allowed; }

.chip {
  font-family: var(--font-text);
  font-size: var(--step-0); letter-spacing: var(--track-label); text-transform: uppercase;
  padding: 0.625rem 1.125rem;
  border: 3px solid var(--ink);
  background: transparent; color: var(--ink);
  cursor: pointer;
}
.chip[aria-pressed="true"], .chip--on { background: var(--ink); color: var(--paper); }

/* ---------- Hero ---------------------------------------------------------- */

.hero > .wrap {
  display: grid; grid-template-columns: minmax(0, 1.15fr) minmax(0, 0.85fr);
  gap: var(--space-7); align-items: center;
  padding-top: var(--space-8); padding-bottom: var(--space-7);
}
.hero h1 {
  font-size: var(--step-6);
  line-height: 0.88; letter-spacing: var(--track-tight);
  overflow-wrap: normal; hyphens: none; text-wrap: balance;
  margin-bottom: 0.5rem;
}
/* Each line sits at its own slight angle, like separately pressed blocks. */
.hero h1 .l1 { display: block; transform: rotate(-0.7deg); }
.hero h1 .l2 { display: block; transform: rotate(0.4deg) translateX(4px); }
.hero h1 .l3 { display: inline-block; background: var(--accent); color: var(--paper); padding: 2px 16px 8px; transform: rotate(-2.5deg); }
.hero .tagline {
  font-family: var(--font-display); font-size: 1.4375rem;
  line-height: 1.25; text-transform: uppercase; letter-spacing: 0.01em;
  max-width: 22ch; margin: var(--space-4) 0 var(--space-3);
  transform: rotate(-0.3deg);
}
.hero .hero-line { font-size: var(--step-2); line-height: 1.5; max-width: 46ch; margin: 0 0 var(--space-6); text-wrap: pretty; }
.hero-art { display: grid; place-items: center; }
.hero-art img { display: block; width: 100%; height: auto; transform: rotate(-1.5deg); }

/* ---------- Full-bleed statement band ------------------------------------- */

.band > .wrap {
  display: flex; flex-direction: column; align-items: flex-start;
  gap: var(--space-4);
  padding-top: var(--space-6); padding-bottom: var(--space-7);
}
.band p {
  margin: 0; font-family: var(--font-display);
  font-size: var(--step-4); line-height: 1.14; text-transform: uppercase;
  width: 100%; text-wrap: pretty;
}
.band .more { font-size: var(--step-0); font-weight: 700; letter-spacing: 0.18em; text-transform: uppercase; border-bottom: var(--rule-hair) solid var(--accent); padding-bottom: 3px; }

/* ---------- Section heading row ------------------------------------------ */

.head-row {
  display: flex; align-items: baseline; justify-content: space-between;
  gap: var(--space-4); flex-wrap: wrap;
  margin-bottom: var(--space-6);
  padding-bottom: var(--space-3);
  border-bottom: var(--rule-frame) solid var(--ink);
}
.head-row h1, .head-row h2 { font-size: var(--step-4); transform: rotate(-0.4deg); }
.head-row .more { font-size: var(--step-0); font-weight: 700; letter-spacing: 0.18em; text-transform: uppercase; white-space: nowrap; }

/* ---------- Flagship block ----------------------------------------------- */

.flagship-block {
  display: grid; grid-template-columns: minmax(0, 0.9fr) minmax(0, 1.1fr);
  gap: var(--space-6); align-items: center;
  border: var(--rule-frame) solid var(--ink);
  background: var(--panel);
  padding: var(--space-5);
  margin-bottom: var(--space-6);
}
.flagship-block h2, .flagship-block h3 { font-size: var(--step-5); line-height: 0.95; margin-bottom: var(--space-3); }
.flagship-block p { font-size: 1.125rem; line-height: 1.55; max-width: 52ch; margin: 0 0 var(--space-4); text-wrap: pretty; }
.flagship-block .facts { display: flex; align-items: center; gap: var(--space-3); flex-wrap: wrap; margin-bottom: var(--space-4); }
.pill { border: var(--rule-hair) solid var(--ink); padding: 0.375rem 0.75rem; font-size: var(--step-0); letter-spacing: var(--track-label); text-transform: uppercase; }

/* ---------- Masonry product grid ----------------------------------------- */
/* CSS columns, not grid. The design lets tall and short prints interlock rather
   than forcing a uniform row height, which matters when aspect ratios come from
   the artwork itself. */

/* GRID, not `columns`, since 2026-08-20. CSS multi-column fills each column top to bottom before
   starting the next, so a catalogue in a deliberate order read 1-2-3 down the left and 4-5-6 down
   the middle — newest-first means nothing when item 2 is below item 1 rather than beside it. Grid
   places left to right, which is the order the page is actually in.
   What is lost is the interlocking of unequal heights that `columns` gave for free. `align-items:
   start` keeps each card its own height rather than stretching a short one to match a tall one, so
   rows still have a ragged bottom edge instead of a uniform one. */
.masonry {
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: var(--space-5);
  align-items: start;
}
.masonry > * { margin: 0; }

.pcard { display: flex; flex-direction: column; gap: 0.8rem; }
.pcard-media { position: relative; }
/* :not(.pcard-add) is the whole fix for the empty box on hover. This rule frames the artwork,
   and it also matched the "Choose options" anchor, which is a sibling `> a`. At (0,1,1) it beat
   `.pcard-add` at (0,1,0) and applied `background-color: var(--paper)` over paper-coloured text,
   so the control was a blank framed box until a direct hover turned the text ink. Same
   specificity trap as the accent button. */
.pcard-media > a:not(.pcard-add) {
  display: block;
  /* Above `.grain`, and on print paper rather than page paper, for the reasons on `.primary-image`:
     the artwork is transparent where the sheet shows, so this surface IS the paper. */
  position: relative; z-index: 61;
  border: var(--rule-frame) solid var(--ink);
  background-color: var(--print-paper);
  background-size: contain; background-position: center; background-repeat: no-repeat;
}
.pcard-title { font-family: var(--font-display); font-size: 1.3125rem; line-height: 1.12; text-transform: uppercase; color: var(--ink); }
.pcard-title:hover { color: var(--accent); text-decoration: none; }
.pcard-meta {
  display: flex; align-items: center; justify-content: space-between; gap: 0.75rem;
  font-size: var(--step-0); letter-spacing: 0.1em; text-transform: uppercase; color: var(--ink-dim);
}
.pcard-price { font-weight: 700; font-size: 0.9375rem; letter-spacing: 0.02em; color: var(--ink); border-bottom: 3px solid var(--accent); padding-bottom: 1px; }

/* Badges over the artwork: scarcity and availability, read before the click.
   Square and solid because the system bans rounded corners, gradients and shadows, so legibility
   over a busy print has to come from fill and weight. z-index clears the framed anchor at 61.
   --accent-text rather than --accent: this is small type on a fill, where AA wants 4.5:1, and the
   two reds exist precisely so the darker one takes that job (5.54:1 against paper). */
.pcard-badges {
  position: absolute; top: 12px; left: 12px; z-index: 62;
  display: flex; flex-direction: column; align-items: flex-start; gap: 4px;
  pointer-events: none;
}
.pcard-badge {
  padding: 0.25rem 0.5rem;
  font-size: var(--step--1); font-weight: 700;
  letter-spacing: var(--track-label); text-transform: uppercase;
  border: var(--rule-hair) solid var(--ink);
}
.pcard-badge--unique { background-color: var(--accent-text); color: var(--paper); border-color: var(--accent-text); }
.pcard-badge--gone { background-color: var(--ink); color: var(--paper); }

/* Quick-pick: one control per option value, for a single-axis product with few values. Stacked
   at the foot of the card in the same place a single Add button would sit. Each is a real form
   post, so it works with JavaScript blocked. */
.pcard-options {
  position: absolute; left: 12px; right: 12px; bottom: 12px;
  display: flex; flex-direction: column; gap: 6px;
  opacity: 0; transform: translateY(8px);
  transition: opacity 0.14s ease, transform 0.14s ease;
}
.pcard-options .pcard-add {
  position: static; left: auto; right: auto; bottom: auto;
  opacity: 1; transform: none;
  padding: 0.625rem 0.5rem;
  font-size: var(--step--1); letter-spacing: 0.1em;
}
/* focus-within, not just hover: the forms are keyboard-reachable and a control you can focus but
   cannot see is worse than one that is simply absent. */
.pcard:hover .pcard-options,
.pcard:focus-within .pcard-options { opacity: 1; transform: none; }
@media (hover: none) { .pcard-options { opacity: 1; transform: none; } }

/* Hover-reveal add control. Hidden by opacity rather than display so it can be
   focused by keyboard, and forced visible where hover does not exist. */
.pcard-add {
  position: absolute; left: 12px; right: 12px; bottom: 12px;
  font-family: var(--font-text); font-weight: 700;
  font-size: var(--step-0); letter-spacing: 0.16em; text-transform: uppercase;
  padding: 0.875rem 0.625rem;
  border: 3px solid var(--ink);
  background: var(--accent-text); color: var(--paper);
  /* Stated rather than inherited from the .btn rule, which no longer matches this: the
     "Choose options" variant is an <a>, and an anchor does not centre its own text. */
  cursor: pointer; text-align: center;
  opacity: 0; transform: translateY(8px);
  transition: opacity 0.14s ease, transform 0.14s ease;
}
.pcard:hover .pcard-add,
.pcard:focus-within .pcard-add,
.pcard-add:focus-visible { opacity: 1; transform: none; }
@media (hover: none) { .pcard-add { opacity: 1; transform: none; } }

/* ---------- Shop heading + filter bar ------------------------------------ */

.shop-heading > .wrap { padding-top: var(--space-7); padding-bottom: var(--space-6); }
.shop-heading h1 { font-size: var(--step-5); line-height: 0.9; transform: rotate(-0.5deg); }
.shop-heading .intro { font-size: var(--step-2); line-height: 1.5; max-width: 56ch; margin: var(--space-4) 0 0; text-wrap: pretty; }

/* Sticks directly below the header, so the offset is the header's own height.
   The 63px literal this replaces was never the rendered height: Chrome measures
   72.47px at 1440px, so the bar sat 9px inside the header, and at 390px the
   header is 154px, which buried the whole chip row behind it. */
.filters {
  /* 70: above artwork at 61. At 40 a product card scrolled over this bar, the same fault as the
     header — see the layering ladder. */
  position: sticky; top: var(--header-h); z-index: 70;
  background: var(--panel);
  border-bottom: var(--rule-major) solid var(--ink);
}
.filters > .wrap {
  display: flex; align-items: center; gap: var(--space-6); flex-wrap: wrap;
  padding-top: var(--space-4); padding-bottom: var(--space-4);
}
.filter-group { display: flex; align-items: center; gap: 0.875rem; flex-wrap: wrap; }
.filter-group > .label { font-size: var(--step--1); letter-spacing: var(--track-wide); font-weight: 700; }
.filter-count { margin-left: auto; font-size: var(--step-0); letter-spacing: 0.12em; text-transform: uppercase; color: var(--ink-dim); white-space: nowrap; }

/* ---------- Product page ------------------------------------------------- */

.pdp > .wrap {
  display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
  gap: var(--space-7); align-items: start;
  padding-top: var(--space-6); padding-bottom: var(--space-8);
}
/* THE ARTWORK IS NOT THEMED, and both halves of this rule are why.
   `background` is the fixed print paper rather than `--paper`: these PNGs are transparent where the
   sheet shows, so the surface behind them IS the paper, and following the theme turned every cream
   letterform near-black in dark mode.
   `z-index` lifts the frame above `.grain`, which is a fixed full-viewport blend layer at 60. Over
   cream it reads as paper texture; over a photograph it is a filter nobody asked for, multiplying in
   one theme and screening in the other. A print should look like itself in both. */
.primary-image { display: block; position: relative; z-index: 61; width: 100%; height: auto; object-fit: contain; border: var(--rule-frame) solid var(--ink); background: var(--print-paper); }

.pdp h1 { font-size: var(--step-5); margin-bottom: var(--space-3); }
.pdp .price { font-family: var(--font-display); font-size: 2.75rem; line-height: 1; margin-bottom: var(--space-5); }
.pdp .description { margin-top: var(--space-5); }

/* The author's facts on a product page. A two-column grid rather than a run of lines: the values
   line up, which is what makes three of them scannable instead of read. `tabular-nums` because half
   of these are measurements and an edition size, and digits that do not line up in a column read as
   sloppy on a page selling printed objects. */
.product-facts {
  display: grid;
  grid-template-columns: auto minmax(0, 1fr);
  gap: var(--space-2) var(--space-4);
  margin: var(--space-6) 0 0;
  padding-top: var(--space-4);
  border-top: var(--rule-hair) solid var(--rule);
  font-variant-numeric: tabular-nums;
}
.product-facts dt {
  font-family: var(--font-text);
  font-size: var(--step-0);
  letter-spacing: var(--track-label);
  text-transform: uppercase;
  color: var(--ink-dim);
}
.product-facts dd { margin: 0; font-size: var(--step-1); }
.pdp .description p { font-size: 1.125rem; line-height: 1.65; max-width: 68ch; text-wrap: pretty; }
/* Long-form product history: paragraphs plus pulled quotes. The description
   arrives as one HTML blob, so the container is the only styling hook. */
.pdp .description blockquote {
  margin: var(--space-4) 0; padding: 4px 0 4px var(--space-4);
  border-left: 5px solid var(--accent);
}
.pdp .description blockquote p { font-family: var(--font-display); font-size: 1.3125rem; line-height: 1.45; text-transform: none; }
.pdp .unavailable { font-family: var(--font-display); font-size: 1.5rem; text-transform: uppercase; color: var(--accent); }

.option-axis { border: 0; padding: 0; margin: 0 0 var(--space-4); }
.option-axis legend { font-size: var(--step--1); letter-spacing: var(--track-wide); font-weight: 700; text-transform: uppercase; padding: 0; margin-bottom: 0.875rem; }
.option-axis > div { display: flex; gap: 0.75rem; flex-wrap: wrap; }
.option-axis button {
  font-family: var(--font-text); font-size: var(--step-1);
  letter-spacing: var(--track-label); text-transform: uppercase;
  min-width: 58px; padding: 0.875rem 1.5rem;
  border: 3px solid var(--ink); background: transparent; color: var(--ink); cursor: pointer;
}
.option-axis button[aria-pressed="true"] { background: var(--ink); color: var(--paper); }

/* ---------- Cart --------------------------------------------------------- */

.cart-split {
  display: grid; grid-template-columns: minmax(0, 1.6fr) minmax(0, 0.9fr);
  gap: var(--space-6); align-items: start;
  padding-top: var(--space-6); padding-bottom: var(--space-8);
}
.cart-items { list-style: none; padding: 0; margin: 0; display: flex; flex-direction: column; gap: var(--space-4); }
.cart-items > li {
  display: grid; grid-template-columns: 112px minmax(0, 1fr) auto;
  gap: var(--space-4); align-items: start;
  padding-bottom: var(--space-4); border-bottom: var(--rule-hair) solid var(--rule);
}
.cart-items img { display: block; width: 100%; border: 3px solid var(--ink); }
/* Offset off the header, like .filters. The 96px literal this replaces held at
   1440px only by luck: the header measures 72.47px there, so it parked the panel
   23px lower than it needed to sit. It would have been 25px short of the 121px
   header below 980px, which does not bite today only because .cart-split is one
   column under 1120px and a single-column grid area leaves sticky nothing to
   travel through. --space-4 keeps roughly the gap the literal produced. */
.order-summary {
  border: var(--rule-frame) solid var(--ink); background: var(--panel);
  padding: var(--space-5); position: sticky; top: calc(var(--header-h) + var(--space-4));
}
.order-summary > div { display: flex; justify-content: space-between; gap: 1rem; font-size: var(--step-1); margin-bottom: 0.75rem; }
.cart-empty { border: var(--rule-frame) dashed var(--ink); padding: var(--space-6) var(--space-5); text-align: center; }
.cart-empty h1 { font-size: 1.625rem; margin-bottom: 0.75rem; }

/* ---------- Forms -------------------------------------------------------- */

label { display: block; font-size: var(--step--1); letter-spacing: var(--track-label); text-transform: uppercase; font-weight: 700; margin-bottom: 0.5rem; }
/* `password` was absent until 2026-08-20, so every password field on this site fell back to the
   browser's own control: a small white box with a hairline border, beside inputs three times its
   width. It showed up on the console's sign-in form and on the preview gate — the two pages whose
   only job is a password. */
input[type="text"], input[type="email"], input[type="password"], input[type="number"], select, textarea {
  width: 100%; font-family: var(--font-text); font-size: var(--step-1);
  padding: var(--space-3); background: var(--paper);
  border: 3px solid var(--ink); color: var(--ink);
  margin-bottom: var(--space-3);
}
.errors { list-style: none; padding: var(--space-3); margin: 0 0 var(--space-4); border: 3px solid var(--accent); color: var(--accent-text); }

/* ---------- Footer ------------------------------------------------------- */

.site-footer { background: var(--ink); color: var(--paper); }
.site-footer a { color: var(--paper); }
.footer-cols {
  display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
  gap: var(--space-6);
  padding-top: var(--space-7); padding-bottom: var(--space-6);
}
.footer-cols h2 { font-family: var(--font-text); font-size: var(--step--1); letter-spacing: var(--track-wide); color: var(--accent); font-weight: 700; margin-bottom: 1rem; }
.footer-cols nav { display: flex; flex-direction: column; gap: 0.7rem; font-size: var(--step-1); }
/* white-space is the load-bearing part. `.wordmark` sets `nowrap` because the header wordmark
   must never break, and the footer override reset display and size but not that — so a
   four-word store name at 1.5rem ran ~950px wide inside a 200px grid column and painted over
   the next column's heading. */
.site-footer .wordmark {
  font-family: var(--font-display); font-size: 1.5rem; line-height: 1.05;
  display: block; white-space: normal; text-wrap: balance;
  margin-bottom: 1rem;
}
.site-footer .tagline { font-size: var(--step-0); line-height: 1.8; opacity: 0.8; margin: 0; }
.footer-base {
  border-top: var(--rule-hair) solid var(--rule-dark);
  display: flex; justify-content: space-between; gap: var(--space-4); flex-wrap: wrap;
  padding-top: var(--space-4); padding-bottom: var(--space-6);
  font-size: var(--step--1); letter-spacing: 0.08em; opacity: 0.7;
}

/* ---------- Editorial pages ---------------------------------------------- */
/* About, Artists and anything later share one narrow-measure treatment. The
   layout puts `custom` on <body> for these; the hooks are plain elements inside
   .site-main, so authored prose needs no per-page classes.

   The page has to supply the element. Base.astro owns the one main landmark and
   emits no .site-main of its own, so these rules are silently conditional on the
   page carrying class="site-main wrap" — on a div, never a second main.
   about.astro does. Anything else using bodyClass="custom" must too. */

body.custom .site-main { max-width: 72ch; }
body.custom .site-main h2 { font-size: var(--step-4); margin: var(--space-7) 0 var(--space-4); padding-bottom: var(--space-3); border-bottom: var(--rule-frame) solid var(--ink); }
body.custom .site-main h3 { font-size: var(--step-3); margin: var(--space-5) 0 var(--space-3); }
body.custom .site-main p { font-size: var(--step-2); line-height: 1.6; margin: 0 0 var(--space-4); text-wrap: pretty; }
body.custom .site-main blockquote { margin: var(--space-4) 0; padding-left: var(--space-4); border-left: 5px solid var(--accent); }

/* ---------- Responsive --------------------------------------------------- */
/* Breakpoints are literals; CSS cannot tokenize them. Keep in step with the
   comment at the bottom of tokens.css. */

@media (max-width: 1120px) {
  .hero > .wrap,
  .flagship-block,
  .pdp > .wrap,
  .cart-split { grid-template-columns: minmax(0, 1fr); gap: var(--space-6); }
}

@media (max-width: 1080px) { .masonry { grid-template-columns: repeat(2, minmax(0, 1fr)); } }

@media (max-width: 980px) {
  .site-nav { display: none; }
  .mobile-menu { display: block; }
  .site-header > .wrap { flex-wrap: wrap; gap: 0.75rem; padding-left: var(--gutter-sm); padding-right: var(--gutter-sm); }
  .wordmark { font-size: 0.875rem; gap: 7px; }
  /* The MENU drawer's summary row lands here, taking the header from 72.47px to
     a measured 120.53px across 641-980px. */
  :root { --header-h: 121px; }
}

@media (max-width: 720px) { .masonry { grid-template-columns: minmax(0, 1fr); } }

@media (max-width: 700px) { :root { --gutter: var(--gutter-sm); } }

@media (max-width: 640px) {
  .wordmark { white-space: normal; font-size: 0.78rem; line-height: 1.12; }
  .cart-link { padding: 7px 10px; font-size: var(--step--1); }
  /* THE CART SHARES THE WORDMARK'S ROW. It was wrapping onto one of its own, which is what made the
     phone header three stacked bars and 154px — 18% of a 390px viewport, permanently, and the reason
     the shop's filter bar had to be un-stuck.
     The wordmark takes the slack and wraps INSIDE its own box; the cart takes only what it needs and
     does not shrink. No abbreviation and no compact mark: the brand name is the brand, and shortening
     it is a decision for whoever owns it, not a way to save a row. */
  .site-header > .wrap { flex-wrap: nowrap; align-items: flex-start; gap: 0.75rem; }
  .wordmark { flex: 1 1 auto; min-width: 0; }
  .header-end { flex: 0 0 auto; }
  .cart-items > li { grid-template-columns: 72px minmax(0, 1fr); }
  /* MEASURED 2026-08-20, after the cart moved onto the wordmark's row: a constant 116px at
     320, 390, 480 and 640px, where it used to be 116px at 480-640, 154px at 390 and 160px at
     320 depending on how the wordmark wrapped. A header whose height depends on the viewport
     is a header this token can only ever overshoot, which is what 161px was. 117px is the
     measurement plus a pixel.
     Below 640px only scroll-margin-top reads this, since the filter bar goes static on the
     next line. */
  :root { --header-h: 117px; }
  /* Sticky is wrong at this width. The chips wrap onto four rows, so the bar
     measures 229.41px at 390px; pinned under a 154px header, the two would hold
     383px of an 844px screen — 45% of the viewport — before the first product. */
  .filters { position: static; }
  /* The line's third child — line total plus the Remove form — auto-placed into
     the 72px thumbnail track at row 2, where the 155px button overhung it by
     83px. Sending it to the text column puts it under the title instead. */
  .cart-items > li > div:last-of-type { grid-column: 2; }
  .order-summary { position: static; }
}

/* Visually hidden but reachable by screen readers. Used where the design shows a control
   with no visible label. */
.sr-only {
  position: absolute; width: 1px; height: 1px;
  padding: 0; margin: -1px; overflow: hidden;
  clip: rect(0 0 0 0); white-space: nowrap; border: 0;
}

/* Modifier for the skip link: hidden on the same terms as .sr-only until it takes
   focus, then it paints itself over the header. Top of the layering ladder at 90 — it has to clear
   the header, which is now above the artwork — so the first control a keyboard user meets is the
   one thing on the page nothing else can cover. Offsets are logical, so it flips with the writing
   direction. */
.sr-only--focusable:focus {
  width: auto; height: auto;
  margin: 0; padding: var(--space-2) var(--space-3);
  overflow: visible; clip: auto;
  top: 0; inset-inline-start: var(--gutter);
  z-index: 90;
  background: var(--ink); color: var(--paper);
  font-size: var(--step-0); letter-spacing: var(--track-label); text-transform: uppercase;
  outline-color: var(--paper);
}


h2 {
  font-size: var(--step-1);
  line-height: 1.15;
  /* A top margin, which this rule did not have. Measured on the live console: every h2 came out
     with margin 0 above and 18px below, so each section heading sat flush against the bottom border
     of the panel above it and then held 18px of air before its own panel. Read top to bottom that
     groups every heading with the WRONG panel, the one it does not label. This is the "cramped" in
     the report. */
  margin: var(--space-6) 0 var(--space-3);
}

/* Except the first, which has the page's own padding above it already. */
.site-main > h2:first-child { margin-top: 0; }

/* h3 had no margin rule at all, so a sub-heading inside a stage sat flush against the bottom border
   of the panel above it and, on a phone, overlapped it. Same fault the h2 rule above was written to
   fix; h3 only started appearing when the stages went in. Less air than an h2 gets, because it
   divides a stage rather than starting one. */
.site-main h3 {
  font-size: var(--step-1);
  line-height: 1.15;
  margin: var(--space-5) 0 var(--space-2);
}
h1 {
  font-size: var(--step-3);
  line-height: 1.05;
}

/* Six overrides that exist because this console borrows the shop's stylesheet whole, and six
   things in it are right for a shop and wrong for a dense admin page. All six are scoped under
   .site-main so nothing here can reach the storefront.

   1. THE STICKY PILE-UP. `.order-summary` is the shop's cart panel, borrowed here as the generic
      bordered box, and base.css makes it `position: sticky` with a reset only inside
      `@media (max-width: 640px)`. Above 640px — every tablet, every desktop — the console's 42
      panels are siblings in one containing block and each pins at the same offset. Sticky siblings
      do not push each other: every later panel scrolls OVER the pinned earlier one, and the panel
      is opaque, so scrolling this page overwrote its own content in place. The cart genuinely wants
      sticky, so this is an override here and not a change there. */
.site-main .order-summary { position: static; }

/*   2. FINE PRINT THAT IS ACTUALLY PROSE. base.css gives `.fine-print` uppercase with 0.14em
      tracking and no font-size, which suits the two- or three-word labels the shop uses it for.
      This console puts 45 full sentences in it, including every "Edit in the CMS" row and the
      upload page's help text, so it was rendering explanatory paragraphs as tracked capitals.
      Set at body size rather than smaller: the scale's next step down is 11.5px, which is a caption
      size, and a sentence somebody has to act on should not be harder to read than the one above it.
      The step down is colour instead. */
.site-main .fine-print {
  text-transform: none;
  letter-spacing: normal;
  font-size: var(--step-1);
  line-height: 1.55;
  color: var(--ink-dim);
}

/*   3. A ROW THAT WILL NOT WRAP. base.css lays out `.order-summary > div` as a non-wrapping flex
      row with `justify-content: space-between` — correct for a cart line, where the description
      goes left and the price goes right. In here `.order-summary` is the generic bordered box, so
      the upload page's grid of staged thumbnails became one flex row eleven images long that ran
      1400px past the right edge of the window on a desktop and off the side of a phone. Found by
      the parameterised layout test, which is the same borrowed-cart-panel hazard as override 1.
      Wrapping rather than re-styling each box: any bare div in one of these panels wants to wrap,
      and naming them one at a time is a list to forget to update. */
.site-main .order-summary > div { flex-wrap: wrap; }

/*   4. A FILE INPUT THAT IS WIDER THAN THE PHONE. base.css styles text, email, password, number,
      select and textarea, and deliberately not `input[type="file"]` — a file input is a button plus
      a filename and cannot be made to look like a text field. What it also cannot do is fit: its
      intrinsic width is the OS-rendered button label, which on CI's Linux Chromium is wider than on
      macOS, so `#upload-file` was 347px inside a 390px viewport and pushed the page 13px sideways.
      That is why this failed only in CI and never once across six local runs, and why the overflow
      assertion now names the element instead of reporting two numbers.

      `max-width` and not `width`: the control should still be its natural size when there is room
      for it. */
.site-main input[type="file"] { max-width: 100%; box-sizing: border-box; }

/*   5. PANELS SIZED FOR A SHOP. The .order-summary rule carries padding: var(--space-5) — 32px, right for
      a cart panel on a product page and far too much for the 40-odd small boxes this console stacks.
      Worse, the paragraph inside keeps its own 16px margins, so a one-line panel was 32+16 above and
      16+32 below one line of text: measured at 155px tall to say "Nothing is waiting". That is the
      "poorly spaced" half of the report, and it is most of the page's height — the per-product list
      alone was 37 panels at 263px each.

      Two changes, and the second is the one that matters: less padding, and the first and last child
      lose their outer margins so the padding is not doubled by them. */
.site-main .order-summary { padding: var(--space-4); }
.site-main .order-summary > :first-child { margin-top: 0; }
.site-main .order-summary > :last-child { margin-bottom: 0; }

/*   6. A MEASURE. The body class "custom" was removed on 2026-08-20 to stop base.css blowing up the
      headings, and it carried a 72ch max-width with it — which nobody noticed until a screenshot
      of the sign-in form on a wide monitor showed a username field two thousand pixels across. A
      form control as wide as a desk is not a form, and an admin page is read in a column like any
      other prose. Restored here, where it belongs, rather than by putting the class back and taking
      the heading sizes with it. */
.site-main { max-width: 72ch; }

/* ---------------------------------------------------------------------------
   THE THREE STAGES, added 2026-09-01.

   The console was one scroll of nine headings in no particular order, and the question it could not
   answer was the only one anybody opens it to ask: what have I changed, what can the team see, and
   what are customers looking at. So the page is three stages in the order work moves through them,
   and each one says where it stands in a sentence.

   Git's vocabulary is demoted rather than removed. "Commit", "branch", "sha" and the run links are
   all still there, in `details.tech`, because they are what you need the moment something is wrong
   and there is nowhere else to get them. They are just not the first thing an artist reads.
   --------------------------------------------------------------------------- */

/* The strip at the top: three boxes, one per stage, each with a state under its name. Flex rather
   than grid so it folds to one column on a phone without a media query. */
.pipeline {
  list-style: none;
  margin: 0 0 var(--space-6);
  padding: 0;
  display: flex;
  flex-wrap: wrap;
  gap: var(--rule-hair);
  border: var(--rule-frame) solid var(--ink);
  background-color: var(--ink);
}
.pipeline li { flex: 1 1 14ch; padding: var(--space-3); background-color: var(--panel); }
.pipeline .where {
  display: block;
  font-size: var(--step--1);
  letter-spacing: var(--track-label);
  text-transform: uppercase;
  color: var(--ink-dim);
}
.pipeline .state { display: block; font-weight: 700; margin-top: 0.25rem; line-height: 1.25; }
/* One box at a time is the thing to look at. Inverted rather than tinted, because the palette has
   no tint that clears contrast on paper and the system bans the gradients that would hide it. */
.pipeline li[data-attention='yes'] { background-color: var(--accent-text); }
.pipeline li[data-attention='yes'] .where { color: var(--paper); opacity: 0.85; }
.pipeline li[data-attention='yes'] .state { color: var(--paper); }

.stage { margin: 0 0 var(--space-7); }
.stage-step {
  display: block;
  font-size: var(--step--1);
  letter-spacing: var(--track-label);
  text-transform: uppercase;
  color: var(--accent-text);
  margin-bottom: 0.25rem;
}
.stage > h2 { margin-top: 0; }
.stage-sub { margin: 0 0 var(--space-3); color: var(--ink-dim); }

/* The escape hatch for anyone who needs the machinery. Closed by default on purpose: it is the
   answer to "why did that not work", not part of reading the page. */
details.tech {
  border: var(--rule-hair) solid var(--rule);
  padding: var(--space-2) var(--space-3);
  margin-top: var(--space-3);
}
details.tech > summary {
  cursor: pointer;
  font-size: var(--step--1);
  letter-spacing: var(--track-label);
  text-transform: uppercase;
  color: var(--ink-dim);
}
details.tech[open] > summary { margin-bottom: var(--space-2); }
details.tech p { font-size: var(--step-1); line-height: 1.55; color: var(--ink-dim); margin: 0 0 var(--space-2); }
details.tech > :last-child { margin-bottom: 0; }

/*   7. TABLES THAT FIT A PHONE. The accounts page has no table CSS at all and no overflow
      container, so on a phone it pushed the page sideways. Scroll the table, never the document.

      `white-space: nowrap` came off every cell on 2026-09-01. It was there to stop a row folding
      into an unreadable stack, and what it actually did was guarantee the row was wider than the
      72ch column: the Reset password button sat past the right edge with only an overlay
      scrollbar — invisible until touched — to say the table could scroll. It read as clipped,
      because it was. Only the timestamp genuinely must not wrap, and it is short enough now not to
      need protecting. */
/* A page with a data table is not prose and does not want the prose measure. Measured on the live
   accounts page: the column is 705px and the table needs 875px, so it scrolled sideways behind an
   overlay scrollbar that is invisible until touched — it read as clipped, which is what was
   reported. The two action columns are 489px of that and neither can shrink usefully: one holds a
   button, the other a labelled confirmation input.

   Scoped with :has() so only the pages that actually have a table widen, and the prose pages keep
   the measure. 96ch clears 875px with room for a longer username. */
.site-main:has(table) { max-width: 96ch; }
.site-main table { width: 100%; border-collapse: collapse; display: block; overflow-x: auto; }
.site-main th, .site-main td {
  text-align: left;
  padding: var(--space-2) var(--space-3) var(--space-2) 0;
  border-bottom: var(--rule-hair) solid var(--rule);
  vertical-align: middle;
}
/* The action cells shrink to their button and stop the text columns stealing their width. */
.site-main td:has(> form) { width: 1%; white-space: nowrap; }
/* A confirmation input sized for the word it wants typed, not for a browser's 20-character default.
   Measured: the delete cell alone was 254px of an 875px table. Usernames here are short. */
.site-main td input[type="text"] { max-width: 12ch; }
.site-main td form + form { margin-top: var(--space-2); }
/* The label above it is a whole sentence, and in a table cell it must be allowed to fold. */
.site-main td label { white-space: normal; display: block; }

/*   8. THE WORDMARK IS THE WAY BACK. It was a `<span>`, so the console had no home link on any
      page but the front one — from /accounts or /restock the only way back was the browser's back
      button, which is not a thing a page should make somebody reach for. It is an `<a>` now and is
      styled to look exactly as it did. */
.site-header .wordmark { color: inherit; text-decoration: none; display: inline-block; }
.site-header a.wordmark:hover .boxed { background-color: var(--accent-text); color: var(--paper); }
