/* ══ THE APP'S STYLESHEET ══════════════════════════════════════════════════════════════════════
   Moved out of app/index.html's inline <style> block (2026-10-15) with NO visual change, so design
   work has a file of its own instead of a block between 3,000 lines of script. Linked from
   app/index.html; scripts/build.mjs puts `?v=<hash>` on that link, so a deploy is never served
   a stale copy of this file next to a fresh page. */
:root {
  /* Dark native controls — scrollbars, checkboxes, date pickers — on every platform. Without it
     Windows draws a white scrollbar and white checkboxes on this black page (2026-10-08). */
  color-scheme: dark;
  --sans: "Inter", -apple-system, BlinkMacSystemFont, "Segoe UI Variable Text", "Segoe UI", system-ui, sans-serif;
  --mono: "JetBrains Mono", ui-monospace, SFMono-Regular, "SF Mono", Menlo, monospace;
  /* ── THE COLOUR TOKENS (2026-10-15, the design upgrade) ─────────────────────────────────
     ONE palette, the brand's: black #0a0b0a, white #e6ebe6, green #3ff07f (brand/BRAND.md §2).
     The blue this page used to carry (#7cc7ff, #cfe8ff on #07080a) is gone. The identity is
     black and white; green is the SIGNAL — the cursor, a live state, a thing you can act on.
     Every colour on the page is one of these tokens, so the light theme below is a token swap
     and nothing else. `--glow` and `--accent` keep their names because the painter's classes
     and the tests read them; they now mean "focus/hover edge" and "primary ink". */
  --bg: #0a0b0a; --bg-raise: #111411; --bg-sunk: #0d0f0d;
  --ink: #e6ebe6; --dim: #8a948a; --faint: #5d675d;
  --line: #1c211c; --line-lit: #2a322a;
  --glow: var(--line-lit); --accent: var(--ink);
  --danger: var(--danger); --warn: #f2c14e;
  --scrim: var(--shadow); --shadow: rgba(0, 0, 0, .5);
  --chrome: rgba(10, 11, 10, .82);
  --hover: rgba(230, 235, 230, .05);
  --halo: #101510;
  --mark-ink: #f2f5f2;
  --tick: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 12 12'%3E%3Cpath d='M2.5 6.2 5 8.6 9.6 3.6' fill='none' stroke='%230a0b0a' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E");
  /* THE SIGNAL GREEN. brand/BRAND.md §2 and §4 (2026-10-15): green means live — the cursor, a
     tag chip, a scheduled box, the mark's electron while a cycle runs. It is not the identity. */
  --green: #3ff07f; --green-hairline: rgba(63, 240, 127, .32);
  --green-wash: rgba(63, 240, 127, .09); --green-soft: rgba(63, 240, 127, .55);

  /* ── THE SPACING SCALE ──────────────────────────────────────────────────────────────────
     Six steps and nothing between them. Every gap, pad and inset on this page is one of
     these or a `max()` of one against a safe-area inset — so "a bit more room here" is a
     step up the ladder rather than a new number, and two things that look aligned are
     aligned because they were given the same token rather than the same guess.
     The old page had .4/.5/.6/.7/.9/1.1/1.15/1.2/1.25/1.4/1.6/2rem, which is not a scale. */
  --s1: .25rem; --s2: .5rem; --s3: .75rem; --s4: 1rem; --s5: 1.5rem; --s6: 2rem;

  /* ── THE TYPE SCALE ────────────────────────────────────────────────────────────────────
     Four sizes for the CHROME. The reading column's heading ladder is deliberately NOT on
     this scale and is deliberately still written in literal rem: tests/app-view-rows.test.mjs
     reads those five font-sizes as NUMBERS and asserts they strictly decrease, and a
     `var()` is not a number to a test that has no browser. They are also five distinct
     values used once each, which is a ladder rather than a magic number written twice. */
  --t1: .68rem;  /* the letterspaced label — the smallest thing that is still a word */
  --t2: .78rem;  /* controls */
  --t3: .88rem;  /* secondary prose, drawer rows */
  --t4: 1rem;    /* body */
  --track: .28em;   /* the letter-spacing that makes a label a label */

  /* ── THE SHELL'S GEOMETRY ──────────────────────────────────────────────────────────────
     `--touch` is 44px at the root font size, which is the minimum target this page will
     offer a thumb. It is a token rather than a number in nine places because the moment it
     is a number in nine places, one of them is 40px and nobody can see which. */
  --touch: 2.75rem;
  --bar-h: 3.25rem;
  /* THE ACTION RAIL. 56px is not a fresh guess: docs/implementation-artifacts/research-polish-
     direction.md §4d measured the reach problem and named the number — "on ≤640px it belongs in
     a bottom bar, 56px tall, sitting above env(safe-area-inset-bottom)". It is 4px taller than
     `--bar-h` because the top bar sets one line of text beside another and this one sets a
     20px icon beside a label, with a 44px target around it.
     `--rail-w` is what the SAME row needs when it is stacked into a column: the icon, the gap,
     and the longest label ("Sign out"), plus the rail's own padding. */
  --rail-h: 3.5rem;
  --rail-w: 8rem;
  /* WHAT THE RAIL RESERVES, and the ONLY two values that differ between the two anchorings.
     The frame's padding is written once, in these; the breakpoint swaps which of them is zero.
     They exist so the rail cannot end up FLOATING over the last line of a view in one anchoring
     and reserving its space in the other — there is one arithmetic and both edges pay it. */
  --rail-bottom: 0px;
  --rail-side: var(--rail-w);
  --drawer-w: min(21rem, 86vw);
  /* THE READING COLUMN'S WIDTH. Was a flat 560px on every screen, which is what the operator's
     own screenshot showed: a narrow strip of text with hundreds of empty pixels either side on
     a wide monitor, most of the window doing nothing. `35rem` (560px) stays as the FLOOR — a
     laptop-width screen is unchanged — and the column now grows with the room the rail has
     actually left it: `100vw` minus the rail reservation (`--rail-side`, 0 on a phone) minus
     the same horizontal padding `body` already pays on both edges (`2 * --s4`) minus one more
     step of breathing margin on each side so the column never touches its own gutter (`2 *
     --s6`). `46rem` (736px) is a ceiling so a very wide monitor still reads as one column
     rather than one very long line — a STRUCTURAL cap, not a claim about the optimal character
     count for a typeface, which is a different, deliberately deferred question (see
     docs/implementation-artifacts/research-polish-direction.md §2/§3a and backlog
     `size-the-column-to-a-declared-measure`). `#entry`/`#boot` are short-form screens, not the
     reading column, and are held back to the old measure below (`--form-w`) rather than
     stretching with it. */
  --col-w: clamp(35rem, calc(100vw - var(--rail-side) - (2 * var(--s4)) - (2 * var(--s6))), 46rem);
  --form-w: 35rem;        /* the entry/boot screens' width — 560px, unchanged, see --col-w above */
  --radius-sm: 6px; --radius: 10px; --radius-lg: 16px; --pill: 999px;

  /* ── THE EDGES OF THE SCREEN ───────────────────────────────────────────────────────────
     The viewport meta carries `viewport-fit=cover`, so on a notched phone the page really
     does run under the notch and the home indicator. Named once here; every fixed edge of
     the shell pays them with `max(step, inset)` so the inset can only ever ADD room. */
  --safe-t: env(safe-area-inset-top, 0px);
  --safe-r: env(safe-area-inset-right, 0px);
  --safe-b: env(safe-area-inset-bottom, 0px);
  --safe-l: env(safe-area-inset-left, 0px);

  /* AREA TAGS (2026-10-15): six quiet hues; a tag takes one by a hash of its name (paint.ts). */
  --area-0: #7fb8a4; --area-1: #9fa8e0; --area-2: #d6a77a; --area-3: #c79ad6; --area-4: #8fbfd9; --area-5: #c9c27a;
  /* One motion, named once, so `prefers-reduced-motion` has exactly one thing to turn off. */
  --slide: .22s cubic-bezier(.2, .8, .25, 1);
}
/* ── THE LIGHT THEME (2026-10-15). Follows the device setting. A token swap and nothing else:
   no rule below this block names a colour of its own. The green darkens to keep its contrast on
   white (#0f9d4a is 3.4:1 on #f6f7f4 for the large marks it draws; text in green is avoided). */
@media (prefers-color-scheme: light) {
  :root:not([data-theme="dark"]) {
    color-scheme: light;
    --bg: #f6f7f4; --bg-raise: #ffffff; --bg-sunk: #eef1ec;
    --ink: #121612; --dim: #5b655b; --faint: #8b958b;
    --line: #e0e5de; --line-lit: #c6cfc4;
    --danger: #c43b3b; --warn: #a86a0c;
    --scrim: rgba(18, 22, 18, .25); --shadow: rgba(18, 22, 18, .14);
    --chrome: rgba(246, 247, 244, .86);
    --hover: rgba(18, 22, 18, .05);
    --halo: #ffffff;
    --mark-ink: #0d100d;
    --area-0: #2f7a62; --area-1: #4d58a8; --area-2: #9a5d24; --area-3: #84489a; --area-4: #2f6f92; --area-5: #7a7320;
    --green: #0f9d4a; --green-hairline: rgba(15, 157, 74, .35);
    --green-wash: rgba(15, 157, 74, .09); --green-soft: rgba(15, 157, 74, .6);
    --tick: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 12 12'%3E%3Cpath d='M2.5 6.2 5 8.6 9.6 3.6' fill='none' stroke='%23ffffff' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E");
  }
}
/* A THEME CHOSEN IN SETTINGS (2026-10-16, app/shell/settings.ts) wins over the device: light here,
   and dark by keeping the default tokens above (the block before this one skips `data-theme="dark"`). */
