/* Crucible web — application shell.
 *
 * Implements mission contract §4: a centre canvas with independent vertical
 * scrolling, left and right sidebars that are persistent at wide widths and
 * overlays at narrow widths, and two header rows.
 *
 * PROGRESSIVE ENHANCEMENT CONTRACT
 * The baseline (no JavaScript) is a plain, linear, fully readable document.
 * The shell layout is applied only under `html.js`. If the script fails to
 * load or throws, the visitor still gets every word of the centre narrative
 * and every navigation link, because they remain in normal document flow.
 * §4 requires the centre narrative to stay readable with JS disabled.
 */

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

html { -webkit-text-size-adjust: 100%; }

body {
  margin: 0;
  background: var(--bg-dark);
  color: var(--text-main);
  font-family: var(--font-sans);
  font-size: var(--type-body1);
  /* Source B's body weight, stated. It was defined in tokens.css and read by
     nothing, so body text inherited the browser default and happened to match. */
  font-weight: var(--weight-body);
  line-height: var(--leading-normal);
}

/* ---------- Skip link: first tabbable thing on the page ---------- */
.skip-link {
  position: absolute;
  left: var(--space-2);
  top: -4rem;
  z-index: 100;
  padding: var(--space-2) var(--space-4);
  background: var(--panel-bg);
  color: var(--text-main);
  border: var(--pane-border) solid var(--border);
  border-radius: var(--radius-sm);
  text-decoration: none;
  transition: top var(--transition-fast);
}
.skip-link:focus { top: var(--space-2); }

/* ---------- Focus visibility (never removed, only shaped) ---------- */
:focus-visible {
  outline: 2px solid var(--accent-blue);
  outline-offset: 2px;
  border-radius: var(--radius-sm);
}

/* ---------- Header: two rows, per §4 ---------- */
.hdr {
  position: sticky;
  top: 0;
  z-index: 30;
  background: var(--nav-bg);
  border-bottom: var(--pane-border) solid var(--border);
}

.hdr-row {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  padding: 0 var(--space-4);
  max-width: 90rem;
  margin: 0 auto;
}

/* Row one: identity and primary navigation. */
.hdr-row-1 { height: var(--header-row-1); }

/* The header row is a QUERY CONTAINER, so the wordmark can be asked whether it
   fits in the ROW rather than in the window. The generated layer emits the
   @container rule; the container itself has to be declared where the row is. */
.hdr-row { container-type: inline-size; container-name: hdr-row; }

/* Row two: contextual. §4 calls for per-page controls here. */
.hdr-row-2 {
  height: var(--header-row-2);
  border-top: var(--pane-border) solid var(--border);
  font-size: var(--type-body2);
  color: var(--text-muted);
}

.brand {
  /* The belt, with the collapse rule as the braces. Even before the wordmark
     is dropped, the brand may not paint outside its own box: at 200% root font
     the text ran 14px past a 109px box and straight through the drawer toggle.
     min-width:0 alone let the BOX shrink while the TEXT kept its full width -
     the two are not the same thing, and only the second one is visible. */
  display: flex;
  gap: var(--space-2);
  align-items: center;
  min-width: 0;
  overflow: hidden;
  white-space: nowrap;
  font-weight: var(--weight-heading);
  letter-spacing: var(--tracking-tight);
  text-decoration: none;
  color: var(--text-main);
  margin-right: auto;
}

/* Section icons. They come from the Crucible icon pack and were VALIDATED on
   every build for cycles while nothing rendered them - the build confirmed the
   key and the page showed nothing. Sized in parti like everything else, and
   given a fixed box so the row does not reflow as the image arrives. */
.section-icon {
  display: block;
  width: var(--icon-size);
  height: var(--icon-size);
  margin-bottom: var(--space-3);
  flex: none;
  /* A raster icon on a dark ground needs no filter; the pack is already dark-
     mode correct. Stated so nobody adds one and calls it a fix. */
}

/* Monochrome treatment: the PNG is a MASK, not a picture. The colour comes
   from currentColor, so the icon reads as the text it belongs to. The source
   bytes are untouched - no grayscale derivative is written - so provenance
   survives the styling decision. The per-icon mask URL is generated into
   surfaces.css, because a style attribute is inline style and CSP refuses it. */
.icon-mono {
  background-color: currentColor;
  mask-repeat: no-repeat;
  mask-position: center;
  mask-size: contain;
  -webkit-mask-repeat: no-repeat;
  -webkit-mask-position: center;
  -webkit-mask-size: contain;
}

