/* =========================================================================
   RSVP Reader v2 — "the projector gate"
   Cool blue-slate instrument; one warm point of light at a column that
   never moves. See DESIGN.md.

   Cascade order in this file (do not reorder):
     1. tokens          5. idle view          9. responsive
     2. reset/base      6. reader: stage      10. reduced motion
     3. utilities       7. reader: chrome     11. session controls
     4. app shell       8. peek / text / finished   12. marketing pages
                                                     13. the drawing layer
                                                     14. the pointer

   11 is last-but-one on purpose: the session control is fixed above every
   view, and the corner it claims is reserved by --session-gutter (§1), which
   §7 reads. 12 dresses the two static documents that sell the app — / and
   /wordglide — while 1-11 dress the reader itself at /read. Rules in 12 are
   scoped to those pages' own classes, so nothing there can reach the reader.
   ========================================================================= */

/* 0. FACES =============================================================== */

/* IBM Plex, self-hosted. IBM drew it for technical documentation, which is
   what a blueprint is, and it carries all three roles this site needs from
   one family: a grotesque for the interface, a serif for reading, a mono for
   the annotations. Latin subsets only, four files, 103 KB.

   Self-hosted rather than fetched from a CDN, so the claim in the README
   holds: the page still makes no external request at runtime. It does need
   font-src 'self' in the CSP, which vercel.json now carries — without it
   default-src 'none' blocks a same-origin font too.

   font-display: swap, so text paints immediately in the fallback and reflows
   once. Every stack below still ends in a real system face and a generic. */

@font-face {
  font-family: "Plex Sans";
  src: url("/fonts/plex-sans-400.woff2") format("woff2");
  font-weight: 400; font-style: normal; font-display: swap;
}
@font-face {
  font-family: "Plex Sans";
  src: url("/fonts/plex-sans-600.woff2") format("woff2");
  font-weight: 600; font-style: normal; font-display: swap;
}
@font-face {
  font-family: "Plex Serif";
  src: url("/fonts/plex-serif-400.woff2") format("woff2");
  font-weight: 400; font-style: normal; font-display: swap;
}
@font-face {
  font-family: "Plex Mono";
  src: url("/fonts/plex-mono-400.woff2") format("woff2");
  font-weight: 400; font-style: normal; font-display: swap;
}


/* 1. TOKENS ============================================================== */

:root {
  /* colour — light. The two products now share one palette, and it is the
     extension's, not an invention: WordGlide draws its guide in #386c46 and
     lights the word under it with #edc961, and those are the two colours this
     whole site is built from. They are read out of wordglide/src, not matched
     by eye — core.ts palette.green, and the .fragment fill in guide.ts.

     Green is the instrument; the amber is the one place your eye is being
     sent. That is true in the extension (guide, then the lit word) and it is
     true here (accent, then the pivot letter), which is why one set of values
     dresses both.

     The ground is flat — no texture, no gradient — and structure comes from
     alternating --paper and --paper-alt bands down the page, so nothing is
     ever drawn behind running text.

     Contrast on --paper: ink 15.0:1, ink-soft 6.7:1, accent 5.8:1,
     signal 5.1:1, danger 6.9:1. The rails are a graphic, not text, and
     measure 3.4:1 against the 3:1 floor. */
  --paper:       #F7F9F4;
  --paper-alt:   #EAEEE4;
  --paper-read:  #F2F5EE;
  /* the extension's own cursor fill, #fffdf5 — the one raised surface */
  --panel:       #FFFDF5;
  --ink:         #18251B;
  --ink-soft:    #4F5C50;
  --rule:        #D6DECD;
  --gate:        #7B8C7C;
  --accent:      #386C46;   /* core.ts palette.green, verbatim */
  --accent-ink:  #FFFFFF;
  --signal:      #9A5B06;
  --danger:      #A6221B;
  --highlight:   rgba(154, 91, 6, 0.18);
  /* The guide dot in the demonstration. core.ts palette.red, verbatim: the
     extension paints this exact value on any ground when you pick red, and
     the demo is not allowed to flatter it. 4.7:1 on the light panel,
     3.4:1 on the dark one — both past the 3:1 floor for a graphic. */
  --dot:         #DC2626;
  --disc-turn:   0deg;

  /* The guide's own colour. It tracked --accent the day the site went green
     and it is kept as a separate token anyway: --accent is a decision about
     this site, --glide is a fact about the extension, and if one of them ever
     moves it should not silently drag the other. */
  --glide:       #386C46;
  --glide-wash:  rgba(56, 108, 70, 0.12);
  /* The guide's highlight wash is the one colour the extension does NOT
     adapt: guide.ts paints .fragment #edc96155 on every ground. Copied
     literally, because the demo is showing the real thing. */
  --glide-fill:  #edc96155;

  /* The ground is flat, in every theme and every state: --paper, and nothing
     drawn on it. Structure comes from alternating bands (§3 .band), never from
     anything behind running text. */
  --paper-image: none;
  --paper-read-image: none;

  /* type */
  --face-flash: "Plex Sans", -apple-system, BlinkMacSystemFont, "Segoe UI",
                Roboto, "Helvetica Neue", Arial, sans-serif;
  --face-prose: "Plex Serif", Charter, "Iowan Old Style", Georgia,
                "Times New Roman", serif;
  --face-data:  "Plex Mono", ui-monospace, SFMono-Regular, Menlo, Consolas,
                "DejaVu Sans Mono", monospace;

  --t-flash: clamp(1.75rem, 7.4vw, 3rem);
  /* One size above --t-xxl, used only by the two marketing pages: a
     product page opens with a name, and the reader's own headings must
     not creep up to match it. */
  --t-hero:  clamp(2.5rem, 6vw, 4rem);
  --t-xxl:   2.5rem;
  --t-xl:    1.75rem;
  --t-lg:    1.25rem;
  --t-md:    1.0625rem;
  --t-sm:    0.9375rem;
  --t-xs:    0.8125rem;
  --t-2xs:   0.75rem;

  /* spacing */
  --s1: 0.25rem;  --s2: 0.5rem;  --s3: 0.75rem;  --s4: 1rem;
  --s5: 1.5rem;   --s6: 2rem;    --s7: 3rem;     --s8: 4.5rem;

  /* radii */
  --r1: 4px; --r2: 8px; --r3: 14px; --r-pill: 999px;

  /* motion */
  --d-fast: 120ms;
  --d-med: 180ms;
  --d-chrome: 400ms;
  --delay-chrome: 1500ms;
  --ease: cubic-bezier(.2, .6, .2, 1);

  /* geometry — the single source of truth for the anchor */
  --anchor: 38%;
  --mask-fade: 2.5rem;
  --tap: 44px;

  /* The session control is fixed over everything, so the corner it occupies
     has to be reserved by whatever else wants that corner. That reservation
     was a bare number in .chrome__top with no relation to the number the
     control was sized from — which is how the disc came to sit exactly on the
     Library button and swallow every click on it. One token now, the way
     --anchor is one token: change the disc and the gutter follows. */
  --session-size: 36px;
  --session-inset-x: var(--s5);
  --session-gutter: calc(var(--session-inset-x) + var(--session-size) + var(--s2));

  color-scheme: light dark;
}

/* Dark is not the light palette dimmed. theme.ts reads the surface the guide
   is about to be drawn on and swaps the guide to #b7f58b the moment that
   surface goes dark; this block does the same thing at the same value, so the
   site and the extension change colour together and for the same reason. */
@media (prefers-color-scheme: dark) {
  :root {
    --paper-image: none;
    --paper-read-image: none;
    --paper:      #0E1310;
    --paper-alt:  #151B16;
    --paper-read: #0A0E0B;
    --panel:      #1A221B;
    --ink:        #E7EFE4;
    --ink-soft:   #98A796;
    --rule:       #293227;
    --gate:       #7E8F7D;
    --accent:     #B7F58B;
    --accent-ink: #0E1310;
    --signal:     #EDC961;
    --danger:     #FF8F86;
    --highlight:  rgba(237, 201, 97, 0.20);
    --dot:        #DC2626;
    --disc-turn:  180deg;
    --glide:      #B7F58B;
    --glide-wash: rgba(183, 245, 139, 0.14);
    --glide-fill: #edc96155;
  }
}

/* the in-page toggle must win in BOTH directions */
:root[data-theme="dark"] {
  --paper-image: none;
  --paper-read-image: none;
  --paper:      #0E1310;
  --paper-alt:  #151B16;
  --paper-read: #0A0E0B;
  --panel:      #1A221B;
  --ink:        #E7EFE4;
  --ink-soft:   #98A796;
  --rule:       #293227;
  --gate:       #7E8F7D;
  --accent:     #B7F58B;
  --accent-ink: #0E1310;
  --signal:     #EDC961;
  --danger:     #FF8F86;
  --highlight:  rgba(237, 201, 97, 0.20);
  --dot:        #DC2626;
  --disc-turn:  180deg;
  --glide:      #B7F58B;
  --glide-wash: rgba(183, 245, 139, 0.14);
  --glide-fill: #edc96155;
  color-scheme: dark;
}

:root[data-theme="light"] {
  --paper-image: none;
  --paper-read-image: none;
  --paper:      #F7F9F4;
  --paper-alt:  #EAEEE4;
  --paper-read: #F2F5EE;
  --panel:      #FFFDF5;
  --ink:        #18251B;
  --ink-soft:   #4F5C50;
  --rule:       #D6DECD;
  --gate:       #7B8C7C;
  --accent:     #386C46;
  --accent-ink: #FFFFFF;
  --signal:     #9A5B06;
  --danger:     #A6221B;
  --highlight:  rgba(154, 91, 6, 0.18);
  --dot:        #DC2626;
  --disc-turn:  0deg;
  --glide:      #386C46;
  --glide-wash: rgba(56, 108, 70, 0.12);
  --glide-fill: #edc96155;
  color-scheme: light;
}


/* 2. RESET / BASE ======================================================== */

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

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

body {
  margin: 0;
  min-height: 100dvh;
  background-color: var(--paper);
  color: var(--ink);
  font-family: var(--face-flash);
  font-size: var(--t-md);
  line-height: 1.5;
  -webkit-font-smoothing: antialiased;
  text-rendering: optimizeLegibility;
}

/* The paper ground sits on its own fixed layer rather than using
   background-attachment: fixed, which makes the browser repaint the whole
   background on every scroll frame — plainly janky on mobile. A fixed
   pseudo-element is composited instead, so scrolling costs nothing. */
body::before {
  content: "";
  position: fixed;
  inset: 0;
  z-index: -1;
  background-color: var(--paper);
  pointer-events: none;
}