:root[data-theme="light"] {
  color-scheme: light;
  --bg: #f6f7f4; --bg-raise: #ffffff; --bg-sunk: #eef1ec;
  --ink: #121612; --dim: #5b655b; --faint: #8b958b;
  --line: #e0e5de; --line-lit: #c6cfc4;
  --danger: #c43b3b; --warn: #a86a0c;
  --scrim: rgba(18, 22, 18, .25); --shadow: rgba(18, 22, 18, .14);
  --chrome: rgba(246, 247, 244, .86);
  --hover: rgba(18, 22, 18, .05);
  --halo: #ffffff;
  --mark-ink: #0d100d;
  --area-0: #2f7a62; --area-1: #4d58a8; --area-2: #9a5d24; --area-3: #84489a; --area-4: #2f6f92; --area-5: #7a7320;
  --green: #0f9d4a; --green-hairline: rgba(15, 157, 74, .35);
  --green-wash: rgba(15, 157, 74, .09); --green-soft: rgba(15, 157, 74, .6);
  --tick: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 12 12'%3E%3Cpath d='M2.5 6.2 5 8.6 9.6 3.6' fill='none' stroke='%23ffffff' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E");
}
* { box-sizing: border-box; }
/* THE SCROLLBAR'S ROOM IS ALWAYS RESERVED, WHETHER OR NOT THERE IS ONE.
   research-polish-direction.md §3b states the frame's one constraint — "opening the drawer must
   not move the reading column horizontally" — and the drawer broke it, by a route that has
   nothing to do with the drawer's own width: `.noscroll` sets `overflow: hidden` on the body,
   the classic scrollbar goes away, and a column that is CENTRED in the frame slides by half the
   width it just gained. Measured at 1440 wide: 485.2px before the drawer opened, 492.0px while
   it was open, back to 485.2px after — 6.8px of horizontal travel on every open and every
   close, five times the 1.3637px this file's suite otherwise polices. Holding the gutter open
   permanently costs a strip of background nobody looks at and makes the arithmetic constant.
   On a phone it is inert: overlay scrollbars take no space to begin with, so there is none to
   reclaim and nothing to move. */
html { margin: 0; height: 100%; scrollbar-gutter: stable; scrollbar-width: thin; scrollbar-color: var(--line-lit) transparent; }
body {
  margin: 0;
  /* `min-height`, NOT `height`. This was `height: 100%` and that quietly threw the bottom
     padding away: a box with a FIXED height does not grow for content that overflows it, and
     overflowing content runs straight past the padding edge instead of stopping short of it.
     Measured at 390×844 before the change — scrolled to the end, the last row's bottom sat at
     1922.28 against a scrollHeight of 1922, so the declared 32px of bottom padding was worth
     0px of actual room. Nothing depended on it while the only thing below the fold was empty
     space; the rail is the first thing that OCCUPIES that space, and with `height` it covered
     the last row of every view by its full 56px. `min-height` keeps the full-viewport floor
     the entry screen wants and lets the page grow past it, which is what puts the padding back
     into the scroll extent. */
  min-height: 100%;
  background: radial-gradient(1200px 800px at 50% -10%, var(--halo) 0%, var(--bg) 60%);
  color: var(--ink);
  font: var(--t4)/1.5 var(--sans);
  font-feature-settings: "cv11", "ss03";
  -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; text-rendering: optimizeLegibility;
  display: flex; align-items: flex-start; justify-content: center;
  /* The menu bar is FIXED, so the column starts below it — the one place that arithmetic is
     written, and it is written in tokens. Left/right pay the safe-area insets so a landscape
     phone does not put the first character of a line under the camera housing.
     THE RAIL IS FIXED TOO, AND IT RESERVES RATHER THAN FLOATS. Its two tokens are added here,
     and exactly one of them is non-zero at a time: `--rail-bottom` on a phone, `--rail-side` on
     a desktop. A bar that floats over the reading column takes the last line of every view with
     it, which is the one thing a reading surface may not do. */
  padding:
    calc(var(--bar-h) + var(--safe-t) + var(--s5))
    max(var(--s4), var(--safe-r))
    calc(var(--rail-bottom) + var(--s6) + var(--safe-b))
    max(calc(var(--rail-side) + var(--s4)), var(--safe-l));
}
.wrap { width: 100%; max-width: var(--col-w); }

/* THE TOUCH BAR — vim's keys as buttons (app/shell/touchbar.ts). Off by default; on where there
   is probably no keyboard. It sits on the rail, or on the phone's keyboard when one is open
   (its `top`, set from `visualViewport`), and the column reserves its height like it reserves the
   rail's, so it never covers the last line. Each mode shows its own buttons. */
:root { --touchbar-h: 0px; }
.touchbar { display: none; }
@media (pointer: coarse), (max-width: 40rem) {
  :root { --touchbar-h: var(--touch); }
  body { padding-bottom: calc(var(--rail-bottom) + var(--touchbar-h) + var(--s6) + var(--safe-b)); }
  .touchbar {
    position: fixed; z-index: 20; left: 0; right: 0;
    bottom: calc(var(--rail-bottom) + var(--safe-b));
    display: flex; gap: var(--s1); overflow-x: auto; scrollbar-width: none;
    height: var(--touchbar-h); align-items: center;
    padding: 0 max(var(--s2), var(--safe-r)) 0 max(calc(var(--rail-side) + var(--s2)), var(--safe-l));
    background: var(--bg); border-top: 1px solid var(--line);
  }
  .touchbar.hidden { display: none; }
  /* TAP TO TYPE (2026-10-15): on a phone a tap on a line edits it, so the key strip is for
     EDITING only. In NORMAL the dock is the chrome; the strip appears with the keyboard. */
  .touchbar[data-mode="NORMAL"] { display: none; }
  .touchbar[data-keyboard] { bottom: auto; }
  .touchkey {
    flex: 1 0 auto; min-width: 2.25rem; height: calc(var(--touch) - var(--s2)); padding: 0 var(--s2);
    background: transparent; color: var(--ink); border: 1px solid var(--line); border-radius: var(--radius);
    font: inherit; font-family: var(--mono); font-size: .875rem;
    touch-action: manipulation;
  }
  .touchkey:active { background: var(--line); }
  /* NO ZOOM ON FOCUS (2026-10-09, operator report from an iPhone: "go to another view, it lands
     at the wrong size"). iOS zooms the page in on any field whose text is under 16px when it
     takes focus, and does not zoom back out. The Views filter and the search box were 13-15px. */
  input, textarea, select { font-size: max(16px, 1em) !important; }
  /* The config screen's line numbers follow its text area to 16px, or they drift off their lines. */
  .config-screen .config-gutter { font-size: max(16px, 1em); }
  .touchbar[data-mode="NORMAL"] .touchkey:not([data-modes~="NORMAL"]),
  .touchbar[data-mode="INSERT"] .touchkey:not([data-modes~="INSERT"]) { display: none; }
}
/* THE TWO SHORT-FORM SCREENS DO NOT STRETCH WITH THE READING COLUMN. `.wrap` now grows on a
   wide screen so the graph section (the reading column, `#graph` > `.viewbody`) can use the
   room — but a sign-in card and a "opening your graph…" line are not prose, and a 736px-wide
   login form is not what "the page sits better" asked for. `margin: 0 auto` re-centres each
   inside the wider `.wrap`, the same way `.wrap` itself centres inside `body`. */
#entry, #boot { max-width: var(--form-w); margin: 0 auto; }
h1 { font-weight: 600; font-size: clamp(1.6rem, 5vw, 2.2rem); margin: var(--s2) 0 var(--s1); }
.tag { color: var(--dim); margin: 0 0 var(--s6); }
button {
  font: inherit; color: var(--bg); background: var(--accent);
  border: 0; border-radius: var(--radius); padding: var(--s3) var(--s4); cursor: pointer;
  font-weight: 600; transition: transform .08s ease, box-shadow .2s ease;
  box-shadow: none;
}
button:hover { box-shadow: 0 0 0 3px var(--hover); }
button:active { transform: translateY(1px); }
button.ghost { background: transparent; color: var(--accent); box-shadow: none; border: 1px solid var(--line); }
button.ghost:hover { border-color: var(--line-lit); box-shadow: none; }
/* One focus ring for the whole page, in the identity's green, on the only thing that can be
   focused without a pointer. It is `:focus-visible` so a mouse click does not draw it. */
button:focus-visible, input:focus-visible { outline: 2px solid var(--green); outline-offset: 2px; }
input {
  font: inherit; color: var(--ink); background: var(--bg-sunk); border: 1px solid var(--line);
  border-radius: var(--radius); padding: var(--s3) var(--s3); width: 100%; outline: none;
}
/* ONE focus ring (2026-10-15). The border used to turn blue AND the green outline drew around it,
   so a focused field had two rings. Now the border stays and the outline is the ring. */
input:focus { border-color: var(--line-lit); }
.row { display: flex; gap: var(--s2); align-items: center; }
.muted { color: var(--dim); font-size: var(--t3); }
/* THE ERROR ROW RESERVES THE ROW IT ACTUALLY HOLDS. It was `1.2em`, and the thing it has to
   hold is one line at the body's `line-height: 1.5` — so at `--t3` it reserved 14.08 × 1.2 =
   16.896px for a line that measures 14.08 × 1.5 = 21.12px. Measured in a browser at 390×844:
   the box went 16.8892 → 21.1151 the moment a message appeared, moving the card's bottom edge
   4.2259px every time this screen said no. That is three times the 1.3637px the reading
   column's own suite polices and infinitely more than the 0.0000px it asserts on focus, and it
   happened on the one screen a stranger meets first. `1.5em` is the same number as the line it
   reserves for, which is why it is the right one rather than a bigger guess. */
.err { color: var(--danger); font-size: var(--t3); min-height: 1.5em; }
.card { border: 1px solid var(--line); border-radius: var(--radius-lg); padding: var(--s4); background: var(--bg-raise); }
.hidden { display: none !important; }

/* ══ THE ENTRY SCREEN ══════════════════════════════════════════════════════════════════════
   TWO PATHS, NOT ONE PATH AND A FOOTNOTE. Measured at 390×844 before this change: the only
   filled, high-emphasis button on the screen was "Continue with a passkey" — the one a person
   who has never been here CANNOT use — at 215.89 × 47.99px, and the new person's path was a
   ghost button under a 14px line reading "New here?". On the screen whose entire purpose is
   now that somebody other than the operator can sign up, the newcomer's path is the one that
   has to be legible, so the emphasis is swapped and each path gets a heading of its own.

   THE RULES ARE STATED BEFORE THEY ARE BROKEN. `#handleHint` carries the handle rule as a
   hint the field points at with `aria-describedby`, rather than as an error the person earns
   by guessing wrong — and `.hint` under the create button says what a passkey is going to do,
   because pressing it summons an operating-system dialog a first-time visitor did not ask for
   and the commonest thing anyone does with an unexpected system dialog is dismiss it. */
.path-head {
  font-size: var(--t1); letter-spacing: var(--track); text-transform: uppercase;
  color: var(--dim); font-weight: 600; margin: 0 0 var(--s2);
}
.hint { font-size: var(--t3); color: var(--dim); margin: var(--s2) 0 0; }
.hint-lead { margin: 0 0 var(--s2); }
/* The field the message is about, marked. `aria-invalid` is set on the same element at the
   same moment — this is the half a screen reader cannot use and that one is the half a sighted
   reader cannot hear, and neither is a substitute for the other. */
input.wrong { border-color: var(--danger); }
input.wrong:focus { border-color: var(--danger); }

