/* ============================================================================
   Inkline — playinkline.com

   Every colour here is lifted from the game rather than picked to match it:
   the five shell values come from InkPalettes.Light/.Dark and the eight
   pigments from RengaInk.CreateDefaultPigments(). If a colour changes in the
   app, change it here from the same source — do not eyedrop a screenshot.

   There are NO third-party requests in this stylesheet, and that is load
   bearing rather than tidy. The page argues that the game collects nothing;
   a font pulled from someone else's CDN hands every visitor's IP to that CDN
   at the exact moment the argument is being made.
   ============================================================================ */

/* ---------------------------------------------------------------- type ---- */

@font-face {
  font-family: 'EB Garamond';
  src: url('../fonts/EBGaramond-Regular.woff2') format('woff2');
  font-weight: 400;
  font-style: normal;
  font-display: swap;
}

/* -------------------------------------------------------------- tokens ---- */

:root {
  /* InkPalettes.Light — the app's five shell values. */
  --paper:      #f2eee3;
  --ink:        #161619;
  --ink-wash:   #4b4e53;
  --ink-faint:  #686b70;
  --seal:       #b42b28;

  /* --ink-wash carries every paragraph on the site and --ink-faint every
     caption. At the app's own values they measure 6.1:1 and 2.6:1 on this
     page — the second of which fails AA for small text outright. They are
     lifted here and ONLY here: the game draws its own text over a field whose
     luminance it controls, and this page does not. */

  /* A wash of the page, for cards and frames that sit on it. */
  --paper-sunk: #eae5d8;
  --rule:       #d8d2c3;

  /* RengaInk's eight nihonga stops, in ramp order. A run walks these; so does
     the page, one per section, which is why they are named rather than reused. */
  --pig-sumi:     #18181d;
  --pig-gunjo:    #4c6cb3;
  --pig-ai:       #165e83;
  --pig-rokusho:  #3c8463;
  --pig-odo:      #c08c2e;
  --pig-shu:      #cb362a;
  --pig-enji:     #932243;
  --pig-murasaki: #5e397a;

  --serif: 'EB Garamond', Georgia, 'Times New Roman', serif;
  --sans:  system-ui, -apple-system, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;

  --measure: 34rem;   /* prose column */
  --page:    68rem;   /* full page column */

  --step: clamp(1rem, 0.55rem + 1.6vw, 1.375rem);

  /* THE SUN AND THE MOON, TAKEN OFF THE GAME'S OWN TOGGLE rather than drawn
     for the web. Shell/UI/ThemeToggle.cs makes both states out of marks this
     project already generates — the sun is an ensō, one measured turn, and
     the moon is RingShape.Crescent, "down for barely half the turn, a hook
     rather than a circle" — because the shell has no icon set anywhere and
     should not start one for a single control.

     The arcs are the recipes to the digit. ProceduralInkTextures.RecipeFor:

       Ensō      start 2.2 rad, sweep 0.90 of a turn, band 0.070
       Crescent  start 1.5 rad, sweep 0.55 of a turn, band 0.092

     Angles are anticlockwise from east, so on a y-down viewBox the endpoints
     are (10 + 7cosθ, 10 − 7sinθ) and the sweep-flag is 0. The ensō's opening
     lands at the top and the crescent's down the whole right side, which is
     what tells the two apart at this size — a closed turn against an open
     hook, exactly as the C# comment argues. The band ratio 0.092/0.070 is
     kept in the two stroke widths.

     MASKED, NOT DRAWN AS AN <img>. The mark has to take --ink-wash and change
     with the theme, and a data: URI in a mask is covered by `img-src data:`
     in the CSP, so no file and no third-party request. */
  --mark-sun:  url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20'%3E%3Cpath d='M5.885 4.337A7 7 0 1 0 9.994 3' fill='none' stroke='black' stroke-width='1.7' stroke-linecap='round'/%3E%3C/svg%3E");
  --mark-moon: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20'%3E%3Cpath d='M10.496 3.018A7 7 0 1 0 11.687 16.793' fill='none' stroke='black' stroke-width='2.24' stroke-linecap='round'/%3E%3C/svg%3E");

  /* THE MARK SHOWS THE PAGE YOU HAVE, NOT THE ONE THE BUTTON WILL GIVE YOU.
     Both conventions exist; this is the one the app follows, and the reason
     is that the mark is simply ink in the palette it lives in — a dark sun on
     cream, a cream moon on dark — so it never has to be explained. The button
     still SAYS what it will do, in its label, where a convention cannot be
     misread. */
  --theme-mark: var(--mark-sun);
}

/* Dark is the app's own dark mode, not an inversion. Defined for the system
   default (no data-theme attribute) and again for the explicit toggle, so the
   toggle wins in both directions. */
@media (prefers-color-scheme: dark) {
  :root:not([data-theme='light']) {
    --paper:      #19191b;
    --ink:        #e6e3da;
    --ink-wash:   #b0aea7;
    --ink-faint:  #8b8983;
    --seal:       #d64f43;

    --paper-sunk: #202023;
    --rule:       #313135;

    /* The pigments are tuned for cream. On a dark page they need lifting, the
       same way RengaInk re-derives Mark against a flooded field — sumi in
       particular is carbon black and would otherwise be a hole in the page. */
    --pig-sumi:     #8a8a94;
    --pig-gunjo:    #7094e1;
    --pig-ai:       #4493c0;
    --pig-rokusho:  #5fae8a;
    --pig-odo:      #d8ab57;
    --pig-shu:      #e0604f;
    --pig-enji:     #c04a6b;
    --pig-murasaki: #8e6bae;

    --theme-mark: var(--mark-moon);
  }
}

:root[data-theme='dark'] {
  --paper:      #19191b;
  --ink:        #e6e3da;
  --ink-wash:   #b0aea7;
  --ink-faint:  #8b8983;
  --seal:       #d64f43;
  --paper-sunk: #202023;
  --rule:       #313135;
  --pig-sumi:     #8a8a94;
  --pig-gunjo:    #7094e1;
  --pig-ai:       #4493c0;
  --pig-rokusho:  #5fae8a;
  --pig-odo:      #d8ab57;
  --pig-shu:      #e0604f;
  --pig-enji:     #c04a6b;
  --pig-murasaki: #8e6bae;

  --theme-mark: var(--mark-moon);
}

/* ---------------------------------------------------------------- base ---- */

*, *::before, *::after { box-sizing: border-box; }

html {
  -webkit-text-size-adjust: 100%;
  scroll-behavior: smooth;
}

@media (prefers-reduced-motion: reduce) {
  html { scroll-behavior: auto; }
  *, *::before, *::after {
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.01ms !important;
  }
}

body {
  margin: 0;
  background-color: var(--paper);
  color: var(--ink);
  font-family: var(--sans);
  font-size: var(--step);
  line-height: 1.6;
  -webkit-font-smoothing: antialiased;

  /* Washi grain. An inline turbulence filter rather than an image: a few
     hundred bytes, no request, and it scales to any display density. */
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='180' height='180'%3E%3Cfilter id='g'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.85' numOctaves='3' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='180' height='180' filter='url(%23g)' opacity='0.4'/%3E%3C/svg%3E");
  background-repeat: repeat;
}

/* The grain must be much quieter on a dark page — the same noise that reads as
   paper fibre on cream reads as sensor noise on near-black. */
body::before {
  content: '';
  position: fixed;
  inset: 0;
  pointer-events: none;
  z-index: -1;
  background: var(--paper);
  opacity: 0.955;
}

@media (prefers-color-scheme: dark) {
  :root:not([data-theme='light']) body::before { opacity: 0.985; }
}
:root[data-theme='dark'] body::before { opacity: 0.985; }

h1, h2, h3, .serif, .figure {
  font-family: var(--serif);
  font-weight: 400;
  line-height: 1.15;
  letter-spacing: 0.005em;
}

a {
  color: inherit;
  text-decoration-color: var(--ink-faint);
  text-underline-offset: 0.18em;
  text-decoration-thickness: 1px;
}
a:hover { text-decoration-color: var(--seal); }

:focus-visible {
  outline: 2px solid var(--seal);
  outline-offset: 3px;
  border-radius: 2px;
}

img, svg, video { max-width: 100%; height: auto; display: block; }

.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;
}

.wrap {
  width: 100%;
  max-width: var(--page);
  margin-inline: auto;
  padding-inline: clamp(1.25rem, 5vw, 3rem);
}

/* ------------------------------------------------------------ wordmark ---- */

/* Drawn from James Stroker, the app's title face, but shipped as outlines
   rather than a webfont: the licence for a display face rarely covers web
   embedding, and a mask lets the mark take the theme colour anyway. */