/* Without a mark to fall back on the word is shortened rather than removed. */
.brand-clip { overflow: hidden; text-overflow: ellipsis; min-width: 0; }
.brand-word { overflow: hidden; text-overflow: ellipsis; min-width: 0; }
.brand-mark { flex: none; }

.hdr-nav { display: flex; gap: var(--space-4); }
.hdr-nav a {
  color: var(--text-muted);
  text-decoration: none;
  font-size: var(--type-button);
}
.hdr-nav a:hover, .hdr-nav a[aria-current="page"] { color: var(--text-main); }

/* Site menu for widths where .hdr-nav is hidden. Real links, not a Pause
   control and not a Page-details drawer that this document does not have. */
.hdr-menu { display: none; position: relative; flex: none; }
.hdr-menu summary {
  list-style: none;
  cursor: pointer;
  color: var(--text-muted);
  font-size: var(--type-button);
}
.hdr-menu summary::-webkit-details-marker { display: none; }
.hdr-menu[open] summary { color: var(--text-main); }
.hdr-menu nav {
  position: absolute;
  right: 0;
  top: calc(100% + 8px);
  z-index: 40;
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
  min-width: 12rem;
  padding: var(--space-3);
  background: var(--nav-bg);
  border: var(--pane-border) solid var(--border);
  border-radius: var(--radius-sm);
}
.hdr-menu nav a {
  color: var(--text-muted);
  text-decoration: none;
  font-size: var(--type-button);
}
.hdr-menu nav a:hover, .hdr-menu nav a[aria-current="page"] { color: var(--text-main); }

.drawer-toggle {
  display: none;               /* only meaningful once JS is present */
  align-items: center;
  gap: var(--space-2);
  height: 34px;
  padding: 0 var(--space-3);
  background: var(--panel-bg);
  color: var(--text-main);
  border: var(--pane-border) solid var(--border);
  border-radius: var(--radius-sm);
  font: inherit;
  font-size: var(--type-button);
  cursor: pointer;
}
.js .drawer-toggle { display: inline-flex; }
.drawer-toggle:hover { border-color: var(--text-muted); }

/* ---------- Baseline (no JS): linear, readable, nothing hidden ---------- */
.shell { max-width: 90rem; margin: 0 auto; padding: 0 var(--space-4); }
.canvas { padding: var(--space-6) 0; }
/* margin-inline: auto is LOAD-BEARING. The column was max-width constrained
   but never centred, so the entire page sat in a left-aligned 800px block
   inside a 1408px canvas - Apollo measured the hero centre 296px off. My
   text-align: center was centring text inside the wrong box, which is why the
   hero looked centred in isolation and wrong on the page. */
/* The reading column is set by MEASURE - characters per line - not by a pixel
 * width. --measure is generated from the harmonic shell and is what typography
 * actually cares about; --canvas-max was 46rem chosen by eye. */
.canvas-inner { max-width: var(--measure); margin-inline: auto; }
.sidebar {
  padding: var(--space-5) 0;
  border-top: var(--pane-border) solid var(--border);
  /* The nesting rule from Ware's plates: the outermost surface takes the
     largest radius, and each surface inside it steps down by one wall. The
     drawer is the largest rounded panel in the shell, so it takes --radius-lg;
     controls within it take --radius-sm. Without this, --radius-lg was defined
     and nothing was ever the outermost thing. */
  border-radius: var(--radius-lg);
}

/* ---------- Enhanced shell ---------- */
.js .shell {
  display: grid;
  grid-template-columns: 1fr;
  gap: var(--space-6);
  align-items: start;
}

/* Independent vertical scrolling for the centre canvas (§4). */
.js .canvas {
  min-height: 0;
  max-height: calc(100vh - var(--header-row-1) - var(--header-row-2));
  overflow-y: auto;
  overscroll-behavior: contain;
  padding: var(--space-6) var(--space-2);
}

.js .sidebar {
  border-top: 0;
  max-height: calc(100vh - var(--header-row-1) - var(--header-row-2));
  overflow-y: auto;
  overscroll-behavior: contain;
  background: var(--panel-bg);
  border: var(--pane-border) solid var(--border);
  border-radius: var(--pane-radius);
  padding: var(--space-4);
}