/* reading dims one step further than idle */
body[data-state="reading"],
body[data-state="paused"],
body[data-state="peek"] { background-color: var(--paper-read); }

body[data-state="reading"]::before,
body[data-state="paused"]::before,
body[data-state="peek"]::before {
  background-color: var(--paper-read);
  background-image: var(--paper-read-image);
}

h1, h2, h3, p, ul, ol, figure, blockquote, pre { margin: 0; }
ul, ol { padding: 0; list-style: none; }

button, input, textarea, select {
  font: inherit;
  color: inherit;
  margin: 0;
}

a { color: var(--accent); text-underline-offset: 0.18em; }

:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
  border-radius: var(--r1);
}


/* 3. UTILITIES =========================================================== */

.u-sr-only {
  position: absolute;
  width: 1px; height: 1px;
  padding: 0; margin: -1px;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
  border: 0;
}

.u-skip {
  position: absolute;
  left: var(--s4); top: -3rem;
  z-index: 60;
  padding: var(--s2) var(--s4);
  background: var(--accent);
  color: var(--accent-ink);
  border-radius: var(--r1);
  text-decoration: none;
  transition: top var(--d-fast) var(--ease);
}
.u-skip:focus { top: var(--s4); }

.eyebrow {
  font-family: var(--face-data);
  font-size: var(--t-2xs);
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--ink-soft);
}

.data { font-family: var(--face-data); font-variant-numeric: tabular-nums; }

.prose-face { font-family: var(--face-prose); }

/* THE LIT RULE — the horizontal counterpart of the two gate marks, and the
   only divider used anywhere in the app. It is brightest at --anchor and fades
   to nothing at both ends, so every rule in the interface is off-centre in
   exactly the way the reading surface is. Flat hairlines across a full width
   were doing the opposite: dividing the page into equal, unrelated bands. */
.rule-lit,
.rule-lit-before::before {
  height: 1px;
  border: 0;
  margin: 0;
  background: linear-gradient(to right,
    transparent 0,
    var(--rule) calc(var(--anchor) * 0.45),
    var(--gate) var(--anchor),
    var(--rule) calc(var(--anchor) + (100% - var(--anchor)) * 0.5),
    transparent 100%);
}
.rule-lit-before { position: relative; }
.rule-lit-before::before {
  content: "";
  position: absolute;
  left: 0; right: 0; top: 0;
}

/* A section on the second ground. box-shadow paints the tint out to the
   window on both sides without the element leaving its container, so
   .view-idle and .product__main keep their max-width and gutter; clip-path
   then trims what hangs past the viewport, which is what stops the shadow
   producing a horizontal scrollbar. Bands alternate down the page and are the
   only structure the ground carries — nothing is drawn behind running text.

   No reading-state override is needed: #idleView is [hidden] while reading,
   so none of this is rendered on a reading surface. */
.band {
  background: var(--paper-alt);
  box-shadow: 0 0 0 100vmax var(--paper-alt);
  clip-path: inset(0 -100vmax);
}

[hidden] { display: none !important; }


/* 4. APP SHELL =========================================================== */

/* buttons ---------------------------------------------------------------- */
.btn {
  --btn-bg: var(--panel);
  --btn-fg: var(--ink);
  --btn-bd: var(--rule);
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: var(--s2);
  min-height: var(--tap);
  padding: var(--s2) var(--s4);
  background: var(--btn-bg);
  color: var(--btn-fg);
  border: 1px solid var(--btn-bd);
  border-radius: var(--r1);
  cursor: pointer;
  transition: background-color var(--d-fast) var(--ease),
              border-color var(--d-fast) var(--ease),
              transform var(--d-fast) var(--ease);
}
.btn:hover:not(:disabled) { --btn-bd: var(--accent); }
.btn:active:not(:disabled) { transform: scale(0.97); }
.btn:disabled { opacity: 0.45; cursor: default; }

.btn--primary {
  --btn-bg: var(--accent);
  --btn-fg: var(--accent-ink);
  --btn-bd: var(--accent);
  font-weight: 600;
  position: relative;
  overflow: hidden;
}
.btn--primary:hover:not(:disabled) { --btn-bd: var(--accent); filter: brightness(1.08); }

.btn--quiet {
  --btn-bg: transparent;
  --btn-fg: var(--ink-soft);
  --btn-bd: transparent;
  min-height: 2rem;
  padding: var(--s1) var(--s2);
  font-size: var(--t-xs);
}
.btn--quiet:hover:not(:disabled) { --btn-fg: var(--ink); --btn-bd: var(--rule); }

.btn--round {
  min-width: var(--tap);
  padding: 0;
  border-radius: var(--r-pill);
}

/* topbar ----------------------------------------------------------------- */
/* No band, no border. The wordmark is a mark, not a masthead; a full-width
   rule under it cut the page into two unrelated halves before the reader had
   read a word. */
.topbar {
  display: flex;
  align-items: center;
  gap: var(--s4);
  padding: var(--s5) var(--s5) 0;
  max-width: 46rem;
  margin-inline: auto;
}
body[data-state="reading"] .topbar,
body[data-state="paused"] .topbar,
body[data-state="peek"] .topbar,
body[data-state="finished"] .topbar { display: none; }

.wordmark {
  font-family: var(--face-data);
  font-size: var(--t-2xs);
  font-weight: 600;
  letter-spacing: 0.18em;
  text-transform: uppercase;
  color: var(--ink);
}
.wordmark span { color: var(--ink-soft); }


/* 5. IDLE VIEW =========================================================== */

/* The gaps are deliberately unequal. Everything used to sit in one column at
   one rhythm, so the field you came to use had exactly the weight of the
   bookmarklet footer. Now: console and library are one working block, close
   together; the prose below is a separate document, pushed well clear. */
/* The sheet is the page, not a column floating on it. 46rem centred left
   roughly 390px of dead air on each side of a laptop, which is what a narrow
   measure costs when it is applied to the whole layout instead of to the
   prose inside it. The measure now lives where it belongs — .about and the
   hero line still cap their own line length — and the layout uses the sheet. */
.view-idle {
  max-width: 82rem;
  margin: 0 auto;
  padding: var(--s5) clamp(var(--s5), 4vw, var(--s8)) var(--s8);
  display: flex;
  flex-direction: column;
  gap: var(--s6);
}

/* Wide enough for two columns: the instrument on the left, what you have in
   progress on the right, instead of one stacked on the other with the rest of
   the screen empty. */
@media (min-width: 64rem) {
  .view-idle {
    display: grid;
    grid-template-columns: minmax(0, 1.5fr) minmax(0, 1fr);
    column-gap: var(--s8);
    row-gap: var(--s6);
    align-items: start;
  }
  /* The gate spans the sheet. It is the one thing here that is better for
     being wide — the dimension line has a real distance to measure, and the
     flash has room to run — and giving the console the full width stops the
     second column being mostly empty, which is what widening the page bought
     on its own. */
  .console { grid-column: 1 / -1; }
  .idle-footer { grid-column: 1; grid-row: 2; }
  .library { grid-column: 2; grid-row: 2; }
  .about { grid-column: 1 / -1; }
  .sitefoot { grid-column: 1 / -1; }
}

/* the instrument at rest: gate, the line that explains it, the one field */
.console {
  display: flex;
  flex-direction: column;
  gap: var(--s4);
}

/* hero: the product demonstrating itself at the real anchor */
.hero__stage {
  position: relative;
  height: 8.5rem;
}
/* Says in words what the moving words are. Nothing on the page used to, which
   is how the product's signature behaviour came to read as a fault. */
.hero__caption {
  font-family: var(--face-data);
  font-size: var(--t-xs);
  color: var(--ink-soft);
}
.hero__head {
  font-family: var(--face-prose);
  font-size: var(--t-hero);
  font-weight: 600;
  line-height: 1.05;
  letter-spacing: -0.025em;
  max-width: 17ch;
  text-wrap: balance;
  margin-top: var(--s4);
}
.hero__line {
  font-family: var(--face-prose);
  font-size: var(--t-lg);
  line-height: 1.5;
  color: var(--ink-soft);
  max-width: 44ch;
  text-wrap: pretty;
}

/* one field, takes a link or the text itself */
.source { display: flex; flex-direction: column; gap: var(--s2); }

/* Error on the left, current settings on the right. Both are one line of
   status about the thing directly above them, so they share a row. */
.source__foot {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--s1) var(--s4);
  min-height: 1.25rem;
}
.readout {
  font-size: var(--t-xs);
  color: var(--ink-soft);
  white-space: nowrap;
  margin-left: auto;
}
.readout:empty { display: none; }

.source__row {
  display: flex;
  flex-direction: column;
  gap: var(--s3);
}

.source__input {
  width: 100%;
  min-height: var(--tap);
  max-height: 40vh;
  padding: var(--s3) var(--s4);
  background: var(--panel);
  color: var(--ink);
  border: 1px solid var(--rule);
  border-radius: var(--r2);
  resize: none;
  overflow-y: auto;
  line-height: 1.5;
  transition: border-color var(--d-fast) var(--ease);
}
.source__input::placeholder { color: var(--ink-soft); }
.source__input:focus-visible { border-color: var(--accent); }

.source__submit { width: 100%; }

/* indeterminate sweep while loading */
.source__submit[aria-busy="true"]::after {
  content: "";
  position: absolute;
  left: 0; bottom: 0;
  height: 2px; width: 40%;
  background: var(--accent-ink);
  opacity: 0.7;
  animation: sweep 1.2s var(--ease) infinite;
}
@keyframes sweep {
  0%   { transform: translateX(-100%); }
  100% { transform: translateX(250%); }
}

/* The messages name a URL often enough that an unbreakable one has to be
   allowed to break, or it pushes the whole column sideways at 360px. */
.error {
  flex: 1 1 20ch;
  min-width: 0;
  color: var(--danger);
  font-size: var(--t-sm);
  max-width: 60ch;
  overflow-wrap: anywhere;
}
.error:empty { display: none; }

/* library ---------------------------------------------------------------- */
/* Rows, not cards. A card is a thing to look at; this is a list of positions
   in documents you are part-way through, and the only questions it answers are
   "which one" and "how far". Fat bordered panels made three of those cost a
   whole screen and gave a half-read spec the visual weight of a feature. */
.library { display: flex; flex-direction: column; gap: var(--s2); }

.library__list {
  display: flex;
  flex-direction: column;
}

.lib-row {
  position: relative;
  display: flex;
  align-items: stretch;
  border-radius: var(--r1);
  transition: background-color var(--d-fast) var(--ease);
}
.lib-row + .lib-row { box-shadow: 0 -1px 0 var(--rule); }
.lib-row:hover { background: var(--panel); }

