/* StorCat site stylesheet — the design system.

   Layers, in order: :root tokens extracted from design/StorCat Site.dc.html, the
   DESIGN-04 nav-reservation contract, base/reset, the rules for the chrome that
   site/site-chrome.js injects, and (appended in Task 3) the D-12 primitive layer
   Phase 5 authors against.

   IT MUST NOT CONTAIN: a literal colour outside the :root block below — written
   as a hexadecimal token, as a colour function taking channels (rgb, rgba, hsl,
   hsla, hwb, lab, lch, oklab, oklch, device-cmyk, color), or as one of the 148
   CSS named colours — because the token block is the palette and a rule that
   spells its own colour has escaped it; a second spelling of the nav bar height,
   which is --nav-h and only --nav-h; a media-query width other than
   --nav-breakpoint; or a rule for a page section Phase 5 owns.

   THE THREE FAMILIES ARE ENUMERATED HERE BECAUSE THE SENTENCE USED TO SAY "in ANY
   form" AND THE PATTERN KNEW TWO. That is how the rule escaped twice. The first
   time the sentence named only the hexadecimal form and the ALPHA form got through
   for a whole phase: the nav ground, the drawer overlay and the primary button glow
   each restated a token as rgba() channels, and every one of them read as correct.
   The repair widened the pattern to the four legacy channel functions and widened
   this sentence to "ANY form" — a claim no pattern can honour, since CSS keeps
   adding colour syntaxes — so `white`, `oklch()` and `color()` then sailed through
   under the success line saying none was found (WR-03). The list above is now the
   same list assert_palette_lives_in_root enumerates in tools/check-design.sh, and
   the two are checkable against each other by reading them.

   NOT OFFENDERS, DELIBERATELY: `transparent` and `currentColor`, which are colour
   keywords naming no hue and so cannot be restating a token; and color-mix(), which
   DERIVES from a token and is the promoted form. A literal written inside a
   color-mix() is still caught, because the scan is for the literal and not for the
   function around it. The nav ground, the drawer overlay and the button glow are
   color-mix() over the token each used to restate, at the same percentage, so
   changing a token changes them.

   OUTSIDE THE ASSERTED SET, AND THEREFORE STILL PROSE: CSS system colours
   (Canvas, LinkText and their siblings) and light-dark(). Neither appears in this
   file. Adding one would be a colour this file spells that nothing here checks —
   widen the pattern and this sentence together, or do not add it.

   The pages that consume it carry no inline style element, no inline script element
   and no onclick attribute: the CSP in site/_headers has no unsafe-inline in
   style-src or script-src, so any of the three would be blocked at runtime.

   (D-08, D-10, D-11, D-12, D-13; DESIGN-01, DESIGN-02, DESIGN-04, DESIGN-06,
    DESIGN-08)
*/

:root {
  /* Palette — grounds and surfaces */
  --surface: #fff;              /* card / control ground (mockup x38) */
  --bg: #fbfcfd;                /* page ground (x4) */
  --bg-alt: #f4f8fa;            /* alternate section ground (x4) */
  --bg-chip: #f6f9fb;           /* chip and caption ground (x15) */

  /* Palette — ink and text */
  --ink: #0f1723;               /* darkest ink; also the dark block ground (x7) */
  --ink-2: #233245;             /* heading-adjacent ink (x17) */
  --text: #546375;              /* body copy (x28) */
  --text-2: #465464;            /* nav link rest state (x10) */
  --text-faint: #7d8b9b;        /* captions (x3) */
  --text-on-dark: #9fb0c2;      /* caption and output text on the ink ground (x4) */

  /* Palette — accent */
  --accent: #0d9aae;            /* primary accent (x14) */
  --accent-ink: #0d6b7a;        /* accent as a text/link colour (x8) */
  --accent-bright: #5fd8e8;     /* accent on the ink ground (x7) */
  --accent-tint: #eaf6f8;       /* pill ground */
  --accent-tint-line: #cfe3e8;  /* pill border */
  --selection: #b8ecf3;         /* ::selection ground */

  /* Palette — hairlines */
  --line: #dde5ec;              /* the hairline (x26) */
  --line-2: #e6ecf2;            /* card and download-card border (x13) */
  --line-3: #e9eef3;            /* nav, footer and section edges (x7) */

  /* Radii */
  --r-sm: 6px;                  /* code chip, small inline control */
  --r-md: 10px;                 /* every button */
  --r-lg: 12px;                 /* small card (x12) */
  --r-xl: 14px;                 /* card and large dark block */
  --r-pill: 99px;               /* pill and dot */

  /* Type scale */
  --fs-mono-cap: 12.5px;        /* mono caption (x18) */
  --fs-eyebrow: 13px;           /* eyebrow, footer caption (x10) */
  --fs-sm: 14px;                /* the most common size in the mockup (x25) */
  --fs-md: 15px;                /* button label, lead-adjacent copy (x12) */
  --fs-lg: 16px;                /* long-form body copy (x7) */
  --fs-h3: 18px;                /* sub-heading */
  --fs-h3-lg: 21px;             /* card heading, nav wordmark (x3) */
  --fs-stat: 34px;              /* secondary stat figure (x4) */
  --fs-stat-lg: 38px;           /* primary stat figure (x4) */
  --fs-display: clamp(38px, 6.4vw, 70px);   /* the one page-title size */
  --fs-h2: clamp(28px, 3.6vw, 40px);        /* every section heading */
  --fs-lead: clamp(16px, 1.7vw, 19px);      /* the one lead paragraph size */

  /* Faces — both already loaded and already allowlisted by the CSP */
  --font-sans: 'Manrope', ui-sans-serif, system-ui, -apple-system, 'Segoe UI', Helvetica, Arial, sans-serif;
  --font-mono: 'IBM Plex Mono', ui-monospace, 'SF Mono', Menlo, Consolas, monospace;

  /* Layout rhythm */
  --container: 1180px;                      /* the mockup's one content width (x12) */
  --gutter: clamp(18px, 4vw, 40px);         /* the mockup's one horizontal gutter (x12) */
  --section-y: clamp(52px, 7vw, 80px);      /* the mockup's one vertical section rhythm (x6) */

  /* Chrome geometry — one number each, consumed in several places */
  --nav-h: 72px;                /* bar height AND the reservation; see the block below */
  --nav-breakpoint: 860px;      /* drawer threshold; site-chrome.js reads this token */

  /* Space ladder */
  --space-1: 8px;
  --space-2: 12px;
  --space-3: 16px;
  --space-4: 20px;
  --space-5: 24px;
  --space-6: 32px;
  --space-7: 40px;
}