/* Narrow: sidebars become overlays. Off-canvas until opened. */
@media (max-width: 63.999rem) {
  .js .sidebar {
    position: fixed;
    top: calc(var(--header-row-1) + var(--header-row-2));
    bottom: 0;
    width: min(var(--sidebar-width), 82vw);
    z-index: 40;
    border-radius: 0;
    transition: transform var(--transition-normal);
  }
  .js .sidebar-left  { left: 0;  transform: translateX(-102%); }
  .js .sidebar-right { right: 0; transform: translateX(102%); }
  .js .sidebar[data-open="true"] { transform: translateX(0); }

  /* Closed drawers must leave the tab order and the a11y tree entirely. */
  .js .sidebar[hidden] { display: none; }
}

/* Wide: both sidebars persistent, three columns. */
@media (min-width: 64rem) {
  .js .shell {
    /* Rails and centre by RATIO. --shell-rail/--shell-centre are generated by
     * harmonic_layout.py and asserted to sum to exactly 1, so the three columns
     * cannot drift apart the way two independent pixel constants could. This
     * was var(--sidebar-width) twice, and it was already inert: a second
     * @media block below restated the same property with --sidebar-golden and
     * won on source order. Two rules, one of them dead, neither derived. */
    grid-template-columns: var(--tracks-shell);
  }
  .js .sidebar { position: static; transform: none; }
  /* A drawer closed on narrow must not stay hidden when the viewport grows. */
  .js .sidebar[hidden] { display: block; }
  .scrim { display: none !important; }
}

/* ---------- Scrim: centre interaction dismisses the overlay (§4) ---------- */
.scrim {
  position: fixed;
  inset: calc(var(--header-row-1) + var(--header-row-2)) 0 0 0;
  z-index: 35;
  background: rgb(0 0 0 / 0.5);
  border: 0;
  padding: 0;
  cursor: pointer;
}
.scrim[hidden] { display: none; }

/* ---------- Content ---------- */
h1 {
  font-size: var(--type-heading);
  font-weight: var(--weight-heading);
  line-height: var(--leading-tight);
  letter-spacing: var(--tracking-tight);
  margin: 0 0 var(--space-4);
}
h2 {
  font-size: var(--type-subheading);
  font-weight: var(--weight-heading);
  line-height: var(--leading-tight);
  letter-spacing: var(--tracking-tight);
  margin: var(--space-7) 0 var(--space-3);
}
h3 {
  font-size: var(--type-title);
  font-weight: var(--weight-heading);
  margin: var(--space-5) 0 var(--space-2);
}
p { margin: 0 0 var(--space-4); }
.lede { font-size: var(--type-title); color: var(--text-muted); line-height: 1.45; }

/* Source B role: sectionLabel — "Tiny ALL-CAPS headers for form sections". */
.section-label {
  font-size: var(--type-section-label);
  text-transform: uppercase;
  letter-spacing: var(--tracking-caps);
  color: var(--text-muted);
  font-weight: var(--weight-emphasis);
  margin: 0 0 var(--space-3);
}

/* Source B role: badge — "Small pill/badge text". */
.badge {
  display: inline-block;
  font-size: var(--type-badge);
  padding: 2px var(--space-2);
  border: var(--pane-border) solid var(--border);
  border-radius: 999px;
  color: var(--text-muted);
}

/* Source B role: pathMono / monospaced. */
code, .mono { font-family: var(--font-mono); font-size: 0.9em; }

.side-nav { list-style: none; margin: 0; padding: 0; }
.side-nav li { margin-bottom: var(--space-1); }
.side-nav a {
  display: block;
  padding: var(--space-2) var(--space-3);
  border-radius: var(--radius-sm);
  color: var(--text-muted);
  text-decoration: none;
  font-size: var(--type-body2);
}
.side-nav a:hover { background: var(--bg-dark); color: var(--text-main); }
.side-nav a[aria-current="page"] { color: var(--text-main); background: var(--bg-dark); }

.status-bar {
  font-size: var(--type-status-bar);
  color: var(--text-muted);
  border-top: var(--pane-border) solid var(--border);
  padding: var(--space-3) var(--space-4);
  max-width: 90rem;
  margin: 0 auto;
}

/* ---------- Reduced motion: honour it, do not merely shorten ---------- */
@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.01ms !important;
    scroll-behavior: auto !important;
  }
}