.lib-open {
  flex: 1;
  min-width: 0;
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: var(--s1) var(--s4);
  min-height: var(--tap);
  padding: var(--s3) var(--s2);
  background: none;
  border: 0;
  text-align: left;
  cursor: pointer;
}
.lib-title {
  flex: 1 1 14rem;
  font-family: var(--face-prose);
  font-size: var(--t-md);
  line-height: 1.3;
  color: var(--ink);
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
  overflow: hidden;
}
/* The progress reading is the reason the row exists, so it is set as data and
   sits on its own edge of the row rather than trailing the title as a caption. */
.lib-meta {
  flex: 0 0 auto;
  font-family: var(--face-data);
  font-variant-numeric: tabular-nums;
  font-size: var(--t-xs);
  color: var(--ink-soft);
}
.lib-row:hover .lib-meta { color: var(--ink); }

.lib-forget {
  flex: 0 0 auto;
  width: var(--tap);
  align-self: stretch;
  background: none;
  border: 0;
  border-radius: var(--r1);
  color: var(--ink-soft);
  cursor: pointer;
  /* Fading it to nothing until :hover meant it did not exist at all on a
     touch screen, which has no hover to give. It is quiet by default and
     resolves on hover or focus — visible everywhere, loud nowhere. */
  opacity: 0.4;
  transition: opacity var(--d-fast) var(--ease), color var(--d-fast) var(--ease);
}
@media (hover: hover) {
  .lib-forget { opacity: 0; }
}
.lib-row:hover .lib-forget,
.lib-forget:focus-visible { opacity: 1; }
.lib-forget:hover { color: var(--danger); opacity: 1; }

/* An empty screen is an invitation, not an apology — and it does not need a
   heading announcing a list that is not there. */
.library:has(.library__empty:not([hidden])) .eyebrow { display: none; }
.library__empty {
  font-family: var(--face-prose);
  color: var(--ink-soft);
  max-width: 50ch;
}

.idle-footer {
  font-family: var(--face-data);
  font-size: var(--t-xs);
  color: var(--ink-soft);
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: var(--s3);
}
.bookmarklet {
  padding: var(--s1) var(--s3);
  border: 1px dashed var(--rule);
  border-radius: var(--r1);
  color: var(--accent);
  text-decoration: none;
  cursor: grab;
}
.bookmarklet:hover { border-color: var(--accent); }

/* Below the fold: not "app chrome with words in it" but a document, set the
   way the reader itself sets a document. It was --ink-soft throughout — nine
   hundred words of the page's only real prose in the secondary colour, which
   is what --ink-soft is for meta, not for body. */
.about {
  position: relative;
  padding-top: var(--s7);
  margin-top: var(--s6);
  max-width: 34em;
  font-family: var(--face-prose);
  font-size: var(--t-md);
  line-height: 1.65;
  color: var(--ink);
  display: flex;
  flex-direction: column;
  gap: var(--s4);
}
.about h2 {
  font-family: var(--face-prose);
  font-size: var(--t-xl);
  font-weight: 600;
  line-height: 1.2;
  color: var(--ink);
  text-wrap: balance;
}
/* No .about h3. The section is three paragraphs now; subheads over a
   paragraph each were chapter headings on a pamphlet. */
.about p { max-width: 34em; }
.about strong { font-weight: 600; }
.about em { font-style: italic; }


/* 6. READER — STAGE, GATE, ANCHOR ======================================== */

.view-reader {
  position: fixed;
  inset: 0;
  display: grid;
  grid-template-rows: 1fr;
}

.stage {
  position: relative;
  width: 100%;
  height: 100%;
  /* the gate is absolutely positioned against this box — a button's default
     padding would offset the anchor by a few px */
  padding: 0;
  border: 0;
  background: transparent;
  cursor: pointer;
}
.stage:focus-visible { outline: 2px solid var(--accent); outline-offset: -4px; }

/* The idle demo is a display, not a control. It must also win the height back
   from .stage — same specificity, and .stage is declared later in the file. */
.hero__stage.stage {
  height: 14rem;
  cursor: default;
}
@media (max-width: 34rem) { .hero__stage.stage { height: 10.5rem; } }
/* The gate stays on screen for everyone. It used to be display:none for anyone
   who had read anything before — so the daily user, the only user there is,
   never saw the hero at all and the home screen opened on a grey sentence and
   a text box. It animates once, for a first-time reader; after that it holds
   the sample frame that ships in the markup, which is the same thing reduced
   motion has always been specified to show. */

/* cursor disappears once the chrome has gone */
body[data-state="reading"]:not(.chrome-on) .stage { cursor: none; }

/* The gate: the aperture the document is pulled through.
   Everything positioned inside it resolves against --anchor. */