/* ==========================================================================
   THE DESIGN-04 CONTRACT — the whole reason this phase does not reuse the
   mockup's scrolling nav treatment.

   The bar is taken out of flow and its height is reserved on the body IN THIS
   STYLESHEET, so the reservation applies at first paint, before site-chrome.js
   has run and before the nav element exists. That is what makes the injected
   chrome cost zero layout shift.

   --nav-h is declared once above and consumed by exactly four declarations
   below. One number, four uses, so the bar and the reservation cannot disagree.
   Do NOT express the bar height as padding on an inner element and do NOT
   restate 72px anywhere: a second spelling is a second source of truth.
   ========================================================================== */

html {
  scroll-behavior: smooth;
  scroll-padding-top: var(--nav-h);   /* in-page anchors clear the fixed bar */
}

body {
  padding-top: var(--nav-h);          /* THE RESERVATION */
}

.nav {
  position: fixed;                    /* out of flow, so injecting it shifts nothing */
  inset: 0 0 auto 0;
  height: var(--nav-h);               /* THE BAR */
  box-sizing: border-box;
}

.nav-noscript {
  position: absolute;
  inset: 0 0 auto 0;
  height: var(--nav-h);               /* the no-JS list occupies the same reserved band */
}

/* ==========================================================================
   Base and reset
   ========================================================================== */

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

body {
  margin: 0;
  font-family: var(--font-sans);
  background: var(--bg);
  color: var(--ink);
  -webkit-font-smoothing: antialiased;
}

::selection {
  background: var(--selection);
  color: var(--ink);
}

a {
  color: var(--accent-ink);
  text-decoration: none;
}

a:hover {
  color: var(--accent);
  text-decoration: underline;
}

/* height: auto is not decoration. HOME-06 requires an explicit width/height pair
   on every product image so the browser can reserve the box before the bytes
   arrive; without this declaration max-width shrinks the rendered WIDTH while the
   height attribute pins the original pixel HEIGHT, so every screenshot renders
   stretched and the aspect-ratio hint the attributes exist to provide is defeated
   too. The constraint and the intrinsic ratio have to be able to agree. */
img {
  max-width: 100%;
  height: auto;
}

h1 {
  font-size: var(--fs-display);
  font-weight: 800;
  letter-spacing: -0.04em;
  line-height: 1.04;
  margin: 0 0 var(--space-5);
  text-wrap: balance;
}

h2 {
  font-size: var(--fs-h2);
  font-weight: 800;
  letter-spacing: -0.035em;
  line-height: 1.1;
  margin: 0 0 var(--space-3);
}

h3 {
  font-size: var(--fs-h3-lg);
  font-weight: 800;
  letter-spacing: -0.02em;
  margin: 0 0 var(--space-2);
}

/* D-13: ONE focus rule for every interactive element, and it is the only one in
   this file. Deliberately NOT paired with a suppression of the plain :focus ring
   — suppressing that removes the ring entirely in any engine lacking the
   modality-aware selector, which is an accessibility regression nothing here
   would report.

   THIS RULE DECLARES THE RING AND NOTHING ABOUT THE ELEMENT'S OWN GEOMETRY, and
   that is a correction rather than an oversight. It used to carry a
   `border-radius: 2px` alongside the outline. What that declaration was reaching
   for is a rounded RING; what it does is round the ELEMENT, because a
   border-radius in a rule matched by the element applies to the element. Measured
   in 03-REVIEW.md § WR-05: on keyboard focus the square brand mark grew a 2px
   radius, and the buttons and the pill COLLAPSED from their own --r-md and
   --r-pill down to 2px — every interactive element on the site visibly changed
   shape the moment it received focus. Every engine this site targets already
   draws the outline following the element's own radius, so the declaration was
   both unwanted and unnecessary. Do not reintroduce it: if the ring's corners
   ever need adjusting, that is what the element's own radius does, and a
   declaration here reshapes the thing being ringed.

   The defect was keyboard-only, so no screenshot, no mockup comparison and
   nothing in the standing gate set could see it — it was found by reading, and
   confirmed by a browser measurement of the computed radius in both states.
   tools/check-design.sh asserts that this rule EXISTS; it does not assert the
   absence of a geometry declaration inside it, so reintroducing one would ship
   green. That is why this paragraph is here rather than a gate clause. */
:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
}

/* ==========================================================================
   Chrome — the nav, drawer, overlay, footer, skip link and no-JS fallback that
   site/site-chrome.js injects, plus the two primitives the chrome itself
   consumes (.btn-nav and .mono-caption). The third, .container, is the
   mockup's canonical content block and is defined once in the primitive layer
   at the foot of this file.
   ========================================================================== */

.nav {
  z-index: 20;
  background: color-mix(in srgb, var(--bg) 88%, transparent);
  backdrop-filter: blur(12px);
  -webkit-backdrop-filter: blur(12px);
  border-bottom: 1px solid var(--line-3);
}

/* No flex-wrap. Wrapping is exactly what gives the mockup's own nav three
   different heights (72, 116 and 149px measured), which the reservation above
   cannot express as one number. */
.nav-inner {
  max-width: var(--container);
  margin: 0 auto;
  height: 100%;
  padding: 0 var(--gutter);
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-2);
}

.brand {
  display: flex;
  align-items: center;
  gap: 11px;
  color: var(--ink);
}

.brand:hover {
  color: var(--ink);
  text-decoration: none;
}