/* ---- Viewport-locked opening beats (PACING_STUDY.md P1) ----
 * The measured reference locks its first three sections to exactly one
 * viewport each, so a newcomer holds one idea per screen. Depth only varies
 * after they have committed. We adopt the BEHAVIOUR, not the appearance.
 *
 * min-height, never fixed height: content must be allowed to exceed the
 * viewport when text is scaled or the window is short, otherwise large-type
 * and zoom users lose content. That is the legibility-wins rule in practice.
 */
.beat {
  min-height: calc(100vh - var(--header-row-1) - var(--header-row-2));
  display: flex;
  flex-direction: column;
  justify-content: center;
  padding: var(--beat-2) 0;
  scroll-margin-top: calc(var(--header-row-1) + var(--header-row-2));
}

/* Past the opening, sections breathe freely (study P1). */
.beat ~ .flow { padding: var(--beat-3) 0; }

/* Short viewports: locking would trap content, so release the lock. */
@media (max-height: 34rem) {
  .beat { min-height: 0; padding: var(--beat-1) 0; }
}

.hero-title {
  /* BASE consumer. --hero-size was only read inside @media(min-width:64rem),
     so every narrow viewport fell through to --type-hero and the Admin value
     did nothing below 64rem. Apollo caught it with CSSOM: token 44px,
     computed 60px. The fallback chain keeps --type-hero as the default when
     no hero token is set. */
  font-size: var(--hero-size, var(--type-hero));
  line-height: var(--leading-hero);
  letter-spacing: var(--tracking-tight);
  font-weight: var(--weight-heading);
  margin: 0 0 var(--space-4);
}

/* The duplicate wide-width grid lived here: a second @media (min-width: 64rem)
 * restating .js .shell grid-template-columns with --sidebar-golden, silently
 * overriding the rule above it. Both are now the single harmonic rule, and the
 * reading measure is the typographic one rather than a pixel constant. */

/* ---- CY4 fix: header overflowed at narrow widths ----
 * Measured defect: at 390px the row (brand + 3 nav links + 2 drawer toggles)
 * produced scrollWidth 451 against innerWidth 390. The "Details" toggle sat at
 * left 383 / right 451 — 61px past the viewport, causing horizontal scroll.
 *
 * Fix: below the breakpoint the inline nav is removed, because those exact
 * destinations are already in the left drawer. Nothing becomes unreachable —
 * §14 requires every transformation to keep an equivalent discoverable control,
 * and the drawer toggle is that control.
 *
 * Horizontal scroll on a phone is not cosmetic: it breaks reading position and
 * makes fixed overlays misalign.
 */
@media (max-width: 63.999rem) {
  .hdr-nav { display: none; }
  .hdr-menu { display: block; }
  .hdr-row { gap: var(--space-2); }
  .drawer-toggle { padding: 0 var(--space-2); }
  .brand { min-width: 0; }
}

/* Belt and braces: nothing may introduce a horizontal scrollbar. This is a
 * guard, NOT the fix — the layout above is the fix. A page that relies on
 * overflow-x:hidden to look correct is hiding a bug rather than solving it. */
html, body { overflow-x: clip; }

/* Draft nav destinations: visible but non-navigable (Apollo P1 nav-draft-leak).
 * Rendering a draft as an <a> produced eight public 404s. Shown greyed with a
 * badge so the information stays discoverable per §14, without a dead link. */
.nav-draft {
  display: block;
  padding: var(--space-2) var(--space-3);
  color: var(--text-muted);
  opacity: 0.65;
  font-size: var(--type-body2);
  cursor: default;
}

/* ---- Hero field ----
 * Fixed, behind everything, aria-hidden. Purely decorative: it carries no
 * information, so a screen reader never announces it and the page is complete
 * without it. pointer-events:none so it can never intercept a click.
 */

/* Stacking above the hero field. The scrim is deliberately EXCLUDED: it must
 * stay position:fixed. Including it here overrode that, so it spanned the whole
 * document instead of the viewport and its centre landed off-screen. */
.hdr, .status-bar { position: relative; z-index: 1; }

/* .shell deliberately gets NO z-index.
   `position: relative` with `z-index: 1` created a STACKING CONTEXT, which
   trapped the drawer (z-index 40) inside it — so the drawer could never rise
   above the root-level scrim (z-index 35) and a pointer click on the drawer
   landed on the scrim instead, dismissing it. Keyboard already worked because
   focus does not care about paint order, which is why this hid from the
   keyboard tests. z-index: auto keeps .shell out of the way and lets the
   drawer compete at root level, where it outranks the scrim. */