.gate {
  position: absolute;
  inset: 0;
  max-width: 64rem;
  margin-inline: auto;
  font-size: var(--t-flash);
  font-weight: 500;
  letter-spacing: -0.005em;
  font-kerning: normal;
  font-variant-numeric: tabular-nums;
  -webkit-mask-image: linear-gradient(to right, transparent 0,
      #000 var(--mask-fade), #000 calc(100% - var(--mask-fade)), transparent 100%);
  mask-image: linear-gradient(to right, transparent 0,
      #000 var(--mask-fade), #000 calc(100% - var(--mask-fade)), transparent 100%);
}

/* THE ANCHOR. .pre and .post are out of flow, so the pivot glyph's box is a
   function of --anchor and the font metrics only. It cannot move. */
.flash {
  position: absolute;
  left: var(--anchor);
  top: 50%;
  transform: translateY(-50%);
  white-space: pre;
  line-height: 1.1;
  color: var(--ink);
}
.pivot {
  position: relative;
  display: inline-block;
  color: var(--signal);
}
.pre {
  position: absolute;
  right: 100%;
  top: 0;
  white-space: pre;
  text-align: right;
  color: var(--ink);
}
.post {
  position: absolute;
  left: 100%;
  top: 0;
  white-space: pre;
  text-align: left;
  color: var(--ink);
}
.flash--quote { font-family: var(--face-prose); font-style: italic; }

/* The aperture marks. Same left as .flash, so they cannot disagree.
   Tapered: darkest pixel nearest the letter. */
.rail {
  position: absolute;
  left: var(--anchor);
  width: 1px;
  height: 10px;
}
.rail--top {
  top: calc(50% - 0.92em - 10px);
  background: linear-gradient(to bottom, transparent, var(--gate));
}
.rail--bottom {
  top: calc(50% + 0.92em);
  background: linear-gradient(to top, transparent, var(--gate));
}

/* the reader's breath, at the anchor column */
.para-dot {
  position: absolute;
  left: var(--anchor);
  top: 50%;
  transform: translateY(-50%);
  color: var(--ink-soft);
  opacity: 0.5;
}

/* headings are read normally, never flashed */
.static-heading {
  position: absolute;
  left: 50%;
  top: 50%;
  transform: translate(-50%, -50%);
  width: min(22ch, 90%);
  text-align: center;
  text-wrap: balance;
  font-size: 0.6em;
  line-height: 1.25;
  color: var(--ink-soft);
}

/* code / table / figure: shown, never flashed */
.card {
  position: absolute;
  left: 50%;
  top: 50%;
  transform: translate(-50%, -50%);
  width: min(24rem, 88%);
  display: flex;
  flex-direction: column;
  gap: var(--s2);
  padding: var(--s5);
  background: var(--panel);
  border: 1px solid var(--rule);
  border-radius: var(--r2);
  text-align: center;
}
.card__label {
  font-family: var(--face-data);
  font-size: var(--t-2xs);
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--ink-soft);
}
.card__note { font-size: var(--t-sm); color: var(--ink); }


/* 7. READER — CHROME, TRANSPORT, RAIL ==================================== */

/* The padding moved off .chrome and on to its rows: the bottom cluster is the
   instrument's panel and its edge has to run full bleed, which it cannot do
   from inside a padded box. */
.chrome {
  position: absolute;
  inset: 0;
  display: grid;
  grid-template-rows: auto 1fr auto;
  padding: 0;
  pointer-events: none;
  opacity: 1;
}
.chrome > * { pointer-events: auto; }

/* Fade OUT only, after a delay. The transition is declared on this rule and
   nowhere else, so the moment JS adds .chrome-on the rule stops matching and
   opacity snaps back to 1 with no transition to run. Chrome returns instantly;
   no JS timer needed for either direction. */
body[data-state="reading"]:not(.chrome-on) .chrome {
  opacity: 0;
  pointer-events: none;
  transition: opacity var(--d-chrome) var(--ease) var(--delay-chrome);
}
body[data-state="reading"]:not(.chrome-on) .chrome > * { pointer-events: none; }

/* top row ---------------------------------------------------------------- */
.chrome__top {
  grid-row: 1;
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: var(--s4);
  /* The exits are right-aligned and the session control is fixed over this
     corner, so the corner is reserved from the control's own token. Without
     this the disc lands on #libraryBtn and eats every click on it. */
  padding: var(--s4) var(--session-gutter) var(--s4) var(--s5);
}
.doc { min-width: 0; display: flex; flex-direction: column; gap: var(--s1); }
.doc__title {
  font-family: var(--face-prose);
  font-size: var(--t-sm);
  line-height: 1.3;
  color: var(--ink);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  max-width: 28ch;
}
.doc__meta {
  font-family: var(--face-data);
  font-variant-numeric: tabular-nums;
  font-size: var(--t-xs);
  color: var(--ink-soft);
}
.chrome__exits { display: flex; gap: var(--s2); flex: 0 0 auto; }

/* middle: the current sentence -------------------------------------------
   It used to be centred in this row and then pushed down by a flat 9rem, so
   its distance from the flash changed with viewport height and with whether
   the keyhint was showing — on a short phone it landed in the transport. It
   now docks to the top of the panel, which is a fixed relation that cannot
   collide, and floats up under the flash only where there is height for it. */
.chrome__mid {
  grid-row: 2;
  align-self: end;
  justify-self: center;
  width: 100%;
  max-width: 34em;
  padding: 0 var(--s5) var(--s4);
  text-align: center;
  font-size: var(--t-sm);
  line-height: 1.5;
  color: var(--ink-soft);
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
  overflow: hidden;
}
body[data-state="reading"] .chrome__mid { visibility: hidden; }

/* Tall viewports: sit it directly under the aperture, offset from the same
   50% and the same --t-flash the bottom hairline is drawn from, so the gap
   scales with the flash instead of being a number that is right at one size. */
@media (min-height: 800px) {
  .chrome__mid {
    position: absolute;
    left: 50%;
    top: 50%;
    align-self: start;
    padding-bottom: 0;
    transform: translate(-50%, calc(var(--t-flash) * 0.92 + 10px + var(--s5)));
  }
}

/* bottom cluster: the instrument's panel ----------------------------------
   No fill and no box — the reading field must not grow edges — but a single
   lit rule that is brightest at the anchor column, so when the panel returns
   it returns as one object with a top edge, not as four loose rows of
   furniture floating on paper. */
.chrome__bottom {
  grid-row: 3;
  position: relative;
  display: grid;
  grid-template-columns: 1fr;
  gap: var(--s4);
  justify-items: center;
  /* clears the rail's 44px hit area, which is fixed above this and was
     swallowing clicks on the bottom edge of the controls */
  padding: var(--s5) var(--s5) calc(var(--tap) + var(--s2));
}
.chrome__bottom::before {
  content: "";
  position: absolute;
  left: 0; right: 0; top: 0;
  height: 1px;
  background: linear-gradient(to right,
    transparent 0,
    var(--rule) calc(var(--anchor) * 0.45),
    var(--gate) var(--anchor),
    var(--rule) calc(var(--anchor) + (100% - var(--anchor)) * 0.5),
    transparent 100%);
}

.controls {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: var(--s3);
  width: min(26rem, 100%);
}

.transport {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: var(--s3);
}
.transport .btn--round { height: var(--tap); }
.transport .transport__play {
  width: 3.5rem;
  height: 3.5rem;
  min-width: 3.5rem;
  min-height: 3.5rem;
  font-size: var(--t-lg);
}

.steppers {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: var(--s3);
  width: 100%;
}

.stepper {
  display: grid;
  grid-template-columns: var(--tap) 1fr var(--tap);
  align-items: center;
  background: var(--panel);
  border: 1px solid var(--rule);
  border-radius: var(--r-pill);
}
/* The −/+ were set two steps larger than the number they change, so the
   controls shouted and the reading — the only thing here carrying information
   — whispered. Reversed: the value is the loudest thing in the control. */
.stepper__btn {
  min-height: var(--tap);
  background: none;
  border: 0;
  border-radius: var(--r-pill);
  color: var(--ink-soft);
  font-size: var(--t-md);
  cursor: pointer;
  transition: color var(--d-fast) var(--ease);
}
.stepper__btn:hover:not(:disabled) { color: var(--accent); }
.stepper__btn:disabled { opacity: 0.35; cursor: default; }
.stepper__value {
  font-family: var(--face-data);
  font-variant-numeric: tabular-nums;
  font-size: var(--t-md);
  color: var(--ink);
  text-align: center;
  white-space: nowrap;
}
.stepper__unit { color: var(--ink-soft); font-size: var(--t-xs); }

.chrome__minor {
  display: flex;
  align-items: baseline;
  justify-content: center;
  gap: var(--s4);
  flex-wrap: wrap;
}

/* Three columns of 1fr / auto / 1fr, with the transport claiming 26rem of the
   middle, leave each side column about 210px on a 900px laptop — and
   "§4 · Rationale" plus "12% · ~19 min left" is wider than that. It wraps to
   two right-aligned lines rather than overflowing into the steppers. */
.position {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  justify-content: flex-end;
  gap: 0 var(--s3);
  min-width: 0;
  text-align: right;
  font-family: var(--face-data);
  font-variant-numeric: tabular-nums;
  font-size: var(--t-xs);
  color: var(--ink-soft);
}
.position > span { max-width: 100%; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }

.keyhint {
  font-family: var(--face-data);
  font-size: var(--t-2xs);
  color: var(--ink-soft);
  text-align: center;
}

/* progress rail — one of the four things visible while playing ---------- */
.progress {
  position: fixed;
  left: 0; right: 0; bottom: 0;
  height: var(--tap);
  display: flex;
  align-items: flex-end;
  background: none;
  border: 0;
  padding: 0;
  cursor: pointer;
  z-index: 5;
}
.progress:focus-visible { outline: 2px solid var(--accent); outline-offset: -4px; }

.progress__track {
  display: flex;
  align-items: flex-end;
  gap: 2px;
  width: 100%;
  height: 5px;
}

.seg {
  flex: var(--seg-w, 1) 1 0;
  min-width: 2px;
  height: 3px;
  align-self: flex-end;
  position: relative;
  background: var(--rule);
  overflow: hidden;
  transition: height var(--d-fast) var(--ease);
}
.seg[data-current] { height: 5px; }

.seg__fill {
  position: absolute;
  inset: 0 auto 0 0;
  width: var(--seg-fill, 0%);
  background: var(--accent);
  transition: width 90ms linear;
}


/* 8. PEEK / TEXT VIEW / FINISHED ========================================= */

.peek {
  position: fixed;
  inset: 0;
  z-index: 20;
  background: var(--paper-read);
  display: grid;
  grid-template-rows: 1fr auto;
  padding: var(--s6) var(--s5) var(--s6);
  overflow-y: auto;
  overscroll-behavior: contain;
  animation: peek-in var(--d-med) var(--ease);
}
@keyframes peek-in {
  from { opacity: 0; transform: translateY(8px); }
  to   { opacity: 1; transform: none; }
}
.peek:focus-visible { outline: none; }

.peek__prose {
  font-family: var(--face-prose);
  font-size: var(--t-lg);
  line-height: 1.65;
  max-width: 34em;
  margin-inline: auto;
  width: 100%;
  color: var(--ink);
  align-self: center;
}
/* the only place outside .pivot where --signal appears */
.peek-current { color: var(--signal); font-weight: 600; }

/* peeking at a code block: line breaks and indentation ARE the content */
.peek__prose--verbatim {
  font-family: var(--face-data);
  font-size: var(--t-sm);
  line-height: 1.5;
  white-space: pre-wrap;
  tab-size: 2;
  overflow-x: auto;
}

.peek__hint {
  font-family: var(--face-data);
  font-size: var(--t-xs);
  color: var(--ink-soft);
  text-align: center;
}

/* read-as-text: the accessible rendering of the document ---------------- */
body.text-mode { overflow: hidden; }
.textview {
  position: fixed;
  inset: 0;
  z-index: 30;
  background-color: var(--paper);
  background-image: var(--paper-image);
  overflow-y: auto;
  /* Top clears the session disc, bottom clears the fixed bar — both are fixed
     over this scroll container, so the prose has to start and end inside them. */
  padding: calc(var(--s7) + var(--s2)) var(--s5) calc(var(--s8) + var(--s7));
}
.textview__inner {
  max-width: 34em;
  margin-inline: auto;
  display: flex;
  flex-direction: column;
  gap: var(--s5);
}
/* The document's own title, set as the document — the recap screen's title
   style was being borrowed here and it is a different kind of thing. */
.textview__title {
  font-family: var(--face-prose);
  font-size: var(--t-xl);
  font-weight: 600;
  line-height: 1.2;
  text-wrap: balance;
}
.textview__body {
  font-family: var(--face-prose);
  font-size: var(--t-lg);
  line-height: 1.65;
  display: flex;
  flex-direction: column;
  gap: var(--s4);
}
.textview__body h2 {
  font-size: var(--t-xl);
  line-height: 1.2;
  text-wrap: balance;
  margin-top: var(--s4);
}
.textview__body h3 { font-size: var(--t-lg); font-weight: 700; }
.textview__body blockquote {
  padding-left: var(--s4);
  border-left: 2px solid var(--rule);
  color: var(--ink-soft);
}
.textview__body ul { list-style: disc; padding-left: 1.2em; }

/* Follow-along highlight -------------------------------------------------
   The flash reader spends --signal on exactly one glyph, so the highlight
   here uses a tinted ground rather than coloured text: it marks a position
   without competing with the anchor's meaning. */
.tw {
  border-radius: var(--r1);
  padding: 0.05em 0.1em;
  margin: 0 -0.1em;
  /* A chunk is what you read in one flash, so it must not break across a line.
     Measured before this: 11% of chunks wrapped, and the marker covers only the
     first line fragment — so the tail of every ninth chunk was left unhighlighted
     and read as skipped words. Lines still break BETWEEN chunks, which is the
     only place a break belongs. */
  white-space: nowrap;
}

/* .tw--on is a state hook only. Inverting the word's colour is what made this
   uncomfortable to read — the text keeps --ink and the marker below supplies
   the highlight. */
.tw--on { }

/* One element that slides along the line, the way a finger tracks text, rather
   than one highlight vanishing while another appears somewhere else. It sits
   behind the words, so nothing about the type changes as it passes. */
.tw-marker {
  position: absolute;
  top: 0;
  left: 0;
  z-index: 0;
  height: 1.35em;
  border-radius: var(--r1);
  background: var(--highlight);
  opacity: 0;
  pointer-events: none;
  will-change: transform, width;
}
.tw-marker--on { opacity: 1; }

/* The marker is positioned, so the prose needs a stacking context of its own
   or it would paint underneath.

   The marker has to be excluded by name: it is also a direct child, so a bare
   `> *` matched it at equal specificity and, being later in the file, overrode
   its position:absolute — dropping it into normal flow as a visible block
   instead of a highlight sitting behind the words. */
.textview__body { position: relative; }
.textview__body > *:not(.tw-marker) { position: relative; z-index: 1; }

/* Bottom bar: every control text mode has, and the only one that stays put.
   The way out used to be a button at the top of this scroll container, which
   the view scrolls away from the instant it opens — so the exit was reliably
   off screen at the moment you needed it.

   The drop shadow is gone with it. It was the only shadow in the app: on paper
   it read as a floating widget from a different product, and on the dark
   ground it was invisible anyway. A hairline and a panel fill do the same job
   in the instrument's own language. */
.textbar {
  position: fixed;
  left: 50%;
  bottom: var(--s5);
  transform: translateX(-50%);
  z-index: 31;
  display: flex;
  align-items: center;
  gap: var(--s3);
  padding: var(--s2) var(--s4) var(--s2) var(--s2);
  background: var(--panel);
  border: 1px solid var(--rule);
  border-radius: var(--r-pill);
  max-width: calc(100vw - 2 * var(--s4));
}
.textbar__back {
  flex: 0 0 auto;
  min-height: var(--tap);
  padding-inline: var(--s3);
  border-radius: var(--r-pill);
  font-size: var(--t-sm);
  gap: var(--s1);
}
.textbar__sep {
  flex: 0 0 auto;
  width: 1px;
  align-self: stretch;
  margin-block: var(--s1);
  background: var(--rule);
}
.textbar__label {
  font-size: var(--t-sm);
  color: var(--ink-soft);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
/* The bar's own stepper: same component, sized for a control strip. */
.stepper--bar {
  flex: 0 0 auto;
  gap: var(--s1);
}
.stepper--bar .stepper__value {
  min-width: 4.5em;
  text-align: center;
  font-size: var(--t-xs);
}

/* Every word is a target — pointing at your place beats hunting for it with a
   transport. The affordance stays quiet so it never competes with the marker. */
.tw {
  cursor: pointer;
}
@media (hover: hover) {
  .tw:hover { background: var(--highlight); }
}

.textbar__pos {
  font-size: var(--t-xs);
  color: var(--ink-soft);
  white-space: nowrap;
}
/* Narrow: the word count survives and the prose label goes. "Following at 350
   wpm" is reassurance; "1,240 / 6,120" is the answer to where am I. */
@media (max-width: 32rem) {
  .textbar { gap: var(--s2); padding-right: var(--s3); }
  .textbar__label { display: none; }
}
.textview__body pre {
  font-family: var(--face-data);
  font-size: var(--t-sm);
  background: var(--panel);
  border: 1px solid var(--rule);
  border-radius: var(--r2);
  padding: var(--s4);
  overflow-x: auto;
}

/* finished ----------------------------------------------------------------
   Three hero numbers over three uppercase labels is the stock ending screen,
   and it answers a question nobody has after finishing a specification. The
   run is one line of readout now — the instrument reporting — the actions sit
   where the eye lands rather than at the foot of a scroll, and the recap,
   which is the only part of this screen with a use, is the subject. */
.view-finished {
  max-width: 38rem;
  margin: 0 auto;
  padding: var(--s7) var(--s5) var(--s8);
  display: flex;
  flex-direction: column;
  gap: var(--s4);
}

.finished__eyebrow { margin-bottom: calc(var(--s2) * -1); }

.finished__title {
  font-family: var(--face-prose);
  font-size: var(--t-xxl);
  font-weight: 600;
  line-height: 1.15;
  text-wrap: balance;
}

.stats {
  display: flex;
  flex-wrap: wrap;
  gap: var(--s1) var(--s5);
  font-size: var(--t-sm);
  color: var(--ink-soft);
}
.stat { white-space: nowrap; }
.stat__value { color: var(--ink); }

.view-finished .rule-lit { margin-block: var(--s3); }

.recap { display: flex; flex-direction: column; gap: var(--s5); }
.recap__list { display: flex; flex-direction: column; gap: var(--s5); }
.recap__section { display: flex; flex-direction: column; gap: var(--s2); }
/* The section title labels the sentence under it, so it is set in the utility
   face like every other label here; the extracted sentence is verbatim from
   the document, so it gets the reading face and full-strength ink. Those were
   the wrong way round — the label was serif and bold, the content was grey. */
.recap__heading {
  font-family: var(--face-data);
  font-size: var(--t-xs);
  font-weight: 600;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--ink-soft);
}
.recap__line {
  font-family: var(--face-prose);
  font-size: var(--t-lg);
  line-height: 1.55;
  color: var(--ink);
  max-width: 44ch;
  text-wrap: pretty;
}

/* A list of what was passed over is a footnote, not a panel. */
.skipped {
  display: flex;
  flex-direction: column;
  gap: var(--s2);
  margin-top: var(--s4);
}
.skipped__list {
  font-family: var(--face-data);
  font-size: var(--t-xs);
  color: var(--ink-soft);
  display: flex;
  flex-wrap: wrap;
  gap: var(--s1) var(--s3);
}

.finished__actions {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--s3);
  margin-top: var(--s2);
}