/* ══ THE EMPTY ENVIRONMENT ═════════════════════════════════════════════════════════════════
   WHAT A BRAND-NEW ACCOUNT SEES, and until this change it was one line of the dimmest colour
   on the page reading "No snapshot yet — run graph-sync push on the laptop." — an instruction
   addressed to exactly one human being on earth, shown to everybody who is not him. Measured
   at 1440×900 it was a single 21.12px line in a 900px-tall window with nothing else in it.

   `#emptyNext` IS A NAMED SLOT AND IT IS DELIBERATELY EMPTY. What an empty account should be
   told to do NEXT depends on where a second user's graph lives, which is not decided — see
   docs/implementation-artifacts/design-someone-else-can-sign-up.md section 3.1, which lists
   what each possible answer would put here. A sentence written now would be a guess, and a
   guess in an empty state is the sentence a person reads first. */
.empty-head { font-size: var(--t4); font-weight: 600; color: var(--ink); margin: 0 0 var(--s2); }
.empty-body { font-size: var(--t3); color: var(--dim); margin: 0; }
.empty-next { font-size: var(--t3); color: var(--dim); margin: var(--s3) 0 0; }

a { color: var(--accent); }

/* ══ THE MENU BAR ═════════════════════════════════════════════════════════════════════════
   The app's one permanent piece of chrome, and the answer to "where am I, who am I, and how
   do I get somewhere else". It is present on the entry screen too — carrying the mark alone —
   because the alternative is a signed-out page with no identity on it at all, which is what
   the retired `.topbar` was for.

   WHAT IT HOLDS, LEFT TO RIGHT: the mark (dot + wordmark — brand/BRAND.md's `● qntm`
   combination mark, and the dot is the named brand device); WHERE YOU ARE (the current view's
   folder and title, the one thing the retired <select> said only while you were looking at
   it); and WHO YOU ARE. Nothing else.

   THE CONTROLS LEFT, AND THAT IS THE POINT. They used to sit here, clustered in the top-right
   corner, on the argument that it "is the side a thumb reaches". Measured on a real 390×844
   viewport, it is not: the button that OPENS the drawer sat at y≈25 and the button that CLOSES
   it — moved to the bottom by this same shell, for this same reason — sat at y≈814. The same
   panel was opened from the top of the screen and dismissed from the bottom, 789px apart on an
   844px screen. The controls now live in `.rail` below, on the edge a hand is already on, and
   there is exactly one of each. This bar is now what it says it is: where you are, and who.

   IT IS FIXED, NOT STICKY. A sticky bar leaves the scroll position deciding whether the way
   to another view exists; on a phone, in a long view, that means scrolling to the top to
   change views. */
.bar {
  position: fixed; inset: 0 0 auto 0; z-index: 20;
  display: flex; align-items: center; gap: var(--s3);
  height: calc(var(--bar-h) + var(--safe-t));
  padding: var(--safe-t) max(var(--s4), var(--safe-r)) 0 max(var(--s4), var(--safe-l));
  background: var(--chrome);
  -webkit-backdrop-filter: blur(14px); backdrop-filter: blur(14px);
  border-bottom: 1px solid var(--line);
}
.bar-mark { display: flex; align-items: center; gap: var(--s2); flex: none; --qmark-ink: var(--mark-ink); --qmark-live: var(--green); }
/* THE MARK ITSELF IS NOT STYLED HERE, and that is the whole of its wiring. It came from
   /app/mark/qmark.css, linked in the head, and the only thing this page says about it is how
   big it is. brand/BRAND.md section 2 called the icon mark "the main gap"; the 8px green dot
   that used to sit here was the brand DEVICE standing in for it, and the responsive pair
   BRAND.md names is full `qntm` ↔ reduced Q, not ↔ dot.

   TWO THINGS THIS DELIBERATELY DOES NOT SAY. It does not set `--qmark-ink`: the mark's colour
   is the mono-luminance dial, which BRAND.md section 5 still has OPEN, so it comes from the
   mark's own token carrying the lab's current value rather than from this page's `--ink`.
   Binding it to `--ink` would settle a brand decision by accident, from a stylesheet. And it
   does not set `--qmark-size` either. The component's default is 1.5rem, which is what this bar
   wants; restating it here would be a second place for one number to live, and the day the two
   disagree the bar would be the one nobody thought to look in. Below 20px the mark's own sheet
   settles the electron rather than orbiting it, so a bar that ever wants a smaller mark gets a
   still one, which is the right trade and is the component's decision, not this page's. */
.bar-word { font-family: var(--mono); font-weight: 500; font-size: var(--t3); color: var(--ink); }
/* WHERE YOU ARE. Takes the slack, and gives it back first: `min-width: 0` is what lets the
   view's title ellipse instead of shoving the controls off a 320px screen. */