/* The aspect is the viewBox of wordmark.svg, which is the mark's REAL ink
   bounds. It was the sum of the advance widths, and a brush script overhangs:
   the final "e" reaches 40 units past its advance and was being clipped down
   the right-hand side. Regenerate both together or this crops again. */
.wordmark {
  display: block;
  width: var(--wordmark-w, 12rem);
  aspect-ratio: 2512 / 1008;
  background-color: currentColor;
  -webkit-mask-image: url('../img/wordmark.svg');
          mask-image: url('../img/wordmark.svg');
  -webkit-mask-repeat: no-repeat;
          mask-repeat: no-repeat;
  -webkit-mask-size: contain;
          mask-size: contain;
  -webkit-mask-position: center;
          mask-position: center;
}

/* ------------------------------------------------------------- chrome ---- */

.site-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 0.75rem 1rem;
  /* Below about 360px the mark and four controls genuinely do not fit on one
     line. Wrapping is the only honest answer; squeezing produces a nav nobody
     can hit with a thumb. */
  flex-wrap: wrap;
  padding-block: 1.5rem 0.75rem;
}

.site-head .wordmark { --wordmark-w: clamp(5rem, 18vw, 6.5rem); }

.site-head nav {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: clamp(0.75rem, 2.5vw, 1.75rem);
  font-size: 0.8rem;
  letter-spacing: 0.08em;
  text-transform: lowercase;
  color: var(--ink-wash);
}
.site-head nav a { text-decoration: none; }
.site-head nav a:hover { color: var(--ink); }

/* A MARK, NOT A WORD. It read "light" / "dark", which is a label that has to
   state a convention before it can be understood — and it sat beside the
   sound control, which is already a mark in a ring, so the nav had one of
   each. Both are rings now, and this one is the toggle the app has had all
   along. Same box as .sound-toggle, deliberately: they are a pair. */
.theme-toggle {
  appearance: none;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 2rem;
  height: 2rem;
  padding: 0;
  background: none;
  border: 1px solid var(--rule);
  border-radius: 999px;
  color: var(--ink-wash);
  cursor: pointer;
  transition: border-color 180ms ease;
}
.theme-toggle:hover { border-color: var(--ink-faint); }

/* The ink is the background and the mark is the hole in it, so the ensō and
   the crescent take --ink-wash and lift to --ink under the cursor without the
   SVG knowing anything about the theme. */
.theme-glyph {
  width: 1.05rem;
  height: 1.05rem;
  background: var(--ink-wash);
  transition: background-color 180ms ease;
  -webkit-mask: var(--theme-mark) center / contain no-repeat;
          mask: var(--theme-mark) center / contain no-repeat;
}

.theme-toggle:hover .theme-glyph { background: var(--ink); }

/* --------------------------------------------------------------- hero ---- */

.hero {
  display: grid;
  gap: clamp(2rem, 6vw, 4rem);
  align-items: center;
  padding-block: clamp(2rem, 7vw, 5rem) clamp(3rem, 8vw, 6rem);
}

@media (min-width: 56rem) {
  .hero { grid-template-columns: 1fr minmax(15rem, 20rem); }
}

.hero .wordmark { width: var(--wordmark-w); }

.hero-line {
  font-family: var(--serif);
  font-size: clamp(2rem, 1.2rem + 3.4vw, 3.4rem);
  line-height: 1.05;
  margin: 0 0 1.25rem;
}

.hero-body {
  max-width: var(--measure);
  color: var(--ink-wash);
  margin: 0 0 2rem;
}

.hero-body strong { color: var(--ink); font-weight: 400; }

/* ------------------------------------------------------------ buttons ---- */

/* Sans, per the app's own rule: serif if the text IS something, sans if it is
   a control. A button is a control. */
.actions {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0.75rem 1rem;
}

.btn {
  display: inline-flex;
  align-items: center;
  gap: 0.5rem;
  font: inherit;
  font-size: 0.9rem;
  letter-spacing: 0.06em;
  text-transform: lowercase;
  text-decoration: none;
  padding: 0.7rem 1.4rem;
  border-radius: 999px;
  border: 1px solid var(--ink);
  background: var(--ink);
  color: var(--paper);
  cursor: pointer;
  transition: transform 120ms ease, opacity 120ms ease;
}
.btn:hover { opacity: 0.88; }
.btn:active { transform: translateY(1px); }

.btn--quiet {
  background: none;
  color: var(--ink);
  border-color: var(--rule);
}
.btn--quiet:hover { border-color: var(--ink-faint); opacity: 1; }

/* "coming to google play" before there is a listing. It LOOKS pending but is a
   real link to /get/, which explains the state — a button styled as disabled
   and wired to nothing is a dead end at the exact place someone is trying to
   act. Only the copy on /get/ itself, which IS the destination, is inert. */
.btn--soon {
  background: none;
  color: var(--ink-wash);
  border-color: var(--rule);
  border-style: dashed;
}
.btn--soon:hover { color: var(--ink); border-color: var(--ink-faint); opacity: 1; }

.btn--inert { pointer-events: none; color: var(--ink-faint); }

.note {
  font-size: 0.8rem;
  letter-spacing: 0.04em;
  color: var(--ink-faint);
  margin: 1rem 0 0;
}

/* -------------------------------------------------------------- frame ---- */

/* The phone the game is built for: 1080x1920 reference, portrait. The demo
   slot inside it is swapped by site.js — a WebGL build where the browser can
   carry one, a video loop where it cannot. */
.frame {
  position: relative;
  aspect-ratio: 9 / 16;
  width: 100%;
  max-width: 20rem;
  margin-inline: auto;
  /* The demo paints the game's own cream page, which on a light page is the
     same colour the site is — without an edge and a shadow the frame simply
     dissolves and the demo stops reading as a screen at all. */
  border: 1px solid var(--frame-edge, var(--ink-faint));
  border-radius: 1.5rem;
  background: var(--paper-sunk);
  box-shadow: 0 1.5rem 3rem -1.5rem rgba(20, 18, 12, 0.35),
              0 0.25rem 0.75rem -0.5rem rgba(20, 18, 12, 0.18);
  overflow: hidden;
  display: grid;
  place-items: center;
}

/* On a dark page the cream field is already the loudest thing on screen; the
   heavy edge would only add a halo. */
@media (prefers-color-scheme: dark) {
  :root:not([data-theme='light']) .frame {
    --frame-edge: var(--rule);
    box-shadow: none;
  }
}
:root[data-theme='dark'] .frame {
  --frame-edge: var(--rule);
  box-shadow: none;
}

/* `display: grid` on .frame-placeholder below outranks the UA's `[hidden]`
   rule, so hiding the placeholder from script did nothing and it kept sitting
   in the layout behind the canvas. */
[hidden] { display: none !important; }

.frame > video,
.frame > canvas,
.frame > iframe {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  border: 0;
}

.frame-placeholder {
  display: grid;
  place-items: center;
  gap: 1.25rem;
  padding: 2rem;
  text-align: center;
}

.frame-placeholder .enso-holder {
  width: min(11rem, 55%);
  color: var(--ink);
}

.frame-state {
  font-size: 0.78rem;
  letter-spacing: 0.1em;
  text-transform: lowercase;
  color: var(--ink-faint);
  margin: 0;
}

/* ------------------------------------------------------------ section ---- */

section[data-pigment] { --accent: var(--pig-sumi); }
section[data-pigment='ai']       { --accent: var(--pig-ai); }
section[data-pigment='gunjo']    { --accent: var(--pig-gunjo); }
section[data-pigment='rokusho']  { --accent: var(--pig-rokusho); }
section[data-pigment='odo']      { --accent: var(--pig-odo); }
section[data-pigment='shu']      { --accent: var(--pig-shu); }
section[data-pigment='enji']     { --accent: var(--pig-enji); }
section[data-pigment='murasaki'] { --accent: var(--pig-murasaki); }

.band {
  padding-block: clamp(3rem, 8vw, 5.5rem);
  border-top: 1px solid var(--rule);
}

.band > .wrap > h2 {
  font-size: clamp(1.6rem, 1rem + 2.2vw, 2.4rem);
  margin: 0 0 0.5rem;
}

.band-kicker {
  display: flex;
  align-items: center;
  gap: 0.6rem;
  font-size: 0.75rem;
  letter-spacing: 0.16em;
  text-transform: lowercase;
  color: var(--ink-faint);
  margin: 0 0 1.25rem;
}

.band-kicker::before {
  content: '';
  width: 0.5rem;
  height: 0.5rem;
  border-radius: 999px;
  /* The ink the stroke is carrying HERE, not the section's nominal accent.
     They are not the same: "the ink" is keyed to rokusho, but the gradient has
     already walked on to odo by the time it reaches that dot, so the mark read
     green against a yellow line. */
  background: var(--link-ink, var(--accent));
  flex: none;
}

