/* ============================================================
   erulia.com v2 — LIVE PRODUCT MOCKS
   Layer 3. Loads after tokens.css and site.css.

   Every product image on this site is live DOM, never a
   screenshot (§2.2). This file holds the primitives those
   mocks are built from, because they recur on /, /readiness,
   /revula and /lurest — a mock defined inside one page is a
   mock the next page re-draws by eye.

   MOCK FIDELITY IS A FLOOR (§4). Every mock that ships carries:
     · a real frame — 24px radius, 1px --line, shadow-2
     · at least three rows of real content
     · one piece of live state (status, delta, timestamp)
     · one anchor a reader could go and check
   And a ceiling: no invented chrome, no colour outside the
   token set, no mock that outweighs the paragraph beside it.

   WHERE THE CONTENT COMES FROM — none of it is invented (§4,
   real-UI rule). Change the app and change these together:
     · readiness 67, trend +4, exam "Midterm · Fri 29 May"
       ....................... revula/…/project/data.jsx SUBJECTS[0]
     · topic scores 88 / 74 / 52 / 34 / 41 and their states
       ....................... same record, .topics
     · the 14-day ladder 42…67 ... data.jsx READINESS_HISTORY
     · source titles, origins, lengths, capture states
       ....................... lurest/Lurest/src/data/sources.ts
     · anchors 04:12 · 08:45 · p.198 … same file, SEG_* consts
     · the six kinds ............ lurest/Lurest/src/data/meta.ts KIND
   Sample data is labelled "Sample" on the surface of every
   frame. That is a compliance rule, not a style choice.

   IN-APP TYPE ROLES, NOT MARKETING TYPE. These are app
   surfaces, so they use tokens.css's seven in-app roles
   (--fs-title / body / ui / meta / mono) and honour the 13px
   floor. A mock set in the marketing scale stops looking like
   the product, which is the only thing it is here to do.
   ============================================================ */

/* ============ the frame ============ */
.mock {
  background: var(--bg);
  border: 1px solid var(--line);
  border-radius: var(--r-xl);
  box-shadow: var(--shadow-2);
  overflow: hidden;
  /* Mocks carry app UI. Reset the marketing body size at the frame
     so nothing inside inherits 17px and drifts off the app scale. */
  font-size: var(--fs-ui);
  line-height: var(--lh-ui);
  color: var(--ink);
  text-align: left;
}

/* Title bar — names the app the mock is of, and carries the Sample label. */
.mock-bar {
  display: flex; align-items: center; gap: var(--space-8);
  padding: 12px var(--space-20);
  border-bottom: 1px solid var(--line);
  background: var(--bg-soft);
}
.mock-bar .app {
  font-family: var(--font-display); font-weight: 550;
  font-size: 15px; letter-spacing: -.01em; color: var(--ink);
}
.mock-bar .where {
  font-size: var(--fs-meta); line-height: var(--lh-meta); color: var(--ink-3);
}
.mock-bar .where::before { content: "·"; margin: 0 6px; }
/* The compliance label. Generalised out of `.mock-bar` in step 7, because
   /download's app tiles carry a real app surface under a head that is NOT
   a mock bar — and the label is a compliance rule, so the one surface it
   is hardest to put it on is exactly the surface that must not get a
   second, slightly-different definition of it. Only the `margin-left`
   belongs to the bar. */
.sample {
  flex: none;
  font-size: 12px; font-weight: 600; letter-spacing: .06em; text-transform: uppercase;
  color: var(--ink-3); background: var(--surface-3);
  border-radius: var(--r-pill); padding: 3px 9px;
}
.mock-bar .sample { margin-left: auto; }
.mock-body { padding: var(--space-20); }
.mock-body > * + * { margin-top: var(--space-16); }

/* Shared in-app text roles inside a mock. */
.m-label {
  font-family: var(--font-ui);
  font-size: var(--fs-meta); line-height: var(--lh-meta);
  font-weight: 600; letter-spacing: .08em; text-transform: uppercase;
  color: var(--ink-3); margin: 0;
}
.m-title { font-size: var(--fs-title); line-height: var(--lh-title); letter-spacing: var(--tr-title); font-weight: 600; margin: 0; }
.m-body { font-size: var(--fs-body); line-height: var(--lh-body); color: var(--ink-2); margin: 0; }
.m-meta { font-size: var(--fs-meta); line-height: var(--lh-meta); color: var(--ink-3); margin: 0; }
.m-anchor {
  font-family: var(--font-mono);
  font-size: var(--fs-mono); line-height: var(--lh-mono); letter-spacing: var(--tr-mono);
  color: var(--ink-2);
}
/* The line at the bottom of a mock that names the arithmetic on screen —
   the scale of a zoomed plot, the decay function, the weight a check
   carries. Added with /readiness, where four mocks needed one; the app
   itself sets the precedent (dashboard.jsx prints the readiness formula
   under its score breakdown). It is a caption, not invented chrome. */
.mock-foot {
  margin-top: var(--space-12);
  padding-top: var(--space-12);
  border-top: 1px solid var(--line);
}

/* Delta chip. Direction is carried by the sign and the word, never by
   colour alone — a green pill that says "+4" is unreadable to a reader
   who can't see the green, and this number is the whole product. */