/* The real app icon, delivered through CSS rather than an injected img src.
   tools/stamp-busters.sh reads url() tokens and its find set is html plus css,
   so this reference is stamped, hash-checked by the cache-buster gate and
   resolvability-checked by the content gate. A reference living inside
   site-chrome.js would be invisible to all three. Never type the ?v= value. */
.brand-mark {
  width: 30px;
  height: 30px;
  flex: none;
  background-image: url(images/appicon.png?v=6a1e7e97);
  background-size: contain;
  background-repeat: no-repeat;
}

.brand-word {
  font-size: var(--fs-h3-lg);
  font-weight: 800;
  letter-spacing: -0.03em;
}

.nav-links {
  display: flex;
  align-items: center;
  gap: clamp(14px, 2.2vw, 28px);
  font-size: var(--fs-sm);
  font-weight: 500;
}

.nav-links a {
  color: var(--text-2);
}

.nav-links a.active,
.nav-links a:hover {
  color: var(--accent-ink);
  text-decoration: none;
}

.nav-right {
  display: flex;
  align-items: center;
  gap: var(--space-2);
}

.btn-nav {
  display: inline-flex;
  align-items: center;
  background: var(--ink);
  color: var(--surface);
  font-size: var(--fs-sm);
  font-weight: 600;
  padding: 10px 20px;
  border-radius: var(--r-md);
}

.btn-nav:hover {
  color: var(--surface);
  text-decoration: none;
}

.nav-toggle {
  display: none;
  align-items: center;
  justify-content: center;
  width: 40px;
  height: 40px;
  padding: 0;
  background: none;
  border: none;
  border-radius: var(--r-sm);
  color: var(--ink-2);
  cursor: pointer;
}

/* Drawer geometry. The off-screen panel is still in the tab order by virtue of
   being translated rather than hidden. That is closed in JS, not here, because
   the inert property also removes the panel from the accessibility tree in the
   same write, and CSS has no equivalent.

   site-chrome.js carries inert on BOTH sides of the drawer's state, and the two
   halves arrived at different times. The closed panel has been inert since
   03-01. The OPEN state was measured in 03-VERIFICATION.md as uncontained —
   main.inert === false and .site-footer.inert === false while the drawer was
   open, so Tab from the last drawer link reached a page link this panel visually
   covers, with body scroll locked so the visitor could not see where focus went.
   Plan 03-09 closed it: openDrawer marks main and .site-footer inert and
   closeDrawer clears both before focus returns to the toggle, on all four close
   paths.

   WHAT THAT DOES AND DOES NOT DO, measured rather than assumed. Tab from the
   last drawer link no longer reaches a covered page link — main and the footer
   are inert, so nothing in them is tabbable. It reaches the END of the document
   instead and focus passes to the browser's own UI, because the drawer is the
   last tabbable region left. This is containment, not a wrap-around trap: there
   is no Tab keydown handler cycling focus back to the first drawer link, and
   adding one would be a second mechanism to keep correct across four close
   paths. The visitor can always see where focus is, which is what DESIGN-06
   promises; they are not silently parked on something the overlay hides.

   THE CONDITION THAT MEASUREMENT RESTS ON, stated because the paragraph above
   reads as an unconditional fact and is not one. The closed panel is present in
   the DOM at every viewport width and is hidden only by a transform, so it is
   still laid out and its links are still real, focusable elements. The single
   thing keeping them out of the tab order — closed OR open, both sides of the
   state — is the `inert` attribute site-chrome.js writes. Nothing in CSS
   contributes to it; the `transform` below is WHY the containment is needed, not
   HOW it is achieved. So the measurement above holds on engines that support
   `inert`, and only on those. On an engine without support the attribute is inert
   in the other sense: the off-screen links join the tab order, the open-state
   containment silently does nothing, and Tab from the last drawer link reaches a
   page link this panel visually covers with body scroll locked. Nothing detects
   the absence and nothing reports it.

   NO FALLBACK IS IMPLEMENTED, AND THAT IS A DECISION RATHER THAN AN OMISSION. A
   fallback that cannot be driven red is not evidence, and no engine available to
   this project can drive the no-support branch — a guard that can only ever print
   green is the exact shape this phase keeps re-shipping and then finding in
   review. It would also be selective: this stylesheet already depends on
   capabilities of the same support era for its shipped palette, so guarding one
   and not the other is theatre with a maintenance cost. WHAT WOULD CHANGE THE
   DECISION: acquiring a way to DRIVE the no-support branch, at which point the
   fallback becomes worth building because it becomes testable. Until then the
   honest artifact is the stated condition, which is this paragraph. Plan 03-09
   recorded the containment decision; this records what it rests on. */
.mobile-drawer {
  position: fixed;
  top: 0;
  right: 0;
  width: min(82vw, 320px);
  height: 100vh;
  z-index: 60;
  padding: var(--space-5);
  display: flex;
  flex-direction: column;
  gap: var(--space-1);
  background: var(--surface);
  border-left: 1px solid var(--line);
  transform: translateX(100%);
  transition: transform .25s ease;
}

.mobile-drawer.open {
  transform: translateX(0);
}

.mobile-drawer-close {
  align-self: flex-end;
  background: none;
  border: none;
  border-radius: var(--r-sm);
  color: var(--text-faint);
  cursor: pointer;
  line-height: 1;
  padding: var(--space-1);
}

.mobile-drawer-link {
  color: var(--text-2);
  font-size: var(--fs-lg);
  font-weight: 600;
  padding: var(--space-2) 0;
  border-bottom: 1px solid var(--line-3);
}

.mobile-drawer-link:hover,
.mobile-drawer-link.active {
  color: var(--accent-ink);
  text-decoration: none;
}

.nav-overlay {
  position: fixed;
  inset: 0;
  z-index: 40;
  background: color-mix(in srgb, var(--ink) 40%, transparent);
  opacity: 0;
  pointer-events: none;
  transition: opacity .25s ease;
}

.nav-overlay.open {
  opacity: 1;
  pointer-events: auto;
}

/* D-03: the no-JS fallback is a plain link list, deliberately not a replica of
   the nav. It sits in the band the reservation already holds open, so a page
   with scripting disabled has chrome in the same place and no shift either. */