.lede {
  max-width: var(--measure);
  color: var(--ink-wash);
  margin: 0 0 2.5rem;
}

/* --------------------------------------------------------------- beats --- */

.beats {
  display: grid;
  gap: clamp(1.75rem, 4vw, 2.75rem);
  margin: 0;
}

@media (min-width: 48rem) {
  .beats { grid-template-columns: repeat(3, 1fr); }
}

.beat > h3 {
  font-size: 1.4rem;
  margin: 0 0 0.5rem;
}

.beat-index {
  font-family: var(--serif);
  font-size: 0.85rem;
  color: var(--link-ink, var(--accent));
  letter-spacing: 0.1em;
  display: block;
  margin-bottom: 0.75rem;
}

.beat > p {
  margin: 0;
  color: var(--ink-wash);
  font-size: 0.95rem;
}

/* --------------------------------------------------------------- claim --- */

.claim h2 {
  font-size: clamp(2rem, 1.1rem + 4vw, 3.6rem);
  line-height: 1.08;
  margin: 0 0 1.5rem;
  max-width: 20ch;
}

/* Emphasis by weight of statement, not by colour: "No tracking." sits in the
   same ink as the words either side of it. A single coloured phrase inside a
   heading reads as a link or as a mistake more readily than as emphasis. */
.claim h2 em {
  font-style: normal;
  color: inherit;
}

.claim-list {
  list-style: none;
  padding: 0;
  margin: 2.5rem 0 0;
  display: grid;
  gap: 1.25rem;
  max-width: 46rem;
}

@media (min-width: 44rem) {
  .claim-list { grid-template-columns: 1fr 1fr; gap: 1.5rem 2.5rem; }
}

.claim-list li {
  display: grid;
  grid-template-columns: 1.25rem 1fr;
  gap: 0.75rem;
  font-size: 0.92rem;
  color: var(--ink-wash);
}

.claim-list li::before {
  content: '';
  width: 0.55rem;
  height: 0.55rem;
  margin-top: 0.5rem;
  border-radius: 999px;
  border: 1px solid var(--accent);
}

.claim-list b { color: var(--ink); font-weight: 400; }

/* ------------------------------------------------------------ pigments --- */

/* The eight names under the ink stroke. The swatch STRIP that used to sit here
   (and its :nth-child colour rules) is gone — the stroke replaced it. */

.pigment-names {
  display: grid;
  grid-template-columns: repeat(8, 1fr);
  font-size: 0.62rem;
  letter-spacing: 0.06em;
  color: var(--ink-faint);
  text-align: center;
  margin: 0;
  padding: 0;
  list-style: none;
}

/* Eight columns do not fit a phone at a legible size. They used to be thinned
   by hiding every other name, which on a row that now carries a pigment each
   threw away four of the eight colours — the one thing the row is for. Two
   rows of four keeps all eight. */
@media (max-width: 34rem) {
  .pigment-names { grid-template-columns: repeat(4, 1fr); row-gap: 1.1rem; }
}

/* --------------------------------------------------------------- table --- */

.spec {
  width: 100%;
  border-collapse: collapse;
  font-size: 0.9rem;
  margin-top: 2rem;
}

.spec th, .spec td {
  text-align: left;
  padding: 0.6rem 1rem 0.6rem 0;
  border-bottom: 1px solid var(--rule);
  vertical-align: top;
}

.spec th {
  font-weight: 400;
  color: var(--ink-faint);
  white-space: nowrap;
  width: 11rem;
}

.scroll-x { overflow-x: auto; }

/* --------------------------------------------------------------- prose --- */

.doc {
  padding-block: clamp(2rem, 6vw, 4rem) clamp(3rem, 8vw, 5rem);
}

.doc-head { margin-bottom: 3rem; }

.doc h1 {
  font-size: clamp(2rem, 1.3rem + 2.6vw, 3rem);
  margin: 0 0 0.5rem;
}

.doc-meta {
  font-size: 0.8rem;
  letter-spacing: 0.06em;
  color: var(--ink-faint);
  margin: 0;
}

/* A COLUMN OF READING UNITS, NOT A RUN OF BLOCKS, and that is what makes the
   page reversible. A heading and the paragraphs under it are one thing to
   read; turn the document over block by block and the heading lands beneath
   its own text. So every h2 and every h3 opens a .doc-sec, the section is
   what gets turned over, and the inside of one is always read downward.

   Flex rather than block flow, because reversing is then one property. The
   cost is that margins between the sections no longer collapse, so the gap
   carries the rhythm and the sections' own outer margins are dropped. */
.doc-body {
  max-width: var(--measure);
  display: flex;
  flex-direction: column;
  gap: 2.5rem;
}

.doc-sec > :first-child { margin-top: 0; }
.doc-sec > :last-child  { margin-bottom: 0; }

.doc-body h2 {
  font-size: 1.45rem;
  margin: 2.75rem 0 0.75rem;
}

.doc-body h3 {
  font-size: 1.1rem;
  font-family: var(--sans);
  letter-spacing: 0.02em;
  margin: 2rem 0 0.5rem;
}

.doc-body p, .doc-body li {
  font-size: 0.95rem;
  color: var(--ink-wash);
}

.doc-body p { margin: 0 0 1rem; }
.doc-body strong { color: var(--ink); font-weight: 600; }
.doc-body ul { padding-left: 1.1rem; margin: 0 0 1.25rem; }
.doc-body li { margin-bottom: 0.5rem; }

.doc-body .callout {
  border-left: 2px solid var(--seal);
  padding: 0.25rem 0 0.25rem 1.25rem;
  margin: 1.75rem 0;
}
.doc-body .callout p:last-child { margin-bottom: 0; }

/* -------------------------------------------------------------- footer --- */

.site-foot {
  border-top: 1px solid var(--rule);
  padding-block: 2.5rem 3.5rem;
  font-size: 0.85rem;
  color: var(--ink-faint);
}

.foot-grid {
  display: flex;
  flex-wrap: wrap;
  gap: 2rem 3rem;
  justify-content: space-between;
  align-items: flex-start;
}

.foot-links {
  display: flex;
  flex-wrap: wrap;
  gap: 0.75rem 1.5rem;
  list-style: none;
  padding: 0;
  margin: 0;
}

.foot-links a { text-decoration: none; }
.foot-links a:hover { color: var(--ink); text-decoration: underline; }

.seal-mark {
  display: inline-block;
  width: 0.7rem;
  height: 0.7rem;
  border-radius: 2px;
  background: var(--seal);
  vertical-align: -0.05em;
  margin-right: 0.4rem;
}

/* ---------------------------------------------------------------- demo --- */

/* The demo paints its own page and that page is ALWAYS the cream washi, in
   both site themes — exactly as the game's field scene forces light. The ink's
   contrast guarantee is derived against paper; a dark page would break the one
   relationship the whole colour system exists to hold. */

.demo-canvas {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  display: block;
  cursor: pointer;
  /* Kills the double-tap zoom without trapping the page scroll — the frame is
     full width on a phone and has to be scrollable past. */
  touch-action: manipulation;
}

.demo-canvas:focus-visible { outline-offset: -3px; }

.demo-over {
  position: absolute;
  inset: 0;
  z-index: 2;
  display: grid;
  align-content: center;
  justify-items: center;
  gap: 0.5rem;
  padding: 2rem 1.5rem;
  text-align: center;
  background: rgba(242, 238, 227, 0.94);
  color: #161619;
}

.demo-score {
  font-family: var(--serif);
  font-size: 1.1rem;
  color: #595c61;
  margin: 0;
}

.demo-score span {
  display: block;
  font-size: 3.6rem;
  line-height: 1;
  color: #161619;
}

.demo-say {
  font-size: 0.82rem;
  line-height: 1.5;
  color: #595c61;
  margin: 0.75rem 0 0;
  max-width: 22ch;
}

.demo-actions {
  display: flex;
  flex-direction: column;
  gap: 0.6rem;
  margin-top: 1.5rem;
  width: 100%;
  max-width: 13rem;
}

/* The overlay is always on paper, so its buttons cannot inherit the page's
   dark-theme ink or they vanish into it. */
.demo-over .btn {
  justify-content: center;
  background: #161619;
  border-color: #161619;
  color: #f2eee3;
}

.demo-over .btn--quiet {
  background: none;
  color: #161619;
  border-color: #c9c2b1;
}

/* ---------------------------------------------------------------- ensō --- */

/* The stroke draws itself on. The rest state is the FINISHED stroke — an
   animation that fails, is blocked, or is turned off then leaves a completed
   ensō rather than an empty box, which is the failure the SMIL version had. */
@keyframes enso-draw {
  from { stroke-dashoffset: 1000; }
  to   { stroke-dashoffset: 44; }
}