.bar-where { flex: 1; min-width: 0; display: flex; align-items: baseline; gap: var(--s2); overflow: hidden; }
.bar-folder { flex: none; font-size: var(--t1); letter-spacing: var(--track); text-transform: uppercase; color: var(--dim); white-space: nowrap; }
.bar-view { font-size: var(--t3); color: var(--ink); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
/* ONE LARGE TITLE (2026-10-15): the page's heading is the title; the bar's copy fades in only
   once that heading has scrolled out of sight (`.scrolled`, set in app/index.html). */
.bar-view { opacity: 0; transition: opacity var(--slide); }
.bar.scrolled .bar-view { opacity: 1; }
.bar-who {
  flex: none; font-size: var(--t2); color: var(--dim); font-family: var(--mono); font-weight: 500;
  min-height: var(--touch); padding: 0 var(--s3); border-radius: var(--pill);
  background: transparent; border: 1px solid transparent; box-shadow: none;
}
.bar-who:hover, .bar-who[aria-expanded="true"] { color: var(--ink); border-color: var(--line-lit); box-shadow: none; }
/* THE ACCOUNT MENU, under the handle it opens from. `[hidden]` is restated because `display: flex`
   below would otherwise out-rank the attribute and leave the menu open on every load. */
.account[hidden] { display: none; }
.account {
  position: fixed; z-index: 30;
  top: calc(var(--bar-h) + var(--safe-t) - var(--s1)); right: max(var(--s4), var(--safe-r));
  display: flex; flex-direction: column; gap: var(--s1); min-width: 11rem; padding: var(--s1);
  background: var(--bg-raise); border: 1px solid var(--line-lit); border-radius: var(--radius);
  box-shadow: 0 12px 32px var(--shadow);
}
/* Every control in the bar is at least --touch tall AND --touch wide. `.barbtn` is now worn by
   the drawer's own close button alone; the rail's controls wear `.railbtn`, which takes the
   same minimum from the same token. */
.barbtn {
  display: inline-flex; align-items: center; justify-content: center; gap: var(--s2);
  min-height: var(--touch); min-width: var(--touch); padding: 0 var(--s3);
  background: transparent; color: var(--dim); border: 1px solid var(--line);
  border-radius: var(--pill); box-shadow: none;
  font-size: var(--t2); font-weight: 500;
}
.barbtn:hover { color: var(--ink); border-color: var(--glow); box-shadow: none; }
.kbd {
  font-family: var(--mono);
  font-size: var(--t1); opacity: .55;
}

/* ══ THE ACTION RAIL ══════════════════════════════════════════════════════════════════════
   THIS IS NOT A TAB BAR, BECAUSE THERE IS NOTHING TO TAB BETWEEN. The app has one section a
   reader can be in (`#graph`); `#entry` is the gate, not a place. A second place, `#app` (a
   to-do capture screen — "The one thing", capture box, "Also held" list), used to sit beside
   them, unreachable — `loadState()` was defined and never called, verified at the time this
   comment last said so — and it was removed entirely (chore/retire-the-dead-landing-app; see
   that PR for the reachability trace and mutation proof). What is left HAS 76 views in ten
   nested folders, and 76 nested destinations is a tree, which is what `.drawer` already is. A
   tab bar over one section would be chrome with nothing behind it.

   SO IT HOLDS ACTIONS, and that is also the shape the operator already reads: `~/qntm/
   .obsidian/app.json` declares `mobileToolbarCommands` — the bar along the bottom of his phone —
   and all 23 entries are commands (undo, indent, toggle-checklist, go-start, command-palette).
   Not one of them is a destination.

   IT IS ONE ELEMENT AT BOTH WIDTHS. The sibling who built the top bar rejected a desktop rail
   because it would mean "two layouts to keep in step". The answer is that there are not two:
   there is one `<nav>`, with one set of children, and the breakpoint changes which EDGE it is
   anchored to and which way its flex runs. No control exists at one width and not the other, no
   control is duplicated between the bar and the rail, and tests/app-shell.test.mjs asserts both
   — so the two cannot drift apart without a red test rather than without someone noticing.

   THE DESKTOP ANCHORING IS THE LEFT EDGE. A mouse has no reach problem, so the desktop rail is
   not for reach — it is for PERSISTENCE, which is the shape of the 200px left panel his own
   Obsidian keeps permanently open (`workspace.json`). At 1440 wide the reading column now grows
   to fill the room the rail leaves it (see `--col-w` above) rather than sitting at a flat 560px
   with the gutter left untouched either side — the rail is still spent out of that space, but
   the column no longer stops taking the pixels once it reaches its old width. */
.rail {
  position: fixed; z-index: 20;
  /* `stretch`, so a column of controls is one column and not three widths — the cross axis of
     a column IS the width. The phone flips this to `center`, where the cross axis is the
     height and centring is what a row wants. One property, one flip, same as the rest. */
  display: flex; align-items: stretch;
  background: var(--chrome);
  -webkit-backdrop-filter: blur(14px); backdrop-filter: blur(14px);
  /* DESKTOP: the left edge, below the bar, running down. */
  inset: calc(var(--bar-h) + var(--safe-t)) auto 0 0;
  flex-direction: column; justify-content: flex-start; gap: var(--s1);
  width: calc(var(--rail-w) + var(--safe-l));
  padding: var(--s4) var(--s2) calc(var(--s4) + var(--safe-b)) max(var(--s2), var(--safe-l));
  border-right: 1px solid var(--line);
}
/* Icon BESIDE label, never instead of it — the same arrangement in the row and in the column,
   so the control is one shape that turns rather than two that resemble each other. */
.railbtn {
  display: flex; align-items: center; gap: var(--s2);
  min-height: var(--touch); min-width: var(--touch); padding: 0 var(--s3);
  background: transparent; color: var(--dim);
  border: 1px solid transparent; border-radius: var(--radius); box-shadow: none;
  font-family: var(--sans); font-size: var(--t2); font-weight: 500; white-space: nowrap;
}
.railbtn:hover { color: var(--ink); background: var(--hover); box-shadow: none; }
.railbtn[aria-busy="true"] { color: var(--green); border-color: var(--green-hairline); }
/* THE CYCLE TOAST (2026-10-15): one short line, centred above the dock. "Details" opens the
   server's summary under it. */
.cycle-note { position: fixed; left: 50%; transform: translateX(-50%);
  bottom: calc(var(--rail-bottom) + var(--s4) + var(--safe-b)); z-index: 40;
  width: max-content; max-width: min(46rem, calc(100vw - 2rem));
  max-height: 60vh; display: flex; flex-direction: column; background: var(--chrome);
  -webkit-backdrop-filter: blur(14px); backdrop-filter: blur(14px);
  border: 1px solid var(--line-lit); border-radius: var(--radius); box-shadow: 0 8px 30px var(--shadow);
  animation: toast-in var(--slide); }
.cycle-note.open { width: min(46rem, calc(100vw - 2rem)); }
.cycle-note.hidden { display: none; }
.cycle-note:not(.open) .cycle-note-body { display: none; }
/* THE SUGGESTION LIST (app/shell/completer.ts places it every frame, under or above its line). */
.tag-picker { position: fixed; z-index: 50; margin: 0; padding: 4px; list-style: none; min-width: 12rem;
  max-width: min(22rem, calc(100vw - 16px)); max-height: 16rem; overflow: auto; overscroll-behavior: contain;
  background: var(--chrome); -webkit-backdrop-filter: blur(14px); backdrop-filter: blur(14px);
  border: 1px solid var(--line-lit); border-radius: 10px;
  box-shadow: 0 12px 32px var(--shadow);
  font-family: var(--mono); font-size: .85rem; }
.tag-picker[hidden] { display: none; }
.key-help { position: fixed; inset: 0; z-index: 60; display: grid; place-items: center; background: var(--scrim); }
.key-help[hidden] { display: none; }
.search-box { position: fixed; top: 4rem; left: 50%; transform: translateX(-50%); z-index: 60; width: min(40rem, calc(100vw - 2rem));
  background: var(--bg-raise); border: 1px solid var(--green-hairline); border-radius: 8px; box-shadow: 0 8px 30px var(--shadow); }
.search-box[hidden] { display: none; }
/* THE CONFIG SCREEN (2026-10-14, app/shell/config.ts): files on the left, one file in a text area. */
.config-screen { position: fixed; inset: 0; z-index: 58; display: flex; flex-direction: column; gap: var(--s2);
  padding: calc(var(--safe-t) + var(--s3)) calc(var(--safe-r) + var(--s4)) calc(var(--safe-b) + var(--s3)) calc(var(--safe-l) + var(--s4));
  background: var(--bg-raise); color: var(--ink); }
.config-screen[hidden] { display: none; }
/* THE SETTINGS SCREEN (2026-10-16, app/shell/settings.ts): a card over a dim page, groups of rows.
   A sheet from the bottom on a phone (the phone block below). */
.settings { position: fixed; inset: 0; z-index: 58; display: grid; place-items: center; padding: var(--s4);
  background: var(--scrim); }
.settings[hidden] { display: none; }
.settings-card { width: min(32rem, 100%); max-height: 100%; overflow: auto; display: flex; flex-direction: column;
  background: var(--bg-raise); border: 1px solid var(--line-lit); border-radius: var(--radius-lg);
  box-shadow: 0 12px 40px var(--shadow); padding: var(--s2) var(--s4) var(--s4); }
.settings-head { display: flex; align-items: center; justify-content: space-between; gap: var(--s2); padding: var(--s2) 0; }
.settings-head h2 { margin: 0; font-size: var(--t5, 1.25rem); font-weight: 600; }
.settings-group { padding: var(--s3) 0; border-top: 1px solid var(--line); }
.settings-group h3 { margin: 0 0 var(--s2); font-size: var(--t1); letter-spacing: var(--track); text-transform: uppercase;
  color: var(--dim); font-weight: 600; }
.settings-row { display: flex; align-items: center; justify-content: space-between; gap: var(--s3); min-height: var(--touch); }
.settings-label { color: var(--ink); }
.settings-note { margin: var(--s1) 0 0; font-size: var(--t1); color: var(--dim); }
.settings-form { display: flex; flex-direction: column; gap: var(--s2); padding-top: var(--s2); }
.settings-input { font: inherit; color: var(--ink); background: transparent; border: 1px solid var(--line);
  border-radius: var(--radius); padding: var(--s1) var(--s2); min-height: var(--touch); }
.settings-checks { display: flex; flex-direction: column; gap: var(--s1); color: var(--ink); }
/* THE NODE PAGE (2026-10-19, app/shell/nodepage.ts): one node's fields, children, links and places,
   in the settings card's shape. A sheet from the bottom on a phone (the phone block below). */
.node-page { position: fixed; inset: 0; z-index: 58; display: grid; place-items: center; padding: var(--s4);
  background: var(--scrim); }
.node-page[hidden] { display: none; }
.node-page-card { width: min(36rem, 100%); max-height: 100%; overflow: auto; display: flex; flex-direction: column;
  background: var(--bg-raise); border: 1px solid var(--line-lit); border-radius: var(--radius-lg);
  box-shadow: 0 12px 40px var(--shadow); padding: var(--s2) var(--s4) var(--s4); }
.node-page-head { display: flex; align-items: center; gap: var(--s2); padding: var(--s2) 0; }
.node-page-title { flex: 1; display: flex; align-items: center; gap: var(--s2); margin: 0; font-size: var(--t5, 1.25rem); font-weight: 600; }
.node-page-fields { display: grid; grid-template-columns: max-content 1fr; gap: var(--s1) var(--s3); margin: 0 0 var(--s2); }
.node-page-fields dt { color: var(--dim); font-family: var(--mono); font-size: var(--t1); }
.node-page-fields dd { margin: 0; color: var(--ink); overflow-wrap: anywhere; }
.node-page-section { padding: var(--s3) 0 0; border-top: 1px solid var(--line); margin-top: var(--s2); }
.node-page-section h3 { margin: 0 0 var(--s2); font-size: var(--t1); letter-spacing: var(--track); text-transform: uppercase;
  color: var(--dim); font-weight: 600; }
.node-page-section ul { list-style: none; margin: 0; padding: 0; }
.node-page-section li { display: flex; align-items: center; gap: var(--s2); min-height: var(--touch); }
.node-page-section small { color: var(--dim); font-family: var(--mono); font-size: var(--t1); }
.node-page-group summary { cursor: pointer; color: var(--ink); font-family: var(--mono); font-size: var(--t1); min-height: var(--touch);
  display: flex; align-items: center; }
.node-page-link { font: inherit; color: var(--ink); background: none; border: 0; padding: 0; text-align: left; cursor: pointer; }
.node-page-link:hover, .node-page-link:focus-visible { color: var(--green); }
.node-page-tick { font: inherit; font-size: 1.1em; line-height: 1; color: var(--ink); background: none; border: 0; padding: 0 2px;
  cursor: pointer; min-width: var(--touch); min-height: var(--touch); }
.node-page-tick[aria-busy="true"] { opacity: .5; }
.node-page-done { color: var(--dim); text-decoration: line-through; }
.node-page-note { color: var(--dim); }
/* Three choices in one control, the chosen one raised. */
.segmented { display: inline-flex; padding: 3px; gap: 2px; border: 1px solid var(--line); border-radius: var(--pill);
  background: var(--bg-sunk); }
.segmented button { min-height: calc(var(--touch) - var(--s2)); padding: 0 var(--s3); border: 0; border-radius: var(--pill);
  background: transparent; color: var(--dim); box-shadow: none; font: inherit; font-size: var(--t2); cursor: pointer; }
.segmented button[aria-checked="true"] { background: var(--bg-raise); color: var(--ink); box-shadow: 0 0 0 1px var(--line-lit), 0 1px 3px var(--shadow); }
.config-head { display: flex; align-items: center; gap: var(--s2); }
.config-head h2 { flex: 1; margin: 0; font-family: var(--sans); font-size: var(--t1); letter-spacing: var(--track); text-transform: uppercase; color: var(--dim); font-weight: 500; }
.config-status { margin: 0; font-size: var(--t3); color: var(--dim); min-height: 1.4em; }
.config-status[data-kind="ok"] { color: var(--green); }
.config-status[data-kind="bad"] { color: var(--danger); }
.config-goto { align-self: flex-start; }
.config-goto[hidden] { display: none; }
.config-body { flex: 1; min-height: 0; display: grid; grid-template-columns: minmax(10rem, 16rem) 1fr; gap: var(--s3); }
.config-files { overflow: auto; border-right: 1px solid var(--line); padding-right: var(--s2); display: flex; flex-direction: column; gap: var(--s2); }
.config-folder { flex: none; display: flex; flex-direction: column; }
.config-folder-name { margin: var(--s2) 0 0; font-family: var(--mono); font-size: .75rem; color: var(--dim); }
.config-file { text-align: left; background: none; border: 0; color: var(--ink); font-family: var(--mono); font-size: .8rem; padding: .15rem .3rem; border-radius: 4px; cursor: pointer; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.config-file[aria-current="true"] { background: var(--green-hairline); }
.config-file-bad { color: var(--danger); }
.config-new { flex: none; display: flex; gap: var(--s1); margin-top: auto; }
.config-new input { flex: 1; min-width: 0; font-family: var(--mono); font-size: .75rem; }
.config-editor { min-width: 0; min-height: 0; display: flex; flex-direction: column; gap: var(--s1); }
.config-path { margin: 0; font-family: var(--mono); font-size: .8rem; color: var(--dim); min-height: 1.2em; flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.config-path-row { display: flex; align-items: center; gap: var(--s2); }
.config-mode { font-family: var(--mono); font-size: .7rem; letter-spacing: var(--track); padding: .15rem .5rem; border-radius: var(--pill); border: 1px solid var(--line); background: none; color: var(--dim); cursor: pointer; }
.config-mode[data-mode="INSERT"] { color: var(--green); border-color: var(--green-hairline); }
.config-hint { display: flex; align-items: center; gap: var(--s2); min-height: 1.6em; font-size: var(--t3); color: var(--dim); }
.config-hint-text { flex: 1; min-width: 0; }
.config-hint-buttons { display: flex; gap: var(--s1); flex: none; }
.config-hint-buttons[hidden] { display: none; }
.config-picker { position: relative; z-index: 2; border: 1px solid var(--green-hairline); border-radius: 8px; background: var(--bg-raise); box-shadow: 0 8px 30px var(--shadow); max-height: 40vh; display: flex; flex-direction: column; }
.config-picker[hidden] { display: none; }
.config-picker input { border: 0; border-bottom: 1px solid var(--line); background: transparent; color: var(--ink); padding: .4rem .6rem; font-family: var(--mono); outline: none; }
.config-picker ul { list-style: none; margin: 0; padding: .2rem 0; overflow: auto; }
.config-picker li { display: flex; gap: var(--s3); align-items: baseline; padding: .25rem .6rem; cursor: pointer; }
.config-picker li b { font-family: var(--mono); font-weight: 500; font-size: .8rem; color: var(--ink); white-space: nowrap; }
.config-picker li small { color: var(--dim); font-size: .75rem; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.config-picker li[aria-selected="true"] { background: var(--green-hairline); }
.config-gutter .config-line-here { color: var(--green); font-weight: 600; }
.config-text { flex: 1; min-height: 0; display: flex; border: 1px solid var(--line); border-radius: 6px; overflow: hidden; }
.config-gutter { overflow: hidden; padding: .5rem .4rem; text-align: right; font-family: var(--mono); font-size: .8rem; line-height: 1.5; color: var(--dim); user-select: none; display: flex; flex-direction: column; min-width: 2.5rem; }
.config-gutter .config-line-bad { color: var(--danger); font-weight: 700; }
.config-text textarea { flex: 1; resize: none; border: 0; outline: none; padding: .5rem; background: transparent; color: var(--ink); font-family: var(--mono); font-size: .8rem; line-height: 1.5; white-space: pre; overflow: auto; tab-size: 2; }
@media (max-width: 40rem) {
  .config-body { grid-template-columns: 1fr; grid-template-rows: minmax(6rem, 30%) 1fr; }
  .config-files { border-right: 0; border-bottom: 1px solid var(--line); padding: 0 0 var(--s2); }
}
.search-box input { width: 100%; box-sizing: border-box; padding: .6rem .75rem; border: 0; border-bottom: 1px solid var(--green-hairline); background: transparent; color: var(--ink); font-size: .95rem; outline: none; }
.search-box ul { list-style: none; margin: 0; padding: .25rem 0; max-height: 60vh; overflow: auto; }
.search-box li { display: flex; justify-content: space-between; gap: 1rem; padding: .3rem .75rem; cursor: pointer; color: var(--ink); font-size: .85rem; }
.search-box li small { color: var(--dim); white-space: nowrap; }
.search-box li span { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
/* A done task in the results (app/shell/search.ts), dimmed and struck like a done line in a view. */
.search-box li.search-done span { color: var(--dim); text-decoration: line-through; }
.search-kind { flex: none; width: 4.5rem; font-style: normal; font-family: var(--sans); font-size: var(--t1); letter-spacing: .06em; text-transform: uppercase; }
.search-kind-view { color: var(--green); }
.search-kind-section { color: var(--accent); }
.search-kind-task { color: var(--dim); }
.search-box li[aria-selected="true"] { background: var(--green-hairline); color: var(--green); }
.key-help-panel { width: min(40rem, calc(100vw - 2rem)); max-height: 85vh; overflow: auto; padding: 1rem 1.25rem;
  background: var(--bg-raise); border: 1px solid var(--green-hairline); border-radius: 8px; }
.key-help-panel h2 { margin: 0 0 .5rem; font-size: 1rem; color: var(--ink); }
.key-help-panel h3 { margin: .9rem 0 .35rem; font-size: .75rem; letter-spacing: .08em; text-transform: uppercase; color: var(--dim); }
.key-help-panel dl { display: grid; grid-template-columns: max-content 1fr; gap: .3rem 1rem; margin: 0; font-size: .85rem; }
.key-help-panel dt { display: flex; gap: .25rem; flex-wrap: wrap; }
.key-help-panel dd { margin: 0; color: var(--ink); }
.key-help-panel kbd { padding: 0 .35rem; border: 1px solid var(--green-hairline); border-radius: 4px; font-family: var(--mono); font-size: .8rem; color: var(--green); }
.tag-picker li { padding: .4rem .6rem; border-radius: 6px; color: var(--ink); cursor: pointer;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.tag-picker li:hover { background: var(--hover); }
.tag-picker li[aria-selected="true"] { background: var(--green-hairline); color: var(--green); }
.cycle-note-head { display: flex; align-items: center; justify-content: space-between; gap: .5rem; padding: .35rem .5rem .35rem .9rem; }
.cycle-note-title { margin-right: auto; }
.cycle-note-title { color: var(--ink); font-size: .85rem; }
.cycle-note-body { margin: 0; padding: 0 .75rem .75rem; overflow: auto; font-size: .72rem; line-height: 1.35; color: var(--dim); white-space: pre; }
/* The drawer's own state, said on the control that owns it — `aria-expanded` is already kept
   in step by openDrawer/closeDrawer, so this marks the rail from the fact rather than from a
   second flag that could disagree with it. */
.railbtn[aria-expanded="true"] { color: var(--green); border-color: var(--green-hairline); }
/* ONE GRID AND ONE WEIGHT, STATED ONCE. Every glyph is a 24-unit box drawn in strokes at 1.5,
   round-capped — so a new icon cannot arrive half a pixel heavier than its neighbours, and the
   markup below carries geometry and nothing else. Inline, because a font or a CDN is a
   dependency that can fail to arrive and leave three unlabelled buttons behind. */
.ico {
  flex: none; width: 1.25rem; height: 1.25rem;
  fill: none; stroke: currentColor; stroke-width: 1.5;
  stroke-linecap: round; stroke-linejoin: round;
}

/* ══ THE VIEW DRAWER ══════════════════════════════════════════════════════════════════════
   The shape the operator already reads in Obsidian: a panel off the right edge holding the
   views as a FOLDER TREE. The folders are the vault's own folders — see `folderOf` in the
   script for where that comes from and why it is `path` and not `domain`.

   IT IS A SLIDE, AND THE SLIDE IS ONE TOKEN. `--slide` is stated once and turned off once, in
   the `prefers-reduced-motion` block at the bottom of this sheet. A drawer that still moves
   for someone who asked it not to is a drawer with the transition written in four places. */
.scrim {
  position: fixed; inset: 0; z-index: 30;
  background: var(--scrim);
  opacity: 0; visibility: hidden;
  transition: opacity var(--slide), visibility var(--slide);
}
.scrim.open { opacity: 1; visibility: visible; }
.drawer {
  position: fixed; inset: 0 0 0 auto; z-index: 40;
  width: var(--drawer-w);
  display: flex; flex-direction: column;
  background: var(--bg-raise); border-left: 1px solid var(--line);
  padding: var(--safe-t) max(var(--s2), var(--safe-r)) var(--safe-b) 0;
  /* CLOSED IS `visibility: hidden`, NOT `display: none`. A translated-away panel that is still
     `visible` keeps its buttons in the tab order, so a keyboard would walk into a drawer
     nobody opened; `display: none` would stop it sliding at all. Visibility does both, and
     transitions as a step so the panel is still drawn while it slides OUT. */
  transform: translateX(100%); visibility: hidden;
  transition: transform var(--slide), visibility var(--slide);
}
/* Visible AT ONCE on open (the slide still runs), so the filter box can take the cursor in the
   same click — a still-hidden element refuses focus (2026-10-08, measured in Chrome). */
.drawer.open { transform: translateX(0); visibility: visible; transition: transform var(--slide), visibility 0s; }
.drawer-head {
  order: 1; flex: none;
  display: flex; align-items: center; justify-content: space-between; gap: var(--s2);
  padding: var(--s2) var(--s2) var(--s2) var(--s4);
  border-bottom: 1px solid var(--line);
}
.drawer-filter { order: 2; flex: none; margin: var(--s2) var(--s2) 0 var(--s4); width: auto; padding: var(--s2) var(--s3); font-size: var(--t3); }
.drawer-title { font-family: var(--sans); font-size: var(--t1); letter-spacing: var(--track); text-transform: uppercase; color: var(--dim); }
.tree {
  order: 2; flex: 1; min-height: 0;
  overflow-y: auto; overscroll-behavior: contain;
  padding: var(--s2) var(--s2) var(--s4);
}
.drawer-note {
  order: 3; flex: none; margin: 0;
  padding: var(--s2) var(--s4) var(--s2);
  border-top: 1px solid var(--line);
  font-size: var(--t1); color: var(--dim);
}
/* A row is a row whether it opens a folder or a view — same height, same padding, same
   rhythm — because the tree is one list and only the leaves are destinations. */
.foldbtn, .viewbtn {
  display: flex; align-items: center; gap: var(--s2);
  width: 100%; min-height: var(--touch); padding: 0 var(--s2);
  background: transparent; border: 0; border-radius: var(--radius); box-shadow: none;
  font-size: var(--t3); text-align: left;
}
.foldbtn { color: var(--dim); font-weight: 600; }
.viewbtn { color: var(--ink); font-weight: 400; }
.foldbtn:hover, .viewbtn:hover { background: var(--hover); color: var(--ink); box-shadow: none; }
.viewbtn.current { color: var(--green); background: var(--green-wash); }
/* `›` points RIGHT at rest, which is "there is more inside this"; a quarter turn down is
   "and here it is". The glyph is one character rather than an SVG because the drawer already
   has 72 rows and 72 inline SVGs is a payload for a chevron. */
.chev { flex: none; width: 1rem; text-align: center; color: var(--dim); transform: rotate(90deg); transition: transform var(--slide); }
.fold.shut > .foldbtn > .chev { transform: rotate(0deg); }
.fold.shut > .foldkids { display: none; }
/* The indent is one step, and it comes with a hairline — the line IS the "this is inside
   that" the indent alone only implies once you are three deep in dev/. */
.foldkids { padding-left: var(--s3); margin-left: var(--s3); border-left: 1px solid var(--line); }
.rowname { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.count { flex: none; font-size: var(--t1); color: var(--dim); font-variant-numeric: tabular-nums; }
.treenote { padding: var(--s3) var(--s2); font-size: var(--t3); color: var(--dim); }
/* THE RECENT GROUP (2026-10-15): the last few views you opened, above the tree. */
.recentviews { padding-bottom: var(--s2); margin-bottom: var(--s2); border-bottom: 1px solid var(--line); }
.treehead { margin: var(--s2) var(--s2) var(--s1); font-size: var(--t1); letter-spacing: var(--track); text-transform: uppercase; color: var(--dim); }
.noscroll { overflow: hidden; }

/* THE VIM MODE BADGE — the one small indicator brief §"Visible state" asks for, so the operator
   can tell NORMAL from INSERT without guessing. Same letterspaced-uppercase-label language as
   `.path-head`/`.one-label`/`.bar-folder` rather than a new one: this is the same KIND of fact
   (a state, named) those already carry. NORMAL reads DIM — it is the app's resting state, not an
   alarm; INSERT reads the identity green, because it is the state in which a keystroke can
   change the file, the same fact the caret's own green already marks on the line itself.

   A FACT ABOUT THE NEXT KEYSTROKE, NOT A REPORT — which is why it survived the retirement of
   `#freshness` and the abstention register beside it (`chore/retire-the-status-line`). Those two
   narrated what the app had just done or could not tell; this states what mode the operator is
   in right now, the same kind of fact a caret's own colour already carries. */
.vim-badge {
  margin: 0 0 var(--s2); font-family: var(--mono);
  font-size: var(--t1); letter-spacing: var(--track); text-transform: uppercase; font-weight: 600;
}
.vim-badge-normal { color: var(--dim); }
.vim-badge-insert { color: var(--green); }
/* The mode is a keyboard fact (2026-10-15): on a touch screen the badge is not shown. */
@media (pointer: coarse) { .vim-badge { display: none; } }

/* THE VIM SELECTION MARK — NORMAL mode's selected line, and deliberately not the caret's own
   device. `.viewbody textarea.rawline` already marks INSERT with `caret-color` and a bottom
   hairline in the identity green; the brief is explicit that NORMAL's mark must "be clearly not
   a text caret", so this reaches for a DIFFERENT part of the row — a left edge bar, the shape a
   selected line in Obsidian's own vim-mode gutter reads as — plus a faint fill so the row is
   still findable at a glance rather than only at its edge. Zero-cost against the row invariant:
   an inset box-shadow paints inside the row's own box and a 3px inset radius change costs no
   layout, so a line gaining or losing the mark cannot be the jump `.viewbody`'s own comment
   block already refuses. */
.viewbody .vim-selected {
  /* When `j`/`k` scroll the page, keep a quarter of the screen visible past the cursor. */
  scroll-margin-block: 25vh;
  background: var(--green-wash);
  box-shadow: inset 3px 0 0 var(--green);
}

/* ══ THE PHONE ════════════════════════════════════════════════════════════════════════════
   Two adaptations, both about a hand rather than about width for its own sake.

   THE BAR SHEDS WORDS, NOT CONTROLS. The wordmark goes (the dot alone is the reduced mark —
   brand/BRAND.md names the responsive full-qntm↔reduced pair as the system) and the folder
   crumb goes, because at 320px the honest choice is to keep the view's TITLE legible rather
   than truncate two things. Every control stays and every control keeps its 44px.

   THE DRAWER'S HEAD MOVES TO THE BOTTOM. On a 6" phone held one-handed the top-right corner
   is the one place a thumb cannot reach, and that is exactly where a drawer's close button
   conventionally sits. `order` moves the whole head — title and close together, one element,
   no second control — into the thumb's arc. The list keeps the rest of the panel and scrolls
   under it. (DOM order is unchanged, so tab order still starts at the close button; a phone
   has no Tab key and the visual/keyboard order only diverges where there is no keyboard.) */
@media (max-width: 40rem) {
  /* The rail moves from the left edge to the bottom one, and the frame's reserved space moves
     with it — one swap, both halves, so a rail can never be drawn somewhere the padding did
     not make room for. */
  :root { --rail-bottom: calc(var(--rail-h) + var(--s3)); --rail-side: 0px; }
  .bar-word { display: none; }
  .bar-folder { display: none; }
  .kbd { display: none; }
  /* ══ THE DOCK (2026-10-15, design upgrade) ══════════════════════════════════════════════
     On a phone the rail is a small glass pill that FLOATS above the page, centred, clear of
     the home indicator: Views · Find · New · Cycle. It replaced a full-width bar of five
     labelled buttons that did not fit a 390px screen ("Views" ran off the right edge). Icons
     only here — every button carries its words in `aria-label` — because four labelled
     buttons in a pill are the same cramped row again. The main button, New, is the brand's
     signal dot. The page still reserves the dock's height plus its gap at the bottom, so the
     last line of a view always scrolls clear of it. */
  .rail {
    inset: auto 0 calc(var(--s3) + var(--safe-b)) 0;
    margin-inline: auto; width: max-content; height: var(--rail-h);
    align-items: center; flex-direction: row; justify-content: center; gap: var(--s2);
    padding: 0 var(--s2);
    border: 1px solid var(--line-lit); border-radius: var(--pill);
    -webkit-backdrop-filter: blur(18px) saturate(1.4); backdrop-filter: blur(18px) saturate(1.4);
    box-shadow: 0 10px 30px var(--shadow);
    transition: transform var(--slide), opacity var(--slide);
  }
  /* Reading down, it shrinks toward the bottom edge and fades back; scrolling up returns it. */
  .rail.tucked { transform: translateY(35%) scale(.82); opacity: .6; }
  /* While a line is being edited the key strip takes the dock's place at the bottom. */
  body:has(.touchbar:not(.hidden):not([data-mode="NORMAL"])) .rail { display: none; }
  body:has(.touchbar:not(.hidden):not([data-mode="NORMAL"])) .touchbar:not([data-keyboard]) { bottom: var(--safe-b); }
  .rail .railbtn { justify-content: center; padding: 0; border-radius: var(--pill); }
  .rail .railbtn > span { position: absolute; width: 1px; height: 1px; overflow: hidden; clip-path: inset(50%); white-space: nowrap; }
  .railbtn-primary {
    min-width: calc(var(--touch) + var(--s1)); min-height: calc(var(--touch) + var(--s1));
    background: var(--green); color: var(--bg);
    box-shadow: 0 0 18px var(--green-soft);
  }
  .railbtn-primary:hover { background: var(--green); color: var(--bg); }
  .railbtn-primary .ico { stroke-width: 2.25; }
  .drawer-head { order: 3; border-bottom: 0; border-top: 1px solid var(--line); }
  .tree { order: 1; }
  .drawer-filter { order: 0; }
  .drawer-note { order: 2; }
  /* A BOTTOM SHEET ON A PHONE (2026-10-15, design blueprint). It rises from the dock's edge to
     most of the screen, so every row is in reach of a thumb; the page shows above it. */
  .drawer {
    inset: auto 0 0 0; width: auto; height: min(85vh, 44rem);
    border-left: 0; border-top: 1px solid var(--line-lit);
    border-radius: var(--radius-lg) var(--radius-lg) 0 0;
    padding: var(--s2) max(var(--s2), var(--safe-r)) var(--safe-b) max(var(--s2), var(--safe-l));
    box-shadow: 0 -12px 40px var(--shadow);
    transform: translateY(100%);
  }
  .drawer.open { transform: translateY(0); }
  .drawer-filter { margin: var(--s2) var(--s2) 0; }
  .settings { place-items: end stretch; padding: 0; }
  .settings-card { width: auto; max-height: 90vh; border-radius: var(--radius-lg) var(--radius-lg) 0 0;
    padding-bottom: calc(var(--s4) + var(--safe-b)); }
  .node-page { place-items: end stretch; padding: 0; }
  .node-page-card { width: auto; max-height: 90vh; border-radius: var(--radius-lg) var(--radius-lg) 0 0;
    padding-bottom: calc(var(--s4) + var(--safe-b)); }
}

/* THE SETTLE AFFORDANCE'S OWN CLASS — app/shell/paint.ts's `settleRow`, roadmap-the-road-
   ahead.md step 3. Carries the SAME `--slide` token every other motion on this page already
   uses ("one motion, named once"), never a duration of its own: `paint.ts` writes a `transform`
   computed from real layout (the FLIP technique) and this rule is the only thing that makes the
   CHANGE to that transform ANIMATE rather than merely apply. A row this class is never put on
   until a placement is actually armed — see `SettleSurface`'s own header — so it costs nothing
   on every other repaint. */
.settle-move { transition: transform var(--slide); }

/* ══ ASKED NOT TO MOVE ════════════════════════════════════════════════════════════════════
   One block, because the motion is one token. The drawer still opens, closes, and traps
   focus; it simply arrives rather than travels. `.settle-move` joins the same block for the
   same reason: the row still relocates under reduced motion (`settleRow`'s own header — the
   admission still happens), it just arrives instead of travelling, exactly like the drawer. */
@media (prefers-reduced-motion: reduce) {
  .scrim, .drawer, .chev, button, .settle-move { transition: none; }
  /* THE PREDICT AFFORDANCE'S OWN ENTRANCE — joins the rule above rather than needing a second one,
     because it is the SAME token-driven `transition` every other motion on this page already
     turns off here. The decoration itself is UNCONDITIONAL: `paint.ts`'s `appendPrediction`
     always appends the chip; this only ever silences the rise-and-fade it arrives with, the same
     "the admission still happens, only the motion is not shown" contract `.settle-move` already
     keeps for a relocated row. */
  .row-prediction { transition: none; }
  /* The cycle toast arrives and changed lines glow without moving (2026-10-15). */
  .cycle-note, .viewbody .glow { animation: none; }
}
/* ── THE READING COLUMN: ONE ROW GEOMETRY, TWO RENDITIONS ──────────────────────────────────
   Every line of a view is one ROW, and a row is `--row` tall whichever way the line is being
   shown — the app's rendition of it, or its verbatim source in an <input> once the cursor
   arrives. That equality is the whole fix for "if I click to edit it jumps around": the painter
   repaints the WHOLE view on every focus change (app/shell/paint.ts), deliberately and
   correctly, so the only place the jump can be stopped is here, by making the two renditions
   occupy the same box.

   WHY THE GEOMETRY IS STATED ONCE AND NEVER PER KIND. The <input> the painter appends carries
   one class, `rawline`, and nothing that says which kind of line it replaced — and its text is
   set as a PROPERTY, so there is no `value` attribute for a selector to read either. A box that
   differs per kind is therefore a box the raw rendition cannot match. So the row height, the
   line box and the vertical rhythm are written once, for every child of .viewbody, and headings
   rank by SIZE and COLOUR rather than by the whitespace around them.

   WHAT THIS DOES NOT REACH: a line whose rendition WRAPS. A wrapped paragraph is two rows, an
   <input> cannot hold a second one, and no rule here can make it. That case needs the painter. */
.viewbody {
  --row: 1.75rem;      /* one line of a view, in either rendition — the body's own line box */
  --row-gap: .375rem;  /* the space BETWEEN lines — the column's, never any one line's */
  --box-gap: .625rem;  /* checkbox to words, sized so the words barely move when the source shows */
  overflow-x: auto;
  scrollbar-width: thin; scrollbar-color: var(--line-lit) transparent;
  font-size: 1.0625rem;
  line-height: var(--row);
}
/* No line owns its own vertical space. A margin on a heading is a margin the <input> that
   replaces it does not have, and that difference IS the jump. */
.viewbody > * { margin: 0; min-height: var(--row); }
.viewbody > * + * { margin-top: var(--row-gap); }
/* A prose line is painted as its own one-line markdown document, so markdown-it's block margins
   land INSIDE the row. Zeroed, with the row's own box holding the space; `flow-root` stops those
   inner margins collapsing out through the line and becoming rhythm the column never asked for. */
.viewbody > div { display: flow-root; }
.viewbody p, .viewbody ul, .viewbody ol, .viewbody li, .viewbody blockquote, .viewbody pre { margin: 0; }
/* The bullet sits where the source's `-` sits, so a list line does not slide sideways when its
   source appears. */
.viewbody ul, .viewbody ol { padding-left: 0; list-style-position: inside; }

/* THE HEADING LADDER. A view's `#` is the page's <h2> — the page owns the <h1> — so `##` becomes
   an <h3> and `###` an <h4>. That demotion is the painter's and it is right. What was missing was
   any rule below <h3>: `####` fell through to the browser default, which is BOLDER and LARGER
   than the <h3> above it, so `## Today` ranked BELOW `### Later`. The whole demoted range is
   stated here, strictly decreasing, and tests/app-view-rows.test.mjs keeps it that way.
   There is no `.viewbody h1` rule because there can be no <h1>: the painter clamps to 2..6. */
.viewbody h2, .viewbody h3, .viewbody h4, .viewbody h5, .viewbody h6 {
  font-weight: 600; line-height: var(--row);
}
.viewbody h2 { font-size: 1.375rem; font-weight: 650; color: var(--ink); letter-spacing: -.01em; }
.viewbody h3 { font-size: 1.05rem; color: var(--accent); }
.viewbody h4 { font-size: .95rem; color: var(--dim); }
.viewbody h5 { font-size: .86rem; color: var(--dim); }
.viewbody h6 { font-size: .78rem; color: var(--dim); }
/* A section heading carries a hairline under it. An inset shadow, so it costs the row no height. */
.viewbody h2, .viewbody h3 { box-shadow: inset 0 -1px 0 var(--line); }
/* Hover marks the line under the pointer. The spread fills the gap above and below and costs no
   layout; the vim selection keeps its own mark. */
.viewbody > :not(.vim-selected):not(.newline):hover { background: var(--hover); box-shadow: 0 0 0 var(--s1) var(--hover); border-radius: 2px; }

.viewbody a { color: var(--glow); }
.viewbody code { background: var(--bg-sunk); border: 1px solid var(--line); border-radius: 6px; padding: .05rem .35rem; font-size: .85em; }
.viewbody table { border-collapse: collapse; width: 100%; font-size: .9rem; }
.viewbody th, .viewbody td { border: 1px solid var(--line); padding: .35rem .55rem; text-align: left; }
/* Centred in its row, so a rule is one line like every other line. */
.viewbody hr { border: 0; border-top: 1px solid var(--line); margin: calc((var(--row) - 1px) / 2) 0; }

.viewbody .task { display: flex; gap: var(--box-gap); align-items: flex-start; padding: 0; cursor: pointer; }
.viewbody .task input {
  flex: none; width: 1rem; height: 1rem; padding: 0; margin-top: calc((var(--row) - 1rem) / 2); cursor: pointer;
  appearance: none; -webkit-appearance: none;
  border: 1.5px solid var(--faint); border-radius: 4px; background: transparent no-repeat center / 75%;
  transition: border-color .12s ease, background-color .12s ease;
}
.viewbody .task input:hover { border-color: var(--green); }
.viewbody .task input:checked {
  border-color: var(--green); background-color: var(--green);
  background-image: var(--tick);
}
/* FINISHED ROWS FADE (2026-10-15): no strike-through, so the words stay easy to read. */
.viewbody .task.done span { color: var(--faint); }
.viewbody .task.syncing { opacity: .5; }
/* Saved but not yet confirmed by the server's cycle (2026-10-07). Colour and underline only. */
/* A line `dd` cut and `p` has not put down yet. Colour only. */
.viewbody .cut { opacity: .45; text-decoration: line-through; text-decoration-color: var(--danger); }
.viewbody .task.unconfirmed span { text-decoration: underline dotted var(--dim); text-underline-offset: 3px; }
/* Declared states other than open/done (2026-10-07): still a real checkbox, marked by its status. */
/* EVERY DECLARED STATE HAS ITS OWN BOX (2026-10-08, operator report: a scheduled `[>]` line
   looked open and was ticked by mistake). Like Obsidian's: scheduled is a glowing green box
   with an arrow; waiting an amber box with a tilde; in progress half filled; cancelled a dash. */
.viewbody .task.status-scheduled input {
  border-color: var(--green); background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 12 12'%3E%3Cpath d='M4.5 3 7.5 6 4.5 9' fill='none' stroke='%233ff07f' stroke-width='1.8' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E");
  box-shadow: 0 0 6px var(--green-soft);
}
.viewbody .task.status-waiting input { border-color: var(--warn); background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 12 12'%3E%3Cpath d='M2.5 6.6c1-1.4 2-1.4 3.5-.4s2.5 1 3.5-.4' fill='none' stroke='%23f2c14e' stroke-width='1.6' stroke-linecap='round'/%3E%3C/svg%3E"); }
.viewbody .task.status-in_progress input { border-color: var(--green); background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 12 12'%3E%3Cpath d='M2 10 10 2 10 10Z' fill='%233ff07f'/%3E%3C/svg%3E"); }
.viewbody .task.status-cancelled input { background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 12 12'%3E%3Cpath d='M3 6h6' stroke='%238a948a' stroke-width='1.6' stroke-linecap='round'/%3E%3C/svg%3E"); }
.viewbody .task.status-cancelled span { color: var(--dim); text-decoration: line-through; }
.viewbody .task span { flex: 1; cursor: text; }

/* THE FOCUSED LINE — the same row, now holding its own source.
   It is a real <input> because every affordance has to be expressible as an edit to the SOURCE
   STRING (app/shell/paint.ts says why), so it is styled to BE the row rather than to look like
   a form control: the font of the line it replaced, the height of the row it sits in, no border
   and no slab of background. What marks it instead is the caret — the identity's electric green,
   the "glowing point" brand/BRAND.md names as the brand device — and a hairline under the row.
   Neither costs a pixel of layout, which is the point. No `margin` is set here: the row rhythm
   above already owns it, and re-stating it would out-specify the gap and reintroduce the jump. */
/* THE LINE WRAPS AND GROWS, IT DOES NOT SCROLL SIDEWAYS (2026-10-08, operator report: a long
   line being edited in a narrow window cut off, with the caret at the right edge). The editor is
   a <textarea> one row tall that grows to fit what it holds — `field-sizing: content` where the
   browser has it, a height set from `scrollHeight` where it does not (`lineEditor` in
   app/shell/paint.ts). `min-height` keeps a short line exactly one row. */
.viewbody textarea.rawline {
  display: block; width: 100%; min-height: var(--row); padding: 0;
  background: transparent; border: 0; border-radius: 0;
  font: inherit; line-height: var(--row); color: var(--ink);
  caret-color: var(--green);
  white-space: pre-wrap; overflow-wrap: anywhere;
  resize: none; overflow: hidden; field-sizing: content;
}
.viewbody textarea.rawline:focus { outline: none; box-shadow: inset 0 -1px 0 var(--green-hairline); }

/* THE SAME ROW, IN NORMAL — the same source characters, in a <div> rather than the <textarea>.
   It carries `rawline` too, deliberately: NORMAL and INSERT are two embodiments of ONE raw
   rendition (app/shell/paint.ts), so they must occupy the SAME box or `i` becomes the jump
   ".viewbody"'s own comment block above exists to refuse. Everything the editor rule sets that a
   div does not already get is restated here and nothing else is.

   `white-space: pre-wrap` IS WHAT A DIV DOES NOT DO BY ITSELF. The engine indents a nested line
   by four spaces and prints double spaces inside chrome cells; a div collapses both, so the raw
   rendition would silently show different characters from the source it claims to be.
   `pre-wrap` keeps every space AND wraps a long line exactly where the editor wraps it, so `i`
   on a long line moves nothing. */
.viewbody div.rawline {
  min-height: var(--row); line-height: var(--row); color: var(--ink);
  white-space: pre-wrap; overflow-wrap: anywhere;
}

/* THE BLOCK CURSOR — NORMAL mode's caret, and a terminal's own device rather than a browser's.
   A readonly <input> was the cheaper route and it was refused on evidence: WebKit and Firefox
   paint no caret in one at all (app/shell/paint.ts cites the CSSWG thread), so on the
   operator's own platform the cursor would have been invisible. A background colour is not an
   optional affordance — every engine paints it — and a block in NORMAL against the bar in INSERT
   makes the mode legible from the cursor itself.

   IT INVERTS RATHER THAN OVERLAYS, so the character under it stays readable: the identity green
   behind, the page's own background colour in front. No border, no outline, no padding — the cell
   is exactly one character wide and costs no layout, so moving it cannot move the line. */
.viewbody .vim-block {
  background: var(--green);
  color: var(--bg);
}

/* THE NEW LINE SPACE — the room below the last line, and the only reason it is here.
   "if I click after, in the new line space, nothing happens" was literally true: the column ends
   where its last row ends, so there was nothing under it to click. The painter now appends one
   empty row there (app/shell/paint.ts) and this is what makes it big enough to hit.

   `--touch` IS THE PAGE'S OWN DECLARED MINIMUM TARGET — "44px at the root font size, which is the
   minimum target this page will offer a thumb" — not a number chosen for this rule. Every other
   control on the page is sized by it and this is a control.

   IT IS EXEMPT FROM THE ROW INVARIANT, AND THE EXEMPTION IS NARROW. That invariant exists because
   the painter REPLACES a line's element when the cursor arrives, so two renditions of one line
   must occupy one box or everything below moves. This row is never a rendition of anything: it
   holds no line, it is never swapped for another element, and it is always the LAST child of the
   column — so there is nothing below it to move. `tests/present-newline.test.mjs` asserts the
   "always last" half against the real painter, which is where that claim can actually be tested;
   `tests/app-view-rows.test.mjs` carries the exemption so it is declared rather than assumed. */
.viewbody .newline { min-height: var(--touch); cursor: text; }

/* THE TAG CHIP — the `wired` rendition of the presentation cascade's `tags` key, and the ONLY
   style this stage adds. Scoped to `.viewbody .tagchip` so it cannot reach anything else on the
   page, and named `tagchip` rather than `tag` because `.tag` is already the hero tagline above.
   Whether a tag IS a chip is not decided here — that is presentation.json and app/present/. This
   rule only says what a chip looks like once the cascade has said there should be one.
   THE GREEN IS THE DECLARED ONE. #3ff07f, brand/BRAND.md section 2 ("Colour system: green
   #3ff07f · black #0a0b0a · white #e6ebe6") — read from the brand department, not sampled off a
   screenshot. It is the same green the caret above is, so both name it once.

   `inline-block` AND `line-height: 1` ARE TWO CONSTRAINTS MEETING, and neither may be dropped.

   `inline-block` is the STRIKE-THROUGH's. A `line-through` on `.task.done span` PROPAGATES to
   every descendant and a descendant cannot refuse it — `text-decoration: none` below simply does
   not work. What is exempt is an ATOMIC inline, which is what `inline-block` makes the chip. Try
   `display: inline` and the rule below stops having any effect: the line is drawn straight
   across the pill, which is the rendering fault that rule exists to prevent. Verified in a
   browser at 3x, both ways, rather than reasoned about.

   `line-height: 1` is THE ROW's. An atomic inline has to fit in the line box whole, so at the
   `1.45` this shipped with, a chipped line measured 24.0199px against 23.9986px for the same
   line without a chip. Tiny, and it is still the jump: a line whose WIRED rendition is taller
   than its raw form moves everything below it the moment the cursor lands, and it accumulates
   down a view full of tags. At `1` the chip's own box is its text's content area, which the row
   already contains by construction, and the pill's shape comes from padding and border — which
   an inline-block does have to count, so they are kept small deliberately. Measured after: a
   line with three chips is 23.9986px, the same as a line with none, to four decimal places. */
.viewbody .tagchip {
  display: inline-block;
  padding: 0;
  color: var(--tag-ink, var(--dim));
  font-family: var(--mono);
  font-size: .78em;
  line-height: 1;
  white-space: nowrap;
  vertical-align: baseline;
}
/* QUIET TAGS (design blueprint, 2026-10-15). Was a green pill on every tag. Now an area tag is
   small text in its own hue, and `#task` — on nearly every line, so it says nothing — is not
   shown. The characters are still in the source and come back when the cursor lands. */
.viewbody .tagchip.tag-task { display: none; }
.viewbody .tagchip.hue-0 { --tag-ink: var(--area-0); }
.viewbody .tagchip.hue-1 { --tag-ink: var(--area-1); }
.viewbody .tagchip.hue-2 { --tag-ink: var(--area-2); }
.viewbody .tagchip.hue-3 { --tag-ink: var(--area-3); }
.viewbody .tagchip.hue-4 { --tag-ink: var(--area-4); }
.viewbody .tagchip.hue-5 { --tag-ink: var(--area-5); }
/* A DATE AS WORDS ("starts today"), painted from `🛫 2026-10-15` (app/present/datewords.ts).
   Atomic and `line-height: 1` for the chip's two reasons above. */
/* A LINE THE LAST CYCLE CHANGED glows green and fades (2026-10-15; `glow` in app/shell/paint.ts).
   Colour only, so it costs the row nothing. */
.viewbody .glow { animation: row-glow 2.4s ease-out; }
.viewbody .datechip {
  display: inline-block;
  color: var(--dim);
  font-size: .85em;
  line-height: 1;
  white-space: nowrap;
}
/* A completed line strikes its text through. The chip is not the text — it is a token beside it,
   and a struck-through pill reads as a rendering fault rather than as "this is done". */
.viewbody .task.done span .tagchip { text-decoration: none; }
/* A `[[Title]]` LINK (app/shell/paint.ts, app/shell/links.ts): in the link colour, and a click
   opens the node. Inline text, so it costs the row nothing. */
.viewbody .linkchip {
  color: var(--green); cursor: pointer;
  text-decoration: underline; text-decoration-color: var(--green-hairline); text-underline-offset: .2em;
}

/* THE IDENTITY MARK — the `wired` rendition of the presentation cascade's `stamp` key. Where the
   engine printed `[[qntm:3]]`, the app draws one dot, and the id is on the element's `title` for
   a hover. Whether a stamp IS a mark is not decided here — that is presentation.json and
   app/present/. This rule only says what the mark looks like once the cascade has said there
   should be one.

   WHY A DOT AND NOT `display: none`. A line the model has never seen and a line it has minted a
   node for would otherwise be pixel-identical, and "has it got this yet" is the one question the
   operator asks of the page every time he presses Enter. The dot answers it in one character
   instead of twelve, so the tail of the line stops moving when the cycle catches up.
   TO HIDE IT COMPLETELY, set `display: none` on this rule. Nothing else changes and nothing is
   rebuilt — the painter still emits the span, the cascade still resolves the key, and the
   characters still come back the moment the cursor lands on the line.

   THE GREEN IS THE DECLARED ONE, at a lower alpha than the chip's: `var(--green)` at 55%.
   Dimmer because the mark is the machine's fact about the line, not the operator's — it should
   be findable and never louder than his own words.

   `inline-block` AND `line-height: 1` FOR EXACTLY THE TWO REASONS THE CHIP ABOVE STATES, and no
   third reason of its own. `inline-block` makes it atomic, which is what exempts it from the
   `line-through` a completed line propagates. `line-height: 1` keeps its box inside the row's.
   WHAT IT DELIBERATELY DOES NOT SET is `width`, `height`, `padding` or `border`: it holds ONE
   CHARACTER, so its box IS that character's content area, which the row already contains by
   construction. That is the chip's own measured argument reused rather than a fresh unmeasured
   claim about a dot of some chosen size — and it is why this rule needs no browser measurement
   where the chip's did. `font-size` shrinks the glyph only; at or below 1em with a unitless
   line-height of 1 the box cannot exceed the row. */
.viewbody .stampmark {
  display: inline-block;
  color: var(--green-soft);
  font-size: .8em;
  line-height: 1;
}
/* Same reason as the chip: an atomic inline is already exempt from an ancestor's line-through,
   and a struck-through dot would read as a rendering fault rather than as "this is done". */
.viewbody .task.done span .stampmark { text-decoration: none; }

/* THE PREDICT AFFORDANCE — THE BROWSER'S ANSWER, painted by app/shell/paint.ts's
   `appendPrediction` from what app/present/predict.ts armed. Two predictions ship on it today (the
   `🆕` created-date stamp on the row just typed, a promotion rule's retype on the row above it) and
   a third or fourth reads this same rule with no new CSS.

   THE PERCEPTION RULE: THE BROWSER IS AUTHORITATIVE UNTIL THE ENGINE DISAGREES, AND LATENCY IS
   NEVER SURFACED. This rule used to draw a dashed, accent-coloured pill on the theory that a
   prediction reading as "provisional" would stop it "masquerading as the fact it is standing in
   for." That argument was made before the browser's own predictions were measured against the
   engine — `docs/implementation-artifacts/design-the-two-rules.md` §5: 2,941 membership cells and
   0 disagreements, ordering and rules likewise 0 on what could be checked. The browser is not
   guessing here; it is computing the same answer the engine computes, ahead of the engine's own
   14-second return. A dashed border was latency surfaced as a border style — the same anti-pattern
   the status line was deleted for — so a prediction now renders EXACTLY as the row's own ordinary
   characters: no pill, no background, no border, no accent colour of its own. If the engine's real
   answer ever disagrees, `predict.ts`'s own withdrawal (below) is the one correction the perception
   rule allows to be visible — that is where a disagreement is shown, never here, on the row while
   it is still merely pending.

   `inline-block` AND `line-height: 1` REMAIN for the same measured reason the chip and the mark
   state: an atomic inline whose box is its own content area cannot make a row taller than the row
   invariant this whole reading column enforces (`.viewbody`'s own header comment) — dropping the
   pill's chrome does not drop that constraint, because the row-height guarantee was never about
   the border. `white-space: nowrap` for the same reason the chip sets it — `.viewbody` scrolls
   horizontally rather than wrapping a row into two, and a multi-word promotion phrase must not
   become the first rendition that does. */
.viewbody .row-prediction {
  display: inline-block;
  margin-left: .3em;
  color: inherit;
  font: inherit;
  line-height: 1;
  white-space: nowrap;
  vertical-align: baseline;
  /* THE ONE-SHOT ARRIVAL, AS A `transition`, NOT AN `animation` — this page's stylesheet may
     declare `@media` and nothing else (tests/app-view-rows.test.mjs's own reader refuses every
     other at-rule, `qmark.css` is where a component's `@keyframes` belongs), so the rise-and-fade
     a NEWLY APPEARING run of text gets is driven the same way `settleRow`'s own FLIP motion is
     (`paint.ts`): `appendPrediction` sets `opacity`/`transform` inline to a START state with
     `transition: none`, then — one frame later, the identical `requestAnimationFrame`-or-
     synchronous-fallback `settleRow` already uses — clears them, and THIS transition is what
     animates the change back to the stylesheet's own resting values. This is content arriving
     smoothly, not a status being reported — the same distinction `settleRow`'s own motion draws
     for position — and it costs nothing on any repaint that is not an arrival, because a repaint
     that is not one never sets the inline start state at all. */
  transition: opacity var(--slide), transform var(--slide);
}
/* A completed line strikes its text through — the chip/mark's own rule, restated for the same
   reason: an atomic inline is already exempt, and a struck-through claim would read as a rendering
   fault rather than as "this task is done". */
.viewbody .task.done span .row-prediction { text-decoration: none; }
/* THE WITHDRAWAL — the one correction the perception rule allows to be visible for CONTENT, the
   same tier as `settleRow`'s own motion for POSITION: `predict.ts`'s one-shot reconciliation
   discovered this repaint that the claim's text is nowhere in what the engine actually sent back.
   Struck through and dimmed rather than simply removed, so "this did not happen" is a fact the
   operator is TOLD, not one he has to infer from text quietly not reappearing — see this rule's own
   header, and predict.ts's, for why silence is reserved for the OPPOSITE case (a confirmed claim,
   now indistinguishable from the row's own ordinary characters, per `.row-prediction` above).
   Shown for exactly one repaint, by construction — `predict.ts`'s `take` clears the arm the instant
   it reports a withdrawal, so nothing here has to time out. NO BORDER, NO BACKGROUND, for the same
   reason `.row-prediction` carries none: the strike-through and the dim colour are the correction,
   and a pill around it would be exactly the "latency surfaced as a border style" this rule exists
   to avoid saying twice. */
.viewbody .row-prediction-withdrawn {
  color: var(--dim);
  text-decoration: line-through;
  opacity: .7;
}