.shell { position: relative; }
.scrim { z-index: 35; }
.hdr { z-index: 30; }

/* The opening beat is centred storytelling: one idea, one screen. */
.beat:first-of-type { text-align: center; align-items: center; }
.beat:first-of-type .lede { max-width: 34rem; margin-left: auto; margin-right: auto; }
.hero-title { max-width: var(--hero-measure, 18ch); margin-left: auto; margin-right: auto; }

@media (min-width: 64rem) {
  /* Was clamp(3.5rem, 6.2vw, 5.5rem) - a hardcoded ramp that ignored the
     control entirely. I emitted --hero-size and never consumed it, so the
     Admin value changed the stylesheet and not the page. That is the third
     time in this session I have shipped a control that emits correctly and
     drives nothing: EMITTED IS NOT APPLIED, and the check is to read the
     computed property, never the generated file. */
  /* Removed: this was the ONLY consumer of --hero-size, which is why the
     control appeared to work at desktop and silently did nothing on mobile.
     The base rule now consumes it at every width. */
}

/* CSP: no inline style attributes anywhere in emitted markup, so a strict
 * style-src 'self' needs no unsafe-inline. Utility classes replace them. */
/* Source B's caption role: "small descriptive text beneath elements". The
   status note under a drawer label is exactly that, and it was reading at
   body2 - a role borrowed because caption had no consumer. */
.meta-note { font-size: var(--type-caption); color: var(--text-muted); }
.stack-top { margin-top: var(--beat-2); }

/* Static fallback shown immediately on WebGL context loss.
 * Implemented with repeating-conic-gradient, which IS a gradient function. An
 * earlier comment here claimed "NOT gradients" while using two of them - that
 * was false. What changed from the rejected version is the SHAPE language: hard
 * angular stops producing hex-edge geometry, rather than soft radial colour
 * clouds. It is a deterministic hex lattice, not fog.
 *
 * A seeded SVG polygon fallback matching the shader's own seed is the better
 * end state and is NOT yet implemented.
 */
  background-color: var(--bg-dark);
  background-image:
    repeating-conic-gradient(from 30deg,
      transparent 0deg 59deg,
      color-mix(in srgb, var(--accent-blue) 26%, transparent) 59deg 60deg),
    repeating-conic-gradient(from 90deg,
      transparent 0deg 59deg,
      color-mix(in srgb, var(--accent-purple) 18%, transparent) 59deg 60deg);
  background-size: 68px 118px, 102px 177px;
  background-position: 0 0, 34px 59px;
}
/* .meta-note and .stack-top were declared TWICE, identically, ~24 lines apart.
   The second copy silently won, so editing the first - as the caption role fix
   above does - would have had no effect at all. Removed rather than kept in
   sync. */

/* Landing hero: eyebrow, headline, small subprint — the exact structure given. */
.eyebrow {
  font-size: var(--type-body1);
  color: var(--text-main);
  letter-spacing: 0.01em;
  margin: 0 0 var(--space-5);
}
.subprint {
  font-size: var(--type-body2);
  color: var(--text-muted);
  max-width: 46ch;
  margin-left: auto;
  margin-right: auto;
  line-height: 1.55;
}

/* Edge vignette on the field, matching the source UI's pipeline canvas which
 * ships one. Keeps the lattice from running hard into the viewport edges. */
  content: "";
  position: absolute;
  inset: 0;
  pointer-events: none;
  background: radial-gradient(ellipse at center,
    transparent 45%, color-mix(in srgb, var(--bg-dark) 92%, transparent) 100%);
}


/* Instrument plane: field attenuation behind the hero copy so the headline
 * reads against a calm surface rather than through the lattice. The reference
 * UI puts message content on quiet slabs, with energy only at boundaries. */
.beat:first-of-type {
  position: relative;
  isolation: isolate;
}
/* Tightened. The first version spanned the whole focal area and therefore
 * masked the focal path it was meant to sit beside. It now hugs the text block
 * only, so the path reads AROUND the copy rather than being erased by it. */
.beat:first-of-type::before {
  content: "";
  position: absolute;
  left: 50%;
  top: 50%;
  transform: translate(-50%, -50%);
  width: min(46rem, 92%);
  height: 62%;
  z-index: -1;
  background: radial-gradient(ellipse at center,
    var(--bg-dark) 0%,
    color-mix(in srgb, var(--bg-dark) 82%, transparent) 55%,
    transparent 100%);
}