.nav-noscript {
  z-index: 20;
  display: flex;
  align-items: center;
  gap: var(--space-5);
  margin: 0;
  padding: 0 var(--gutter);
  list-style: none;
  font-size: var(--fs-sm);
  font-weight: 500;
  background: var(--bg);
  border-bottom: 1px solid var(--line-3);
}

.nav-noscript a {
  color: var(--text-2);
}

.site-footer {
  margin-top: var(--section-y);
  border-top: 1px solid var(--line-3);
  background: var(--bg);
  font-size: var(--fs-eyebrow);
  color: var(--text-faint);
}

/* The footer keeps its own vertical rhythm rather than the section rhythm the
   primitive layer gives .container in Task 3. */
.site-footer .container {
  padding-top: var(--space-6);
  padding-bottom: var(--space-6);
  display: flex;
  align-items: center;
  justify-content: space-between;
  flex-wrap: wrap;
  gap: var(--space-3);
}

.site-footer p {
  margin: 0;
}

.site-footer nav {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-5);
}

.site-footer a {
  color: var(--text-faint);
}

.site-footer a:hover {
  color: var(--accent-ink);
}

.mono-caption {
  font-family: var(--font-mono);
  font-size: var(--fs-mono-cap);
  color: var(--text-faint);
}

/* Keyed on :focus, not :focus-visible, so it cannot fail to appear under a
   user-agent modality heuristic. z-index clears the nav's 20 so the two share
   the top band without colliding. */
.skip-link {
  position: absolute;
  left: -9999px;
  top: 0;
  z-index: 100;
  background: var(--surface);
  color: var(--accent-ink);
  border: 1px solid var(--line);
  border-radius: var(--r-md);
  padding: var(--space-1) var(--space-3);
  font-size: var(--fs-sm);
  font-weight: 600;
}

.skip-link:focus {
  left: var(--gutter);
  top: var(--space-1);
}

.page-placeholder {
  font-size: var(--fs-lead);
  line-height: 1.6;
  color: var(--text);
  max-width: 56ch;
  margin: 0;
}

/* The one place besides --nav-breakpoint where the breakpoint is a number: a
   media query cannot read a custom property. THIS LITERAL MUST EQUAL
   --nav-breakpoint in the token block above. site-chrome.js does not repeat it
   — it reads the token through getComputedStyle. */
@media (max-width: 860px) {
  .nav-links,
  .nav-right {
    display: none;
  }

  .nav-toggle {
    display: inline-flex;
  }
}

/* THE REDUCED-MOTION ESCAPE. Three declarations in this file animate:
   scroll-behavior: smooth on html, the drawer transform transition and the
   overlay opacity transition. A visitor who has asked their operating system
   for less motion gets none of them.

   THE GUARD IS GLOBAL RATHER THAN PER RULE, deliberately. A per-rule override
   has to be extended by hand for every animating rule Phase 5 adds, and the
   rule nobody remembers to extend it for is the one that animates somebody who
   asked it not to. The wildcard below covers declarations that do not exist
   yet, which is the whole point: the two transitions above arrived in a phase
   carrying an accessibility contract and neither author thought about this.

   IT MOVES NO RENDERED VALUE. Only durations, iteration count and scroll
   behaviour appear here — no layout, no colour, no spacing — so the page at
   rest under the preference is pixel-identical to the page at rest without it.
   A human compares this stylesheet against the mockup under D-11, and a motion
   guard that also moved a value would be a design change smuggled inside an
   accessibility fix. Keep it that way.

   .01ms rather than 0s: a zero duration can skip transitionend entirely, and
   near-zero keeps the event firing for anything that ever comes to depend on
   it while still being imperceptible.

   !important is required, not sloppy: these override component rules of equal
   or greater specificity, and a preference the visitor set at the OS level
   outranks anything this file says about how a drawer should slide.

   THIS QUERY CARRIES NO WIDTH FEATURE, which is what keeps it outside the
   breakpoint pairing above. assert_breakpoint_is_one_number considers only
   width-bearing queries and assert_motion_has_a_reduced_escape considers only
   this one, so neither claims the other queries. Deleting this block turns
   tools/check-design.sh red — it is asserted precisely because its absence is
   invisible to anyone not running with the preference set. */
@media (prefers-reduced-motion: reduce) {
  html {
    scroll-behavior: auto;
  }

  *,
  *::before,
  *::after {
    transition-duration: .01ms !important;
    animation-duration: .01ms !important;
    animation-iteration-count: 1 !important;
  }
}