.enso-arc {
  animation: enso-draw 1.5s cubic-bezier(0.16, 0.8, 0.3, 1) 0.35s 1 backwards;
}

.enso-holder svg { width: 100%; height: auto; display: block; }

/* ------------------------------------------------ former inline styles --- */

/* These were style="" attributes. They are classes now so the Content Security
   Policy can forbid inline styles outright — allowing them with
   'unsafe-inline' would permit every inline style on the site to buy four
   margins, which is the wrong trade for a page whose pitch is verifiability. */


.doc--short        { min-height: 46vh; }
.doc-body--wide    { max-width: 46rem; }
.actions--spaced   { margin-top: 2.5rem; }
.lede--spaced      { margin-top: 2.5rem; }
.foot-who          { margin: 0; }

/* ------------------------------------------------------------- media --- */

/* A section whose heading sits beside a portrait capture, echoing the hero's
   text-then-frame rhythm without repeating its exact layout. */
.band-head {
  display: grid;
  gap: clamp(2rem, 5vw, 3.5rem);
  align-items: center;
  margin-bottom: clamp(2.5rem, 6vw, 4rem);
}

@media (min-width: 52rem) {
  .band-head { grid-template-columns: 1fr minmax(13rem, 17rem); }
}

.band-head .lede { margin-bottom: 0; }

/* The media frame is the demo frame without the interactive parts. */
.frame--media {
  margin: 0;
  max-width: 17rem;
  display: block;
}

.frame--media video,
.frame--media img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
}

/* -------------------------------------------------------- the gallery --- */

/* Narrower than the page column on purpose. Three portrait phone captures at
   full width are 330px each and take over the section; the pigment band above
   them is the thing that wants the full measure. */
.gallery {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: clamp(0.75rem, 2.5vw, 1.25rem);
  max-width: 44rem;
  margin: 2.5rem 0 0;
}

.gallery figure {
  margin: 0;
  min-width: 0;
}

.gallery img {
  width: 100%;
  height: auto;
  aspect-ratio: 9 / 16;
  object-fit: cover;
  border: 1px solid var(--rule);
  border-radius: 0.75rem;
  background: var(--paper-sunk);
}

.gallery figcaption {
  margin-top: 0.6rem;
  font-size: 0.72rem;
  letter-spacing: 0.1em;
  text-transform: lowercase;
  color: var(--ink-faint);
  text-align: center;
}

/* ------------------------------------------------------------- media --- */

/* A section whose heading sits beside a portrait capture, echoing the hero's
   text-then-frame rhythm without repeating its exact layout. */
.band-head {
  display: grid;
  gap: clamp(2rem, 5vw, 3.5rem);
  align-items: center;
  margin-bottom: clamp(2.5rem, 6vw, 4rem);
}

@media (min-width: 52rem) {
  .band-head { grid-template-columns: 1fr minmax(13rem, 17rem); }
}

.band-head .lede { margin-bottom: 0; }

/* The media frame is the demo frame without the interactive parts. */
.frame--media {
  margin: 0;
  max-width: 17rem;
  display: block;
}

.frame--media video,
.frame--media img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
}

/* -------------------------------------------------------- the gallery --- */

/* Narrower than the page column on purpose. Three portrait phone captures at
   full width are 330px each and take over the section; the pigment band above
   them is the thing that wants the full measure. */
.gallery {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: clamp(0.75rem, 2.5vw, 1.25rem);
  max-width: 44rem;
  margin: 2.5rem 0 0;
}

.gallery figure {
  margin: 0;
  min-width: 0;
}

.gallery img {
  width: 100%;
  height: auto;
  aspect-ratio: 9 / 16;
  object-fit: cover;
  border: 1px solid var(--rule);
  border-radius: 0.75rem;
  background: var(--paper-sunk);
}

.gallery figcaption {
  margin-top: 0.6rem;
  font-size: 0.72rem;
  letter-spacing: 0.1em;
  text-transform: lowercase;
  color: var(--ink-faint);
  text-align: center;
}

/* --------------------------------------------------------- the ink walk --- */

/* One stroke carrying the whole ramp, drawn as the section scrolls. It replaced
   a strip of eight swatches: a swatch strip documents the colours, and the
   claim above it is about colour TRAVELLING, which a chart cannot show. */

.ink-walk { margin: 2.5rem 0 0; }

.ink-stroke {
  display: block;
  width: 100%;
  height: clamp(4.5rem, 11vw, 8rem);
}

/* The page soaks as the run goes on, so the band does too — at the GAME's
   strength, which is a few percent, not a hero gradient. `--p` is the scroll
   progress site.js writes; at rest it is 1 and the wash is simply present. */
.ink-walk {
  --p: 1;
  position: relative;
  padding: clamp(1rem, 3vw, 2rem) 0;
}

/* The wash is FULL WIDTH and fades in with the progress, rather than being
   clipped to it. Clipping put a hard vertical edge in the middle of the band
   where the wash stopped, which reads as a rendering fault rather than as ink.
   The stroke is what shows the travelling; this only has to deepen. */
.ink-walk::before {
  content: '';
  position: absolute;
  inset: 0;
  z-index: 0;
  border-radius: 0.75rem;
  opacity: var(--p);
  transition: opacity 260ms ease;
  background:
    linear-gradient(90deg,
      color-mix(in srgb, var(--pig-sumi)     7%, transparent),
      color-mix(in srgb, var(--pig-rokusho)  7%, transparent),
      color-mix(in srgb, var(--pig-murasaki) 8%, transparent));
}

.ink-walk > * { position: relative; z-index: 1; }

/* ---- the glow that walks the row ---- */

/* A run's ink travelling, shown as the thing travelling. The eight marks say
   which pigments exist; this says they are visited in an order, which is the
   whole claim the section makes and the one part a still row cannot put
   across.

   ONE GLOW PER MARK, IN TURN, rather than one glow sliding across all eight.
   The sweep was smooth and said the wrong thing: ink does not slide between
   pigments, a run ARRIVES at one and later arrives at the next. So each mark
   takes its own light, and the envelope is deliberately lopsided — a long
   gather and a quick release, which is a pigment being reached rather than a
   lamp being panned across a shelf. */

.ink-walk .pigment-names li { position: relative; }

.ink-walk .pigment-names li::after {
  content: '';
  position: absolute;
  z-index: -1;
  /* Centred on the MARK, not on the cell: the mark sits at the top of the
     item with the name under it, and a glow centred on the pair lights the
     word more than the pigment. */
  left: 50%;
  top: 0.25rem;
  translate: -50% -50%;
  width: 4.2rem;
  height: 4.2rem;
  border-radius: 999px;
  pointer-events: none;
  background: radial-gradient(closest-side,
              color-mix(in srgb, var(--pig) 58%, transparent),
              transparent 72%);
  opacity: 0;
  /* Eight marks, 1.4s each, so the loop closes exactly on the eighth. */
  animation: ink-breath 11.2s linear infinite;
  animation-delay: calc(var(--i) * 1.4s);
}

/* Each mark owns one eighth of the loop: 12.5%. Inside its slice the light
   gathers over seven tenths and goes in the remaining three, and the two
   easings pull it further apart still — ease-in builds from nothing, ease-out
   drops the moment it is let go. */
@keyframes ink-breath {
  0%     { opacity: 0; animation-timing-function: ease-in; }
  8.75%  { opacity: 1; animation-timing-function: ease-out; }
  12.5%  { opacity: 0; }
  100%   { opacity: 0; }
}

@media (prefers-reduced-motion: reduce) {
  .ink-walk .pigment-names li::after { animation: none; opacity: 0; }
}

.ink-walk .pigment-names {
  margin-top: 0.85rem;
  padding-inline: 0.5rem;
  /* These are eight words nobody has read before, set small, under a mark
     that is the loud thing in the row. --ink-faint is the caption colour and
     it was doing far too much work: at this size an unfamiliar word needs
     weight and full ink or the eye simply skips it. The body font is a system
     sans, so 600 is a real weight and not a synthesised one. */
  font-size: 0.72rem;
  font-weight: 600;
  letter-spacing: 0.07em;
  color: var(--ink);
}

/* Each name lights as the stroke reaches it. Eight items, eight thresholds —
   `--i` comes from :nth-child because a CSP that forbids inline styles also
   forbids setting a custom property per element in the markup. */
.ink-walk .pigment-names li {
  /* The floor was 0.22, set when these were eight words of grey and the only
     question was legibility. A pigment mark at 0.22 on paper is not faint, it
     is absent — and a row that looks empty until you have scrolled past it is
     the fault this section was fixed for. High enough to read as eight
     pigments waiting, low enough that arriving still reads as arriving. */
  opacity: clamp(0.34, calc((var(--p) * 8.6 - var(--i)) * 1.4), 1);
  transition: opacity 220ms ease;
}