/* ---- scroll reveal -------------------------------------------------------
   The .reveal class is applied by scroll-reveal.js, NEVER by the emitted HTML.
   That ordering is the whole safety property: if the script never runs, these
   rules never match anything and every section stays plainly visible. A reveal
   that fails closed leaves a blank page, which is worse than one that never
   animates.

   Motion here is a CSS transition driven by a class toggle. No JavaScript
   animation loop is involved, so no requestAnimationFrame callback is ever
   scheduled -- which is how the reduced-motion gate is passed honestly rather
   than by cancelling frames after the fact. */
.canvas-inner > section.reveal {
  opacity: 0;
  transform: translateY(14px);
}

.canvas-inner > section.reveal.is-visible {
  opacity: 1;
  transform: none;
  transition:
    opacity 620ms cubic-bezier(0.22, 0.61, 0.36, 1) var(--reveal-delay, 0ms),
    transform 620ms cubic-bezier(0.22, 0.61, 0.36, 1) var(--reveal-delay, 0ms);
}

/* Belt and braces. scroll-reveal.js already returns early under reduced
   motion, so these rules should be unreachable. They exist because "should be"
   is not a guarantee: if a future edit applies .reveal unconditionally, this
   keeps the content visible and still rather than hidden pending an animation
   that will never be allowed to run. */
@media (prefers-reduced-motion: reduce) {
  .canvas-inner > section.reveal,
  .canvas-inner > section.reveal.is-visible {
    opacity: 1;
    transform: none;
    transition: none;
  }
}

/* ---- section choreography ------------------------------------------------
   Derived from the captured reference (structure only, no assets or copy).
   The finding: the headline sits LEFT with its supporting copy in a NARROW
   RIGHT column. Deliberately asymmetric. A centred paragraph under a centred
   headline reads as a slide rather than a page, and centring everything is a
   large part of why the earlier pages felt generic.

   `beat` stays centred and is the only type that does — so the opening reads
   as arrival and everything after it is off-axis. */

/* Anchored sections must not land underneath the sticky header. Codex,
   COD-...-006: a section landing clipped its copy against the header, which is
   invisible until someone actually follows a link to it - the page looks right
   when scrolled by hand. scroll-margin is the whole fix and costs nothing. */
.canvas-inner > section { scroll-margin-top: calc(var(--header-row-1) + var(--space-4)); }

/* Primary action. Restrained on purpose: a filled slab would be the "thick
   borders and splash of colour" Oscar keeps rejecting. Weight and a hairline
   carry it; colour is reserved for meaning. */
.actions { display: flex; gap: var(--space-4); flex-wrap: wrap; margin-top: var(--space-5); }
.action {
  color: var(--text-main);
  text-decoration: none;
  font-size: var(--type-button);
  padding: var(--space-2) var(--space-4);
  border: var(--pane-border) solid var(--border);
  border-radius: var(--radius-sm);
}
.action-primary { border-color: var(--text-main); font-weight: var(--weight-heading); }
.action:hover { border-color: var(--text-main); }
.action:focus-visible { outline: 2px solid var(--accent-blue); outline-offset: 2px; }

.canvas-inner > section.beat {
  text-align: center;
  max-width: none;
  padding-block: var(--space-movement, 60px);
}

.canvas-inner > section.split {
  display: grid;
  gap: var(--space-loose, 24px);
  padding-block: var(--space-movement, 60px);
  /* phi-minor : phi-major. The measure column is narrow on purpose: 42
     characters reads quickly and leaves the headline dominant. */
  /* Third time on this line, and the reason is worth keeping. Unitless ratios
     were INVALID and the declaration was dropped. Percentages were VALID and
     still wrong: 38.1966% + 61.8034% = 100%, and gap then added 24px OUTSIDE
     that, so the section demanded 628.91 inside a 605 box. A share of free
     space cannot make that mistake, because gaps come out first. */
  grid-template-columns: var(--tracks-split);
  align-items: start;
}

/* A grid ITEM has an automatic minimum size of min-content, so even inside a
   minmax(0, ...) track a long word can push past its column. The tracks are
   bounded above; this bounds the items, so text wraps instead of widening the
   layout. Both halves are needed - the 390px measurement showed the tracks
   fitting while three <p> elements still sat 24.66px past the edge. */
.canvas-inner > section > * { min-width: 0; overflow-wrap: break-word; }