/* ==========================================================================
   THE D-12 PRIMITIVE LAYER — the vocabulary Phase 5 writes sections against,
   so it does not invent one per section. Every value below traces to a quoted
   line of design/StorCat Site.dc.html, expressed through the tokens above: no
   new literal colour, radius or type size appears past this point.

   ------------------------------------------------------------------------
   D-11 NORMALIZATION LIST — the sixteen decisions that turned the mockup's
   ~30 colours, 8 radii and 15 sizes into the token block at the top of this
   file. A reviewer looking at a colour is looking here. Counts are mockup
   occurrences.

    1  #d5dee7 x1, #dbe4ee x1, #dae4ea x1  -> --line #dde5ec
       LOOK HERE FIRST. #d5dee7 is the hero ghost-button border (:53), a
       control at the top of the home page. Three hairlines within three units
       of each other are one hairline.
    2  radius 18px x1, 16px x1             -> --r-xl 14px
       LOOK HERE SECOND. 18px is the 1180px dark stat band (:81), the largest
       corners on the site. If rejected, a fifth --r-2xl: 18px reverses it in
       one line.
    3  13.5px x7                           -> --fs-eyebrow 13px
       The largest snap by count, and snapped DOWN: both uses (:83, :341) sit
       below body text in the hierarchy, so rounding up would put them at body
       size.
    4  17.5px x5                           -> --fs-h3 18px      half pixel buys nothing
    5  14.5px x3                           -> --fs-sm 14px      collapses into the x25 size
    6  15.5px x1, 16.5px x1                -> --fs-md 15px, --fs-lg 16px   one-off half steps
    7  20px x1 (nav wordmark)              -> --fs-h3-lg 21px
       The wordmark grows 1px. It does not move --nav-h: the measured 39px
       content row is set by the button and the 30px icon, not the wordmark.
    8  clamp(28px,3.8vw,42px), clamp(26px,3.2vw,34px) -> --fs-h2 clamp(28px,3.6vw,40px)
       Three section-heading clamps within 2px of each other at both ends.
    9  #eef3f7 x2, #eef2f6 x1              -> --bg-chip #f6f9fb   three grounds inside three units
   10  #e2e8ee x3                          -> --line-2 #e6ecf2    download-card border joins the card
   11  #8496a8 x3                          -> --text-on-dark #9fb0c2  terminal text joins caption-on-dark
   12  #e8f0f4 x1                          -> --accent-tint #eaf6f8   pill ground
   13  #ffffff x1                          -> --surface #fff     same colour, one spelling
   14  radius 4px x2, 7px x4               -> --r-sm 6px         small radii collapse
   15  radius 8px x1 (nav), 9px x2 (hero)  -> --r-md 10px        every button radius is one number
   16  the ten theme swatches at :196-:236 -> NOT TOKENIZED
       They are Dracula, Nord, Solarized Dark and Light, One Dark, Monokai,
       GitHub Dark, Gruvbox and the two StorCat defaults: content data
       depicting the app's themes, not palette. Snapping them would render
       Nord and One Dark the same colour. Phase 5 authors them as literals.

   Kept verbatim, no normalization: every colour, radius and size listed in
   the token block above with an occurrence count.
   ------------------------------------------------------------------------

   ONE PRIMITIVE IS STILL DELIBERATELY ABSENT, NOT FORGOTTEN. The mockup has three
   single-use blocks where the Premature Abstraction rule bites: the pill, the
   terminal-output block and the stat band. The stat band was the second of the
   three and is no longer absent — plan 05-04 authored the home page's band and
   defined .stat-band and .stat alongside its content, which is exactly the
   sequence this paragraph specified. The terminal-output block is the one that
   remains: the home page shows real recorded terminal frames as images instead,
   so nothing yet needs a block that imitates one. The pill is defined below for Phase
   5's version pills alone, and it costs five declarations. NO PAGE IN THIS TREE
   USES IT, or any of the other eight primitives: the four shipped pages render a
   heading and a placeholder line and reach only for .container, .skip-link,
   .nav-noscript and .page-placeholder. An earlier version of this comment said
   this phase's own docs page reached for the pill; 03-VERIFICATION.md measured
   every primitive as zero-use, and docs.html does not use it. The primitive stays
   as deliberate Phase 5 vocabulary under D-11 and D-12 — kept for a reason that
   is true rather than one that reads better. The other two are single-use blocks whose
   CONTENT is being rewritten around v3.0.0, so styling them now is CSS for
   content that does not exist yet. Phase 5 defines them alongside the copy.

   ------------------------------------------------------------------------
   TWO TOKENS ARE DECLARED AND NEVER REFERENCED, ON PURPOSE — the same
   Chesterton's fence as the primitives above, one level down. The block at the
   top of this file is the D-11 vocabulary Phase 5 writes sections against, so a
   token whose only consumer is a section nobody has authored yet has zero
   references by construction. This note said SIX until plan 05-04; four of them
   now have consumers and the count moved with them, because a count that does
   not move is a count the next reader has to re-derive before trusting anything
   under it. Recounted over this file, `var(--token)` occurrences, by enumeration
   rather than transcribed from a review:

     --fs-h3           sub-heading, D-11 normalization #4 (17.5px x5 -> 18px)
     --fs-stat         secondary stat figure (x4)

   FOUR OF THE ORIGINAL SIX HAVE SINCE BEEN CONSUMED and are struck from the list
   above rather than left in it, because a list that names a token any grep can
   show is referenced teaches the next reader to distrust the whole note.
   --space-4 landed in the download-page rows (plan 05-03); --accent-bright,
   --text-on-dark and --fs-stat-lg landed in the home page's stat band (plan
   05-04), which is the content this note predicted they were waiting for. The two
   that remain are still measured mockup vocabulary and still have no consumer.

   Every one of them has a mockup occurrence count above zero, which is the
   justification: they are not speculative, they are measured vocabulary for
   copy that lands in Phase 5. The stat pair and --accent-bright belong to the
   dark stat band and the terminal block — the two primitives named as
   deliberately absent above — so deleting the tokens and the primitives is the
   same decision, and it is Phase 5's to make once the content exists.

   --nav-breakpoint IS THE EXCEPTION AND IS NOT IN THAT LIST. It also has zero
   `var()` references in this file, which makes it look identical to the six by
   any grep, and it is NOT unused: site-chrome.js reads it through
   getComputedStyle().getPropertyValue('--nav-breakpoint') to build its
   matchMedia query, throwing if it is unreadable, and tools/check-design.sh
   asserts under DESIGN-05 that it is declared exactly once and that every
   width-bearing media query in this file switches on its value. Deleting it
   breaks the drawer at runtime and turns the gate red. A reference through
   computed style is invisible to a reference count; that is why this sentence
   exists.
   ------------------------------------------------------------------------

   Nothing below styles a page section. Phase 5 owns that.
   ========================================================================== */

/* The mockup's canonical content block, repeated verbatim at :122, :139, :186,
   :245, :287 and :313. Width, gutter and vertical rhythm in one class. */
.container {
  max-width: var(--container);
  margin: 0 auto;
  padding: var(--section-y) var(--gutter);
}

/* A full-bleed band that declares its own ground; the .container inside it
   keeps the rhythm. A section that does not say which ground it is on is a
   section that inherits one by accident. */
.section {
  background: var(--bg);
}

.section--alt {
  background: var(--bg-alt);
  border-top: 1px solid var(--line-3);
  border-bottom: 1px solid var(--line-3);
}

/* The two rhythm variants the mockup actually uses (:44 and :61), as modifiers
   rather than as new tokens — a one-off padding is not a scale. */