/* 9. RESPONSIVE ========================================================== */

@media (min-width: 560px) {
  .source__row { flex-direction: row; align-items: flex-start; }
  .source__submit { width: auto; flex: 0 0 auto; min-width: 7rem; }
}

@media (min-width: 720px) {
  .hero__stage.stage { height: 10rem; }
  .doc__title { max-width: 44ch; }
  .chrome__bottom {
    grid-template-columns: minmax(0, 1fr) auto minmax(0, 1fr);
    align-items: end;
    justify-items: center;
  }
  .chrome__bottom > .chrome__minor:first-child { justify-self: start; }
  .controls { grid-column: 2; }
  .position { justify-self: end; }
}

@media (max-width: 559px) {
  .steppers { grid-template-columns: 1fr; }
  /* The steppers stack here, so the panel grows by 56px at exactly the width
     where there is least room above it — on a 640px-tall phone the panel's top
     edge crossed the flash. The minor buttons and the position readout share a
     row instead of taking one each, which is what DESIGN.md §4.4 asks for and
     buys back more than the steppers cost. */
  .chrome__bottom {
    grid-template-columns: auto minmax(0, 1fr);
    grid-template-areas:
      "minor position"
      "controls controls";
    padding-top: var(--s3);
    gap: var(--s3);
  }
  .chrome__minor { grid-area: minor; justify-self: start; }
  .position { grid-area: position; justify-self: end; align-self: center; }
  .controls { grid-area: controls; }
  /* only the left changes — the right is the shared session gutter */
  .chrome__top { padding-left: var(--s4); }
  .chrome__bottom { padding-inline: var(--s4); }
  .chrome__mid { padding-inline: var(--s4); }
  .view-idle { gap: var(--s5); padding-inline: var(--s4); }
  .topbar { padding-inline: var(--s4); }
  /* the library title takes the row and the reading drops below it */
  .lib-title { flex: 1 1 100%; }
}

/* Short viewports. The flash sits at 50% of the viewport and cannot move, so
   everything below it has to fit into the bottom half or give way. The panel
   tightens first; then the sentence line goes, which is the declared order —
   it is the cheap version of peek, and peek itself (↓, or a long press) is
   still there and is the real feature. */
@media (max-height: 700px) {
  .chrome__bottom { padding-top: var(--s3); gap: var(--s3); }
  .keyhint { display: none; }
  .chrome__mid { display: none; }
}


/* 10. REDUCED MOTION ===================================================== */

@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    transition-duration: 0.001ms !important;
    transition-delay: 0ms !important;
    animation-duration: 0.001ms !important;
    animation-iteration-count: 1 !important;
    scroll-behavior: auto !important;
  }
  /* hard clip instead of a soft mask edge */
  .gate {
    -webkit-mask-image: none;
    mask-image: none;
    overflow: hidden;
  }
  .source__submit[aria-busy="true"]::after { animation: none; width: 100%; }
}

/* Forced colours: keep the anchor findable when the palette is overridden */
@media (forced-colors: active) {
  .rail--top, .rail--bottom { background: CanvasText; }
  .pivot { color: Highlight; }
  .seg__fill { background: Highlight; }
  .btn { border-color: ButtonBorder; }
}


/* 11. SESSION CONTROLS ==================================================== */

/* Theme is a session setting, not a transport action. It sat inside .topbar,
   which is display:none while reading, and later inside .chrome, which fades
   to pointer-events:none a second after you stop moving — so by the time you
   aimed at it, it was gone. This cluster sits outside both and is clickable in
   every state. */
.session {
  position: fixed;
  top: var(--s4);
  right: var(--session-inset-x);
  z-index: 40;
  display: flex;
  gap: var(--s2);
  transition: opacity var(--d-med) var(--ease);
}

/* While the words are moving the control recedes to 0.28 — and it used to stay
   clickable at that opacity, sitting above the full-screen stage button whose
   whole job is tap-to-pause. On touch there is no hover to bring it back, so a
   reader tapping the top-right corner to pause was aiming at something they
   could barely see and flipping the entire app between light and dark instead.
   Dimmed and inert are now the same state: the tap falls through to the stage
   and pauses, which is what was being asked for. Nothing is lost — any pointer
   move or touch restores the chrome (app.js binds pointermove/touchstart), so
   the control is solid and clickable in the same place one gesture later, and
   it is fully live in every other state including reading-with-chrome. */
body[data-state="reading"]:not(.chrome-on) .session {
  opacity: 0.28;
  pointer-events: none;
}
/* Keyboard focus is not pointer hit-testing, so Tab still reaches the control
   through the inert state and restores it. There is deliberately no :hover
   escape: an element with pointer-events:none cannot be hovered, and a rule
   that can never match is worse than no rule — the pointermove that would have
   triggered it has already brought the whole chrome back anyway. */
.session:focus-within { opacity: 1 !important; pointer-events: auto !important; }

/* The control is the two grounds you can read on, with the pivot on the seam.
   The halves are fixed colours, not tokens: it is a preview of both options,
   so it must not repaint itself into whichever one you are already using. */
.ground {
  position: relative;
  width: var(--session-size);
  height: var(--session-size);
  padding: 0;
  display: grid;
  place-items: center;
  background: var(--panel);
  border: 1px solid var(--rule);
  border-radius: var(--r-pill);
  cursor: pointer;
  transition: border-color var(--d-fast) var(--ease),
              transform var(--d-fast) var(--ease);
}
/* The visible pill stays --session-size — it is meant to recede — but the thing
   you can hit is the required --tap. A control this small, in a screen corner,
   is exactly the one that needs the slack. Derived from both tokens so it stays
   at 44px if either changes, rather than being a -4px that silently stops
   adding up. */
.ground::after {
  content: "";
  position: absolute;
  inset: calc((var(--tap) - var(--session-size)) / -2);
  border-radius: inherit;
}
.ground:hover { border-color: var(--accent); }
.ground:active { transform: scale(0.94); }