.canvas-inner > section.split > h2,
.canvas-inner > section.split > h3 {
  grid-column: 1;
  margin: 0;
  max-width: 14ch;              /* forces the two-line editorial headline */
}

.canvas-inner > section.split > p {
  grid-column: 2;
  max-width: 42ch;              /* the measure from the choreography spec */
}

/* Alternating side. Set from content rather than nth-child so reordering a
   page cannot silently flip every layout. */
.canvas-inner > section.split-reverse {
  /* The root-5 rectangle rather than the golden one. Alternating sections are
     asymmetric in DIFFERENT consonant proportions, so the page reads as
     composed rather than as one ratio applied twice. Nothing draws it. */
  grid-template-columns: var(--tracks-split-reverse);
}
.canvas-inner > section.split-reverse > h2,
.canvas-inner > section.split-reverse > h3 { grid-column: 2; }
.canvas-inner > section.split-reverse > p  { grid-column: 1; }

/* A bridge is deliberately empty. The reference uses vast white transitions to
   separate ideas, and the pacing rule is that the next headline must not be
   visible while the previous one is still being read. */
.canvas-inner > section.bridge {
  min-height: 38vh;
  padding: 0;
}

.canvas-inner > section.ledger {
  display: grid;
  /* The label column on a perfect fifth; the values take the complement.
     `auto` for the value column was the other half of the 390px defect: an
     auto track is sized by its CONTENT, so it added its width on top of a
     label track that had already claimed 80% of the box. */
  grid-template-columns: var(--tracks-ledger);
  gap: var(--space-snug, 6px) var(--space-loose, 24px);
  padding-block: var(--space-section, 36px);
}

.canvas-inner > section.ledger h3 { grid-column: 1; margin: 0; font-weight: 400; }
.canvas-inner > section.ledger p  { grid-column: 2; margin: 0; text-align: right; }

/* Single column below the fold width: an asymmetric grid at 390px is just two
   cramped columns, which is worse than an honest stack. */
@media (max-width: 60rem) {
  .canvas-inner > section.split,
  .canvas-inner > section.ledger { grid-template-columns: 1fr; }
  .canvas-inner > section.split > h2,
  .canvas-inner > section.split > h3,
  .canvas-inner > section.split > p,
  .canvas-inner > section.split-reverse > h2,
  .canvas-inner > section.split-reverse > h3,
  .canvas-inner > section.split-reverse > p { grid-column: 1; }
  .canvas-inner > section.bridge { min-height: 18vh; }
}

/* REMOVED: a two-column ledger below the fold width.
   It set `--band-third auto` - an 80% label column plus a content-sized value
   column - which at 390px demanded 390.66 inside a 342 box and pushed three
   <p> elements 24.66px past the screen. At 200% root font it demanded 483.74.
   It also contradicted the rule stated 20 lines above it, that "an asymmetric
   grid at 390px is just two cramped columns, which is worse than an honest
   stack". The stack from that rule now applies, and the value sits under its
   label reading left-to-right like everything else at that width. */
@media (max-width: 60rem) {
  .canvas-inner > section.ledger p { text-align: left; }
}

/* ---- scroll-staged split sections ----------------------------------------
   The reference alternates sticky copy against moving media. We have no media
   yet, so the same mechanism is applied to the headline: it holds while its
   supporting copy scrolls past, then releases. That is the pacing the capture
   shows, achieved with position: sticky rather than a scroll handler — so it
   costs no JavaScript, schedules no animation frame, and degrades to a normal
   static heading wherever sticky is unsupported.

   Deliberately NOT applied below the fold width: a sticky heading in a single
   column eats a phone screen. */
/* Between 60rem and the wide shell the canvas is narrower, so the split moves
   to squarer consonances: root-2 for the golden sections and the fourth for
   their mirrors. The proportion changes with the space available, which is the
   whole point of holding these as ratios rather than pixels. */
@media (min-width: 60rem) and (max-width: 63.999rem) {
  .canvas-inner > section.split {
    grid-template-columns: var(--tracks-split-mid);
  }
  .canvas-inner > section.split-reverse {
    grid-template-columns: var(--tracks-split-reverse-mid);
  }
}

@media (min-width: 60rem) {
  .canvas-inner > section.split > h2,
  .canvas-inner > section.split > h3 {
    position: sticky;
    /* Held just below the header, on a harmonic stop rather than a round number. */
    top: calc(var(--header-row-1, 56px) + var(--space-loose, 24px));
    align-self: start;
  }
}