.section--hero {
  background: linear-gradient(180deg, var(--bg-alt) 0%, var(--bg) 100%);
}

.section--hero > .container {
  padding-top: calc(var(--section-y) + var(--space-1));
  padding-bottom: 0;
}

.section--features > .container {
  padding-top: calc(var(--section-y) - var(--space-1));
  padding-bottom: calc(var(--space-7) + var(--space-3));   /* fixed, not rhythmic */
}

/* :62 */
.card {
  background: var(--surface);
  border: 1px solid var(--line-2);
  border-radius: var(--r-xl);
  padding: var(--space-6);
}

/* :156 */
.card--sm {
  border-radius: var(--r-lg);
  padding: var(--space-5);
}

/* :63 */
.eyebrow {
  font-size: var(--fs-eyebrow);
  font-weight: 700;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--accent);
  margin-bottom: var(--space-3);
}

/* :46 */
.pill {
  display: inline-flex;
  align-items: center;
  gap: var(--space-1);
  background: var(--accent-tint);
  border: 1px solid var(--accent-tint-line);
  border-radius: var(--r-pill);
  color: var(--accent-ink);
  font-size: var(--fs-mono-cap);
  font-weight: 600;
  padding: var(--space-1) var(--space-3);
}

/* :52. Two primary treatments exist in the mockup, not one variant of one: the
   nav's ink-on-ink button (.btn-nav, above with the chrome) and this accent one
   with a glow. They are siblings, because neither is a variation of the other.
   .btn-primary is the accent shape, the one Phase 5 will reach for most. */
.btn-primary {
  display: inline-flex;
  align-items: center;
  background: var(--accent);
  color: var(--surface);
  font-size: var(--fs-md);
  font-weight: 700;
  padding: var(--space-3) var(--space-6);
  border-radius: var(--r-md);
  box-shadow: 0 8px 22px color-mix(in srgb, var(--accent) 28%, transparent);
}

.btn-primary:hover {
  color: var(--surface);
  text-decoration: none;
}

/* :53 */
.btn-ghost {
  display: inline-flex;
  align-items: center;
  background: var(--surface);
  border: 1px solid var(--line);
  color: var(--ink-2);
  font-size: var(--fs-md);
  font-weight: 600;
  padding: var(--space-3) var(--space-5);
  border-radius: var(--r-md);
}

.btn-ghost:hover {
  color: var(--ink-2);
  text-decoration: none;
}

/* :143 — an inline command or flag inside running prose. Padding in em so the
   chip scales with whatever size the surrounding copy is set at. */
.code-chip {
  font-family: var(--font-mono);
  font-size: var(--fs-sm);
  background: var(--bg-chip);
  color: var(--ink-2);
  padding: 0.2em 0.45em;
  border-radius: var(--r-sm);
}

/* ------------------------------------------------------------------------
   Download-page primitives (Phase 5, D-05/D-06). Appended after .code-chip,
   at the end of the primitive layer, and expressed entirely in the tokens
   above: no new token, no literal colour, and no raw length where a --space-*
   or --r-* already names one.
   ------------------------------------------------------------------------ */

/* One platform's whole story in one block — what the build is, the rows you can
   take, and the command that goes with them. The nine artifacts split 2 macOS /
   2 Windows / 5 Linux, which is lopsided enough that a flat nine-row table would
   read as a manifest; a tile absorbs an uneven split where a table does not. */
.dl-tile {
  background: var(--surface);
  border: 1px solid var(--line-2);
  border-radius: var(--r-xl);
  padding: var(--space-6);
  margin-bottom: var(--space-6);
}

/* The rows, grouped inside one hairline. Individual rows are separated from each
   other rather than boxed each: the panel already declares the boundary. */
.dl-panel {
  border: 1px solid var(--line-2);
  border-radius: var(--r-lg);
  margin: var(--space-5) 0;
}

.dl-opt {
  display: flex;
  align-items: center;
  justify-content: space-between;
  flex-wrap: wrap;
  gap: var(--space-4);
  padding: var(--space-4) var(--space-5);
}

.dl-opt + .dl-opt {
  border-top: 1px solid var(--line-2);
}

/* min-width: 0 is load-bearing, not a reset. A flex item refuses to shrink below
   its content's min-content width by default, and an artifact filename is one
   long unbreakable token — without this the row overflows its panel at narrow
   viewports and the overflow-wrap below never gets the chance to apply. */
.dl-opt .info {
  min-width: 0;
}

.dl-opt .name {
  color: var(--ink-2);
  font-size: var(--fs-md);
  font-weight: 700;
}

/* THE LINE THE CONTENT GATE READS. Its filename and its size figure sit on one
   physical line of source with the digits and the unit adjacent plain text, which
   is what makes check-content.sh assertion 3 examine this row at all. A long
   filename is therefore made to fit by wrapping here, in CSS, and never by a line
   break in the markup. */
.dl-opt .meta {
  color: var(--text-faint);
  font-family: var(--font-mono);
  font-size: var(--fs-mono-cap);
  margin-top: var(--space-1);
  overflow-wrap: anywhere;
}

/* A command the visitor is meant to run, beside the button that copies it. */
.dl-cmd {
  display: flex;
  align-items: center;
  justify-content: space-between;
  flex-wrap: wrap;
  gap: var(--space-3);
  background: var(--bg-chip);
  border: 1px solid var(--line-2);
  border-radius: var(--r-md);
  padding: var(--space-3) var(--space-4);
}

.dl-cmd code {
  color: var(--ink-2);
  font-family: var(--font-mono);
  font-size: var(--fs-sm);
  overflow-wrap: anywhere;
}

/* Decoration, not content: it is what a shell prompt looks like, and a visitor who
   pastes it into a shell has pasted a syntax error. It is safe to show because the
   copy button carries its payload in a data attribute rather than reading this
   element, so what gets copied cannot include it. user-select keeps a manual
   drag-select honest too. */
.dl-cmd .prompt {
  color: var(--text-faint);
  margin-right: var(--space-1);
  user-select: none;
}