/* THE PIGMENT ITSELF, which the row had been missing entirely. A strip of
   eight swatches was rejected here once, and for the right reason — a chart
   cannot show colour TRAVELLING — but the answer taken instead was eight words
   of grey, which left the section arguing about colour while showing none. The
   MARK carries the pigment and the NAME stays in ordinary ink: at this size a
   name set in odo or rokusho is both unreadable and a contrast failure, and
   the mark is the game's own vocabulary anyway. The travelling is still real —
   `--p` lights them in the order a run reaches them, left to right. */
.ink-walk .pigment-names li::before {
  content: '';
  display: block;
  width: 0.5rem;
  height: 0.5rem;
  /* The name sits further under the mark than it needs to, so the glow has
     somewhere to be. Tighter, the light came up behind the word instead of
     behind the pigment. */
  margin: 0 auto 0.85rem;
  border-radius: 999px;
  background: var(--pig);
  /* The halo the game draws under a live mark, at a quarter of its weight. */
  box-shadow: 0 0 0 0.2rem color-mix(in srgb, var(--pig) 18%, transparent);
}

.ink-walk .pigment-names li:nth-child(1) { --i: 0; --pig: var(--pig-sumi); }
.ink-walk .pigment-names li:nth-child(2) { --i: 1; --pig: var(--pig-gunjo); }
.ink-walk .pigment-names li:nth-child(3) { --i: 2; --pig: var(--pig-ai); }
.ink-walk .pigment-names li:nth-child(4) { --i: 3; --pig: var(--pig-rokusho); }
.ink-walk .pigment-names li:nth-child(5) { --i: 4; --pig: var(--pig-odo); }
.ink-walk .pigment-names li:nth-child(6) { --i: 5; --pig: var(--pig-shu); }
.ink-walk .pigment-names li:nth-child(7) { --i: 6; --pig: var(--pig-enji); }
.ink-walk .pigment-names li:nth-child(8) { --i: 7; --pig: var(--pig-murasaki); }

/* Asked for less motion: the whole stroke, all eight names, no scrubbing. */
@media (prefers-reduced-motion: reduce) {
  .ink-walk { --p: 1 !important; }
  .ink-walk .pigment-names li { opacity: 1; }
}

/* The three beats live inside the split's text column, stacked. Beside a
   portrait frame a heading and one line of lede leave the column floating in
   the middle of a tall box; the beats are what give it height to sit against. */
.beats--stacked {
  grid-template-columns: 1fr;
  gap: clamp(1.25rem, 3vw, 1.75rem);
  margin-top: clamp(2rem, 4vw, 2.75rem);
}

.beats--stacked .beat {
  display: grid;
  grid-template-columns: 2.25rem 1fr;
  align-items: baseline;
  gap: 0 0.5rem;
}

.beats--stacked .beat-index { margin: 0; }
.beats--stacked .beat > p { grid-column: 2; }

/* ---------------------------------------------------------- the spine --- */

/* One stroke down the whole document. It lines up with the left edge of the
   content column so the section kickers sit ON it as marks, which makes the
   page read as a run linking them.

   Absolutely positioned against <body> rather than fixed, so its length IS the
   document's length: the drawn head then stays at roughly one screen height
   while the content falls past it, which is the game's camera exactly — the
   brush stands still and the page falls away. */

body { position: relative; }

:root { --spine-hang: 1.7rem; }   /* how far left of the text the stroke runs */

.spine {
  position: absolute;
  top: 0;
  bottom: 0;
  width: 1.75rem;
  /* Same arithmetic as .wrap — centre the page column, add its inner padding —
     and then step LEFT into the gutter. Aligned with the content edge instead,
     the stroke runs straight down the first character of every line. */
  left: max(calc((100% - var(--page)) / 2), 0px);
  margin-left: calc(clamp(1.25rem, 5vw, 3rem) - var(--spine-hang) - 0.875rem);
  pointer-events: none;
  z-index: 0;
}

.spine-ink {
  width: 100%;
  height: 100%;
}

/* Below the point where there is a real gutter the stroke would run under the
   text rather than beside it. It tucks into the page edge instead, narrower
   and quieter, so it is still the same line rather than absent. */
@media (max-width: 56rem) {
  .spine {
    margin-left: 0;
    width: 1.1rem;
    transform: translateX(0.15rem);
    opacity: 0.55;
  }
}

/* Everything the reader touches sits above the stroke. */
.site-head, main, .site-foot { position: relative; z-index: 1; }

/* The brush at the head of the stroke. It rides ON the path — site.js asks the
   centreline for its own coordinates — and it is positioned in pixel space
   rather than inside the SVG, which is stretched vertically by a factor of ten
   and would turn the ring into a long ellipse. */
.spine-brush {
  position: absolute;
  left: 50%;
  top: 0;
  width: 1.9rem;
  aspect-ratio: 1;
  translate: -50% -50%;
  color: var(--ink);
  opacity: 0;
  transition: opacity 400ms ease;
}

/* Wet ink at the head: the stroke is freshest where the brush still is. */
.spine-brush::before {
  content: '';
  position: absolute;
  inset: -55%;
  border-radius: 50%;
  background: radial-gradient(circle, rgb(var(--run-ink, 130 130 140) / 0.22), transparent 68%);
}

.spine-brush .enso-holder { width: 100%; }
/* The ensō is drawn on arrival elsewhere; here it is already the brush. */
.spine-brush .enso-arc { animation: none; }

/* The kicker's dot HANGS in the margin so it lands on the stroke, while the
   kicker's text stays aligned with the body column. Each section is a mark the
   line links, which is the page's whole structure borrowed from the game. */
.band-kicker { position: relative; }

.band-kicker::before {
  position: absolute;
  left: calc(-1 * var(--spine-hang));
  top: 50%;
  translate: -50% -50%;
  width: 0.62rem;
  height: 0.62rem;
  /* The ring is the paper showing through, so the mark sits ON the ink. */
  box-shadow: 0 0 0 0.26rem var(--paper);
  transition: opacity 320ms ease, scale 320ms ease, box-shadow 320ms ease;
}

/* THE APPROACH FADE, applied to the page's structure. A section the brush has
   not reached is a faint mark; the one it is arriving at is lit; the ones
   behind it are ink. The same reading horizon the game runs on, standing in
   for a progress indicator — which is what it is. */
.band[data-mark='far'] .band-kicker::before    { opacity: 0.55; scale: 0.78; }
.band[data-mark='live'] .band-kicker::before   { opacity: 1; scale: 1.35; }
.band[data-mark='linked'] .band-kicker::before { opacity: 1; scale: 1; }

/* THE HEADING ANSWERS TO THE LINK. Before the brush reaches a section its
   heading is held back in the wash; on the link it comes up to full ink and
   takes a quarter of the section's pigment with it — the colour the brush was
   carrying when it got there.

   A quarter, not the whole pigment: odo on cream is about 2.8:1, which fails
   even the relaxed threshold large text gets. The luminance step from wash to
   ink is what the eye actually reads; the tint is the flavour. */
.band > .wrap h2 {
  /* Not the wash itself. At 6.1:1 an unlinked heading was legible but read as
     disabled content rather than as content not yet reached; pulled most of
     the way to ink it sits near 8.9:1 and the link still lands as a step. */
  color: color-mix(in srgb, var(--ink-wash) 52%, var(--ink));
  transition: color 480ms ease;
}

/* The ink the stroke is carrying where this section's dot sits. site.js hands
   over the two neighbouring pigments and the blend between them, so the mix
   resolves against --pig-* and follows a theme change on its own. */
.band,
.hero {
  --link-ink: color-mix(in srgb,
                var(--pa, var(--accent)) calc((1 - var(--pf, 0)) * 100%),
                var(--pb, var(--accent)));
}

/* Near enough the pigment itself. The four sections land on gunjo, ai/rokusho,
   shu and murasaki, all of which clear the large-text threshold on both
   grounds — 3.8:1 at the worst of them on cream. Odo is the one that would not
   (2.8:1), and no section sits on it. A tenth of ink keeps it from going
   fluorescent without pulling the colour back out. */
.band[data-mark='linked'] > .wrap h2 {
  color: color-mix(in srgb, var(--link-ink) 90%, var(--ink));
}



/* The section arriving leans in slightly before it lands. */
.band[data-mark='live'] > .wrap h2 {
  color: color-mix(in srgb, var(--ink-wash) 24%, var(--ink));
}

/* No scripting, no states — every heading stays at full ink. */
.band:not([data-mark]) > .wrap h2 { color: var(--ink); }

/* The mark lands. One beat as the brush reaches it, the same event the game
   fires on a link — and it must not loop, or the page acquires a pulse. */