/* prefers-reduced-motion does NOT disable sticky: sticky is position, not
   motion. It moves only because the page is being scrolled by the visitor, and
   removing it would change layout rather than reduce animation. Recorded here
   because the opposite assumption is an easy mistake to make. */

/* ---- hero occupies the full canvas ---------------------------------------
   Apollo's visual rejection: the beat was centred INSIDE a narrow column, so
   the whole landing stack sat on the left phi rail and the title broke to
   three lines. The approved references and the real capture are horizontally
   centred across the full canvas on two lines.

   The asymmetric phi grid is for LATER split sections. The hero is not one. */
/* Anchored sections must not land underneath the sticky header. Codex,
   COD-...-006: a section landing clipped its copy against the header, which is
   invisible until someone actually follows a link to it - the page looks right
   when scrolled by hand. scroll-margin is the whole fix and costs nothing. */
.canvas-inner > section { scroll-margin-top: calc(var(--header-row-1) + var(--space-4)); }

/* Primary action. Restrained on purpose: a filled slab would be the "thick
   borders and splash of colour" Oscar keeps rejecting. Weight and a hairline
   carry it; colour is reserved for meaning. */
.actions { display: flex; gap: var(--space-4); flex-wrap: wrap; margin-top: var(--space-5); }
.action {
  color: var(--text-main);
  text-decoration: none;
  font-size: var(--type-button);
  padding: var(--space-2) var(--space-4);
  border: var(--pane-border) solid var(--border);
  border-radius: var(--radius-sm);
}
.action-primary { border-color: var(--text-main); font-weight: var(--weight-heading); }
.action:hover { border-color: var(--text-main); }
.action:focus-visible { outline: 2px solid var(--accent-blue); outline-offset: 2px; }

.canvas-inner > section.beat {
  max-width: none;
  width: 100%;
  margin-inline: 0;
  padding-inline: var(--space-loose, 24px);
}

.canvas-inner > section.beat > .hero-title,
.canvas-inner > section.beat > h1 {
  /* Was a hardcoded 18ch, which overrode the earlier var(--hero-measure)
     purely by source order — I wrote the control and then defeated it four
     hundred lines later in the same file. */
  max-width: var(--hero-measure, 18ch);
  margin-inline: auto;
  text-wrap: balance;
}

.canvas-inner > section.beat > .eyebrow,
.canvas-inner > section.beat > .subprint {
  margin-inline: auto;
  max-width: 52ch;
}

/* ---- reference pages -----------------------------------------------------
   The full-viewport opening beat is right for a page somebody is being told a
   story by, and wrong for a page somebody is looking something up in. Credits
   and downloads are the second kind: a screen of empty space above a 122-row
   legal register does not build anticipation, it hides the register.

   Written with the same `.canvas-inner > section.beat` shape as the rules above
   rather than as a bare class. The first attempt used `.beat-reference` alone,
   which is one point less specific than the centring at line 620, so the
   stylesheet said `text-align: left` and the browser kept centring. A variant
   has to out-specify the rule it is opting out of, or it only changes the
   declarations nothing else was setting - which is the worst outcome, because
   it half-works and reads as applied. */
.canvas-inner > section.beat.beat-reference {
  min-height: 0;
  padding-block: var(--beat-1);
  text-align: left;
  align-items: stretch;
}

/* These titles are sentences, not slogans; the house measure of 18ch broke
   "Credits, Licences & Open-Source Honour" across four lines. */
.canvas-inner > section.beat.beat-reference > .hero-title,
.canvas-inner > section.beat.beat-reference > h1 {
  max-width: 34ch;
  margin-inline: 0;
  font-size: var(--type-heading, 2.6rem);
}

.canvas-inner > section.beat.beat-reference > .eyebrow,
.canvas-inner > section.beat.beat-reference > .subprint {
  margin-inline: 0;
  max-width: 62ch;
}

/* The radial scrim exists to calm the field behind centred hero copy. Left-set
   reference copy is not over that field, so it would only be a smudge. */
.canvas-inner > section.beat.beat-reference::before { content: none; }

/* Empty bridges on the public pages are blank pacing, not content. 34vh/38vh
   left a screen of nothing between real sections; keep a short breath only. */
.canvas-inner > section.bridge:empty {
  min-height: 8vh;
}
@media (max-width: 60rem) {
  .canvas-inner > section.bridge:empty { min-height: 4vh; }
}