/* Rendered only where it can actually work. The chrome script feature-detects the
   clipboard API once and REMOVES these buttons when it is absent rather than
   binding a listener that cannot succeed — so this rule never styles a control
   that lies. The command stays visible and selectable either way. */
.copy {
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: var(--r-sm);
  color: var(--ink-2);
  cursor: pointer;
  font-family: var(--font-sans);
  font-size: var(--fs-eyebrow);
  font-weight: 600;
  padding: var(--space-1) var(--space-3);
}

.copy:hover {
  border-color: var(--accent);
  color: var(--accent-ink);
}

.copy.copied {
  background: var(--accent-tint);
  border-color: var(--accent-tint-line);
  color: var(--accent-ink);
}

/* The explanatory line that sits beside a row or a command and says what the thing
   above it actually is — the trade-off the AppImage's size buys, the distributions
   the Debian package was built for, the command name each channel installs. It is a
   note rather than body copy because it answers a question the row raised, so it is
   set quieter and tighter than the tile's own prose and reads as an aside rather than
   as a second paragraph competing with the first. */
.dl-note {
  color: var(--text-faint);
  font-size: var(--fs-sm);
  line-height: 1.55;
  margin: var(--space-3) 0 0;
}

/* Consecutive notes are one stack, not two paragraphs with a paragraph gap between
   them: the AppImage trade-off and the archive contents are two answers to the same
   question and should read as a list of asides. */
.dl-note + .dl-note {
  margin-top: var(--space-2);
}

/* ------------------------------------------------------------------------
   Home-page layout (Phase 5, plan 05-04; D-16, D-17, D-20, D-21, D-22).
   Appended after the download-page primitives and expressed in the same
   tokens: no new token, no literal colour, and no raw length where a
   --space-* or --r-* already names one. The two raw pixel values below are
   grid track minimums, which no token in this file names.
   ------------------------------------------------------------------------ */

/* The mockup centres its hero (:45) and centres nothing else. The modifier
   already exists and already owns the hero's rhythm, so the alignment belongs
   with it rather than in a second class the markup would have to remember. */
.section--hero > .container {
  text-align: center;
}

/* RUNNING COPY DECLARES ITS OWN MEASURE, because this stylesheet has no element
   rule for p and deliberately never grew one - the chrome owns no prose, and a
   global p rule would reach into the download page's tiles, which were authored
   and verified against the browser default. So the measure is declared here, for
   the two blocks that hold most of the home page's prose plus a class for the
   paragraphs that stand alone. 68ch is a line length, not a spacing step; no
   --space-* token names it and none should. */
.prose,
.feature-grid p,
.faq-item p,
.docs-layout article p,
.docs-layout article li {
  color: var(--text);
  font-size: var(--fs-lg);
  line-height: 1.7;
  margin: 0 0 var(--space-3);
  max-width: 68ch;
}

/* A centred column needs its own centring: text-align above centres the LINES,
   and a max-width block still sits hard left without this. */
.section--hero .prose {
  margin-left: auto;
  margin-right: auto;
}

/* One capability: the copy that describes it and the frame that shows it, as
   siblings. HOME-05 is structural - each image sits beside the copy it belongs
   to - so this is the unit the section repeats rather than a two-column page
   layout the content is poured into. auto-fit collapses it to one column below
   two 330px tracks, which puts the frame directly under its own copy rather
   than under someone else's. */
.feature-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(330px, 1fr));
  gap: var(--space-7);
  align-items: center;
}

.feature-grid + .feature-grid {
  margin-top: var(--space-7);
}

/* A product frame and the caption that says what it is. The mockup wraps its
   screenshots in a drop-shadow; a drop-shadow is a colour this stylesheet is not
   allowed to spell outside the token block, and a hairline reads the same at
   this scale, so the hairline is what ships. */
.shot {
  margin: 0;
}

/* width: 100% is the pair of the explicit width/height attributes in the markup,
   not a duplicate of them. The attributes give the browser the intrinsic ratio so
   it can reserve the box before the bytes arrive; this makes the box the column's
   width, and img { height: auto } near the top of this file is what lets the two
   agree instead of stretching the frame. */
.shot img {
  display: block;
  width: 100%;
  border: 1px solid var(--line-2);
  border-radius: var(--r-lg);
}

.shot figcaption {
  color: var(--text-faint);
  font-family: var(--font-mono);
  font-size: var(--fs-mono-cap);
  line-height: 1.5;
  margin-top: var(--space-2);
}

/* THE STAT BAND, AND WHAT IT IS NOT (HOME-03, HOME-04, D-20). The mockup put a
   single headline download size in this slot. That figure is false for five of the
   nine published builds, so the slot carries counts instead - every one of them a
   count of something enumerated in .planning/FACTS.md and therefore re-checkable
   rather than asserted. It carries NO popularity number: the repository has zero
   stargazers and zero forks, and rendering a zero is strictly worse than rendering
   nothing. Do not add one later "once there are some" - the band would then need a
   maintainer, and a number nobody refreshes is the stalest thing on a page.
   This block is the first consumer of --accent-bright, --text-on-dark and
   --fs-stat-lg, which the note further up this file recorded as declared and
   unreferenced pending exactly this content. */
.stat-band {
  background: var(--ink);
  border-radius: var(--r-xl);
  padding: var(--space-7) var(--space-6);
  margin: var(--space-7) 0 var(--space-6);
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(160px, 1fr));
  gap: var(--space-5);
}

.stat b {
  display: block;
  color: var(--accent-bright);
  font-size: var(--fs-stat-lg);
  font-weight: 800;
  letter-spacing: -0.03em;
  line-height: 1.1;
}

.stat span {
  display: block;
  color: var(--text-on-dark);
  font-size: var(--fs-eyebrow);
  line-height: 1.5;
  margin-top: var(--space-1);
}