.band[data-mark='live'] .band-kicker::after {
  content: '';
  position: absolute;
  left: calc(-1 * var(--spine-hang));
  top: 50%;
  width: 0.62rem;
  height: 0.62rem;
  border-radius: 999px;
  border: 1px solid var(--link-ink, var(--accent));
  translate: -50% -50%;
  animation: mark-land 620ms ease-out 1 both;
  pointer-events: none;
}

@keyframes mark-land {
  from { scale: 1; opacity: 0.85; }
  to   { scale: 3.4; opacity: 0; }
}

.band[data-mark='far'] .band-kicker { color: var(--ink-wash); }

/* Below the gutter breakpoint the stroke tucks into the page edge, so the dot
   comes back into the flow beside its label. */
@media (max-width: 56rem) {
  .band-kicker::before {
    position: static;
    translate: none;
    box-shadow: none;
  }
}

/* The pigment names lost their container when the horizontal stroke did. */
/* ------------------------------------------------------ the run reacts --- */

/* While the demo is being played, the run's own pigment reaches the page.
   demo.js writes --run-ink and --run-flood; everything below is what the page
   does with them. ACCENTS AND A WASH ONLY — body text keeps its contrast,
   because a page that becomes unreadable because someone reached murasaki is a
   worse page than one that does not react at all. */

:root { --run-flood: 0; }

/* The accents follow the run: the marks on the spine, the seal, the numerals.

   SHAPES take a background, TEXT takes a colour, and mixing the two is how the
   rule numerals briefly became solid red blocks with their digits hidden
   inside. Keep these two selectors apart. */
:root[data-run] .band-kicker::before,
:root[data-run] .seal-mark {
  background: rgb(var(--run-ink));
  transition: background-color 420ms ease;
}

:root[data-run] .beat-index {
  color: rgb(var(--run-ink));
  transition: color 420ms ease;
}


/* The death flood. The page takes whatever pigment the run had reached, then
   lets it recede — the same thing the game does to the field, one level up.
   A pulse rather than a state: it must never sit there dyeing the page. */
body::after {
  content: '';
  position: fixed;
  inset: 0;
  z-index: 40;
  pointer-events: none;
  opacity: var(--run-flood);
  background: rgb(var(--run-ink, 24 24 29));
  transition: opacity 900ms cubic-bezier(0.22, 0.8, 0.3, 1);
}

:root[data-run='dead'] body::after { transition-duration: 220ms; }

@media (prefers-reduced-motion: reduce) {
  body::after { display: none; }
}

/* The glow pass sits under the core at a fraction of its weight — the bloom,
   not a second line. */
.spine-glow { opacity: 0.16; }
.spine-core { opacity: 1; }

/* While a run is live the stroke takes its pigment, so the two features are
   one thing rather than two effects sharing a page. */
:root[data-run] .spine-core,
:root[data-run] .spine-glow {
  fill: rgb(var(--run-ink));
  transition: fill 500ms ease;
}

/* The 404 says the line breaks there. It does: the stroke stops part way and
   the brush is left sitting at the break with a blot where it lifted. */
[data-spine-break] .spine-brush::before {
  inset: -80%;
  background: radial-gradient(circle, rgb(var(--run-ink, 150 150 160) / 0.3), transparent 62%);
}

@media (max-width: 56rem) {
  .spine-brush { width: 1rem; }
}

@media (prefers-reduced-motion: reduce) {
  .band-kicker::before { opacity: 1 !important; scale: 1 !important; }
  .band[data-mark] .band-kicker::after { display: none; }
  .band > .wrap h2 { color: var(--ink); transition: none; }
}

/* The brush mark takes the theme's ink. An <img> cannot — an external SVG's
   `currentColor` resolves against its own empty context, so it renders black
   and disappears on a dark page. Masked, like the wordmark. */
.spine-mark {
  display: block;
  width: 100%;
  aspect-ratio: 1;
  background-color: currentColor;
  -webkit-mask: url('../img/brush.svg') no-repeat center / contain;
          mask: url('../img/brush.svg') no-repeat center / contain;
}



/* ------------------------------------------------------- hero lockup --- */

/* "An endless" sits above the mark and reads into it, so the hero opens on a
   phrase rather than on a logo. Set in the serif because it is content, not a
   control — the app's own rule. */
.hero-lockup {
  /* The mark's width lives here so the line above can be measured against it. */
  --wordmark-w: clamp(13rem, 34vw, 22rem);
  margin: 0 0 1.75rem;
  display: block;
}

.hero-an {
  display: block;
  font-family: var(--serif);
  font-size: clamp(1.15rem, 0.7rem + 1.5vw, 1.9rem);
  line-height: 1;
  color: var(--ink-wash);
  letter-spacing: 0.01em;
  /* Indented by the mark's own left padding — 70 units of the 2512-unit
     viewBox, so 2.8% of whatever width the mark is drawn at. Aligned to the
     boxes instead, the two lines start nearly ten pixels apart. Pulled tight
     underneath as well: the mark carries the same padding on its top edge, so
     the default gap reads as two separate lines rather than one phrase. */
  margin: 0 0 -0.34em calc(var(--wordmark-w) * 0.028);
}

.hero-lockup .wordmark { margin-bottom: 0; }


/* ==================================================== the page runs UP === */

/* Every page is read from the bottom. The visitor arrives at the foot of the
   document, where the mark and the nav are, and scrolls UPWARD through the
   content — the brush climbing a falling page, which is the game.

   THE ORDER IS REVERSED IN THE MARKUP, not in CSS, wherever the markup is
   short enough to carry it. `flex-direction: column-reverse` on the body is
   the obvious way and it does order the page correctly — but it makes visual
   order and DOM order disagree, and its one real advantage, a scroll container
   resting at its visual bottom, is not dependable for the ROOT scroller and
   did not happen here. So the landing page's bands are written in reverse, the
   block flow is ordinary, and site.js does the anchoring explicitly.

   THE DOCUMENT PAGES ARE TURNED OVER TOO, and they are the exception to that
   rule. The three landmarks — the footer, the main and the header — are
   reversed in the markup exactly as the bands are. The prose inside is not,
   and could not sensibly be: a privacy policy whose source runs from section
   twelve to section one is a policy nobody will maintain correctly, and a
   screen reader given it that way gets the document backwards. So each
   heading and the text under it is wrapped as a .doc-sec, and the two flex
   containers below turn the SECTIONS over while the source, the tab order and
   the reading order inside each one stay as written.

   Which is the same bargain the landing page makes, at a different grain. A
   section is the unit the eye reads in one go while scrolling up; a paragraph
   is not, and a page reversed any finer than this reads as broken. */

/* The header now sits at the foot of the page, so its padding turns over and it
   takes the rule that used to divide it from what came after. */
:root[data-flow='up'] .site-head {
  padding-block: 1rem 2rem;
  border-top: 1px solid var(--rule);
  margin-top: 0.5rem;
}

/* The footer is the last thing reached going up, at the head of the document,
   so its own dividing rule belongs on the other edge. */
:root[data-flow='up'] .site-foot {
  border-top: 0;
  border-bottom: 1px solid var(--rule);
  padding-block: 3.5rem 2.5rem;
}

/* Each band's rule divided it from what came before it down the page; going up,
   that is the edge below. */
:root[data-flow='up'] .band {
  border-top: 0;
  border-bottom: 1px solid var(--rule);
}

/* A SHORT PAGE STILL HAS TO ARRIVE AT THE BOTTOM OF THE SCREEN. /404.html and
   /get/ are both shorter than a desktop viewport, and with nothing to scroll
   the landing point is wherever the content happens to end — which left the
   mark and the nav floating in the middle of the page with a third of a screen
   of paper under them. The landing page never showed this because it has never
   been shorter than a viewport.

   `margin-top: auto` on <main> gives the free space to the gap ABOVE the
   content, so the footer stays at the head of the document and everything the
   reader arrives on is pinned to the foot. svh rather than dvh: dvh changes as
   a phone's URL bar hides, and a min-height that moves reflows the page under
   the reader. */
:root[data-flow='up'] body {
  min-height: 100vh;
  min-height: 100svh;
  display: flex;
  flex-direction: column;
}

:root[data-flow='up'] main { margin-top: auto; }

/* THE TITLE IS THE LAST THING IN THE DOCUMENT AND THE FIRST THING SEEN. The
   two children of <main> swap, so the visitor lands on the h1 with the nav
   just below it and scrolls up into section one. The padding turns over with
   them, and the head's gap moves to the edge that now faces the body. */
:root[data-flow='up'] .doc {
  display: flex;
  flex-direction: column-reverse;
  padding-block: clamp(3rem, 8vw, 5rem) clamp(2rem, 6vw, 4rem);
}