.ground__disc {
  position: relative;
  width: 18px;
  height: 18px;
  border-radius: var(--r-pill);
  background: linear-gradient(105deg, #F0EBE0 0 50%, #0F1219 50% 100%);
  box-shadow: inset 0 0 0 1px rgb(0 0 0 / 0.18);
  transform: rotate(var(--disc-turn));
  transition: transform var(--d-med) var(--ease);
}

/* One point of light, on the seam — the same idea the reader is built around. */
.ground__pivot {
  position: absolute;
  top: 50%;
  left: 50%;
  width: 5px;
  height: 5px;
  margin: -2.5px 0 0 -2.5px;
  border-radius: var(--r-pill);
  background: #C97A16;
}

@media (prefers-reduced-motion: reduce) {
  .ground__disc, .session { transition: none; }
}

/* Narrow: the control tucks closer to the edge. Overriding the token rather
   than the rule means the gutter every other cluster reserves shrinks with it,
   instead of the two drifting apart — which is the bug this section caused. */
@media (max-width: 30rem) {
  :root { --session-inset-x: var(--s4); }
  .session { top: var(--s3); }
}


/* 12. THE PRODUCT PAGE, AND THE NAV THAT REACHES IT ====================== */

/* /wordglide, plus the one component both pages share: the bar that switches
   between them.

   The nav carries .topbar as well as .nav, deliberately — §4 already hides
   .topbar in every reading state, and the bar has to obey that. The reader's
   whole direction is that the chrome leaves once words are moving; a nav that
   stayed would be the one thing on screen contradicting it.

   --signal, amber, stays the reader's anchor and --glide, green, stays the
   extension's guide. Each shows up in exactly two places outside its own
   demonstration: the dot before an eyebrow, and the tag in the nav. --accent
   remains the only interactive colour, so no button ever competes with a
   demonstration for the eye. */

.nav {
  /* .topbar still carries the original design's max-width: 46rem and auto
     margins, from when it was a mark sitting over a 46rem column. The nav is
     a full-bleed bar with its own background, so it has to undo both — left
     as they were, the bar painted a 736px panel floating in the middle of the
     window with two hard edges. */
  max-width: none;
  margin-inline: 0;
  width: 100%;
  position: sticky;
  top: 0;
  /* One below .session (§11), which is fixed over this same corner. The
     gutter below keeps them from overlapping at all, but if that ever slips
     the disc stays on top and stays clickable rather than being swallowed by
     the bar — which is the exact bug §11 was rebuilt to fix once already. */
  z-index: 39;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--s4);
  /* The session disc is fixed over this corner; --session-gutter is the one
     number that reserves it, so the two cannot drift apart. */
  padding: var(--s3) var(--session-gutter) var(--s3) clamp(var(--s5), 4vw, var(--s8));
  /* Translucent and blurred, so content passing under it stays legible as a
     hint rather than a hard cut. The ground is flat now, so the fallback fill
     below is the same colour and cannot seam. */
  background: var(--paper);
  background: color-mix(in srgb, var(--paper) 78%, transparent);
  -webkit-backdrop-filter: saturate(1.4) blur(10px);
  backdrop-filter: saturate(1.4) blur(10px);
}
/* The lit rule again, as the bar's own underline: brightest at --anchor,
   fading out at both ends, so even the nav is off-centre in the way the
   reading surface is. */
.nav::after {
  content: "";
  position: absolute;
  left: 0; right: 0; bottom: 0;
  height: 1px;
  background: linear-gradient(to right,
    transparent 0,
    var(--rule) calc(var(--anchor) * 0.45),
    var(--gate) var(--anchor),
    var(--rule) calc(var(--anchor) + (100% - var(--anchor)) * 0.5),
    transparent 100%);
}

.nav .wordmark { font-size: var(--t-md); }
.wordmark--link { color: inherit; text-decoration: none; }
.wordmark--link:hover { color: var(--accent); }

.nav__links {
  display: flex;
  align-items: center;
  gap: var(--s1);
  min-width: 0;
}

/* Text, not pills. Two destinations do not need two buttons — the current one
   is marked by weight and a hairline under it, which is the same device the
   gate uses to say "here", and it survives with colour unavailable. */
.nav__link {
  display: inline-flex;
  align-items: center;
  gap: var(--s2);
  min-height: 2.25rem;
  padding: 0 var(--s3);
  border-radius: var(--r1);
  font-size: var(--t-sm);
  color: var(--ink-soft);
  text-decoration: none;
  white-space: nowrap;
  transition: color var(--d-fast) var(--ease), background-color var(--d-fast) var(--ease);
}
.nav__link:hover { color: var(--ink); background: var(--panel); }
.nav__link:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
.nav__link.is-current { color: var(--ink); font-weight: 600; }
.nav__link.is-current::after {
  content: "";
  position: absolute;
  bottom: -1px;
  left: var(--s3); right: var(--s3);
  height: 2px;
  border-radius: 2px;
  background: var(--accent);
}
.nav__link { position: relative; }

/* The extension's own colour, naming what the other destination is. It is the
   first thing to go when the bar runs out of room: at 390px the wordmark, two
   links, this tag and the theme disc do not fit, and the disc — which is
   fixed over the corner — was landing on top of it. The tag is the only one
   of the four that nothing depends on. */
/* Narrow sheets: the bar has to hold a mark, two links and the theme disc in
   390px, and the larger type scale pushed that over — 'WordGlide' was being
   clipped by the disc. The tag goes first because nothing depends on it, then
   the mark gives back its tracking and a size step. Measured to fit at 360px
   with the session gutter still reserved. */
@media (max-width: 34rem) {
  .nav__tag { display: none; }
  .nav .wordmark { font-size: var(--t-sm); letter-spacing: 0.04em; }
  .nav__link { padding: 0 var(--s2); font-size: var(--t-xs); }
  .nav__links { gap: 0; }
}
.nav__tag {
  font-size: var(--t-2xs);
  color: var(--glide);
  padding: 2px var(--s2);
  border-radius: var(--r-pill);
  background: var(--glide-wash);
}


/* / and /wordglide. Static documents, no state machine, sharing every token
   above — the reader at /read is the app, these two sell it.

   The argument both pages have to make visually is that the products are
   OPPOSITES, so the design makes that structural rather than stating it: the
   home page is one field split in two, and the only real difference between
   the halves is the direction their demonstration moves. Left, the column is
   fixed and the words are pulled through it. Right, the words are fixed and
   the guide is pulled through them. Same paper, same faces, opposite motion.

   Two signals, each confined to its own product: --signal, amber, is the
   reader's anchor and appears nowhere else; --glide, green, is the
   extension's guide and appears nowhere else. --accent stays the only
   interactive colour on both pages, so a tinted button never competes with a
   demonstration for the eye. */

.product__main {
  max-width: 82rem;
  margin: 0 auto;
  padding: var(--s4) clamp(var(--s5), 4vw, var(--s8)) var(--s8);
  display: flex;
  flex-direction: column;
  gap: var(--s7);
}

.section-title {
  font-family: var(--face-prose);
  font-size: var(--t-xl);
  font-weight: 600;
  line-height: 1.2;
  text-wrap: balance;
}

/* The wordmark is a link home on these pages, not a masthead. */
.wordmark--link { color: inherit; text-decoration: none; }
.wordmark--link:hover { color: var(--accent); }



/* ── product page: the hero ─────────────────────────────────────────────── */

/* The demonstration is the pitch, so it sits beside the headline in the first
   frame rather than below it. Sized to what it holds — never to the viewport,
   which would push the page out of its own opening view. */
.hero {
  display: grid;
  gap: var(--s5);
  align-items: start;
  padding-right: var(--session-gutter);
}
.hero__title { margin-top: var(--s2); }
.hero__pitch { display: flex; flex-direction: column; gap: var(--s3); min-width: 0; }
.hero__demo { min-width: 0; }
/* The page's title, being read by the thing the page is selling: the wordmark
   sits on a guide underline with the extension's own hand at the end of it,
   exactly where the cursor would be a moment after it started. It is the most
   characteristic image in this product's world, it is drawn with the
   product's own geometry, and it is the only flourish on the page — the
   demonstration does the rest of the arguing, so everything around it stays
   quiet. Static: the hand marks a position, it does not animate across a
   heading while you are trying to read it. */
.hero__title {
  font-family: var(--face-prose);
  font-size: var(--t-hero);
  font-weight: 600;
  line-height: 1;
  letter-spacing: -0.02em;
  /* room for the hand, which hangs below the underline out of flow */
  padding-bottom: 0.42em;
}
/* inline-block so the underline and the hand measure the word, not the line */
.hero__word {
  position: relative;
  display: inline-block;
  padding-bottom: 0.16em;
}
/* The guide, drawn under the word the way the extension draws it: 2px, the
   glide colour, running the width of the text and no further. */
.hero__word::after {
  content: "";
  position: absolute;
  left: 0; right: 0; bottom: 0;
  height: 2px;
  border-radius: 2px;
  background: var(--glide);
}
/* Out of flow, so the mark can sit exactly where the cursor would without
   any chance of disturbing the heading it belongs to. The tip rests on the
   underline at the end of the word — one moment after the guide set off. */
.hero__hand {
  position: absolute;
  right: -0.08em;
  top: 100%;
  margin-top: -0.04em;
  width: 0.5em;
  height: 0.5em;
  color: var(--glide);
  filter: drop-shadow(0 1px 1px #193b3826);
}
.hero__hand svg { display: block; width: 100%; height: 100%; overflow: visible; }
.hero__thesis {
  font-size: var(--t-lg);
  line-height: 1.5;
  color: var(--ink-soft);
  max-width: 40ch;
  text-wrap: pretty;
}


/* ── the store button ───────────────────────────────────────────────────── */

.cta {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--s2) var(--s3);
  margin-top: var(--s2);
}
.btn--store { gap: var(--s3); padding-inline: var(--s5); font-size: var(--t-md); }
.btn__store-mark { display: inline-flex; }
.btn__store-mark svg { display: block; }

/* Not yet published. It is styled as the primary action because that is what
   it will be, but it is a <span role="link" aria-disabled>, not an <a>: there
   is nowhere to send anybody yet, and a button that goes nowhere is worse
   than one that says so. */
/* Not "disabled" — not yet. At 0.55 the fill went grey and the button read
   as broken; it now keeps its colour and says what it is in words beside it,
   which is the only thing that was ever load-bearing here. */
.btn--store.is-pending {
  cursor: default;
  opacity: 0.8;
  transform: none;
}
.btn--store.is-pending:hover { filter: none; }
/* Plain words beside the button, not a badge. A dashed pill with tracked-out
   capitals is decoration standing in for the one thing that actually needs
   saying, which is that you cannot click this yet. */
.cta__soon {
  font-family: var(--face-data);
  font-size: var(--t-xs);
  color: var(--signal);
  white-space: nowrap;
}
.cta__alt {
  font-size: var(--t-sm);
  color: var(--ink-soft);
}
.cta__alt a { color: var(--accent); }


/* ── at a glance ────────────────────────────────────────────────────────── */

/* One instrument band read across, not five cards. This is the only place on
   either page that takes a fill, because it is the only place that is a
   readout rather than prose. */
.glance {
  background: var(--panel);
  border: 1px solid var(--rule);
  border-radius: var(--r2);
  padding: var(--s4) var(--s5);
}
.glance__list {
  display: flex;
  flex-wrap: wrap;
  gap: var(--s4) var(--s6);
  margin: 0;
}
.glance__list > div { display: flex; flex-direction: column; gap: var(--s1); }
.glance__list dt {
  font-size: var(--t-sm);
  color: var(--ink-soft);
}
.glance__list dd {
  margin: 0;
  font-family: var(--face-data);
  font-size: var(--t-xs);
  color: var(--ink-soft);
  font-variant-numeric: tabular-nums;
}
.glance__value {
  font-weight: 600;
  font-size: var(--t-lg);
  color: var(--ink);
  margin-right: var(--s1);
}