/* THE FAQ IS MULTI-COLUMN, NOT A GRID, AND THE DIFFERENCE IS VISIBLE. Answers here
   vary a lot in length - the network-behaviour answer runs to three paragraphs and
   the licence answer to two sentences - and a grid aligns ROWS, so the tallest item
   in a row sets the height of every cell beside it and the two short answers next
   to the long one each open a column of dead whitespace. Measured in a browser
   before this rule was rewritten. Multi-column flows the items instead and balances
   the columns itself, which is what "independent columns" actually requires.
   break-inside is what stops an answer being split down the middle by a column
   boundary; without it the layout is worse than the grid it replaced. */
.faq {
  columns: 320px 3;
  column-gap: var(--space-7);
}

.faq-item {
  break-inside: avoid;
  margin-bottom: var(--space-6);
}

.faq-item p:last-child {
  margin-bottom: 0;
}

/* ------------------------------------------------------------------------
   Docs-page layout (Phase 5, plan 05-05; DOCS-01, DOCS-02, D-26, D-28).
   Appended after the home-page layout and expressed in the same tokens: no
   new token, no literal colour, and no raw length where a --space-* or --r-*
   already names one. The one raw pixel value below is a grid track width,
   which no token in this file names — the same carve-out the two grid track
   minimums above take.

   ONE NAVIGATION, NOT TWO. The sibling this shape is ported from ships a
   .docs-side list on one edge and a .docs-toc list on the other. Only the
   first is here: two navigations over one document is two things to keep in
   sync for one reader benefit, and the second is the one that goes stale.
   The heading contract that pairs with it is written into site/docs.html's
   head, and the observer that drives .active is in site/site-chrome.js.
   ------------------------------------------------------------------------ */

.docs-layout {
  display: grid;
  grid-template-columns: 240px 1fr;
  gap: var(--space-7);
  align-items: start;
  margin-top: var(--space-6);
}

/* top clears the fixed bar the same way html { scroll-padding-top } does, from
   the same token, so the sticky list and an anchor landing cannot disagree. */
.docs-side {
  position: sticky;
  top: calc(var(--nav-h) + var(--space-5));
  font-size: var(--fs-sm);
}

.docs-side ul {
  display: grid;
  gap: 2px;
  list-style: none;
  margin: 0;
  padding: 0;
}

/* The rest state carries the border-left already, in the hairline colour, so
   the active state changes its COLOUR rather than adding a border — otherwise
   every link shifts sideways by two pixels as the reader scrolls past it. */
.docs-side a {
  border-left: 2px solid var(--line);
  border-radius: var(--r-sm);
  color: var(--text-2);
  display: block;
  padding: var(--space-1) var(--space-2);
}

.docs-side a:hover {
  background: var(--bg-chip);
  color: var(--ink-2);
  text-decoration: none;
}

.docs-side a.active {
  background: var(--bg-chip);
  border-left-color: var(--accent);
  color: var(--accent-ink);
  font-weight: 600;
}

/* THE ARTICLE IS A GRID ITEM, AND A GRID ITEM'S AUTOMATIC MINIMUM IS ITS
   MIN-CONTENT SIZE — which is what lets a single un-wrappable child widen the
   entire page. Added in plan 05-06 with the pre rule at the foot of this file,
   and the two are ONE fix rather than two: reproduced program output is
   white-space: pre, so its min-content is its longest line, and the 1fr track
   grew to fit it. MEASURED before this rule, in a real browser at a 375px
   viewport: the pre rendered 519px wide, .docs-side and the article both
   stretched to 521, and document.scrollWidth reached 539 against a clientWidth
   of 360 — so the WHOLE PAGE scrolled sideways and the pre, having been given
   all the room it asked for, never scrolled at all. min-width: 0 lets the track
   shrink past that minimum, at which point the pre's own overflow-x: auto
   finally does the job it was added for. Deleting either one restores the
   sideways page. */
.docs-layout article {
  min-width: 0;
}

/* The article's own rhythm. h2 and h3 carry bottom margins from the element
   rules near the top of this file and no top margin at all, which runs a
   reference page's sections together. */
.docs-layout article > h2 {
  margin-top: var(--space-7);
}

.docs-layout article > h2:first-child {
  margin-top: 0;
}

.docs-layout article > h3 {
  margin-top: var(--space-5);
}

.docs-layout article ol,
.docs-layout article ul {
  margin: 0 0 var(--space-3);
  padding-left: var(--space-5);
}

/* At the drawer threshold the list stops being a column and becomes a header
   for the article below it. Same number as every other width query in this
   file, from the same token — see the DESIGN-05 note beside --nav-breakpoint. */
@media (max-width: 860px) {
  .docs-layout {
    gap: var(--space-5);
    grid-template-columns: 1fr;
  }

  .docs-side {
    border-bottom: 1px solid var(--line);
    padding-bottom: var(--space-3);
    position: static;
  }
}

/* Reproduced program output: the CLI's usage block and its create summary, both on
   the docs page (Phase 5, plan 05-06; DOCS-01).

   AN ELEMENT RULE RATHER THAN A NEW CLASS, and rather than nothing at all. The
   plan for this page composes from .dl-cmd, .copy and .code-chip and adds no
   class, and none of the three can hold multi-line output: .dl-cmd is a flex row
   whose code child does not preserve newlines, and .code-chip is inline. An
   unstyled pre would render, but it does not wrap and it sits inside a grid item
   whose min-width is auto — so one 56-character line of usage text widens the
   article column past the viewport on a phone and takes the whole page's
   horizontal scroll with it. overflow-x is therefore the load-bearing declaration
   here; everything else matches .dl-cmd's chip treatment so the two read as the
   same family.

   NO OTHER PAGE SHIPS A pre TODAY, which is why this is an element rule and not a
   class: a class would be a name the markup has to remember for the one shape
   that already has an element. If a pre ever needs a different treatment, that is
   the point at which the class earns its keep. */
pre {
  background: var(--bg-chip);
  border: 1px solid var(--line-2);
  border-radius: var(--r-md);
  color: var(--ink-2);
  font-family: var(--font-mono);
  font-size: var(--fs-sm);
  line-height: 1.6;
  margin: var(--space-3) 0;
  overflow-x: auto;
  padding: var(--space-3) var(--space-4);
}