:root[data-flow='up'] .doc-head {
  margin-bottom: 0;
  margin-top: 3rem;
}

/* And the sections themselves. Each one is still read downward; it is the
   order they arrive in that is turned over. */
:root[data-flow='up'] .doc-body { flex-direction: column-reverse; }

/* The same programmatic focus the hero takes, for the same reason — without
   it the first Tab on a document page throws the keyboard to the footer,
   which is now the far end of the reading order. */
.doc-head[tabindex]:focus { outline: none; }


/* Absolutely positioned, so the spine is out of the flex flow entirely and
   still spans the document. Its stroke was regenerated to climb. */


/* site.js puts the keyboard here when the page lands, so the hero is a focus
   target — but a programmatic focus on a whole section should not draw a ring
   round half the screen. It is tabindex="-1" and unreachable by Tab, so there
   is no keyboard affordance being suppressed here. */
.hero[tabindex]:focus { outline: none; }


/* The mark and the words above it take the stroke's ink once it has passed
   them. The hero sits at the foot of the page, where the stroke is still in
   its first pigments, so the wordmark reads blue there and the run's own
   colour overrides it while the demo is being played. */
.hero .wordmark,
.hero-an {
  transition: background-color 520ms ease, color 520ms ease;
}

.hero[data-mark='linked'] .wordmark { background-color: var(--link-ink); }
.hero[data-mark='linked'] .hero-an  { color: var(--link-ink); }

/* The hero's own mark, beside "An endless" — the same dot the sections wear,
   hanging in the same gutter on the same stroke. It is measured from this
   element too, so where it is drawn and where it links are the same place. */
.hero-an { position: relative; }

.hero-an::before {
  content: '';
  position: absolute;
  /* Nudged left of the nominal gutter line. The stroke wanders by a few pixels
     either side of it — it is a hand-drawn wobble, not a ruled line — and at
     the hero's height it runs left of centre, so the dot needs to follow it
     rather than sit on the line's average. */
  left: calc(-1 * var(--spine-hang) - 0.3rem);
  top: 50%;
  translate: -50% -50%;
  width: 0.62rem;
  height: 0.62rem;
  border-radius: 999px;
  background: var(--link-ink, var(--ink-faint));
  box-shadow: 0 0 0 0.26rem var(--paper);
  opacity: 0.3;
  scale: 0.72;
  transition: opacity 320ms ease, scale 320ms ease;
}

.hero[data-mark='live'] .hero-an::before   { opacity: 1; scale: 1.35; }
.hero[data-mark='linked'] .hero-an::before { opacity: 1; scale: 1; }

/* Below the gutter breakpoint the stroke tucks into the page edge and the
   section dots return to the flow; this one has no flow to return to, so it
   simply goes. */
@media (max-width: 56rem) {
  .hero-an::before { display: none; }
}

@media (prefers-reduced-motion: reduce) {
  .hero-an::before { opacity: 1; scale: 1; }
}


/* ============================================ the run OVERRIDES the page === */

/* While a run is live its ink wins over the stroke's, everywhere. Both states
   want the same properties on the same elements, so this is decided on
   specificity rather than on source order:

     :root[data-run] .wordmark            was (0,3,0)
     .hero[data-mark='linked'] .wordmark  is  (0,3,0)

   A tie, and the link rule is written later, so the stroke's colour was
   beating the run's and the title stayed blue through a whole game. Naming the
   ancestor takes these to (0,4,0), which wins wherever they end up in the file.

   Nothing here needs a matching "off" state: when the run ends demo.js removes
   data-run, these stop applying, and the transitions already on the elements
   carry them back to the stroke's ink. */

:root[data-run] .hero .wordmark,
:root[data-run] .site-head .wordmark {
  background-color: rgb(var(--run-ink));
}

:root[data-run] .hero .hero-an {
  color: rgb(var(--run-ink));
}


/* ======================================= the board, brushes, music === */

/* Three sections arrived when the landing page stopped carrying the claim and
   the facts table, both of which now live on /promise/. Everything below
   belongs to those three and to the sound control they share. */

.note--aside {
  margin-top: 1.5rem;
  color: var(--ink-faint);
  font-style: italic;
}

/* The claim list was a band a third of a screen tall; on its own page it has
   a whole column and does not need the band's top margin. */
.claim-list--page { margin-top: 0; }


/* ---------------------------------------------------------- the board --- */

.board {
  margin-top: 2.25rem;
  max-width: 32rem;
  border: 1px solid var(--rule);
  border-radius: 0.75rem;
  background: var(--paper-sunk);
  overflow: hidden;
}

.board-tabs {
  display: flex;
  border-bottom: 1px solid var(--rule);
}

.board-tab {
  flex: 1;
  appearance: none;
  background: none;
  border: 0;
  border-bottom: 2px solid transparent;
  /* Deliberately over the 24px minimum rather than at it. The footer's links
     are this site's smallest targets and these are not going to join them. */
  min-height: 2.75rem;
  padding: 0.5rem 0.4rem;
  font: inherit;
  font-size: 0.78rem;
  letter-spacing: 0.04em;
  color: var(--ink-faint);
  cursor: pointer;
  transition: color 180ms ease, border-color 180ms ease;
}

.board-tab:hover { color: var(--ink-wash); }

.board-tab[aria-selected="true"] {
  color: var(--ink);
  border-bottom-color: var(--accent);
}

.board-rows {
  list-style: none;
  margin: 0;
  padding: 0.35rem 0;
  /* Four rows tall whatever is in it, so switching tabs never jumps the page
     under the thumb that switched them. */
  min-height: 12.5rem;
}

.board-row {
  display: grid;
  grid-template-columns: 1.75rem 1fr auto;
  align-items: baseline;
  gap: 0.75rem;
  padding: 0.62rem 1rem;
  font-size: 0.9rem;
  /* Written in rather than simply present: each row arrives a beat after the
     one above it, which is a board filling the way playing fills it. */
  opacity: 0;
  translate: 0 0.4rem;
  transition: opacity 420ms ease, translate 420ms ease;
}

.board-row[data-in] { opacity: 1; translate: 0 0; }

.board-rank {
  font-size: 0.72rem;
  color: var(--ink-faint);
  font-variant-numeric: tabular-nums;
}

.board-name { color: var(--ink-wash); }

.board-score {
  font-variant-numeric: tabular-nums;
  color: var(--ink);
}

/* The visitor's own demo run, dropped into the board where it would land. */
.board-row--you .board-name,
.board-row--you .board-score { color: var(--accent); }

.board-row--you .board-name::after {
  content: "your demo run";
  margin-left: 0.5rem;
  font-size: 0.66rem;
  letter-spacing: 0.06em;
  color: var(--ink-faint);
}

.board-row--empty {
  grid-template-columns: 1fr;
  color: var(--ink-faint);
  font-style: italic;
  padding-block: 1.5rem;
}

.board-foot {
  margin: 0;
  padding: 0.7rem 1rem 0.9rem;
  border-top: 1px solid var(--rule);
  font-size: 0.74rem;
  color: var(--ink-faint);
}

/* A mark carrying the pigment that run reached — the board's own version of
   the thing the ink section argues. */
.board-ink {
  display: inline-block;
  width: 0.42rem;
  height: 0.42rem;
  border-radius: 999px;
  margin-right: 0.45rem;
  translate: 0 -0.08em;
  background: var(--row-ink, var(--ink-faint));
}


/* --------------------------------------------------------- the brush --- */

/* One stage rather than six. Held to a column: at full band width the stroke
   became a hairline across a metre of paper, which is the opposite of what a
   loaded brush looks like. */
.brush-solo {
  margin: 2.25rem 0 0;
  max-width: 34rem;
}

.brush-stage {
  border: 1px solid var(--rule);
  border-radius: 0.6rem;
  /* Sunk paper, so the flying white where the ring runs dry has something to
     be lighter than. */
  background: var(--paper-sunk);
  overflow: hidden;
  /* The box is fitted to the drawing, not the other way round: the scale is
     pinned at life size, so this is the only thing left to change. Taller
     than wide-ish, because the brush CLIMBS, and the rows it is heading for
     are the part of the card that has to have room in it. */
  aspect-ratio: 3 / 2;
}

.brush-canvas {
  display: block;
  width: 100%;
  height: 100%;
}

.brush-solo figcaption {
  margin-top: 0.85rem;
  font-size: 0.88rem;
  color: var(--ink-wash);
}

.brush-solo figcaption b {
  font-weight: 400;
  color: var(--ink);
}


/* ---------------------------------------------------------- the music --- */

.music-panel {
  margin-top: 2.25rem;
  /* Held to a column rather than the full band: a control and a meter spread
     across 1280px is a strip of furniture, not a thing to press. */
  max-width: 42rem;
  border: 1px solid var(--rule);
  border-radius: 0.75rem;
  background: var(--paper-sunk);
  padding: 1.1rem 1.15rem;
  display: flex;
  align-items: center;
  gap: 1.15rem;
}