.chip {
  display: inline-flex; align-items: center; gap: 5px;
  font-size: var(--fs-meta); line-height: 1; font-weight: 600;
  border-radius: var(--r-pill); padding: 5px 10px;
  background: var(--accent-50); color: var(--accent-700);
  white-space: nowrap;
}
.chip.down { background: #FBECEA; color: var(--danger); }
.chip.flat { background: var(--surface-3); color: var(--ink-2); }

/* Status pip — Lurest's capture-state vocabulary (meta.ts STATUS). */
.pip-s { display: inline-flex; align-items: center; gap: 7px; font-size: var(--fs-meta); color: var(--ink-2); white-space: nowrap; }
.pip-s i { width: 7px; height: 7px; border-radius: var(--r-pill); background: var(--ink-3); flex: none; }
.pip-s.captured i { background: var(--accent-500); }
.pip-s.processing i { background: var(--warn); }
.pip-s.queued i { background: var(--line-strong); }

/* ============ the readiness ring ============
   Geometry is arithmetic, not eyeballing — keep it that way:
     r = 50 → circumference 2π·50 = 314.159
     dashoffset = (1 − score/100) × 314.159
   67 → 103.67. Change the score, recompute the offset. */
.ring { width: 132px; height: 132px; flex: none; display: block; transform: rotate(-90deg); }
.ring .track { fill: none; stroke: var(--surface-3); stroke-width: 9; }
.ring .val {
  fill: none; stroke: var(--accent-500); stroke-width: 9; stroke-linecap: round;
  stroke-dasharray: 314.159;
  stroke-dashoffset: 103.67;      /* = (1 − 0.67) × 314.159 */
}
.ring-wrap { position: relative; display: inline-flex; flex: none; }
.ring-num {
  position: absolute; inset: 0;
  display: flex; flex-direction: column; align-items: center; justify-content: center;
  font-family: var(--font-display); font-weight: 550;
  font-size: 44px; line-height: 1; letter-spacing: -.03em; color: var(--ink);
}
.ring-num small { font-family: var(--font-ui); font-size: var(--fs-meta); font-weight: 500; letter-spacing: 0; color: var(--ink-3); margin-top: 4px; }

/* ---- the ring at full size — /readiness opens on it (§5) ----
   The stroke drops from 9 to 6.8 so it renders at the same ~10px the
   small ring does (9 ÷ 120 × 132 ≈ 9.9; 6.8 ÷ 120 × 176 ≈ 10.0): the arc
   gets bigger, not fatter. r stays 50, so the dashoffset arithmetic
   above is unchanged. The app draws this arc at 372px with the 112px
   hero numeral (the one data number per screen); 52px is that numeral
   at this arc's scale.
   THESE MUST STAY BELOW THE BASE RING RULES. `.ring--lg .track` and
   `.ring .track` are both two classes, so the modifier only wins on
   source order — put this block above them and the large ring silently
   renders with the small ring's 9px stroke and 44px numeral. It did. */
.ring--lg { width: 176px; height: 176px; }
.ring--lg .track, .ring--lg .val { stroke-width: 6.8; }
.ring-num--lg { font-size: 52px; }
.ring-num--lg small { font-size: var(--fs-ui); margin-top: 6px; }

.score-row { display: flex; align-items: center; gap: var(--space-24); }
.score-meta { min-width: 0; }
.score-meta > * + * { margin-top: 8px; }

/* ============ topic rows + meters (Revula subject topics) ============ */
.topics { list-style: none; margin: 0; padding: 0; }
.topics li + li { margin-top: var(--space-12); }
/* `.t-row`, not `.row` — site.css owns `.row` for the alternating marketing
   row, and a mock nested in one would inherit a two-column grid. It happens
   to render correctly through specificity today; that is not a reason to
   leave two different geometries sharing a class name. */
.topics .t-row {
  display: flex; align-items: baseline; justify-content: space-between; gap: var(--space-12);
  margin-bottom: 6px;
}
.topics .name { font-size: var(--fs-ui); font-weight: 500; color: var(--ink); }
.topics .state { font-size: var(--fs-meta); color: var(--ink-3); white-space: nowrap; }
.topics .state.decaying { color: var(--warn); }
.topics .state.weak { color: var(--danger); }
.meter { height: 6px; border-radius: var(--r-pill); background: var(--surface-3); overflow: hidden; }
.meter i { display: block; height: 100%; border-radius: var(--r-pill); background: var(--accent-500); }
.meter i.decaying { background: var(--warn); }
.meter i.weak { background: var(--danger); }

/* ============ the 14-day history (READINESS_HISTORY) ============
   The four down-ticks in the real series are the entire point of this
   mock — they are the evidence for the sentence beside it. Do not smooth
   them, and do not resample the series to make the line prettier.

   Why a line on a LABELLED axis and not bars: the series runs 42→67, so
   bars from a zero baseline hide every down-tick inside one or two pixels,
   and bars from a 40 baseline are the textbook misleading chart. A line
   with both bounds named is the honest way to zoom.

   All labelling is HTML at --fs-meta, never <text> inside the plot: this
   svg is width:100% with a viewBox, so anything in it scales with the box
   and a size that clears the 13px floor in a wide mock breaks it in a
   narrow one. The two gridlines are the axis bounds; the caption names
   them in words. */
.spark { display: block; width: 100%; height: auto; }
.spark .grid { stroke: var(--line); stroke-width: 1; }
.spark .line { fill: none; stroke: var(--accent-500); stroke-width: 2; stroke-linejoin: round; stroke-linecap: round; }
.spark .dip { fill: var(--bg); stroke: var(--warn); stroke-width: 2; }
.spark .now { fill: var(--accent-600); }
.spark-x { display: flex; justify-content: space-between; margin: 6px 0 0; }

/* ============ Lurest capture rows ============ */
.queue { list-style: none; margin: 0; padding: 0; }
.queue li {
  display: flex; align-items: center; gap: var(--space-12);
  padding: var(--space-12) 0;
  border-top: 1px solid var(--line);
}
.queue li:first-child { border-top: 0; padding-top: 0; }
.kind {
  flex: none; width: 34px; height: 34px; border-radius: var(--r-sm);
  display: inline-flex; align-items: center; justify-content: center;
  background: var(--surface-2); color: var(--ink-2);
}
.kind svg { width: 17px; height: 17px; }
.q-main { min-width: 0; flex: 1; }
.q-main .t {
  font-size: var(--fs-ui); font-weight: 500; color: var(--ink);
  display: block; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.q-main .s { display: flex; flex-wrap: wrap; gap: 4px 10px; margin-top: 2px; }
.q-side { flex: none; text-align: right; }

/* Progress line for a source that is still being processed. This is the
   one piece of the site that would animate outside the four-item motion
   budget, so it does not animate: it is a static, honest position. */
.q-progress { height: 3px; border-radius: var(--r-pill); background: var(--surface-3); margin-top: 8px; overflow: hidden; }
.q-progress i { display: block; height: 100%; width: 38%; border-radius: var(--r-pill); background: var(--warn); }

/* ============ commit-before-reveal cards ============
   Two of Revula's four practice modes are built on the same card: a
   prompt, a place to answer, and the reveal locked behind a commit.
   Recall (`/`) types the answer; Check (`/readiness`) picks one of four.
   The frame, the prompt, the lock line and the commit button are shared —
   only the middle differs, so only the middle is scoped separately. */
.recall > * + *, .check > * + * { margin-top: var(--space-16); }
.recall .prompt, .check .prompt { font-size: var(--fs-body); line-height: var(--lh-body); color: var(--ink); font-weight: 500; }
.recall .answer {
  border: 1px solid var(--line-strong); border-radius: var(--r-md);
  padding: 12px var(--space-16); min-height: 84px;
  font-size: var(--fs-body); line-height: var(--lh-body); color: var(--ink);
  background: var(--bg);
}
.recall .answer .caret {
  display: inline-block; width: 1.5px; height: 1.05em;
  background: var(--accent-600); vertical-align: -.18em; margin-left: 1px;
}
.recall .actions, .check .actions { display: flex; align-items: center; justify-content: space-between; gap: var(--space-16); }
.recall .commit, .check .commit {
  display: inline-flex; align-items: center; justify-content: center;
  font-family: var(--font-ui); font-size: var(--fs-ui); font-weight: 600; line-height: 1;
  padding: 10px 18px; border-radius: var(--r-md); border: 1px solid transparent;
  background: var(--accent-600); color: var(--on-accent);
}
.recall .locked, .check .locked { display: inline-flex; align-items: center; gap: 7px; }
.recall .locked svg, .check .locked svg { width: 14px; height: 14px; color: var(--ink-3); flex: none; }

/* Check's four options. The mock shows one PICKED and nothing revealed —
   which is the mode's whole argument, so the picked state must never be
   styled as "correct". No tick, no green tint on a right answer, because
   at this point in a round the app does not know either. */
.mcq { list-style: none; margin: 0; padding: 0; display: grid; gap: var(--space-8); }
.mcq li {
  display: flex; align-items: flex-start; gap: var(--space-12);
  padding: 11px var(--space-16);
  border: 1px solid var(--line); border-radius: var(--r-md);
  font-size: var(--fs-body); line-height: var(--lh-body); color: var(--ink-2);
}
.mcq .key {
  font-family: var(--font-mono); font-size: var(--fs-mono); letter-spacing: var(--tr-mono);
  color: var(--ink-3); flex: none;
}
.mcq li.picked { border-color: var(--accent-500); background: var(--accent-50); color: var(--ink); }
.mcq li.picked .key { color: var(--accent-700); }

/* ============ mechanism tiles (block 4) ============
   A feature grid is fine; a feature grid with gradient icon chips is
   banlist item 4. Monoline icons, 1.5px stroke, ink-coloured. */
.tiles { display: grid; grid-template-columns: repeat(4, 1fr); gap: var(--space-24); }
.tile {
  background: var(--bg); border: 1px solid var(--line); border-radius: var(--r-lg);
  box-shadow: var(--shadow-1); padding: var(--space-24);
}
.tile .ico { color: var(--ink); margin-bottom: var(--space-16); display: block; }
.tile .ico svg { width: 24px; height: 24px; display: block; }
.tile h3 { font-size: 18px; line-height: 1.3; letter-spacing: -.012em; margin: 0 0 var(--space-8); }
.tile p { font-size: 15px; line-height: 1.6; color: var(--ink-2); margin: 0; }

/* ============ source-kind strip (block 5) ============ */
.kinds { display: flex; flex-wrap: wrap; align-items: center; gap: var(--space-12) var(--space-32); }
.kinds .k { display: inline-flex; align-items: center; gap: var(--space-8); font-size: 15px; font-weight: 500; color: var(--ink-2); }
.kinds .k svg { width: 18px; height: 18px; color: var(--ink-3); flex: none; }

/* ============ two apps, one number (block 6) ============
   Lurest left, Revula right, Erulia drawn as the line between them with
   the score sitting on it. The line is the argument: it is why there are
   three names. On narrow screens the line turns vertical rather than
   disappearing — the diagram is the content, not decoration. */
.bridge { display: grid; grid-template-columns: 1fr auto 1fr; align-items: center; gap: var(--space-24); }
.bridge-app {
  background: var(--bg); border: 1px solid var(--line); border-radius: var(--r-lg);
  box-shadow: var(--shadow-1); padding: var(--space-24);
}
.bridge-app .m-label { margin-bottom: var(--space-12); }
.bridge-app h3 { font-size: 22px; margin: 0 0 var(--space-8); }
.bridge-app p { font-size: 15px; line-height: 1.6; color: var(--ink-2); margin: 0 0 var(--space-16); }
.bridge-app ul { list-style: none; margin: 0; padding: 0; }
.bridge-app li {
  display: flex; align-items: baseline; gap: var(--space-8);
  font-size: var(--fs-meta); line-height: 1.9; color: var(--ink-3);
}
.bridge-app li::before { content: ""; width: 4px; height: 4px; border-radius: var(--r-pill); background: var(--line-strong); flex: none; transform: translateY(-2px); }

.bridge-mid { display: flex; flex-direction: column; align-items: center; gap: var(--space-12); text-align: center; }
.bridge-mid .wire { width: 100%; min-width: 96px; height: 1px; background: var(--line-strong); }
.bridge-score {
  display: inline-flex; flex-direction: column; align-items: center; justify-content: center;
  width: 104px; height: 104px; border-radius: var(--r-pill);
  background: var(--accent-600); color: var(--on-accent);
  font-family: var(--font-display); font-weight: 550; font-size: 38px; line-height: 1; letter-spacing: -.03em;
  box-shadow: var(--shadow-2);
}
.bridge-score small { font-family: var(--font-ui); font-size: 12px; font-weight: 600; letter-spacing: .08em; text-transform: uppercase; margin-top: 6px; opacity: .82; }

/* ============================================================
   ADDED WITH /readiness
   Four more real app surfaces, each one the screen the mechanism
   beside it actually runs on: the score-breakdown disclosure, the
   concept map, the due queue, and the all-subjects strip. Plus the
   one interactive on the site — the seven-day ladder.
   ============================================================ */

/* ---- the score breakdown (dashboard.jsx Disclosure) ----
   The app's own "How calculated" panel, shown open. This is the single
   most load-bearing mock on the site: it is the product conceding, in
   its own UI, exactly how the number is put together. */
.ghost-btn {                      /* the affordance the panel hangs off */
  display: inline-flex; align-items: center; gap: 5px;
  font-size: var(--fs-meta); line-height: 1; color: var(--ink-3);
  background: var(--surface-2); border: 1px solid var(--line);
  border-radius: var(--r-sm); padding: 6px 10px;
}
.ghost-btn svg { width: 13px; height: 13px; flex: none; }

.panel {
  background: var(--bg-soft); border: 1px solid var(--line);
  border-radius: var(--r-md); padding: var(--space-16);
}
.panel > * + * { margin-top: var(--space-12); }

/* Six rows, two columns. The app stacks all six because its panel is a
   narrow popover; in a hero mock a six-row column is 170px of ledger
   under a 200px ring, and the mock stops being able to sit beside a
   paragraph (§4 ceiling). Same six values, half the height. */
.kv { display: grid; grid-template-columns: 1fr 1fr; gap: 7px var(--space-24); margin: 0; }
.kv > div { display: flex; align-items: baseline; justify-content: space-between; gap: var(--space-12); }
.kv dt { font-size: var(--fs-ui); color: var(--ink-3); }
.kv dd { margin: 0; font-size: var(--fs-ui); font-weight: 500; color: var(--ink); }
.kv dd.up { color: var(--accent-700); }
.kv dd.down { color: var(--danger); }

/* ---- the concept map (topic.jsx Map tab) ----
   Node coordinates, labels, links and notes are the app's, on its own
   760×440 space. The LINES are svg on that space; the LABELS are HTML
   positioned on the same percentages, because <text> in a width:100%
   svg scales with the box — 13px in a wide mock is 9px in a narrow one,
   under the floor. preserveAspectRatio="none" plus a fixed aspect-ratio
   on the box means the two coordinate systems stay in register. */
.cmap-plot {
  position: relative;
  aspect-ratio: 760 / 440;
  background: var(--surface-2);
  border: 1px solid var(--line);
  border-radius: var(--r-md);
}
.cmap-plot > svg { position: absolute; inset: 0; width: 100%; height: 100%; }
.cmap-plot .edge { stroke: var(--line-strong); stroke-width: 1; }
.cmap-plot .edge.hot { stroke: var(--accent-500); stroke-width: 1.6; }
.cnode {
  position: absolute; transform: translate(-50%, -50%);
  white-space: nowrap;
  font-size: var(--fs-meta); line-height: 1;
  padding: 6px 11px; border-radius: var(--r-pill);
  background: var(--bg); border: 1px solid var(--line); color: var(--ink-3);
}
.cnode.nb { background: var(--surface-3); border-color: var(--line-strong); color: var(--ink-2); }
.cnode.on { background: var(--accent-50); border-color: var(--accent-500); color: var(--accent-700); font-weight: 600; }

/* The app's aside, which sits at 280px beside a 760px plot. At the width
   an alternating row gives a mock there is no room for a fourth column,
   so it lies down under the plot: label and title on one line, the note
   under both. The app's "connects to" chips are dropped rather than
   ported — they are navigation buttons, and a static mock full of
   buttons that do nothing is weaker than one without them. The four lit
   edges already say what the selected concept connects to. */
.cmap-sel { display: grid; grid-template-columns: auto minmax(0, 1fr); gap: 2px var(--space-12); align-items: baseline; }
.cmap-sel .m-label { grid-area: 1 / 1; }
.cmap-sel .m-title { grid-area: 1 / 2; }
.cmap-sel .m-body  { grid-area: 2 / 1 / 3 / -1; }

/* ---- the due queue (dashboard.jsx, ordered by data.jsx computeDueQueue) ----
   Reuses the Lurest capture rows: same geometry, and the app calls this
   one a queue too. Only the leading tile differs — a topic score instead
   of a source-kind icon. Colour is carried by the number's own state
   word as well, never by the tile alone. */
.kind--score {
  font-family: var(--font-display); font-weight: 600;
  font-size: 15px; letter-spacing: -.01em;
}
.kind--score.decaying { color: var(--warn); }
.kind--score.weak { color: var(--danger); }
.pip-s.due i { background: var(--warn); }
.queue + .more { margin-top: var(--space-12); }

/* ---- the all-subjects strip (dashboard.jsx SubjectStrip) ----
   The one mock on the site where the score is shown going DOWN and
   sitting at zero. That is why it is here: §5 asks this page to prove
   the number can be bad news, and a claim is weaker than a screen. */
.subs { list-style: none; margin: 0; padding: 0; }
.subs li {
  display: flex; align-items: center; gap: var(--space-16);
  padding: var(--space-12) 0; border-top: 1px solid var(--line);
}
.subs li:first-child { border-top: 0; padding-top: 0; }
.subs .s-main { flex: 1; min-width: 0; }
.subs .s-name { display: block; font-size: var(--fs-ui); font-weight: 500; color: var(--ink); }
.subs .s { display: flex; flex-wrap: wrap; gap: 4px 10px; margin-top: 2px; }
.subs .s-score {
  flex: none; font-family: var(--font-display); font-weight: 550;
  font-size: 22px; line-height: 1; letter-spacing: -.02em; color: var(--ink);
}
.subs .s-score.zero { color: var(--ink-3); }
.subs .chip { flex: none; min-width: 104px; justify-content: center; }

/* ---- the seven-day ladder (the one interactive on the site, §5) ----
   Drag through the week and the ring and the marker follow. The range
   input is a real range input: it gets drag, touch, arrow keys, a
   focus ring and a value announcement for free, and nothing hand-rolled
   would have got all five right.
   This is NOT a fifth motion (§4). Nothing animates — the ring, the
   marker and the readout are redrawn at the position the handle is
   already at. Direct manipulation, like the accordion, not page motion. */
.ladder { display: grid; grid-template-columns: 168px minmax(0, 1fr); gap: var(--space-32); align-items: center; }
.ladder-read { text-align: center; }
.ladder-read > * + * { margin-top: var(--space-12); }
.ladder-read p { margin: 0; }
.spark .guide { stroke: var(--line-strong); stroke-width: 1; stroke-dasharray: 3 3; }
.ladder-plot .spark-x { margin-top: 6px; }
.ladder-plot > .m-meta { margin-top: 6px; }

.ladder-range {
  -webkit-appearance: none; appearance: none;
  display: block; width: 100%; height: 24px;
  margin: var(--space-16) 0 0; padding: 0;
  background: transparent; cursor: grab;
}
.ladder-range:active { cursor: grabbing; }
.ladder-range::-webkit-slider-runnable-track { height: 4px; border-radius: var(--r-pill); background: var(--surface-3); }
.ladder-range::-moz-range-track { height: 4px; border-radius: var(--r-pill); background: var(--surface-3); }
.ladder-range::-webkit-slider-thumb {
  -webkit-appearance: none; appearance: none;
  width: 20px; height: 20px; margin-top: -8px;
  border-radius: var(--r-pill); background: var(--accent-600);
  border: 2px solid var(--bg); box-shadow: var(--shadow-2);
}
.ladder-range::-moz-range-thumb {
  width: 20px; height: 20px; box-sizing: border-box;
  border-radius: var(--r-pill); background: var(--accent-600);
  border: 2px solid var(--bg); box-shadow: var(--shadow-2);
}

/* ============================================================
   ADDED WITH /revula
   Six more real app surfaces, and not one of them repeats a screen
   the site already shows. `/` owns the readiness card, the Lurest
   capture row and the Recall commit; `/readiness` owns the score
   breakdown, the concept map, the Check round, the due queue and
   the all-subjects strip. What was left of the product is exactly
   what this page needed: the dashboard's own readiness moment, the
   synthesized note, the practice picker, the Tonight list, the
   group's comprehension read, and the citation round-trip — the one
   mock on the site that spans both apps at once.

   WHERE THE CONTENT COMES FROM (in addition to the list at the top):
     · kicker / headline / sub / CTA caption on the readiness moment
       ................ revula/…/dashboard.jsx ReadinessHero, every
                        string derived the way the app derives it
     · "Fri 29 May", "Exam" ....... calendar.jsx EVENTS + nextDueFor
     · the note text and its six inline citations
       ................ topic-data.jsx TOPIC_CONTENT['Entropy…'].note
     · the four practice modes, their blurbs, counts and badges
       ................ topic.jsx, the `modes` array
     · Tonight's three rows and their reason lines ... dashboard.jsx
     · 61 / 67, 38 pooled sources, the three weakest group topics and
       who could teach each ......... data.jsx GROUP + group-study.jsx
     · the transcript segment at 04:12 and its speaker
       ................ lurest/…/src/data/sources.ts SEG_ENTROPY_L
     · that the citation resolves at all .... same file, REVULA_CITATIONS
   ============================================================ */

/* Every <p> inside a mock is an app label, not prose, so the browser's
   1em default margin is 13–14px of silent air that the `.mock-body`
   rhythm then stacks another 16px on top of. The classes added before
   this step each zeroed it individually; these are the rest, zeroed in
   one place ABOVE their own rules so the few that want a specific
   margin can still set one. Found by measuring: the group readiness
   block came out 177px tall for 83px of content. */
.dash .whisper, .mode .mode-top, .mode .mode-foot, .quiet-btn,
.grp-n, .weak .w-avg, .weak .w-teach, .clip-pop .pop-head { margin: 0; }

/* ---- the readiness moment (dashboard.jsx ReadinessHero) ----
   The app's home screen. The only mock on the site carrying the app's
   own display type — 38px Hanken, which is the sanctioned in-app
   display role (≥28px), not the marketing scale.
   The ring is the SITE's ring, not the app's 372px semicircular arc.
   See the note above `.ring--lg`: the site settled on one ring shape
   on `/`, and a second shape here would read as a different product. */
.dash {
  display: flex; flex-direction: column; align-items: center;
  text-align: center; padding: var(--space-12) 0 var(--space-8);
}
.dash > * + * { margin-top: var(--space-20); }
.dash .kick { display: inline-flex; align-items: center; gap: 9px; }
.dash .kick i {
  width: 6px; height: 6px; border-radius: var(--r-pill);
  background: var(--danger); flex: none;
}
.dash .whisper {
  display: inline-flex; align-items: center; gap: 7px;
  font-size: var(--fs-ui); color: var(--ink-3);
}
.dash .whisper b { font-weight: 500; color: var(--accent-700); }
/* Its own class, not `.dash h3` — the same lesson as `.refusal .refuse`. */
.dash .dash-h {
  font-family: var(--font-display); font-weight: 550;
  font-size: clamp(26px, 3vw, 38px); line-height: 1.05; letter-spacing: -.022em;
  color: var(--ink); margin: 0; max-width: 17ch; text-wrap: balance;
}
.dash .dash-h em { font-style: italic; color: var(--accent-700); }
.dash .dash-sub {
  font-size: var(--fs-title); line-height: var(--lh-title);
  color: var(--ink-2); margin: 0; max-width: 40ch; text-wrap: pretty;
}
.dash .dash-sub b { font-weight: 400; color: var(--ink); }
.dash .dash-act { display: flex; flex-direction: column; align-items: center; gap: 14px; }
/* The app's primary button, at the app's 12px radius — NOT the marketing
   pill. That split is the whole point of D3: the pill is the site's, 12px
   is the product's, and a mock that borrows the pill stops looking like
   the product. Same geometry as `.check .commit`, one size up. */
.app-btn {
  display: inline-flex; align-items: center; justify-content: center; gap: 8px;
  font-family: var(--font-ui); font-size: var(--fs-ui); font-weight: 600; line-height: 1;
  padding: 13px 22px; border-radius: var(--r-md); border: 1px solid transparent;
  background: var(--accent-600); color: var(--on-accent);
}
.app-btn svg { width: 15px; height: 15px; flex: none; }

/* ---- the synthesized note (topic.jsx NoteView) ----
   One note per topic, merged from every source, with the citations
   inline in the prose rather than filed at the bottom. The chip IS the
   claim's receipt, so it renders at the app's exact geometry: mono, the
   source's kind icon, the anchor, and a 1px border that lights up when
   it is open. */
.note > * + * { margin-top: var(--space-16); }
.note .note-h { font-size: var(--fs-ui); font-weight: 600; color: var(--ink); margin: 0; }
.note p.tx { font-size: var(--fs-body); line-height: var(--lh-body); color: var(--ink-2); margin: 0; }
.clip {
  display: inline-flex; align-items: center; gap: 4px;
  padding: 1px 7px 1px 5px; margin: 0 1px;
  border-radius: var(--r-sm); vertical-align: baseline;
  background: var(--surface-2); border: 1px solid var(--line-strong);
  font-family: var(--font-mono); font-size: var(--fs-mono);
  letter-spacing: var(--tr-mono); font-variant-numeric: tabular-nums;
  color: var(--ink-3); white-space: nowrap;
}
.clip svg { width: 11px; height: 11px; flex: none; }
.clip.on { background: var(--accent-50); border-color: var(--accent-500); color: var(--accent-700); }

/* The chip's open state. The app hangs this off the chip as an absolutely
   positioned popover; here it sits in flow with a caret pointing back at
   the chip, because `.mock` clips its overflow and an absolute popover in
   a mock is one narrow viewport away from being cut in half. Same content,
   same width, and it cannot get clipped. */
.clip-pop {
  position: relative; width: 264px; max-width: 100%;
  padding: 13px 15px; border-radius: var(--r-md);
  background: var(--bg); border: 1px solid var(--line-strong);
  box-shadow: var(--shadow-3);
}
.clip-pop::before {
  content: ""; position: absolute; top: -5px; left: 22px;
  width: 8px; height: 8px; transform: rotate(45deg);
  background: var(--bg);
  border-left: 1px solid var(--line-strong); border-top: 1px solid var(--line-strong);
}
.clip-pop .pop-head { display: flex; align-items: center; gap: var(--space-8); margin-bottom: var(--space-8); }
.clip-pop .pop-ico {
  width: 26px; height: 26px; border-radius: var(--r-sm); flex: none;
  display: inline-flex; align-items: center; justify-content: center;
  background: var(--surface-2); color: var(--ink-3);
}
.clip-pop .pop-ico svg { width: 14px; height: 14px; }
.clip-pop .pop-title { display: block; font-size: var(--fs-ui); color: var(--ink); margin-bottom: 3px; }
/* The source detail is a <span> so the app's own markup ports over
   unchanged, and `.m-meta` is inline — which put "47-min lecture" and
   "Open in Lurest" on one line and ate the 11px above the link. */
.clip-pop > .m-meta { display: block; }
.clip-pop .pop-open {
  display: inline-flex; align-items: center; gap: 5px; margin-top: 11px;
  font-size: var(--fs-meta); color: var(--accent-700);
}
.clip-pop .pop-open svg { width: 12px; height: 12px; flex: none; }

/* ---- the practice picker (topic.jsx) ----
   Four modes, and the badge is the load-bearing part: two of them say
   they move the score and two say they are practice. That split is the
   argument the row beside this makes, so it must be legible in the mock
   without reading the blurbs. */
.modes { display: grid; grid-template-columns: 1fr 1fr; gap: var(--space-12); }
.mode {
  border: 1px solid var(--line); border-radius: var(--r-md);
  padding: var(--space-16); background: var(--bg);
}
.mode.graded { border-color: var(--accent-500); background: var(--accent-50); }
.mode .mode-top { display: flex; align-items: center; gap: var(--space-8); margin-bottom: 7px; }
.mode .mode-top svg { width: 18px; height: 18px; color: var(--ink-2); flex: none; }
.mode .mode-name { font-size: var(--fs-ui); font-weight: 600; color: var(--ink); }
.mode .mode-desc { font-size: var(--fs-meta); line-height: var(--lh-meta); color: var(--ink-2); margin: 0; }
.mode .mode-foot { display: flex; align-items: center; gap: var(--space-8); flex-wrap: wrap; margin-top: 11px; }
.badge {
  font-size: var(--fs-meta); line-height: 1; font-weight: 600;
  border-radius: var(--r-pill); padding: 5px 10px;
  background: var(--surface-3); color: var(--ink-2); white-space: nowrap;
}
.badge.graded { background: var(--accent-50); color: var(--accent-700); }

/* ---- Tonight (dashboard.jsx, the needs-work list) ----
   Ordered by score ascending, weak before decaying, strong hidden behind
   the app's own "Show 2 strong topics". */
.focus { list-style: none; margin: 0; padding: 0; }
.focus li {
  display: flex; align-items: center; gap: var(--space-12);
  padding: 14px var(--space-16);
  border: 1px solid var(--line); border-radius: var(--r-md);
}
.focus li + li { margin-top: var(--space-8); }
.focus .dot {
  width: 9px; height: 9px; border-radius: var(--r-pill); flex: none;
  background: var(--danger);
  box-shadow: 0 0 0 4px color-mix(in oklab, var(--danger) 12%, transparent);
}
.focus .dot.decaying {
  background: var(--warn);
  box-shadow: 0 0 0 4px color-mix(in oklab, var(--warn) 12%, transparent);
}
.focus .f-main { flex: 1; min-width: 0; }
.focus .f-name { display: block; font-size: var(--fs-body); font-weight: 500; color: var(--ink); }
.focus .f-why { display: block; font-size: var(--fs-meta); color: var(--ink-3); margin-top: 3px; }
.focus .f-go { display: inline-flex; align-items: center; gap: 5px; font-size: var(--fs-meta); color: var(--ink-3); flex: none; }
.focus .f-go svg { width: 13px; height: 13px; }
.focus .f-score {
  font-family: var(--font-display); font-weight: 550;
  font-size: 28px; line-height: 1; letter-spacing: -.022em;
  color: var(--ink); flex: none; min-width: 42px; text-align: right;
}
.focus .f-state {
  font-family: var(--font-ui); font-size: var(--fs-meta); font-weight: 600;
  letter-spacing: .08em; text-transform: uppercase;
  color: var(--danger); flex: none; min-width: 72px; text-align: right;
}
.focus .f-state.decaying { color: var(--warn); }
.quiet-btn {
  display: inline-flex; align-items: center; gap: var(--space-8);
  margin-top: var(--space-8); padding: 10px var(--space-16);
  font-size: var(--fs-ui); color: var(--ink-3);
}
.quiet-btn svg { width: 13px; height: 13px; flex: none; }

/* ---- group study (group-study.jsx over data.jsx GROUP) ----
   The app tints each member's avatar with an authored `hue`. Those hues
   are outside the token set, and the mock ceiling in §4 bans that, so the
   avatars here are neutral. It costs nothing: the avatar's job in this
   mock is to say a named person could teach the topic, not to be a colour. */
.grp-read { display: flex; align-items: flex-end; gap: var(--space-28); }
.grp-n {
  font-family: var(--font-display); font-weight: 550;
  font-size: 52px; line-height: 1; letter-spacing: -.03em; color: var(--ink);
}
.grp-n small { font-family: var(--font-display); font-size: 34px; color: var(--ink-3); }
.grp-read .you .grp-n { font-size: 36px; color: var(--ink-2); }
.grp-read .you .grp-n small { font-size: 24px; }
.grp-read .m-label { margin-top: 10px; }
.grp-read .you .m-label { color: var(--ink-3); }
.grp-div { width: 1px; height: 46px; background: var(--line-strong); margin-bottom: 22px; flex: none; }

/* The app lays these out as three cards side by side, because it has a
   full app column to do it in. Measured at the width an alternating row
   gives a mock, three columns is 134px a card — "Free Energy &
   Spontaneity" folds onto four lines and the card grows to 331px tall,
   which is a mock outweighing the paragraph beside it (the §4 ceiling).
   Same three items, same order, same severity stripe, lying down. */
.weaks { display: flex; flex-direction: column; gap: var(--space-8); }
.weak {
  position: relative; overflow: hidden;
  display: flex; align-items: center; gap: var(--space-16);
  border: 1px solid var(--line); border-radius: var(--r-md);
  padding: var(--space-12) var(--space-16) var(--space-12) var(--space-20);
}
.weak::before {
  content: ""; position: absolute; top: 0; left: 0;
  width: 3px; height: 100%; background: var(--danger);
}
.weak.mid::before { background: var(--warn); }
.weak .w-body { flex: 1; min-width: 0; }
.weak .w-name { font-size: var(--fs-ui); font-weight: 600; color: var(--ink); margin: 2px 0 6px; }
.weak .w-avg { flex: none; text-align: right; }
.weak .w-avg b {
  display: block;
  font-family: var(--font-display); font-weight: 550;
  font-size: 32px; line-height: 1; letter-spacing: -.022em; color: var(--ink);
}
.weak .w-teach { display: flex; align-items: center; gap: var(--space-8); }
.av {
  width: 26px; height: 26px; border-radius: var(--r-pill); flex: none;
  display: inline-flex; align-items: center; justify-content: center;
  background: var(--surface-3); color: var(--ink-2);
  font-size: var(--fs-meta); font-weight: 600; line-height: 1;
}
.weak .w-teach .m-meta b { font-weight: 500; color: var(--ink); }

/* ---- the citation round-trip (both apps at once) ----
   Two frames and a wire: Revula's note on the left with one chip open,
   Lurest's transcript on the right with the segment that chip resolves
   to. Same grid as `.bridge` on `/` — it is the same idea, one level
   more concrete, so it should not be a new shape. */
.roundtrip { display: grid; grid-template-columns: 1fr auto 1fr; gap: var(--space-24); align-items: center; }
.roundtrip .rt-mid { display: flex; flex-direction: column; align-items: center; gap: var(--space-12); text-align: center; }
.roundtrip .rt-mid .wire { width: 100%; min-width: 72px; height: 1px; background: var(--line-strong); }
.roundtrip .rt-mid .m-label { white-space: nowrap; }

/* Lurest's transcript segments (sources.ts Segment[]). */
.segs { list-style: none; margin: 0; padding: 0; }
.segs li {
  display: flex; gap: var(--space-12);
  padding: 10px var(--space-12);
  border-radius: var(--r-sm);
}
.segs .at {
  font-family: var(--font-mono); font-size: var(--fs-mono);
  letter-spacing: var(--tr-mono); font-variant-numeric: tabular-nums;
  color: var(--ink-3); flex: none; min-width: 46px;
}
.segs .tx { font-size: var(--fs-body); line-height: var(--lh-body); color: var(--ink-2); margin: 0; }
.segs li.hit { background: var(--accent-50); }
.segs li.hit .at { color: var(--accent-700); }
.segs li.hit .tx { color: var(--ink); }

/* ============================================================
   ADDED WITH /lurest
   Five more app surfaces, and again none of them repeats one the
   site already shows. `/` owns the Lurest library queue and the
   processing row; /revula owns the transcript segments. What was
   left of Lurest is the front of it: the capture doors, a job in
   flight, a door taking input, the emitted object, and the
   handoff — the second mock on the site that spans both apps,
   and the one that shows the direction of travel rather than a
   citation resolving backwards.

   WHERE THE CONTENT COMES FROM (in addition to the list at the top):
     · the five doors, their titles, subtitles and kind letters
       ................ lurest/…/src/data/meta.ts DOORS
     · "Write a note", its subtitle, and that it is NOT a door
       ................ src/views/capture.tsx CaptureLanding
     · the job stages, per kind ..... src/engine/index.ts STAGES
     · "No fake progress bar — each step is a real job stage."
       ................ src/views/capture.tsx, the line under the stepper
     · the sample inputs on a door, and which of them fail
       ................ src/views/capture.tsx SAMPLES
     · the failure reasons ......... src/engine/index.ts evaluateInput
       (and, on the shelf, sources.ts s-soc-fail `reason`)
     · the five status labels ....... src/data/meta.ts STATUS
     · anchor type per kind (time/page/slide/para) ... meta.ts KIND
     · segment counts per source ......... sources.ts SEG_* lengths
     · "Revula receives these 3 source objects…", "Revula runs the
       synthesis — Lurest just hands over the sources."
       ................ src/views/connection.tsx SynthesizeModal
     · Sources shared 12 · Topics synthesizable 2
       ................ connection.tsx, counted over SOURCES by
                        `status === 'captured'` and synthesisReadyTopics
   ============================================================ */

/* Same one-line reset the step-4 block opens with, for the same reason:
   these are app labels, not prose, and the browser's 1em default is
   silent air that `.mock-body > * + *` then stacks 16px on top of. */
.door .d-title, .door .d-sub, .stage .st-label, .drop .d-hint,
.obj-row .o-k, .obj-row .o-v, .hand-top, .hand-h,
.hand-row .h-t, .hand-row .h-m, .conn-kv .n { margin: 0; }

/* Two frames side by side, for a block whose argument needs both at once
   — the object and its states, the handoff and what it connects. Not
   `.roundtrip`: that grid has a labelled wire down the middle because
   the two frames there are one claim and its proof. These two are a
   pair, not a round trip, so they get equal columns and no connector. */
.objpair { display: grid; grid-template-columns: 1fr 1fr; gap: var(--space-24); align-items: start; }

/* ---- segments from more than one source ----
   /revula's round-trip shows three segments of ONE lecture, so the frame
   title says which source they are. A board drawn from four sources has
   to name the source on the segment itself, or four kinds of anchor sit
   in a column with nothing saying what they anchor to. */
.segs .s-body { min-width: 0; }
.segs .src { font-size: var(--fs-meta); line-height: var(--lh-meta); color: var(--ink-3); margin: 4px 0 0; }
/* The kind letter carries a rule rather than only a gap, so `S` and
   `slide 12` don't read as one token. The space after it in the markup is
   load-bearing too — without it the accessible name is "Sslide 12" — so
   the margin here is 4px and the word-space makes up the rest. */
.segs .at .k {
  color: var(--accent-700); font-weight: 600;
  padding-right: 7px; margin-right: 4px; border-right: 1px solid var(--line-strong);
}

/* ---- the capture doors (capture.tsx CaptureLanding) ----
   A LIST, not a grid of tiles: the app stacks the five doors with a
   hairline between them, and a marketing-shaped card wall would be the
   one thing on this page that isn't the product. */
.doors { border: 1px solid var(--line); border-radius: var(--r-lg); overflow: hidden; }
.door {
  display: flex; align-items: center; gap: 15px;
  padding: 13px var(--space-16);
  border-top: 1px solid var(--line);
}
.doors > .door:first-child { border-top: 0; }
.door .d-ico {
  width: 38px; height: 38px; border-radius: var(--r-md); flex: none;
  display: flex; align-items: center; justify-content: center;
  background: var(--accent-50); color: var(--accent-700);
}
.door .d-ico svg { width: 18px; height: 18px; }
.door .d-main { flex: 1; min-width: 0; }
.door .d-title { font-size: var(--fs-ui); font-weight: 500; color: var(--ink); }
.door .d-sub { font-size: var(--fs-meta); line-height: var(--lh-meta); color: var(--ink-3); margin-top: 2px; }
/* The kind letter. This is the contract's own vocabulary showing through
   the UI — L/Y/P/S/A/N are the values in the source object, not badges
   drawn for the site — so it is set in the mono role like every other
   piece of machine data on the page. */
.door .d-id {
  font-family: var(--font-mono); font-size: var(--fs-mono);
  letter-spacing: var(--tr-mono); color: var(--ink-3); flex: none;
}
/* The card's own header row — the app's collapsed "New capture" state,
   which is a door-shaped row sitting above the list rather than in it. */
.door--head { padding: 0 0 var(--space-16); }
/* The note affordance sits OUTSIDE `.doors` with its own border, exactly
   as the app does it: authored, not captured, and deliberately not a
   sixth door. See the note in meta.ts. */
.door--note { border: 1px solid var(--line); border-radius: var(--r-lg); margin-top: var(--space-12); }
.door--note .d-ico { background: var(--surface-3); color: var(--ink-2); }

/* ---- the stage stepper (shell.tsx StageStepper) ----
   Real discrete stages, never a percentage on a timer: the rail between
   two dots is full only where the job has actually been. */
.stages { list-style: none; margin: 0; padding: 0; }
.stage { display: flex; gap: var(--space-12); align-items: stretch; }
.stage .st-mark { display: flex; flex-direction: column; align-items: center; flex: none; }
.stage .st-dot {
  width: 18px; height: 18px; border-radius: var(--r-pill); flex: none;
  display: flex; align-items: center; justify-content: center;
  border: 1.5px solid var(--line-strong); background: transparent; color: var(--ink-3);
}
.stage .st-dot svg { width: 11px; height: 11px; }
.stage .st-dot i { width: 5px; height: 5px; border-radius: var(--r-pill); background: var(--line-strong); }
.stage .st-rail { width: 2px; flex: 1; min-height: 18px; margin: 3px 0; border-radius: var(--r-pill); background: var(--line-strong); }
.stage .st-rail i { display: block; width: 100%; height: 0; border-radius: var(--r-pill); background: var(--accent-500); }
.stage .st-label { font-size: var(--fs-ui); line-height: var(--lh-ui); color: var(--ink-3); padding: 1px 0 15px; }
.stage:last-child .st-label { padding-bottom: 0; }

.stage.done .st-dot { border-color: var(--accent-500); background: var(--accent-500); color: #FFFFFF; }
.stage.done .st-rail i { height: 100%; }
.stage.done .st-label { color: var(--ink-2); }
.stage.now .st-dot { border-color: var(--warn); color: var(--warn); }
.stage.now .st-dot i { background: var(--warn); }
.stage.now .st-label { color: var(--ink); font-weight: 500; }
.stage.fail .st-dot { border-color: var(--danger); color: var(--danger); }
.stage.fail .st-label { color: var(--danger); }

/* ---- a door taking input (capture.tsx UrlField / DropField + SampleChips) ---- */
.drop {
  border: 1px dashed var(--line-strong); border-radius: var(--r-lg);
  padding: var(--space-20) var(--space-16); text-align: center;
}
.drop .d-hint { font-size: var(--fs-ui); color: var(--ink-2); }
.drop .d-file {
  display: inline-flex; align-items: center; gap: var(--space-8);
  margin-top: var(--space-12); padding: 9px 14px;
  border: 1px solid var(--line-strong); border-radius: var(--r-md);
  background: var(--bg); color: var(--ink);
  font-family: var(--font-mono); font-size: var(--fs-mono); letter-spacing: var(--tr-mono);
}
.drop .d-file svg { width: 14px; height: 14px; color: var(--ink-3); flex: none; }
.samples { display: flex; flex-wrap: wrap; gap: var(--space-8); }
.sample-chip {
  display: inline-flex; align-items: center; gap: 7px;
  padding: 6px 11px; border-radius: var(--r-pill);
  border: 1px solid var(--line); background: var(--bg-soft);
  font-family: var(--font-mono); font-size: 12px; letter-spacing: var(--tr-mono);
  color: var(--ink-2); max-width: 100%;
}
/* An SVG in a flex row with no width of its own takes the line's height as
   its intrinsic box and grows to fill it: these chips measured 131px and
   108px tall before this rule, against the ~30px they are in the app.
   Every other icon in this file is sized at its own selector — this one
   was the miss, and it cost the row its mock-to-paragraph ratio. */
.sample-chip svg { width: 14px; height: 14px; flex: none; }
.sample-chip .why {
  font-family: var(--font-ui); font-size: var(--fs-meta); letter-spacing: 0;
  color: var(--ink-3); white-space: nowrap;
}
/* The chips that fail are marked by their own words ("will fail"), never
   by the border alone — the border is the second signal, not the only one. */
.sample-chip.bad { border-color: #E7C4BF; background: #FBECEA; }
.sample-chip.bad .why { color: var(--danger); }

/* ---- the emitted source object ----
   The frozen contract (types.ts Source) with this capture's real values
   in it. A key-value list, not a code block: it is the app's data, and
   the page is not asking anyone to read TypeScript. */
.obj { border: 1px solid var(--line); border-radius: var(--r-lg); overflow: hidden; }
/* 112px, not 92: the longest key is `segments[].at`, which measures 102px
   at the 13px mono role. A 92px column doesn't wrap it — the key column
   has no overflow of its own — it just bleeds into the gap and sits a
   hair off the value beside it. Measured, not eyeballed. */
.obj-row {
  display: grid; grid-template-columns: 112px minmax(0, 1fr);
  gap: var(--space-16); align-items: baseline;
  padding: 9px var(--space-16); border-top: 1px solid var(--line);
}
.obj > .obj-row:first-child { border-top: 0; }
.obj-row .o-k {
  font-family: var(--font-mono); font-size: var(--fs-mono);
  letter-spacing: var(--tr-mono); color: var(--ink-3);
}
.obj-row .o-v { font-size: var(--fs-ui); color: var(--ink); min-width: 0; }
.obj-row .o-v .m-anchor { color: var(--accent-700); }
.obj-row.frozen { background: var(--accent-50); }

/* The status vocabulary, all five of them, on one line. */
.statuses { display: flex; flex-wrap: wrap; gap: var(--space-12) var(--space-24); }
.pip-s.failed i { background: var(--danger); }
.pip-s.synth i { background: var(--accent-700); }
.pip-s.off { color: var(--ink-3); }

/* A failed capture, with the engine's own reason under it. The site does
   not get to show this product without this row. */
.fail-note {
  margin-top: var(--space-8);
  padding: 11px var(--space-12);
  border-left: 2px solid var(--danger);
  background: #FBECEA;
  border-radius: 0 var(--r-sm) var(--r-sm) 0;
  font-size: var(--fs-meta); line-height: var(--lh-meta); color: var(--ink-2);
}

/* ---- the handoff (connection.tsx SynthesizeModal) ----
   The direction of travel: Lurest hands three source objects over and
   Revula does the synthesizing. The round-trip mock on /revula shows a
   citation resolving backwards; this one shows the material going
   forwards, which is the only reason the citation has anywhere to land. */
.hand-top { display: flex; align-items: center; gap: 11px; }
.mark {
  width: 30px; height: 30px; border-radius: var(--r-sm); flex: none;
  display: flex; align-items: center; justify-content: center;
  font-family: var(--font-display); font-weight: 600; font-size: 14px;
  background: var(--accent-600); color: #FFFFFF;
}
.mark.erulia { background: var(--ink); }
.hand-top .arrow { color: var(--ink-3); display: flex; }
.hand-top .arrow svg { width: 16px; height: 16px; }
.hand-h { font-size: var(--fs-title); line-height: var(--lh-title); font-weight: 600; color: var(--ink); }
.hand-list { border: 1px solid var(--line); border-radius: var(--r-md); overflow: hidden; }
.hand-row {
  display: flex; align-items: center; gap: var(--space-12);
  padding: 11px var(--space-12); border-top: 1px solid var(--line);
  background: var(--bg-soft);
}
.hand-list > .hand-row:first-child { border-top: 0; }
.hand-row .h-ico {
  width: 30px; height: 30px; border-radius: var(--r-sm); flex: none;
  display: flex; align-items: center; justify-content: center;
  background: var(--surface-3); color: var(--accent-700);
}
.hand-row .h-ico svg { width: 15px; height: 15px; }
.hand-row .h-main { flex: 1; min-width: 0; }
.hand-row .h-t { font-size: var(--fs-meta); color: var(--ink); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.hand-row .h-m {
  font-family: var(--font-mono); font-size: var(--fs-mono);
  letter-spacing: var(--tr-mono); color: var(--ink-3); margin-top: 1px;
}
/* The two numbers the connection screen shows once Revula is connected.
   1px gap on a --line ground is the app's own hairline-grid trick. */
.conn-kv {
  display: grid; grid-template-columns: 1fr 1fr; gap: 1px;
  background: var(--line); border-radius: var(--r-md); overflow: hidden;
}
.conn-kv > div { background: var(--bg); padding: 12px var(--space-16); }
.conn-kv .n {
  font-family: var(--font-display); font-size: 26px; line-height: 1.1;
  font-weight: 600; color: var(--ink); margin: 0;
}
.conn-kv .m-label { margin-top: 7px; }

@media (max-width: 899px) {
  .tiles { grid-template-columns: 1fr 1fr; }
  .bridge { grid-template-columns: 1fr; }
  .bridge-mid { flex-direction: row; }
  .bridge-mid .wire { width: 1px; min-width: 0; height: 40px; }
  /* Same turn as `.bridge`: the wire lies down and the two frames stack.
     Revula's note first, because it is the frame that makes the claim. */
  .roundtrip { grid-template-columns: 1fr; }
  .roundtrip .rt-mid { flex-direction: row; }
  .roundtrip .rt-mid .wire { width: 1px; min-width: 0; height: 40px; }
  /* Same turn, no connector to lie down: the pair simply stacks, and the
     frame that states the thing keeps its place above the one that
     qualifies it. */
  .objpair { grid-template-columns: 1fr; gap: var(--space-32); }
}
@media (max-width: 699px) {
  .ladder { grid-template-columns: 1fr; gap: var(--space-24); }
}
@media (max-width: 639px) {
  /* The concept map's labels are real 13px type and may not shrink below
     the floor, so below this width the plot pans instead of squeezing. */
  .cmap-scroll { overflow-x: auto; }
  .cmap-scroll .cmap-plot { min-width: 520px; }
}
@media (max-width: 599px) {
  .tiles { grid-template-columns: 1fr; }
  .mock-body { padding: var(--space-16); }
  .score-row { gap: var(--space-16); }
  .ring { width: 108px; height: 108px; }
  .ring-num { font-size: 36px; }
  .ring--lg { width: 168px; height: 168px; }
  .ring-num--lg { font-size: 46px; }
  .subs li { flex-wrap: wrap; gap: var(--space-8) var(--space-12); }
  .subs .s-main { flex: 1 0 100%; order: -1; }
  .modes { grid-template-columns: 1fr; }
  /* Five things on one row at ~300px is 40px a column. The name and the
     reason take the full width, the score and the state keep the right
     edge, and "Review →" goes — it is the one item the row can lose,
     because the whole row is the tap target in the app anyway. */
  .focus li { flex-wrap: wrap; gap: var(--space-8) var(--space-12); }
  .focus .f-main { flex: 1 0 calc(100% - 21px); }
  .focus .f-go { display: none; }
  .grp-read { gap: var(--space-20); }
  .grp-n { font-size: 44px; }
  /* A door row at ~300px is the icon, the title and the kind letter with
     nothing left for "Live mic, or upload audio" — so the subtitle takes
     the full width under them rather than folding to three lines beside
     a 38px square. The kind letter stays: it is the contract. */
  .door { flex-wrap: wrap; gap: var(--space-12); }
  .door .d-sub { flex: 1 0 100%; margin-top: 0; }
  .door .d-main { flex: 1; }
}
@media (max-width: 479px) {
  /* Two columns of the breakdown means ~125px a cell, which folds
     "Influence per topic" onto a second line and puts the ledger back
     to the height the two columns were bought to avoid. */
  .kv { grid-template-columns: 1fr; }
  /* The object's keys are field names from the contract and may not be
     abbreviated, so at this width the value goes under the key rather
     than into a 40px column. */
  .obj-row { grid-template-columns: 1fr; gap: 2px; }
  .conn-kv { grid-template-columns: 1fr; }
}

/* ============================================================
   STEP 7 · /download
   Two shapes, both of them app surfaces the site has not shown.

   The PLATFORM CARD is deliberately not here — it lives in
   site.css, because it is the one thing on this site that is
   not a picture of the product and not marketing either: it is
   a real control reading the real browser. It carries no
   `Sample` label for the same reason. Nothing on it is sample
   data; if it says iPhone, you are on an iPhone.
   ============================================================ */

/* ONE margin reset, at the top of the block and above every rule that
   wants a margin — the same reset the step-4 block opens with, for the
   same reason, because this block re-made the same mistake. The
   browser's default `<p>` margin is 1em, which is 13–17px of silent air
   that the `> * + *` stacks then add to. Measured before the fix:
   `.plat-act` 17px, `.at-act` 14px, `.seam-tag` 13px top AND bottom.
   Only the BOTTOM is zeroed: every intentional margin in this file is a
   margin-top set by a `+` rule, so a bottom-only reset collides with
   none of them and does not have to win a specificity argument. */
.apptile p, .seam p { margin-bottom: 0; }
.seam-tag { margin: 0; }                /* both — it is the first child */

/* ---- the app tile (block 4) ----
   Identity, one sentence, a real app surface, two actions.

   "Live app UI behind each at LOW CONTRAST" is the wireframe's
   phrasing and this is where it is honoured differently: the strip is
   recessed by GROUND, not by opacity. Dropping opacity on real 13px app
   text is how a page ships type under the contrast bar while looking
   fine — and these two strips are the app's own --fs-meta and --fs-mono,
   already at the 13px floor. --bg-soft under a hairline recesses the
   surface without touching a single measured ratio. */
.apptiles {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: clamp(24px, 3vw, 32px);
}
.apptile { display: flex; flex-direction: column; }
.at-head {
  display: flex; align-items: center; gap: var(--space-12);
  padding: var(--space-16) var(--space-20);
  border-bottom: 1px solid var(--line);
}
.at-head .mark { width: 34px; height: 34px; font-size: 15px; }
.at-id { flex: 1; min-width: 0; }
/* Both are spans inside a flex item, so both need an explicit `block`:
   as inline they ran onto ONE line — "Revula Version 1.0 ·
   revula.erulia.org" — and the vertical margin below silently did
   nothing, because vertical margins don't apply to inline boxes. It
   didn't break; the name and the address just stopped being two
   things. Same family as the `.ring--lg` and `.g-t` failures: a rule
   that can't apply doesn't error, it goes quiet. */
.at-name, .at-v { display: block; }
.at-name {
  font-family: var(--font-display); font-weight: 600;
  font-size: 17px; letter-spacing: -.012em; color: var(--ink);
  margin: 0; line-height: 1.25;
}
/* Version only. The RELEASE DATE the wireframe asks for is the same
   number as the footer's status line, and the footer does not have it
   either — README's deploy list carries both. A made-up build date on a
   download page is worse than a missing one: it is the field a reader
   uses to tell whether the thing is maintained. */
.at-v {
  font-family: var(--font-mono); font-size: var(--fs-mono);
  letter-spacing: var(--tr-mono); color: var(--ink-3);
  margin: 2px 0 0; line-height: 1.4;
}
.at-body { padding: var(--space-20); display: flex; flex-direction: column; flex: 1; }
.at-body > * + * { margin-top: var(--space-16); }
.at-say { max-width: 42ch; }
/* The recessed app surface. Real markup, real sample data, real sizes. */
.at-screen {
  border: 1px solid var(--line); border-radius: var(--r-md);
  background: var(--bg-soft); padding: var(--space-12) var(--space-16);
  margin-top: auto;                     /* actions stay on the tile's floor */
}
.at-screen + .at-act { margin-top: var(--space-20); }
.at-row { display: flex; align-items: center; gap: var(--space-12); }
.at-row + .at-row { margin-top: 11px; padding-top: 11px; border-top: 1px solid var(--line); }
.at-row .at-k { flex: 1; min-width: 0; font-size: var(--fs-meta); line-height: var(--lh-meta); color: var(--ink-2); }
.at-row .at-n {
  font-family: var(--font-display); font-size: 19px; line-height: 1;
  font-weight: 600; color: var(--ink); flex: none;
}
.at-act { display: flex; align-items: center; gap: var(--space-20); flex-wrap: wrap; }
.at-note { color: var(--ink-3); }

/* ---- the seam (block 5, connection.tsx SeamFlow) ----
   The one piece of the connection screen the site has never shown.
   /lurest already ships the two counted numbers off the same view; this
   is the diagram above them — Lurest, the source objects, Revula, and
   the Erulia pill over the pair. Same view, and deliberately the part
   /lurest did NOT use, so the two pages don't run the same mock twice. */
.seam-tag {
  display: inline-flex; align-items: center; gap: var(--space-8);
  padding: 5px 13px 5px 5px; border-radius: var(--r-pill);
  border: 1px solid var(--line); background: var(--bg-soft);
  font-size: var(--fs-meta); line-height: 1; font-weight: 600;
  letter-spacing: .08em; text-transform: uppercase; color: var(--ink-3);
}
.seam-tag .mark { width: 22px; height: 22px; font-size: 11px; border-radius: 6px; }
.seam {
  display: flex; align-items: flex-start; gap: var(--space-20);
  max-width: 560px; margin: var(--space-24) auto 0;
}
.seam-node { width: 96px; flex: none; text-align: center; }
.seam-node .n-name { font-size: var(--fs-title); line-height: var(--lh-title); font-weight: 600; color: var(--ink); margin: var(--space-12) 0 0; }
.seam-node .n-role { font-size: var(--fs-meta); line-height: var(--lh-meta); color: var(--ink-3); margin: 1px 0 0; }
/* Lurest's signature is a canvas aurora orb. A radial field in the one
   accent is the static reading of it — no second colour, no animation,
   and no attempt to redraw a canvas the site cannot run. */
.orb {
  /* `display: block` is load-bearing, not tidiness: as an inline span it
     measured 0×0 and the Lurest node rendered with no orb at all —
     60px tall against Revula's 132px. Width and height do not apply to
     an inline box. */
  display: block;
  width: 72px; height: 72px; border-radius: var(--r-pill); margin: 0 auto;
  background:
    radial-gradient(58% 58% at 38% 34%, var(--accent-100), transparent 70%),
    radial-gradient(90% 90% at 50% 62%, var(--accent-500), var(--accent-700));
}
.seam-node .mark { width: 72px; height: 72px; border-radius: var(--r-lg); font-size: 30px; margin: 0 auto; }
.seam-wire { flex: 1; min-width: 0; position: relative; margin-top: 33px; display: flex; align-items: center; }
.seam-wire .w-label {
  position: absolute; top: -22px; left: 50%; transform: translateX(-50%);
  white-space: nowrap;
  font-size: var(--fs-meta); line-height: 1; font-weight: 600;
  letter-spacing: .08em; text-transform: uppercase; color: var(--accent-600);
}
.seam-wire .w-line { flex: 1; height: 1px; background: var(--accent-500); }
.seam-wire .w-tip {
  width: 0; height: 0; margin-left: -1px; flex: none;
  border-top: 6px solid transparent; border-bottom: 6px solid transparent;
  border-left: 9px solid var(--accent-500);
}

@media (max-width: 899px) {
  .apptiles { grid-template-columns: 1fr; }
}
@media (max-width: 559px) {
  /* The wire is the whole point of the diagram, so it TURNS rather than
     compressing: at ~300px the three columns give the connector 40px,
     which is shorter than the two words sitting over it. Same move
     `.bridge-mid` and `.rt-mid` make above, in the other direction —
     and the arrowhead has to be rebuilt, not rotated, because it is a
     border triangle and its point is which border carries the colour. */
  .seam { flex-direction: column; align-items: center; gap: var(--space-12); }
  .seam-wire {
    /* `flex: none` is the whole fix. Turning the row into a column makes
       HEIGHT the main axis, so the wire's `flex: 1` — which is
       `flex-basis: 0%` — becomes its height, and a column container with
       no height of its own has no free space to grow into. The wire
       measured 8px tall and the line inside it 0. An explicit height
       cannot win against a flex-basis on the main axis; it has to stop
       being a flex item first. */
    flex: none;
    margin-top: 0; width: auto; height: 56px;
    flex-direction: column; align-items: center;
  }
  .seam-wire .w-line { width: 1px; height: auto; }
  .seam-wire .w-label {
    top: 50%; left: auto; right: calc(100% + 10px);
    transform: translateY(-50%);
  }
  .seam-wire .w-tip {
    margin-left: 0; margin-top: -1px;
    border-left: 6px solid transparent; border-right: 6px solid transparent;
    border-top: 9px solid var(--accent-500); border-bottom: 0;
  }
}

/* ============================================================
   STEP 8 · /pricing
   One shape: Lurest's TrialTimeline, the component its own
   onboarding shows at the moment it asks for money.

   It is here rather than in site.css because it IS a product
   surface — the same test the platform card failed in step 7
   and passed the other way. That card reads the reader's real
   browser and carries no Sample label; this is app markup on
   the app's own copy, so it carries one.

   Source: lurest/Lurest/src/views/onboarding.tsx:236-276.
   Geometry taken from the component, not from the screenshot:
   13px gap, an 11px dot at 3px from the top, a 2px connector
   in --line-strong with 3px of air either end, and 13px of
   padding under every point but the last.
   ============================================================ */

/* Rendered at trialDays = 14, so the middle marker is Day 12 —
   the component computes trialDays − 2 rather than storing it. */
.tline {
  border: 1px solid var(--line); border-radius: var(--r-md);
  background: var(--bg); padding: var(--space-16) 18px;
}
.tpt { position: relative; display: flex; align-items: stretch; gap: 13px; }
.tdot { width: 11px; height: 11px; border-radius: var(--r-pill); margin-top: 3px; flex: none; }
.tpt:not(.tpt--last) { padding-bottom: 13px; }
/* The connector is a pseudo on the POINT, not on the dot. The dot's box
   is 11×11, so a connector positioned inside it can only ever be 11px
   long — `top: 14px; bottom: -3px` in that box resolves to a height of
   zero, which is the version of this rule that was written first and
   drew nothing at all. Hung off the point it spans the whole row:
   starting 3px under the dot (3 margin + 11 dot + 3) and stopping at
   the row's floor, which is exactly 3px above the next dot's top,
   because that dot carries the same 3px margin. */
.tpt:not(.tpt--last)::before {
  content: ""; position: absolute;
  left: 4.5px; top: 17px; bottom: 0; width: 2px;
  background: var(--line-strong); border-radius: var(--r-pill);
}
.tsay { min-width: 0; }
/* The three dot colours are the app's three states — captured,
   processing, signal. The LABEL is not: the app paints the label in the
   state colour too, and --accent-500 at 13px measures 3.52:1 on white,
   which clears the 3:1 a dot needs and fails the 4.5:1 text needs. So
   state stays on the dot, where the ratio it has is the ratio it needs,
   and the label is ink. Nothing is lost — each point's sentence says
   its state in words, which is the `.chip` rule (never colour alone)
   applied to a component that was breaking it. */
.tdot.captured { background: var(--accent-500); }
.tdot.processing { background: var(--warn); }
.tdot.signal { background: var(--accent-600); }
.tk {
  font-family: var(--font-mono);
  font-size: var(--fs-meta); line-height: var(--lh-meta);
  font-weight: 600; letter-spacing: .08em; text-transform: uppercase;
  color: var(--ink); margin: 0 0 2px;
}
.tt { font-size: var(--fs-meta); line-height: var(--lh-meta); color: var(--ink-2); margin: 0; }