/* ── features ───────────────────────────────────────────────────────────── */

.features { display: flex; flex-direction: column; gap: var(--s5); }
/* Deliberately not cards: eight bordered, rounded, shadowed boxes would give
   eight equal edges to look at and flatten the whole page. A hairline above
   each is enough to group a heading with its paragraph. */
.features__grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(16rem, 1fr));
  gap: var(--s5) var(--s6);
}
.feature {
  display: flex;
  flex-direction: column;
  gap: var(--s2);
  padding-top: var(--s3);
  border-top: 1px solid var(--rule);
  min-width: 0;
}
.feature h3 {
  font-family: var(--face-data);
  font-size: var(--t-xs);
  font-weight: 600;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--ink);
}
.feature p {
  font-family: var(--face-prose);
  font-size: var(--t-sm);
  line-height: 1.6;
  color: var(--ink-soft);
  text-wrap: pretty;
}
.feature strong { color: var(--ink); font-weight: 600; }


/* ── versus ─────────────────────────────────────────────────────────────── */

.versus {
  position: relative;
  padding-top: var(--s6);
  display: flex;
  flex-direction: column;
  gap: var(--s4);
}
.versus__lede {
  font-family: var(--face-prose);
  font-size: var(--t-md);
  line-height: 1.6;
  max-width: 56ch;
  text-wrap: pretty;
}
.versus__table { width: 100%; border-collapse: collapse; text-align: left; }
.versus__table th,
.versus__table td {
  padding: var(--s3) var(--s4) var(--s3) 0;
  vertical-align: top;
  border-bottom: 1px solid var(--rule);
}
.versus__table tr:last-child th,
.versus__table tr:last-child td { border-bottom: 0; }
.versus__table thead th {
  font-family: var(--face-prose);
  font-size: var(--t-sm);
  font-weight: 600;
  color: var(--ink-soft);
  white-space: nowrap;
}
/* The column this page is arguing for. Named rather than merely coloured, so
   it still reads with colour unavailable. */
.versus__table thead .versus__mine { color: var(--glide); }
.versus__table tbody th {
  font-family: var(--face-data);
  font-size: var(--t-xs);
  font-weight: 400;
  color: var(--ink-soft);
  white-space: nowrap;
}
.versus__table tbody td {
  font-family: var(--face-prose);
  font-size: var(--t-sm);
  line-height: 1.5;
  color: var(--ink);
}
.versus__table tbody td:last-child { padding-right: 0; }
.versus__note { font-family: var(--face-data); font-size: var(--t-sm); }
.versus__note a { color: var(--accent); text-decoration: none; }
.versus__note a:hover { text-decoration: underline; }


/* ── install ────────────────────────────────────────────────────────────── */

.install {
  position: relative;
  padding-top: var(--s6);
  display: flex;
  flex-direction: column;
  gap: var(--s5);
}
/* The only card on either marketing page, and it earns the border: everything
   else in this section is prose, and this is the action. */
.install__lead {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--s4) var(--s6);
  padding: var(--s5);
  background: var(--panel);
  border: 1px solid var(--rule);
  border-radius: var(--r2);
}
/* Wraps on its own at the width where the sentence and the button stop
   fitting side by side, which is a different number on every screen and not
   one a breakpoint should be guessing. */
.install__lead-copy {
  flex: 1 1 22rem;
  font-family: var(--face-prose);
  font-size: var(--t-md);
  line-height: 1.6;
  max-width: 44ch;
  text-wrap: pretty;
}
.install__lead .cta {
  flex: 0 0 auto;
  flex-direction: column;
  align-items: flex-start;
  gap: var(--s2);
  margin-top: 0;
}

.install__source { display: flex; flex-direction: column; gap: var(--s3); }
/* Read as a sentence, not stamped as a label. The tracked-out mono capitals
   that used to sit here were a caption pretending to be structure. */
.install__h3 {
  font-family: var(--face-prose);
  font-size: var(--t-lg);
  font-weight: 400;
  color: var(--ink);
}
.install__lede,
.install__note {
  font-family: var(--face-prose);
  font-size: var(--t-sm);
  line-height: 1.6;
  color: var(--ink-soft);
  max-width: 56ch;
  text-wrap: pretty;
}

/* The ordinals are data; the steps are prose. Five commands in order is one of
   the few places on this site where a sequence really is a sequence. */
.steps {
  display: flex;
  flex-direction: column;
  gap: var(--s3);
  counter-reset: step;
  list-style: none;
  padding: 0;
  max-width: 62ch;
  font-family: var(--face-prose);
  font-size: var(--t-sm);
  line-height: 1.5;
}
/* NOT display: grid. A grid container makes a grid item of every child, and
   the children of a step are the <a>, the <code> and the <strong> inside its
   sentence: each one was promoted out of the text, dropped into the 1.5rem
   ordinal track, and overflowed straight across the words beside it. The
   ordinal comes out of flow instead and the sentence stays a sentence. */
.steps li {
  counter-increment: step;
  position: relative;
  padding-left: 2.5rem;
}
.steps li::before {
  content: counter(step, decimal-leading-zero);
  position: absolute;
  left: 0;
  top: 0;
  /* the step's own line box, so the ordinal sits on the first line's baseline */
  line-height: calc(var(--t-sm) * 1.5);
  font-family: var(--face-data);
  font-size: var(--t-2xs);
  color: var(--ink-soft);
}
.steps a, .privacy a, .limits a { color: var(--accent); }
/* Scoped: this stylesheet also dresses the reader, whose text view renders
   article code and must not pick up a panel fill from a marketing rule. */
.product code {
  font-family: var(--face-data);
  font-size: 0.9em;
  padding: 0.1em 0.35em;
  background: var(--panel);
  border: 1px solid var(--rule);
  border-radius: var(--r1);
}


/* ── privacy, limits, footers ───────────────────────────────────────────── */

.privacy, .limits {
  position: relative;
  display: flex;
  flex-direction: column;
  gap: var(--s3);
}
.privacy { padding-top: var(--s6); }
.privacy p, .limits p {
  font-family: var(--face-prose);
  font-size: var(--t-md);
  line-height: 1.65;
  max-width: 60ch;
  text-wrap: pretty;
}
.limits p { font-size: var(--t-sm); color: var(--ink-soft); }

/* ── the footer, the same on both pages ─────────────────────────────────── */

/* It used to be three links on one line, which is a colophon, not a footer.
   The end of either page is the one place where the other product is the most
   useful thing on screen, so that is what most of it is.

   The divider is .rule-lit-before — §3's lit rule, brightest at --anchor — and
   the guide's dot rests on that column. Every other mark on this site is at
   38% for a reason; this one closes the page there. It is the single piece of
   decoration in the footer, and nothing else in here is dressed at all. */
.sitefoot {
  position: relative;
  padding-top: var(--s7);
  display: flex;
  flex-direction: column;
  gap: var(--s5);
}
.sitefoot__dot {
  position: absolute;
  left: var(--anchor);
  top: 0;
  width: 9px;
  height: 9px;
  /* centred on the 1px rule and on the anchor column, not beside them */
  margin: -4px 0 0 -4.5px;
  border-radius: 50%;
  background: var(--dot);
}

.sitefoot__cols {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(17rem, 1fr));
  gap: var(--s5) var(--s6);
  align-items: start;
}
.sitefoot__col {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: var(--s2);
  min-width: 0;
}
.sitefoot__kicker {
  font-family: var(--face-prose);
  font-size: var(--t-sm);
  color: var(--ink-soft);
}
/* The green underline is the WordGlide wordmark's, reused: a line with
   something travelling along it. It goes red on hover — the guide arriving —
   which is the only hover effect on either page and is spent here. */
.sitefoot__cross {
  font-family: var(--face-prose);
  font-size: var(--t-xl);
  line-height: 1.2;
  color: var(--ink);
  text-decoration: none;
  padding-bottom: 2px;
  border-bottom: 2px solid var(--accent);
  transition: border-color var(--d-med) var(--ease);
}
.sitefoot__cross:hover { border-bottom-color: var(--dot); }
.sitefoot__line {
  font-family: var(--face-prose);
  font-size: var(--t-sm);
  line-height: 1.6;
  color: var(--ink-soft);
  max-width: 42ch;
  text-wrap: pretty;
}

/* Two items, spaced apart. Not margin-left: auto on the second one — that
   holds the indent when the row wraps, so on a phone the line arrived pushed
   halfway across the screen. space-between puts a wrapped line's only item
   back at the left, with no breakpoint to get wrong. */
.sitefoot__fine {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--s2) var(--s5);
  font-family: var(--face-data);
  font-size: var(--t-xs);
  color: var(--ink-soft);
}
.sitefoot__src {
  display: flex;
  flex-wrap: wrap;
  gap: var(--s2) var(--s5);
}
.sitefoot__fine a { color: var(--accent); text-decoration: none; }
.sitefoot__fine a:hover { text-decoration: underline; }


/* ── the guide demonstration, on both pages ─────────────────────────────── */

.glide { display: flex; flex-direction: column; gap: var(--s3); }

/* The prose is real, selectable, screen-reader-readable text in the body
   face — because that is the claim being made: the extension does not lift
   the words out of the page or re-set them, it lights the ones already
   there. The guide is drawn over the top and never touches the text nodes,
   which is exactly what the extension does to your article. */
.glide__page {
  position: relative;
  padding: var(--s5);
  background: var(--panel);
  border: 1px solid var(--rule);
  border-radius: var(--r2);
}
.glide__page:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }

.glide__prose {
  position: relative;
  z-index: 1;
  font-family: var(--face-prose);
  font-size: var(--t-lg);
  line-height: 1.75;
  max-width: 34em;
  text-wrap: pretty;
}
.glide__prose + .glide__prose { margin-top: var(--s4); }

/* Above the text, because that is where the extension's overlay is — one
   fixed host under documentElement, drawn over the article rather than woven
   into it. Pointer-transparent throughout: the guide never takes a click away
   from the page it is reading. */
.glide__layer {
  position: absolute;
  inset: 0;
  z-index: 2;
  pointer-events: none;
  overflow: hidden;
  border-radius: inherit;
}

.gw { position: relative; }

/* No transition: the sweep is driven frame by frame off the reading clock,
   the way guide.ts drives it. A CSS transition here would be a second, slower
   clock fighting the first.

   Red, not green, and deliberately so. The guide is the one thing on the page
   that is moving and the one thing you are meant to watch, and on a page built
   out of the extension's green it would be camouflage. core.ts ships red as a
   real choice in palette.red, so this is a setting, not a liberty. */