.music-play {
  appearance: none;
  display: inline-flex;
  align-items: center;
  gap: 0.55rem;
  flex: none;
  min-height: 2.5rem;
  padding: 0.5rem 1rem;
  border: 1px solid var(--rule);
  border-radius: 999px;
  background: var(--paper);
  font: inherit;
  font-size: 0.8rem;
  letter-spacing: 0.03em;
  color: var(--ink-wash);
  cursor: pointer;
  white-space: nowrap;
  transition: border-color 180ms ease, color 180ms ease;
}

.music-play:hover { border-color: var(--ink-faint); color: var(--ink); }

.music-glyph {
  width: 0.62rem;
  height: 0.72rem;
  flex: none;
  background: var(--accent);
  clip-path: polygon(0 0, 100% 50%, 0 100%);
}

/* Two bars while it plays. A separate clip rather than a swapped glyph, so
   the control never changes size under the press. */
.music-play[aria-pressed="true"] .music-glyph {
  clip-path: polygon(0 0, 32% 0, 32% 100%, 0 100%, 0 0, 68% 0, 100% 0, 100% 100%, 68% 100%, 68% 0);
}

.music-viz {
  flex: 1;
  min-width: 0;
  height: 4rem;
  display: block;
}

@media (max-width: 34rem) {
  .music-panel { flex-wrap: wrap; }
  .music-viz { flex-basis: 100%; height: 3.5rem; }
}


/* ------------------------------------ the bed's wash, preview only --- */

/* ONE LAYER, AND IT PAINTS NOTHING UNTIL THE MUSIC PLAYS. opacity starts at 0
   and only the driving loop raises it, so a visitor who never asks for sound
   carries a fixed div that is never composited.

   z-index -1 and LATER IN THE DOM than body::before, which is the paper wash
   at the same depth, so it paints over the paper and under every word on the
   page. Only `opacity` is written per frame — a compositor property, so the
   page is not restyled or relaid out sixty times a second — and it is written
   to this element rather than as a custom property on :root, which would
   invalidate inherited style across the whole document on every frame.

   No transition. The value arrives already smoothed, with a fast attack and a
   slow release, and a CSS transition on top of that would only lag the beat. */
.bed-wash {
  position: fixed;
  inset: 0;
  z-index: -1;
  pointer-events: none;
  opacity: 0;
}

/* THE COLOUR IS THE LINE'S, NOT THE INK'S. Both shapes below were drawn in
   --ink first, which was the safe choice and the wrong one: colour is the score
   in this game, the stroke down the page walks the eight pigments as you read,
   and a wash that answered the music in neutral grey was the one thing on the
   page NOT saying what everything else says.

   --pa, --pb and --pf are written onto this element by site.js, from the
   pigment the stroke is carrying at its own head. Same three values the marks
   get, mixed the same way --link-ink is, so the colour follows the reader down
   the page and survives a theme change without recomputing anything. The
   fallback matters: with no scripting there is no stroke and no pigment, so it
   falls back to --accent and then to --ink rather than to nothing. */
.bed-wash {
  --line-ink: color-mix(in srgb,
                var(--pa, var(--accent, var(--ink))) calc((1 - var(--pf, 0)) * 100%),
                var(--pb, var(--accent, var(--ink))));
}

/* AND A RUN BEATS THE STROKE, which is the rule the wordmark already follows.
   While the demo is being played the page's colour is the run's, so the wash
   takes it too rather than arguing with body::after over the same screen. */
:root[data-run] .bed-wash { --line-ink: rgb(var(--run-ink, 24 24 29)); }

/* MULTIPLY, AND THAT IS WHERE THE VIBRANCE COMES FROM. Laid over cream at a
   low alpha a pigment goes chalky long before it goes strong: alpha compositing
   walks it toward the paper, so at 16% rokusho is a grey with a hint of green
   in it, and the only way to get the hue back is to turn the alpha up until the
   page is a coloured panel. Multiply darkens through the hue instead — the
   paper keeps its grain and its warmth and takes the pigment's colour, which is
   what ink on paper actually does.

   On a dark page multiply would drive everything to black, so it screens
   instead: the same move in the other direction, the pigment lifting off the
   page rather than soaking into it. Both branches, because the toggle is one
   attribute and the theme is another. */
.bed-wash[data-bed-fx] { mix-blend-mode: multiply; }

@media (prefers-color-scheme: dark) {
  :root:not([data-theme='light']) .bed-wash[data-bed-fx] { mix-blend-mode: screen; }
}
:root[data-theme='dark'] .bed-wash[data-bed-fx] { mix-blend-mode: screen; }

/* LINE: the pigment bleeding off the stroke itself into the paper beside it.
   The most literal reading of "the colour of the line" there is, and the only
   shape here anchored to something rather than to an edge.

   THE GUTTER IS THE ONE COLUMN THAT IS NEVER COVERED. The first shape tried
   was a tide rising from the foot of the screen, and it was intermittent: at
   two of five reading positions the board or the hero frame reached the bottom
   of the viewport and the wash, which sits behind the content at z-index -1,
   simply vanished. The left gutter is where the stroke runs precisely because
   nothing else is ever there.

   Faded from the left because that gutter holds in both layouts — wide, where
   the stroke sits just inside the page column, and narrow, where it tucks into
   the page edge. Deliberately NOT pinned to the stroke's exact x with .spine's
   arithmetic: that has a narrow-viewport override, and keeping a bloom in step
   with it across two breakpoints costs more than the twenty pixels it buys
   under a gradient already two-thirds transparent there. */
.bed-wash[data-bed-fx='line'] {
  background-image:
    linear-gradient(90deg, var(--line-ink) 0%, transparent 52%),
    radial-gradient(34% 78% at 3% 46%, var(--line-ink) 0%, transparent 76%);
}

/* RAMP: the alternative that lost, kept because it is one attribute away and
   because the reason it lost is a number worth being able to re-check rather
   than take on trust. Three consecutive stops at once, swept up the page the
   way the stroke sweeps them.

   IT IS STRONGEST AT THE EDGES AND WEAKEST THROUGH THE MEASURE, which is not
   a style choice — it is what keeps the body text legible, and it is not
   enough: it shares the page with the prose, so it is capped by contrast where
   `line` is capped by nothing. The prose column is
   34rem in a 68rem page, so on a desktop there is a great deal of margin to
   colour and the centre can be left nearly clear. On a phone there is no
   margin, so the whole thing leans on the ceiling instead; see BED_MAX in
   sections.js, which is set from a measured contrast ratio rather than by eye. */
.bed-wash[data-bed-fx='ramp'] {
  background-image:
    linear-gradient(180deg,
      var(--p2, var(--line-ink)) 0%,
      var(--p1, var(--line-ink)) 48%,
      var(--p0, var(--line-ink)) 100%),
    radial-gradient(58% 46% at 50% 50%,
      rgb(0 0 0 / 0) 0%, rgb(0 0 0 / 0) 34%, rgb(255 255 255 / 0.55) 100%);
  background-blend-mode: screen;
}

/* ------------------------------------------- the sound control, in nav --- */

/* In the header rather than beside the player, because the music can be
   started from the demo as well as from its own section — so the way to stop
   it has to be reachable from anywhere, not only from what started it. */
.sound-toggle {
  appearance: none;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 2rem;
  height: 2rem;
  padding: 0;
  border: 1px solid var(--rule);
  border-radius: 999px;
  background: none;
  cursor: pointer;
  transition: border-color 180ms ease;
}

.sound-toggle:hover { border-color: var(--ink-faint); }

/* Three bars, flat when silent and risen when not. Drawn from gradients
   rather than set as a character so it reads at this size in both themes. */
.sound-glyph {
  --bar: var(--ink-faint);
  --h1: 0.22rem;
  --h2: 0.22rem;
  --h3: 0.22rem;
  width: 0.8rem;
  height: 0.8rem;
  background:
    linear-gradient(var(--bar), var(--bar)) 0    50% / 0.16rem var(--h1) no-repeat,
    linear-gradient(var(--bar), var(--bar)) 50%  50% / 0.16rem var(--h2) no-repeat,
    linear-gradient(var(--bar), var(--bar)) 100% 50% / 0.16rem var(--h3) no-repeat;
}

.sound-toggle[aria-pressed="true"] .sound-glyph {
  --bar: var(--seal);
  --h1: 0.42rem;
  --h2: 0.74rem;
  --h3: 0.52rem;
}


/* A run's ink still wins over everything, this included. */
:root[data-run] .music-glyph { background: rgb(var(--run-ink)); }