.guide-cursor {
  position: absolute;
  top: 0;
  left: 0;
  color: var(--dot);
  filter: drop-shadow(0 1px 1px #193b3826);
  transform: translate3d(-100px, -100px, 0);
  will-change: transform;
}
.guide-cursor svg { display: block; width: 100%; height: 100%; overflow: visible; }

/* guide.ts's trail, rule for rule: 30% opacity to nothing, 180ms, linear, and
   it stays gone when it is done. Under the dot, so it reads as something the
   dot left rather than something it is dragging. */
.guide-trail { position: absolute; inset: 0; }
.trail-dot {
  position: absolute;
  top: 0;
  left: 0;
  border-radius: 50%;
  background: var(--dot);
  animation: trail-fade 180ms linear forwards;
}
@keyframes trail-fade { from { opacity: 0.3; } to { opacity: 0; } }

/* 4px radius, the 130ms cap and the easing curve are guide.ts's .fragment. */
.guide-box {
  position: absolute;
  top: 0;
  left: 0;
  border-radius: 4px;
  transform: translate3d(-100px, -100px, 0);
  will-change: transform, width, height;
  transition: transform 130ms cubic-bezier(.22, .7, .24, 1),
              width 130ms cubic-bezier(.22, .7, .24, 1),
              height 130ms cubic-bezier(.22, .7, .24, 1);
}
.guide-box[data-mode="highlight"] { background: var(--glide-fill); }
.guide-box[data-mode="outline"] { border: 2px solid var(--glide); background: none; }

.glide[data-playing="false"] .guide-cursor,
.glide[data-playing="false"] .guide-box { opacity: 0.55; }

.glide__controls {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--s3);
}
.glide__foot {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--s1) var(--s4);
}
.glide__hint {
  font-family: var(--face-data);
  font-size: var(--t-xs);
  color: var(--ink-soft);
}

/* Three exclusive choices, so radios — not three buttons and a class. The
   visual is a segmented control; the semantics are a radiogroup. */
.modes {
  display: inline-flex;
  border: 1px solid var(--rule);
  border-radius: var(--r-pill);
  background: var(--panel);
  padding: 2px;
}
.modes__opt { position: relative; display: inline-flex; }
.modes__opt input { position: absolute; inset: 0; opacity: 0; margin: 0; cursor: pointer; }
.modes__opt span {
  display: inline-flex;
  align-items: center;
  min-height: 2.25rem;
  padding: 0 var(--s4);
  border-radius: var(--r-pill);
  font-size: var(--t-sm);
  color: var(--ink-soft);
  white-space: nowrap;
  transition: background-color var(--d-fast) var(--ease), color var(--d-fast) var(--ease);
}
.modes__opt input:hover + span { color: var(--ink); }
.modes__opt input:checked + span {
  background: var(--glide-wash);
  color: var(--ink);
  font-weight: 600;
}
.modes__opt input:focus-visible + span { outline: 2px solid var(--accent); outline-offset: 2px; }


/* ── responsive ─────────────────────────────────────────────────────────── */

/* Stacked at every width, the same shape as the home page: the demonstration
   leads, then the name, the sentence and the button. Two columns would put the
   demo in whichever track it was given rather than at full size, and the demo
   is the argument this page makes. */

@media (max-width: 559px) {
  .product__main {
    padding: var(--s3) var(--s4) var(--s7);
    gap: var(--s6);
  }
  .glide__page { padding: var(--s4); }
  .glide__prose { font-size: var(--t-md); }
  .modes__opt span { padding: 0 var(--s3); font-size: var(--t-xs); }
  .glance { padding: var(--s4); }
  .glance__list { gap: var(--s4); }
  .install__lead { padding: var(--s4); }
  .steps li { padding-left: 2.25rem; }

  /* The table stops being a table and becomes two labelled readings per row:
     three columns at 360px is four words a line and unreadable. */
  .versus__table, .versus__table tbody, .versus__table tr { display: block; }
  .versus__table thead { display: none; }
  .versus__table tr { padding-block: var(--s3); border-bottom: 1px solid var(--rule); }
  .versus__table tr:last-child { border-bottom: 0; }
  .versus__table tbody th,
  .versus__table tbody td { display: block; border: 0; padding: 0 0 var(--s1); }
  .versus__table tbody td::before {
    content: attr(data-label) " — ";
    font-family: var(--face-data);
    font-size: var(--t-2xs);
    letter-spacing: 0.12em;
    text-transform: uppercase;
    color: var(--ink-soft);
  }
  .versus__table tbody td:last-child::before { color: var(--glide); }
}

/* The demonstrations are motion over text, which is exactly what reduced
   motion is about. Neither auto-runs; the guide renders once on the opening
   word, the gate holds the sample frame in the markup, and every control —
   play, mode, speed, the arrow keys, click-a-word — still works. Reduced
   motion means no unrequested animation, not no product. */
@media (prefers-reduced-motion: reduce) {
  .guide-box { transition: none; }
  /* glide-demo.js already refuses to drop one; this is the second lock. */
  .trail-dot { display: none; }
}


/* 13. THE FIGURE ======================================================== */

/* All that survives of the Blueprint pass, and it earns its place: the
   drawing on /wordglide shows the guide sweeping a word, stopping at the line
   end and settling before it resumes, with the diagonal it does not take
   drawn faint. Prose can describe one of those; a drawing shows all three at
   once, beside the live demo they can be checked against.

   The dimension line and callout that annotated the home gate are gone. They
   were the other half of that pass, and drawing more lines beside a gate
   somebody already found confusing was the opposite of what it needed. The
   gate carries a caption in words instead. */

/* ── the figure ─────────────────────────────────────────────────────────── */

.figure {
  position: relative;
  display: flex;
  flex-direction: column;
  gap: var(--s3);
}
.figure__lede {
  font-family: var(--face-prose);
  font-size: var(--t-md);
  line-height: 1.6;
  color: var(--ink-soft);
  max-width: 56ch;
  text-wrap: pretty;
}
/* The drawing sits on the panel rather than the sheet, so the grid does not
   show through the thing being drawn — a drawing is made ON paper, not out
   of it. It scrolls rather than shrinking below its legible size: a diagram
   squeezed to 320px is a diagram nobody can read. */
.drawing {
  margin: var(--s3) 0 0;
  padding: var(--s4);
  background: var(--panel);
  border: 1px solid var(--rule);
  border-radius: var(--r2);
  overflow-x: auto;
}
.drawing svg { display: block; width: 100%; min-width: 34rem; height: auto; }

.dw-text { font-family: var(--face-prose); font-size: 21px; fill: var(--ink); }
.dw-path { stroke: var(--glide); stroke-width: 2; fill: none; }
.dw-arrow { fill: var(--glide); }
.dw-tick  { stroke: var(--glide); stroke-width: 1; opacity: 0.45; }
.dw-dash  { stroke: var(--glide); stroke-width: 2; stroke-dasharray: 3 4; fill: none; }
.dw-dim   { stroke: var(--signal); stroke-width: 1; fill: none; }
/* The path it deliberately does not take, drawn faint so the absence is
   legible. A drawing can show a rejected option; prose has to describe it. */
.dw-ghost { stroke: var(--ink-soft); stroke-width: 1; stroke-dasharray: 2 5; opacity: 0.4; fill: none; }
.dw-anno {
  font-family: var(--face-data);
  font-size: 11px;
  letter-spacing: 0.03em;
  fill: var(--ink-soft);
}
.dw-anno--key { fill: var(--signal); }
.dw-anno--no  { fill: var(--ink-soft); opacity: 0.75; }


/* 14. THE POINTER ======================================================= */

/* The front page replaces the mouse cursor with the extension's dot and its
   trail; js/cursor.js has the argument for why, and the four things it
   refuses to do. Everything here is inert unless that file has mounted and
   put .has-pointer on the body, which it does only in the idle states — so
   the reader itself never sees a rule in this section.

   Specificity: (0,1,1), one class and one element. That beats the
   cursor: pointer on .btn and every other single-class rule in the sheet,
   and the text-field rule is the same weight and comes after, so it wins
   where it has to. No !important anywhere. */
body.has-pointer,
body.has-pointer * { cursor: none; }
body.has-pointer input,
body.has-pointer textarea { cursor: text; }
/* (0,2,0) and (0,2,1), so both beat the (0,1,1) above. The demonstration panel
   draws the real guide; the mouse hands it back to the browser rather than
   putting a second red dot in the same box. */
body.has-pointer .glide__page,
body.has-pointer .glide__page * { cursor: auto; }

.pointer {
  position: fixed;
  inset: 0;
  z-index: 80;
  pointer-events: none;
}

/* Nothing is drawn until the mouse has actually moved. A dot parked in the
   corner of a page nobody has touched reads as a rendering fault. */
.pointer:not([data-seen]) .pointer__dot { opacity: 0; }

.pointer__dot {
  position: absolute;
  top: 0;
  left: 0;
  width: 12px;
  height: 12px;
  border-radius: 50%;
  background: var(--dot);
  /* Position is set frame by frame in JS and must not be transitioned — a
     lagging cursor is a broken cursor. Only the shape eases, which is why
     the dot is centred with a second translate rather than by offsetting
     its own box: it stays under the pointer at every size. */
  transition: width var(--d-med) var(--ease),
              height var(--d-med) var(--ease),
              background-color var(--d-med) var(--ease),
              box-shadow var(--d-med) var(--ease),
              opacity var(--d-fast) linear;
}

/* Over anything clickable the dot opens into the extension's outline mode:
   same colour, hollow instead of solid, and big enough to ring what it is
   pointing at rather than cover it. */
.pointer[data-shape="ring"] .pointer__dot {
  width: 34px;
  height: 34px;
  background: transparent;
  box-shadow: inset 0 0 0 2px var(--dot);
}
/* Over a text field the caret is the pointer; inside the demonstration the
   guide is. */
.pointer[data-shape="text"] .pointer__dot,
.pointer[data-shape="cede"] .pointer__dot { opacity: 0; }
.pointer[data-press] .pointer__dot { width: 9px; height: 9px; }
/* after the two above, because both match a pressed ring */
.pointer[data-press][data-shape="ring"] .pointer__dot { width: 28px; height: 28px; }

.pointer__drop {
  position: absolute;
  top: 0;
  left: 0;
  width: 5px;
  height: 5px;
  border-radius: 50%;
  background: var(--dot);
  animation: pointer-fade 260ms linear forwards;
}
@keyframes pointer-fade { from { opacity: 0.35; } to { opacity: 0; } }
