/* One look for every page of the web sower, in two themes.
 *
 * Every page is the same structure: the wordmark above a card of rows --
 * headings, key-values, links, inputs, actions -- driven by keyboard and
 * touch alike. Grove is the designed theme: a forest field with the rows
 * on a pane of warm glass, sans for words and monospace for stamps.
 * Terminal is the opposite promise -- flat, monospaced, one font size,
 * heading marks drawn in characters. Everything below is written against
 * the tokens, so a theme is a short list of structural overrides at the
 * foot of this file, never a second stylesheet to keep in step.
 *
 * The tokens below are the app's design, not grove's: one semantic palette
 * and one set of measures, both themes wearing them, with terminal's neutral
 * canvas and card declared beside them. A theme block sets tokens and states
 * its type; it may not name a colour or write a measure. Two designs are two
 * things to keep in step, and they did not stay in step.
 */

:root {
  --ink: #0e1712;
  --terminal-ink: #151515;

  /* Glass, not paint: a pale film the field shows through. */
  --surface: rgb(216 234 218 / 7.5%);
  --terminal-surface: #333;
  --surface-line: rgb(232 240 228 / 15%);
  --surface-lip: rgb(255 255 255 / 8%);
  --surface-blur: blur(10px);
  --radius: 1.4rem;

  /* Lifted a quarter turn of saturation on 2026-08-02 -- "the theme is
     very pale ... make the colors pop a little" -- hue and lightness
     kept, so the earth reads the same, just less washed. The mint is the
     one exception, put back to its unlifted value the same day: it is the
     colour the headings wore in the first grove, and the wordmark is
     asked to be that colour and not a brighter cousin of it. */
  --bone: #f5edd9;
  --ash: #a6997b;
  --pollen: #f6de96;
  --bark: #e0aa5f;
  --live: #7fe8ac;
  --rule: rgb(246 222 150 / 20%);
  --alarm: #eb9a82;

  /* The role ladder, read top to bottom as the thing it describes: the
     wordmark is the bloom, alive and the only mint on the page (--live,
     one colour whatever the network says -- only trouble gets a marker),
     and every heading under it steps further down into earth -- pollen,
     bark, then a brown with most of the colour gone out of it. Metadata
     is below all of them, so the last heading has to stay clear of ash:
     a subheading the tone of a timestamp is a rung nobody reads as a
     rung. */
  --h1: var(--pollen);
  --h2: var(--bark);
  --h3: #bf9059;
  --text: var(--bone);
  --dim: var(--ash);

  /* Two reds, kept apart so each stays legible. --fail is news, said
     bright: a refused form, a page that never arrived, a capture the
     server would not take. --danger is a control that loses something --
     a Remove, a Cancel of stashed work -- said quietly, so the pill
     warns without shouting and the bright red keeps meaning something
     went wrong. */
  --fail: #e8594a;
  --danger: #e07666;
  --hover: rgb(246 222 150 / 7%);
  --select: rgb(246 222 150 / 16%);

  /* The solid tone an autofilled field is painted back to: the glass film
     flattened onto the ink, since the inset-shadow trick cannot carry
     transparency. */
  --field: #1a231d;

  /* Two voices: sans for headings and normal text, monospace for metadata
     and ids. */
  --face-text: system-ui, sans-serif;
  --face-stamp: ui-monospace, "SF Mono", menlo, monospace;

  /* The size ladder, one step per rank, and the rule that orders it: every
     heading is larger than normal text, and normal text is larger than
     metadata. Written as tokens so a size is chosen once and named where it
     is used -- a page picking 0.9rem because it looked right is how two rows
     of the same rank end up different sizes. Terminal flattens the lot to one
     size, where hierarchy is blank lines and the marks instead. */
  --size-h1: 1.4rem;
  --size-h2: 1.15rem;
  --size-h3: 1.05rem;
  --size-text: 0.95rem;
  --size-meta: 0.85rem;

  /* Three sizes outside the ladder: the wordmark's display size and the
     frame's two whispers, the liveness line and Back. Tokens for the same
     reason the ladder is: terminal flattens every size to one, and a raw
     measure in a rule below is a size that flattening cannot reach --
     which is exactly how the offline line stayed small there. */
  --size-wordmark: 1.6rem;
  --size-link-state: 0.68rem;
  --size-back: 0.8rem;

  /* A group is a heading and what belongs under it. The clearance above a
     heading is what separates one group from the last; the gap under it is
     nearly nothing, so its content reads as attached rather than as the next
     line down. One pair of measures for every heading on every page. */
  --group-gap: 1.25rem;
  --group-tight: 0.3rem;

  /* And the clearance from a shut fold to the next heading: a hair. A
     run of shut folds is a list of questions, not a stack of sections,
     and the air a page's sections do get stands after content rather
     than around headings (`.fold-part + h2.fold` below). The capture page
     went from two folds to six on the phone walk, and six section
     clearances was "that ridiculous amount of space between
     subheadings". A hair was too little in the other direction -- "a
     little more spacing between folded subheadings" -- so it is a third
     of a section's own gap: enough that a run of them reads as a run of
     rows rather than one block. */
  --fold-gap: 0.45rem;

  /* And what a subheading inside a fold takes above it, which is less
     again: it is part of the section it stands in, and the gap is what
     says so ("a little less between subheading content and
     subsubheading"). */
  --fold-inner-gap: 0.25rem;

  /* The air under a page's title, whatever comes first beneath it: wider
     than a heading to its own content, narrower than the gap between two
     sections -- one measure, so no two pages open on different rhythms. */
  --title-air: 1rem;

  /* The register form's own key column, the one list of fields that is
     never a .kv: its rows are inputs, not stored values, so there is
     nothing here for a list to size a key column to but this. Wide enough
     for the longest label anywhere, the register form's "repeat
     password*". */
  --key-column: 8.5rem;

  /* The room a row's line is never squeezed out of: three glyphs and the
     three dots that say the rest is there, which is the shortest thing a
     cut line can say at all (format.js floorLine).

     A row's line shares its tracks with labels the app writes -- the kind
     column, the queue's outcome -- and those are sized to their own words
     while the line takes what is left. That is the right
     way round only while there is something left: a track sized to its
     content is maximized before a `1fr` is expanded, so the label helps
     itself first however little it leaves. It cost nothing while a kind
     was `link` or `jot`; `event reasoning` on the remove consent took
     125px of 209 and left every line on the page 25px, which is under a
     character and a half, so each of them gave up and went blank
     (format.js floorLine, correctly). The floor is what makes the labels
     compete for what is over rather than the line. */
  --line-floor: 6ch;

  /* The measures a theme is allowed to re-cut, and the whole of them.
     Grove spaces in its own proportions and terminal counts whole rows,
     so the two genuinely differ here -- but they differ in a number, not
     in a rule: every measure below is written into exactly one rule, and
     a theme sets the token. A theme that wrote `padding` of its own would
     be a second rule to keep in step, which is how the card's padding,
     the row gutter, the paragraph gap and the blank line all drifted
     apart before. */
  --card-pad-y: 1.4rem;
  --card-pad-x: 1.5rem;
  --card-shadow: 0 24px 64px rgb(0 0 0 / 50%), inset 0 1px 0 var(--surface-lip);
  --row-gutter: 0.6rem;
  --row-pad-y: 0.3rem;
  --heading-row-pad: 0.2rem;

  /* Half of what a 44px tap target still needs once the text's own line is
     taken out, split above and below it -- never less than nothing, for a
     line already tall enough on its own. Font size is not this rule's to
     change: a target grows by the room around the words, not the words. */
  --tap-pad: max(0px, calc((44px - 1lh) / 2));

  /* Back is chrome above the card's own heading. Grove paints the link as
     a pill, so the link carries the gutter; terminal paints the whole row
     as a bar, so the row does -- one gutter, two places it can sit. */
  --back-lift: -0.35rem;
  --back-gap: var(--group-tight);
  --back-pad-x: 0;
  --back-link-pad-x: var(--row-gutter);

  /* The air a paragraph opens with, the consent pair's own, the gap in a
     menu of heading-links, the hair under a day, and the blank line drawn
     before sign-out. */
  --para-gap: 0.75rem;
  --acts-gap: 1.1rem;
  --menu-gap: var(--group-tight);

  /* The air between a heading's words and whatever rides beside them --
     the waiting count, the day's relative word, the update mark -- and,
     in terminal, between the heading's mark and its words. One measure
     for every rank, so the day line and a section heading cannot open on
     different rhythms. */
  --heading-gap: 0.5rem;
  --day-gap: 0.15rem;
  --blank-line: 1.4rem;

  /* Both themes are dark; saying so keeps the browser's own furniture --
     autofill tints, scrollbars, pickers -- from arriving styled for a
     white page. */
  color-scheme: dark;
}

* {
  box-sizing: border-box;
}

/* A form groups its fields; it is never a box of its own. Rows bleed
   into the card's padding for the selection band, and a form that made a
   content-width box of itself would turn that designed bleed into a
   sideways overflow of the one element nothing ever styles. */
form {
  display: contents;
}

/* Buttons are rows and controls here, never chrome: the browser's own
   padding and border would push their text off the shared left gutter. */
button {
  padding: 0;
  font: inherit;
  color: inherit;
  background: none;
  border: 0;
  text-align: left;
  cursor: pointer;
}

/* The hidden attribute always wins: an element's own display rule must
   never resurrect what a page hid. */
[hidden] {
  display: none !important;
}

/* Held by the root as well as the body, so the frame around a short page --
   and the moment before the stylesheet's body rules apply -- is never the
   browser's default white. */
html {
  background: var(--ink);
}

/* The browser's own pull-to-refresh, chrome and reload alike, would
   otherwise fire alongside pull.js's own drag handling every time a list
   is pulled from the top. On html and body both: the spec reads this off
   the root, but Chrome propagates it from body -- measured on the
   emulator, where the rule on html alone left native pull-to-refresh
   reloading the page. */
html,
body {
  overscroll-behavior-y: contain;
}

/* Top-anchored, not centered: the wordmark sits at one height and the
   card's top edge never moves between pages, so a control in the card's
   frame -- Back above every heading -- stays under the pointer across
   repeated presses. Centering is a landing-page idiom; an app you walk
   through holds still. */
body {
  font: var(--size-text) / 1.55 var(--face-text);
  color: var(--text);
  margin: 0;

  /* svh, not dvh: dvh tracks the viewport as the phone's address bar
     retracts, so a full-height page overflows by exactly the bar's height
     and every screen picks up a short, pointless scroll. */
  min-height: 100svh;
  display: grid;
  grid-template-rows: auto auto auto 1fr;
  place-items: start center;
  padding: 0 1.25rem 2.5rem;
}

/* The field is painted once on a fixed layer: a single composition scaled
   to the viewport, shaped by a vignette wide enough to be felt, not seen. */
body::before {
  content: "";
  position: fixed;
  inset: 0;
  z-index: -1;
  background:
    url("data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' width='220' height='220'><filter id='g'><feTurbulence type='fractalNoise' baseFrequency='0.85' numOctaves='3'/></filter><rect width='220' height='220' filter='url(%23g)' opacity='0.05'/></svg>"),
    url("data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' width='1600' height='1000' fill='none' stroke='%23ecd9a0' stroke-opacity='0.07' stroke-width='1.2'><path d='M-40 1010 C160 900 250 730 340 560 C410 430 470 300 500 120'/><path d='M340 560 C260 490 170 470 60 465'/><path d='M340 560 C300 640 250 690 175 730'/><path d='M500 120 C540 60 600 30 680 -10'/><path d='M500 120 C440 80 400 40 380 -20'/><path d='M240 1010 C330 880 380 740 430 600'/><path d='M430 600 C520 570 570 520 610 450'/><path d='M430 600 C380 545 330 515 265 500'/><path d='M610 450 C640 380 640 310 615 235'/><path d='M560 1020 C600 900 640 820 660 720'/><path d='M660 720 C700 690 740 670 790 660'/><path d='M660 720 C620 780 600 850 600 930'/><path d='M760 1030 C800 880 870 800 890 660 C910 530 875 420 900 290'/><path d='M890 660 C990 620 1050 560 1090 480'/><path d='M890 660 C790 640 730 580 690 500'/><path d='M900 290 C940 220 990 180 1050 150'/><path d='M900 290 C850 230 820 170 815 90'/><path d='M1090 480 C1150 440 1215 425 1290 425'/><path d='M1240 1020 C1200 870 1130 790 1120 650 C1110 520 1150 420 1130 300'/><path d='M1120 650 C1040 600 990 540 960 470'/><path d='M1130 300 C1080 250 1050 195 1040 130'/><path d='M1500 1020 C1440 900 1380 820 1360 700'/><path d='M1360 700 C1420 650 1470 620 1540 600'/><path d='M1360 700 C1300 745 1265 800 1250 870'/><path d='M1640 1030 C1420 940 1310 800 1230 640 C1160 500 1130 340 1155 150'/><path d='M1230 640 C1340 570 1440 550 1545 535'/><path d='M1155 150 C1200 90 1270 60 1350 30'/><path d='M60 465 C40 400 45 330 75 260'/><path d='M175 730 C120 760 75 805 45 865'/><path d='M1545 535 C1580 470 1590 400 1575 325'/><g fill='%23ecd9a0' stroke='none'><circle cx='340' cy='560' r='3.6' fill-opacity='0.13'/><circle cx='890' cy='660' r='3.6' fill-opacity='0.13'/><circle cx='1230' cy='640' r='3.6' fill-opacity='0.13'/><circle cx='1120' cy='650' r='3' fill-opacity='0.12'/><circle cx='430' cy='600' r='3' fill-opacity='0.12'/><circle cx='1090' cy='480' r='3' fill-opacity='0.12'/><circle cx='660' cy='720' r='2.6' fill-opacity='0.11'/><circle cx='1360' cy='700' r='2.6' fill-opacity='0.11'/><circle cx='900' cy='290' r='2.6' fill-opacity='0.11'/><circle cx='610' cy='450' r='2.6' fill-opacity='0.11'/><circle cx='1130' cy='300' r='2.6' fill-opacity='0.11'/><circle cx='500' cy='120' r='2.4' fill-opacity='0.1'/><circle cx='1155' cy='150' r='2.4' fill-opacity='0.1'/><circle cx='175' cy='730' r='2.4' fill-opacity='0.1'/><circle cx='60' cy='465' r='2.2' fill-opacity='0.1'/><circle cx='1545' cy='535' r='2.2' fill-opacity='0.1'/><circle cx='690' cy='500' r='2.2' fill-opacity='0.1'/><circle cx='960' cy='470' r='2.2' fill-opacity='0.1'/><circle cx='265' cy='500' r='2.2' fill-opacity='0.1'/><circle cx='1290' cy='425' r='2.2' fill-opacity='0.1'/><circle cx='1540' cy='600' r='2' fill-opacity='0.09'/><circle cx='790' cy='660' r='2' fill-opacity='0.09'/><circle cx='600' cy='930' r='2' fill-opacity='0.09'/><circle cx='1250' cy='870' r='2' fill-opacity='0.09'/></g></svg>")
      center center / cover no-repeat,
    radial-gradient(
      170% 140% at 50% 48%,
      transparent 62%,
      rgb(0 0 0 / 42%) 100%
    ),
    var(--ink);
}

/* Basically flush with the top, just some padding -- and one constant,
   nothing of the viewport in it: the card's top edge is the same distance
   down on every page and every screen. The zone's height is fixed at the
   taller theme's wordmark and the mark bottom-aligns inside it, so
   switching theme moves where the logo starts and never where the card
   does. test_card_position.py pins the sum. */
header {
  display: flex;
  flex-direction: column;
  justify-content: flex-end;
  align-items: center;
  height: 4.6rem;
  margin: 1.25rem 0 1.15rem;
  text-align: center;
}

/* The ever-present title, in the live green whatever the network says:
   online is the expected state and says nothing, so the liveness story
   is the offline marker's alone (#link-state, shown only in trouble).
   No page repeats the title as a heading. */
.wordmark {
  display: inline-block;
  color: var(--live);
  font-size: var(--size-wordmark);
  font-weight: 300;
  letter-spacing: 0.42em;
  text-indent: 0.42em;
  text-decoration: underline;
  text-decoration-thickness: 1px;
  text-underline-offset: 0.3em;
}

.wordmark svg {
  display: block;
  width: 100%;
  height: 1.9rem;
  margin-top: 0.15rem;
  overflow: visible;

  /* The drawing wears the word's own colour -- currentColor, so the
     logo and the title cannot drift apart -- softened to a half voice
     so the roots sit under the word rather than beside it. */
  stroke: currentcolor;
  stroke-opacity: 0.5;
  stroke-width: 1.1;
  fill: none;
}

.wordmark svg circle {
  fill: currentcolor;
  stroke: none;
}

/* Green is the system confirming right now -- the liveness word here, the
   copied mark on an invite, the update download filling -- and never
   decoration or emphasis. The word sits right under the card, at the end
   of the page it describes -- and only once there is something to say:
   the line is hidden until the probe answers, since a word that has to be
   corrected a second later is worse than no word at all. */
#link-state {
  margin: 0.8rem 0 0;
  color: var(--dim);
  font-size: var(--size-link-state);
  letter-spacing: 0.22em;
  text-transform: uppercase;
}

/* The dot takes the word's colour rather than its own, so the state is
   declared once and the two halves of it cannot drift apart. */
.link-dot {
  display: inline-block;
  width: 0.4rem;
  height: 0.4rem;
  margin-right: 0.55rem;
  vertical-align: 0.08em;
  background: currentcolor;
  border-radius: 50%;
}

/* The way back: part of the card's frame, not its content. A chevron-led
   label tucked into the top-left, dim until reached for, with a full line
   of air before the page's own heading opens the content. The hover and
   selection paint the pill around the control, not a full-width bar --
   background across the whole row would make wayfinding shout. */

/* Element-qualified against the generic .row box declared later: the pill
   carries the inset itself, so the row box must not add a second one. */
p.back-row {
  display: flex;
  justify-content: space-between;
  align-items: center;

  /* Chrome above, the page's own heading below: the same tight gap a
     heading keeps to its own content, not the wide one that opens a
     section -- Back is frame, not a section the heading follows. */
  margin: var(--back-lift) calc(-1 * var(--row-gutter)) var(--back-gap);
  padding: 0 var(--back-pad-x);
  font-size: var(--size-back);
}

/* The keybindings link, at the row's other end: Back leads, this trails,
   and the pill either wears is the same one Back's own hover/selected paint
   already draws (.back-row a). */
.keys-link {
  font-family: var(--face-stamp);

  /* Trails the row on its own, not just beside Back's space-between: the
     front page carries this link with no Back beside it to be pushed by. */
  margin-left: auto;
}

/* The pill's inset matches every row's, so the control begins on the same
   column as every heading: the chevron leads in the flow of the line, the
   word after it -- an edge hanging left of the headings' own reads as a
   misaligned card corner, not as wayfinding.

   Element-qualified for the same reason the row box below is: `.page a`
   takes every link back to `inherit` further down the file, at the same
   specificity, so an unqualified rule here loses on order alone -- which
   is how Back and the keys reference came to paint in the text's own
   white. They are frame rather than content, and frame is metadata. */
p.back-row a {
  display: inline-block;
  padding: var(--tap-pad) var(--back-link-pad-x);

  /* A 44px tap height without a 44px row: the padding above reaches it,
     and the margin here gives back exactly what that padding added, so
     Back and the keys link press at their own true size while the
     heading below still opens where it always has. */
  margin-block: calc(var(--row-pad-y) - var(--tap-pad));
  border-radius: calc(var(--radius) / 2);
  color: var(--dim);
  transition:
    color 0.15s ease,
    background 0.15s ease;
}

.back-row svg {
  width: 0.5em;
  height: 0.85em;
  margin-right: 0.3em;
  vertical-align: -0.05em;
  fill: none;
  stroke: currentcolor;
  stroke-width: 1.6;
  stroke-linecap: round;
  stroke-linejoin: round;
}

/* Element-qualified: the generic .row.selected and .row:hover live later
   in the file, and the frame's control must beat the content rows' bar. */
p.back-row.selected,
p.back-row:hover {
  background: none;
}

/* Back and the keys link paint one at a time, whichever rows.js says is
   current -- never both, since they are two independent things sharing
   this row for layout, not one selection to highlight as a whole. */
.back-row .back-link.selected,
.back-row .keys-link.selected {
  background: var(--select);
  color: var(--text);
}

@media (hover: hover) {
  p.back-row a:hover {
    background: var(--hover);
    color: var(--text);
  }
}

body.offline #link-state {
  color: var(--alarm);
}

body.online #link-state {
  color: var(--live);
}

/* A device the server will not authorise: online, and still not getting
   through. Trouble's colour whatever the route is doing -- the green that
   means "answering right now" over the words "sign in again" would read as
   the line disagreeing with itself. After the rule it overrides, which is
   how this stylesheet settles two that could both apply. */
body.unauthorized #link-state {
  color: var(--alarm);
}

/* The whole line is the press, so the link takes the line's colour rather
   than a link's own; the underline stays, since that is what says a thing
   can be pressed. */
#link-state a {
  color: inherit;
}

/* ------------------------------------------------------------------ */

/* The page: one card of rows, every screen the same pane at the same
   width, so moving between pages moves the contents and nothing else. */

.page {
  width: 100%;

  /* Eighty monospace characters, and the padding around them.
     Measured rather than reckoned, and terminal is the case that decides:
     every line there is the stamp face at 0.94rem, the widest a monospace
     line gets in either theme, and that theme's padding is one line plus one
     character each side. So the room needed is eighty-two characters and
     3rem -- 49.5rem on this machine's ui-monospace -- and the half rem over
     it is headroom for a machine whose resolves wider. One width for both
     themes, because switching theme must not resize the card.
     A phone is narrower than this cap and takes the width above instead,
     which is why raising it leaves the layout it was drawn against alone. */
  max-width: 50rem;

  /* A grid item refuses to shrink below its content by default, so one
     unbreakable line inside would push the card past a phone's edge. */
  min-width: 0;
  padding: var(--card-pad-y) var(--card-pad-x);
  background: var(--surface);
  border: 1px solid var(--surface-line);
  border-radius: var(--radius);
  box-shadow: var(--card-shadow);
  backdrop-filter: var(--surface-blur);
}

/* The heading ladder: one colour and one size per level, and one pair of
   measures around every one of them -- clearance above, near nothing below --
   so a heading and its content read as one group and the next group starts
   where the space does. */

/* A heading is one line: it names, it does not paragraph. The one line
   long enough to overflow -- the waiting count -- is cut by measure to
   ASCII dots (queue.js); the clip here is only the backstop for the
   frame before a cut runs.

   Laid out as a line of parts, so whatever rides beside the words -- the
   waiting count, the day's relative word, the update mark, and in
   terminal the heading's own mark -- is separated by one measure rather
   than by whitespace somebody wrote into the content. A heading with
   nothing beside its words is unchanged by this: one part, no gap to
   apply. That whitespace was the bug twice over -- trimmed away by flex
   ("##to sow"), then counted twice when the gap arrived beside it
   ("##  2026-08-02"). */
.page h1,
.page h2,
.page h3 {
  display: flex;
  align-items: baseline;
  gap: var(--heading-gap);
  overflow: hidden;
  white-space: nowrap;
}

.page h1 {
  margin: 0 0 var(--title-air);
  color: var(--h1);
  font-size: var(--size-h1);
  font-weight: 400;
  letter-spacing: 0.04em;
}

/* A title carrying a capture's stamp (node.js entitleNode) may take a
   second line, where a bare heading never wraps: "Image - 2026-08-28
   19:55" is wider than a 300px card, and every other way out is worse.
   Cutting it would drop the minute that says which capture this is;
   ellipsis is the unicode glyph the lint rule refuses and terminal never
   meets; a width to hide it below is a number that is wrong on some
   phone. Two lines is none of those. */
.page h1.stamped {
  display: block;
  white-space: normal;
}

/* The stamp is a fact about the page, not the page's name: dim, and at
   the size of every other stamp, so the kind stays the title. */
.page h1 .stamp {
  font-size: var(--size-meta);
  letter-spacing: 0;
}

.page h2 {
  margin: var(--group-gap) 0 var(--group-tight);
  color: var(--h2);
  font-size: var(--size-h2);
  font-weight: 400;
  letter-spacing: 0.03em;
}

/* A run of heading-links -- the front's sections -- reads as a menu, and
   a menu's rows sit close: the wide clearance is for a heading opening a
   section of content, not for the next line of the same list. */
.page h2.row + h2.row {
  margin-top: var(--menu-gap);
}

/* Where the air between folds goes: after content, not around headings.

   The rhythm every typography reference agrees on -- space above a
   heading about twice the paragraph gap, space below it the paragraph gap
   itself, so a heading reads as belonging to what follows and separate
   from what came before. A fold's heading keeps the tight gap to its own
   body (`.page h2 + *`), and the wide one stands between a body and the
   next heading: "a paragraph under a subheading is clearly separate from
   the next subheading".

   A shut fold has no body on the screen, so the next heading stands
   nearer it (--fold-gap) and a run of shut folds reads as the list of
   questions it is. A hidden body is still the sibling between them, which
   is what lets the stylesheet tell the two apart. */
.page .fold-part:not([hidden]) + h2.fold {
  margin-top: var(--group-gap);
}

.page .fold-part[hidden] + h2.fold,
.page .fold-part[hidden] + h3.fold {
  margin-top: var(--fold-gap);
}

/* A subheading inside a fold belongs to it, and has to look like it: the
   Reasoning under Tags stood as far from the tag line above it as the
   next section's own heading did, so nothing said which of the two it
   was part of -- "it's not clear that reasoning subsubheading is a part
   of tags subheading". It takes the tighter gap, which is the same
   statement the rank already makes.

   By containment and not by what precedes it: the Reasoning heading
   follows the line of tags, which is a paragraph rather than a fold's
   own body, so an adjacency rule matched nothing at all. What makes it a
   subheading is the fold it stands inside. */
.page .fold-part > h3.fold {
  margin-top: var(--fold-inner-gap);
}

.page h3 {
  margin: var(--group-gap) 0 var(--group-tight);
  color: var(--h3);
  font-size: var(--size-h3);
  font-weight: 400;
}

.page h1:first-child,
.page h2:first-child {
  margin-top: 0;
}

/* Every link is underlined, and underline marks nothing else -- that is
   how an actionable row is told from a static one. A heading that is a
   link keeps its rank's colour. */
.page a {
  color: inherit;
  text-decoration: underline;
  text-decoration-thickness: 1px;
  text-underline-offset: 0.22em;
}

/* Keyboard focus keeps a mark, and it is the one the page already uses.
   The outline was turned off because this app moves its own selection with
   the keys and paints it as a pill, and two marks disagreeing is worse than
   one -- but turning it off left anybody arriving by Tab with no idea where
   they were, which is every reader on a keyboard who has not learnt j and
   k. The same pill answers both, so there is still one mark, whichever way
   the reader got here. */
.page a:focus-visible {
  outline: none;
  border-radius: calc(var(--radius) / 2);
  background: var(--select);
  color: var(--text);
}

/* A row: the unit the selection moves over. Selection is colour only --
   nothing shifts, indents, or reflows -- and hover is the same statement,
   quieter, on the pointers that can make it. */
.row {
  position: relative;
  margin: 0 calc(-1 * var(--row-gutter));
  padding: var(--row-pad-y) var(--row-gutter);
  border-radius: calc(var(--radius) / 2);
  transition: background 0.15s ease;
}

@media (hover: hover) {
  .row:hover {
    background: var(--hover);
  }
}

.row.selected {
  background: var(--select);
}

/* The mark and the target are one thing. Wherever the selection paints the
   whole bar, a press anywhere in that bar opens the row -- the empty space
   to the right of the words included, which on a list row is most of it.
   A .plain row and the back pill paint their own words instead, so their
   links stay the size of what they highlight and stay out of this. A
   folding subheading holds no link at all (format.js headingFold), so it
   has nothing here to exclude. */
li.row > a::after,
.page h2.row > a::after,
.page h3.row > a::after {
  content: "";
  position: absolute;
  inset: 0;
}

/* A row that is a line of prose rather than an entry in a list. Every other
   row is a heading or a list item, so a filled pill reads as one; under a
   heading it reads as a second heading. This one takes the selection in its
   own text and leaves the background where it found it. */

/* A paragraph's leading, because that is what this row is. A heading with
   its text flush beneath reads as a caption on the heading, and the same
   heading elsewhere on the page is followed by a line of air. */
.row.plain {
  margin-top: var(--para-gap);
}

/* The sheet's verdict, the events it grew, and its tags are rows so that
   the keyboard walk can reach the links in them (sow.js showSaved,
   showEvents, showTerms), never because they are one of a list: they
   stand among the sheet's own paragraphs and keep their left edge. A
   .plain row highlights its links rather than its bar, so the gutter
   given up here was carrying nothing. */
#result.row,
#analysis.row,
#events .row {
  margin-inline: 0;
  padding-inline: 0;
}

/* What a subheading opened sits directly under it, whatever kind of thing
   it is: a paragraph, a list of rows, a block of fields, a plain row. A
   heading and the one answer it introduces are one group, and a line of
   air between them reads as the heading belonging to nothing.

   Stated once over everything rather than per element, because the list of
   elements is what went wrong: `.row.plain` was named, `.content` was not,
   and the node page grew a blank line under half its subheadings and none
   under the other half. A rule with no list cannot be forgotten. The runs
   that are genuinely not a heading-and-its-answer -- a menu of heading
   links, a day over its rows -- name themselves and win on specificity. */
.page h1 + *,
.page h2 + *,
.page h3 + * {
  margin-top: var(--group-tight);
}

/* Except a subheading: a heading is never another heading's answer. A
   section heading directly over a subheading -- Settings' "App settings"
   over "Theme" -- opens a group whose first member is itself a group, and
   two headings flush together read as one broken line. The subheading
   keeps the clearance every heading has, so the section opens with air.
   The title keeps its own measure (--title-air) over whatever comes
   first, subheading included, so it is not in this list. */
.page h2 + h3 {
  margin-top: var(--group-gap);
}

.row.plain.selected {
  background: none;
}

/* Hover is a preview of the same mark, on the pointers that can make it --
   guarded the way every other row's hover is (.row above), or a touch
   that cannot leave the pointer over anything left the mark standing on
   whatever it last tapped, with no press there to explain it. */
@media (hover: hover) {
  .row.plain:hover {
    background: none;
  }
}

/* Its link is the colour of the prose it sits in, not of the headings above
   it: a gold underlined line under a gold underlined heading reads as a
   second heading however wide the paint is. */
.row.plain a {
  color: var(--text);

  /* A 44px tap height, the same rule button.action keeps: padding, not
     the words' own size. Inline padding does not move a neighbouring
     line -- only the pressable area around the words grows. */
  padding-block: var(--tap-pad);
}

/* The selection is a mark on the words, sized to them. The ring is drawn
   with a shadow rather than padding so marking a line never moves it.
   Whatever the control is: a link on a node's content row, a button where
   the choice is the app's own rather than somewhere to go (Settings).

   Keyed off the control itself rather than the row: a row can carry more
   than one -- the made-from line's recipe and its prompt, a line of tags
   -- and marking the whole row lit every one of them together, on a
   hover meant for one and a walk that had already told them apart.

   A wrapped mark is drawn once per line rather than once around the whole
   run of them: without it, the ring above traced the box a single-line
   mark would have made, cutting a notch out of a line that wrapped short
   of it (walk finding #50). */
.row.plain :is(a, button).selected {
  background: var(--select);
  border-radius: 0.15rem;
  box-shadow: 0 0 0 0.2rem var(--select);
  box-decoration-break: clone;
}

@media (hover: hover) {
  .row.plain :is(a, button):hover {
    background: var(--select);
    border-radius: 0.15rem;
    box-shadow: 0 0 0 0.2rem var(--select);
    box-decoration-break: clone;
  }
}

/* The page's last row carries both of its acts: Remove at the left where
   leaving lives, Germinate pushed to the right edge. They had a row each,
   one under the other, which read as two endings to one page. Space
   between rather than a gap, so the pair frames the page's foot the way
   the consent's own two acts do -- and a row holding only Remove, which
   is every page that is not a spore's, is unchanged by it. */
.row.plain.foot-acts {
  display: flex;
  justify-content: space-between;
  align-items: baseline;

  /* Nearer what it ends than a new section would be: the acts belong to
     the page above them, and a full section's clearance over a row of
     two words read as a gap the page had stopped at -- "less space
     before node pages bottom row with remove button". */
  margin-top: var(--group-tight);
}

/* Germinate wears the x's underline in the prose colour: the same kind of
   control as the Remove beside it, and not the one that destroys
   anything. Every measure here is button.x's, because a pair that
   disagreed on tap height or underline offset would read as two
   unrelated things put on one line.

   The refusal is worded on the press itself, so the failure colour goes
   on the press -- qualified past the row's own control colour. */
button.germinate {
  padding: var(--tap-pad) 0.4rem;
  color: var(--text);
  background: none;
  border: 0;
  border-radius: calc(var(--radius) / 2);
  line-height: inherit;
  text-decoration: underline;
  text-decoration-thickness: 1px;
  text-underline-offset: 0.22em;
  cursor: pointer;
}

.row.plain button.germinate.failed {
  color: var(--fail);
}

/* A link that is a picture has no words to size the mark to: the anchor's
   line box keeps a zero-width strut beside the block image, and painting
   it put tiny slivers at the picture's top-left corner. The mark rings
   the picture itself instead. */
.row.plain.selected a:has(.capture-image) {
  background: none;
  box-shadow: none;
}

.row.plain.selected a:has(.capture-image) .capture-image {
  box-shadow: 0 0 0 0.2rem var(--select);
}

@media (hover: hover) {
  .row.plain:hover a:has(.capture-image) {
    background: none;
    box-shadow: none;
  }

  .row.plain:hover a:has(.capture-image) .capture-image {
    box-shadow: 0 0 0 0.2rem var(--select);
  }
}

/* The heading margin shorthand above zeroes the row's negative side
   margins, which would push a heading-row's text off the shared left
   gutter -- every row's first glyph starts on one line, the card's
   padding, whatever the row is. */
.page h2.row,
.page h3.row {
  margin-left: calc(-1 * var(--row-gutter));
  margin-right: calc(-1 * var(--row-gutter));
  padding-top: var(--heading-row-pad);
  padding-bottom: var(--heading-row-pad);
}

/* Rows that need the server gray out while it is away; a held action is
   the same statement about the form above it. Dim alone still reads as
   text, so the opacity drop is what makes "not yet" unmistakable. */
.inert,
button.action:disabled {
  color: var(--dim);
  opacity: 0.4;
  text-decoration: none;
  pointer-events: none;
}

/* The rows' selection band bleeds one gutter past the text (.row); a list
   of rows absorbs it the way the card's padding does, so the band is
   never an overflow of the list's own box -- an overflow is reserved for
   text a reader is actually losing. Every list here, because a field list
   carries selectable rows too: Account's values are presses. */
ul.rows,
ul.fields,
ul.queue {
  margin: 0 calc(-1 * var(--row-gutter));
  padding: 0 var(--row-gutter);
  list-style: none;
}

/* ------------------------------------------------------------------ */

/* The two voices, and the one place that decides between them. Which one a
 * node's words wear is decided once in JS, by format.js nodeVoice, so the
 * line in a list row and the block on the page it opens cannot come out
 * different.
 *
 * What a person wrote reads as words: normal text, in the text face. What the
 * machine holds -- a media type, an address, an id, a timestamp, a byte count,
 * a state, a kind, a key and its value -- reads as metadata: gray, monospace,
 * one step smaller. That is the whole colour system. A role not on one of
 * these two lists has not been decided, and deciding it here costs less than
 * deciding it again on every page.
 *
 * Red appears in one place only, further down: something went wrong. */

.said,
.excerpt,
.content {
  color: var(--text);
  font-family: var(--face-text);
  font-size: var(--size-text);
}

/* Authorship, told by the slant rather than by the tone. What a person
   wrote is content and reads white wherever it stands: the jot leading a
   row, the name and email on Account, the selection lifted from a site.
   What came from outside is italic with it, in both themes -- terminal's
   one face has an italic and this is the one thing it is spent on -- so a
   quoted line is telling at a glance: "makes it different from buttons
   and other sources of text".

   Gray was tried for it first and taken back off: "otherwise it blends in
   with timestamps, metadata etc". Gray means metadata everywhere else on
   this page, and what somebody else wrote is still what the reader came
   to read.

   `outside` is the marker, and it is only ever needed where a person
   wrote the words: everything the machine supplies is metadata already.
   It is a separate axis from `theirs`, which is the promise that no theme
   recases what somebody typed -- a capture is somebody's typing whoever
   they were. */
.outside {
  color: var(--text);
  font-style: italic;
}

/* The account's own fields are the one place a value is the reader's own
   words rather than the machine's, so they are painted in the words
   voice rather than the token one (account.js) and need no rule here.
   `theirs` stays on them for the promise it carries: no theme recases
   what somebody typed. */

.token,
.dim,
.kind,
.meta-label,
.kv .key,
.kv .value,
.day .relative,
li.row time,
.queue-state,
.queue-detail,
.row.file a,
a.waiting-mark,
button.x,
ul.fields,
.settings .rows,
label.input-row {
  color: var(--dim);
  font-family: var(--face-stamp);
  font-size: var(--size-meta);
}

.dim {
  margin: 0.1rem 0 0;
}

/* Margin, colour, and the empty-hides-itself rule all live on [role="alert"]
   below -- every .error carries that role too, and a value repeated on both
   selectors is the value that silently drifts when only one gets edited. */
.error {
  /* One refusal per line: a form can be wrong in several places at once,
     and running them together hides the second behind the first. */
  white-space: pre-line;
}

/* ------------------------------------------------------------------ */

/* Listing rows: the time in the stamp face, the saved text cut to one
   line, the kind (on soil) dim at the end. The day heading sits flush on
   its first row -- no gap. */

/* The day is a heading like any other -- the rank's colour, the rank's size,
   the text face -- because it is what groups the rows beneath it, and a run
   of days set as metadata leaves a list with no group markers at all. Only
   the relative word beside it is metadata.
   Both ranks: Events draws the day under "Past" one rank down from the
   one leading the page (events.js pastSection), and the two still sit
   flush on their own first row the same way. */
.page h2.day,
.page h3.day {
  margin-bottom: 0;
}

/* The face is the two-voices list's; only the tracking is the word's own. */
.day .relative {
  letter-spacing: 0.06em;
}

h2.day + ul.rows,
h3.day + ul.rows {
  margin-top: var(--day-gap);
}

li.row {
  display: grid;
  grid-template-columns: auto 1fr auto;
  column-gap: 0.7rem;
  align-items: baseline;
}

/* A list that owns column tracks lends them to every entry in it, so each
   entry's columns start on the edge its neighbours' do however long one
   of them runs. Written once for the lists that do it -- a listing's three
   columns, an event day's two, the invites table's three, any card's own
   keys -- because the borrowing is one idea and the tracks borrowed are
   each list's own, declared in its own block below. */
ul.rows.listing > li.row,
ul.rows.dated > li.row,
ul.rows.aligned > li.row,
ul.fields > li.kv {
  grid-column: 1 / -1;
  grid-template-columns: subgrid;
}

/* A stamp never wraps: a date's hyphens are break points a squeezed
   column would fold at, and a row is one line by design. */
li.row time {
  white-space: nowrap;
}

/* The one thing on a row that wraps instead of being cut: a term's reason,
   which is a sentence and not a label, and five words of a sentence answers
   nothing. It takes the row's whole width under the term rather than a
   column beside it, so the words it is about stay the row's first line and
   the argument for them reads as prose underneath. The gap is smaller than
   a paragraph's -- these two are one row, not two things stacked. */
li.row > .under {
  grid-column: 1 / -1;
  margin-block-start: 0.15rem;
}

/* The reason is quoted, not stated: it is the model's argument for the word
   above it, and italic is what marks a sentence as being reported rather
   than asserted by the page. On the class rather than on `.under`, which is
   a slot a later row could put something else in. */
.reason {
  font-style: italic;
}

/* One line, cut by measure to ASCII dots (format.js refitLines) rather
   than wrapped; the clip is the backstop for the frame before the cut
   runs. One class, because both lists build their rows through the one
   builder (format.js rowLine): a saved row's line and a queued capture's
   are the same line at two moments of its life. */
.excerpt {
  overflow: hidden;
  white-space: nowrap;
}

/* The reader's own press on the cut above (format.js expandLine) undoes
   it: the line wraps to say the rest instead of being cut to it, and
   nothing here re-measures it against a room it no longer answers to. */
.excerpt[data-expanded] {
  overflow: visible;
  white-space: normal;
  overflow-wrap: anywhere;
}

/* ------------------------------------------------------------------ */

/* Waiting: the shape of the answer, at the size the answer will be.
   Drawn only when there is no remembered copy to paint instead, and
   replaced in place the moment the read lands -- so the card never grows
   or shrinks around what arrives. A bar sits in the line box like the
   words it stands in for, which is what keeps the row's height the row's
   own. The breath is slow on purpose: fast is a progress bar, and this is
   not measuring anything. */

@keyframes breathing {
  0%,
  100% {
    opacity: 0.22;
  }

  50% {
    opacity: 0.09;
  }
}

.skeleton {
  display: inline-block;
  height: 0.8em;
  max-width: 100%;
  border-radius: 0.15rem;
  background: currentcolor;
  animation: breathing 1.8s ease-in-out infinite;
}

/* A bar standing alone in a grid or flex row has to carry the row's height
   itself: those rows hold no text of their own to set it, so a row of bars
   would come out a third the height of the row of words it stands in for --
   exactly the jump this is here to prevent. In a paragraph the line is
   already there, and adding to it would overshoot. */
li.row > .skeleton,
h2.row > .skeleton,
h2.day > .skeleton {
  margin-block: calc((1lh - 0.8em) / 2);
}

/* A waiting shape is one shape: a day heading's own bar took the gold a
   day heading wears once it has words, and stood out from every row bar
   under it -- a list waiting for its rows should not read as two kinds
   of thing waiting. */
h2.day > .skeleton {
  color: var(--text);
}

/* A reader who asked for less motion gets the shape without the breathing:
   the shape is the information, the breath is only the reassurance. */
@media (prefers-reduced-motion: reduce) {
  .skeleton {
    opacity: 0.16;
    animation: none;
  }
}

/* ------------------------------------------------------------------ */

/* Key-value rows and content: the record's own voice. Keys and values
   wear the stamp face -- they are metadata -- and content is normal text,
   in full, wrapped, never cut. */

/* The list owns its key column, sized to the widest key this particular
   list actually holds, and every row borrows it (the same pattern a
   listing's own tracks use below) -- so Account's five short keys are not
   paying rent to a stored row's `produced_by_processor_version`, and a
   value is cut only where its own list genuinely has no room left for it.
   Per-row sizing was tried once already and undone (li.kv.row): each row's
   key at its own width put every Account value on a different edge. */
ul.fields {
  display: grid;
  grid-template-columns: max-content minmax(0, 1fr) auto;
  column-gap: 0.7rem;
  line-height: 1.8;
  overflow-wrap: anywhere;
}

/* The one rule that separates a page's own sections. Land leads with what a
   reader came for -- what is waiting, and the terms they retrieve by -- and
   keeps the two whole-tier listings below it: those answer "is the substrate
   really there", which is a question asked while showing somebody the thing
   rather than while using it. A rule rather than a heading, because naming
   the halves would be claiming they are two kinds of thing. */
main.page hr {
  margin: var(--menu-gap) 0;
  border: 0;
  border-top: 1px solid var(--rule);
}

/* The heading after the rule is still a menu row, and has to be told so:
   the rule breaks the `h2.row + h2.row` run, so without this the heading
   below takes the wide between-sections margin while the one above keeps
   the tight menu one -- and the line sits visibly nearer the half above
   it. Margins collapse to the larger on each side, so matching the rule's
   own margin to the menu gap on both sides is what centres it. */
.page hr + h2.row {
  margin-top: var(--menu-gap);
}

/* A heading that folds its section rather than opening another page.
   The button carries the heading's own size and colour instead of an
   action's, because what it is is the heading -- pressing it is how it
   behaves, not what it is.

   It is also the whole row rather than a control sitting in one, stretched
   to the full width so there is a single thing to hover, focus and press:
   two boxes -- the row and the word inside it -- read as two levels of
   focus over one action, which is what the metadata heading looked like.
   The chevron is the state for anyone looking, and `aria-expanded` the
   same fact for anyone whose reader speaks the page.

   The words come first for the reader and second in the walk: the fold is
   the row's own action, so it is the first control in the markup, and it is
   ordered back behind the link whose section it folds (format.js
   headingFold). The fold moves rather than the link, because the heading's
   own mark is a `::before` and generated content orders from where it is
   generated -- the head of the line, which is where terminal's `##` has to
   stay.

   Written without its rank: a fold under another fold is the same control
   one heading down (node.js detailsSection), and a rule per rank would be
   the same declarations twice. */
.fold > button.action {
  display: flex;
  flex: 1;
  order: 1;
  align-items: baseline;
  gap: 0.45em;
  color: inherit;
  font: inherit;
  text-align: left;
  text-decoration: none;

  /* The heading's box is its line, not a 44px tap box: "the box around
     subheading is too big - subheadings are too damn far apart". An act's
     button pads itself out to 44px (button.action) because an act is a
     few characters wide; a fold heading is the full width of the card and
     a line tall, which is past WCAG 2.5.8's 24px minimum on its own, and
     six of them padded to 44 was a page of boxes. */
  padding-block: 0;
}

/* What a fold hides is one box, so hiding it is one attribute; the box
   itself adds nothing, and its first child's own leading would otherwise
   push the section off the heading it belongs to. */
.fold-body > :first-child {
  margin-top: 0;
}

/* A row inside a fold reaches the card's own edges, like a row anywhere
   else: every `.row` leans out by the gutter so its pill meets the card
   rather than floating inside it. The box has to have that room to lean
   into, which is the page's own arrangement one level down -- the padding
   is what the rows occupy and the margin gives it straight back, so
   nothing moves and the box is finally as wide as what it holds. Without
   it every fold holding rows measured narrower than its own contents,
   which is what a clipped section looks like to anything measuring. */
.fold-body {
  margin-inline: calc(-1 * var(--row-gutter));
  padding-inline: var(--row-gutter);
}

/* The way back's own chevron, turned to say which way the fold stands:
   right for closed, down for open. The drawn shape starts pointing left,
   which is what the back link wants and what these two turns are measured
   from. */
.fold > button.action > svg {
  width: 0.5em;
  height: 0.75em;
  flex: none;
  transform: rotate(180deg);
  transition: transform 0.12s ease;
  fill: none;
  stroke: var(--dim);
  stroke-width: 1.5;
  stroke-linecap: round;
  stroke-linejoin: round;
}

.fold > button.action[aria-expanded="true"] > svg {
  transform: rotate(270deg);
}

@media (prefers-reduced-motion: reduce) {
  .fold > button.action > svg {
    transition: none;
  }
}

/* The row that opens the stored bytes wears the same voice as the values
   above it, because that is what it holds: a media type, not a sentence.
   Only the underline says it is a door. */

/* Every value on a page starts on one line whatever kind of row carries
   it. The third track is for a row that confirms something -- Account's
   copied mark -- and is nothing wide on the rows that carry no such
   thing; the tracks themselves are the list's, borrowed above, so `.kv`
   only has to be a grid for the borrowing to mean anything. */
.kv {
  display: grid;
}

/* A field row that is also a walkable row is still a field row: `.row`'s
   own padding stands every fact two lines tall on top of the leading a
   list of fields already sets itself (1.8 above). */
li.kv.row {
  padding-block: 0;
}

/* The one list drawn as a list of things rather than a list of rows: the
   warnings an event grew, the reasoning behind it, and the events a
   reading answered are read together as a set. The rule here adds none
   of a row's furniture -- no pill, no mark, nothing the walk stops on of
   its own -- a bullet that needs one, an event's, carries it as its own
   row nested inside. Indented by its own marker's width, so the words
   start where a row's do rather than a bullet's width further in. */
ul.bullets {
  margin: 0;
  padding-left: 1.2em;
  list-style: disc;
}

ul.bullets > li {
  margin-block: var(--row-pad-y);
}

/* An event's line and its presses -- a nested bulleted list of their own
   (node.js exportRow) -- stand inside their own bullet rather than
   bleeding to the card's edge like every other row: the bullet already has
   its own indent, and a row's own outdent (.row) bled past it on the side
   the bullet has nothing to absorb it with. */
ul.bullets .row {
  margin-inline: 0;
  padding-inline: 0;
}

/* The presses under one event (node.js exportRow): each its own tight
   line, so the indentation the nested list already carries -- not a gap
   the width of the one between two events -- is what tells them from the
   next event's own line. Only Remove keeps a 44px target here: these are
   read and followed, not the one press that undoes something. */
.presses > li {
  margin-block: 0;
}

.presses .row.plain {
  margin-top: 0;
}

.presses .row.plain :is(a, button) {
  padding-block: 0;
}

/* The one press among them that acts rather than opens, worn as the
   others are: underlined like every link on the page, in the colour
   already inherited, with none of button.action's pill or tap height. */
.presses button {
  text-decoration: underline;
  text-decoration-thickness: 1px;
  text-underline-offset: 0.22em;
}

/* Two columns, not the general three: an event's own fields never carry
   the trailing confirmation mark Account's copied press does. */
ul.fields.tight {
  grid-template-columns: max-content minmax(0, 1fr);
}

/* The Keys page's own keys are sentences about a chord
   ("h / Left / Backspace") rather than a single word, but the list's own
   column (ul.fields, above) is already sized to the longest one it holds
   and every row already starts its description on that same edge
   (subgrid). What is left to guard is a phone narrow enough to compress
   that column below its own ideal width, which is what once wrapped
   "Backspace" under the rest of its own key. */
#keys-fields .key {
  white-space: nowrap;
}

/* A value that is a press fills its column, like the span it replaced: a
   button sizes to its own words rather than stretching into a grid
   track, which also left the fitted cut measuring the text instead of
   the room it has. */
.kv > button.value {
  justify-self: stretch;
  width: 100%;
}

/* A value that carries a fitted cut (format.js refitLines) is one line: it
   needs a box with a width to be cut to, and the clip is the backstop for
   the frame before the measurement runs. Wrapping instead would double the
   row's height over an address nobody reads the tail of, and push every
   fact below it down the page.

   Keyed on the cut rather than on the class, because `.value` is also the
   ordinary paint on pages whose values were never fitted -- clipping one
   of those would hide text with nothing to say there is more. */
.kv .value[data-full] {
  overflow: hidden;
  white-space: nowrap;
}

/* The reader's own press on the cut above (format.js expandLine) undoes
   it: the value wraps to say the rest instead of being cut to it. */
.kv .value[data-full][data-expanded] {
  overflow: visible;
  white-space: normal;
  overflow-wrap: anywhere;
}

/* A value whose cut would lose the only thing it is for: an id, a hash, a
   raw stamp are recognised whole or not recognised at all, so Metadata
   wraps them rather than fitting a cut to them (node.js wrappedRow) -- no
   `data-full` stands on one of these, so the rule above never clips it.
   `anywhere` is for the values with no space or hyphen to wrap at, a hex
   hash above all. */
.kv .value.wrap {
  overflow-wrap: anywhere;
}

/* A value marked as a failure says so through whatever paints it. The cut
   draws the words in a voice span of its own (format.js paintLine), and
   that span's own colour would otherwise be the one thing the cut threw
   away -- leaving a page that never arrived stated in the ordinary voice. */
.kv .value.failed > span {
  color: inherit;
}

/* A wrapped key doubles its row's height and pushes the field off the line
   it belongs to, so the key never wraps -- the track above is sized to hold
   the longest one. */
.input-row .key {
  color: var(--dim);
  white-space: nowrap;
}

/* A key and its value are one fact the record holds, so they wear one voice.
   Colouring the value differently makes a reader weigh which half matters,
   which is an argument the page should not be starting. */

/* A fetch that did not land -- in the list, in the record's fields, and in
   the sheet's own verdict. Red, so it is legible as a failure before it is
   read as a word: the line around it is ordinary news, and this is the part
   that is not.
 *
 * The mark's whole face is stated here, slant included, because the mark
 * rides wherever its line stands -- sometimes inside a quoted `.outside`
 * block -- and it is the app speaking, not the quoted words: left to
 * inheritance it wears whatever its ancestors do, which is the one word
 * reading upright in one place and italic in another.
 *
 * The second selector is not decoration: a field's value is coloured by
 * `.kv .value`, which is two classes deep and would otherwise win on
 * specificity and paint a missing page in the ordinary text tone. */
.failed,
.kv .value.failed {
  color: var(--fail);
  font-style: normal;
}

/* Placement only. The colour is the two voices' above, and stating it a
   second time here is what quietly beat the authorship rule between them:
   two rules of one specificity, the later one winning, neither of them
   wrong on its own.

   Only the top margin, because a content block that is also a row carries
   the row's own side margins -- the negative pair every row wears so its
   selection band bleeds one gutter wider than its words. Written as a
   shorthand, this rule zeroed them and left the row's padding standing
   alone: a saved address sat one gutter right of the title above it, which
   is the whole page's left edge missing by a character. */
.content {
  margin-block: var(--para-gap) 0;
  overflow-wrap: anywhere;
  white-space: pre-wrap;
}

/* ------------------------------------------------------------------ */

/* Input rows and actions. A field is a row like any other: the key dim on
   the left, the value written into the line. Locked fields -- future
   features -- keep the shape and drop the invitation. */

/* The same vertical rhythm as every other row: the field is written into
   the line, not set in a taller box of its own. */

/* Each row is its own grid, so the track cannot be max-content without the
   keys losing their alignment down the column. */
label.input-row {
  display: grid;
  grid-template-columns: var(--key-column) 1fr;
  column-gap: 0.7rem;
  align-items: baseline;
  padding-top: 0;
  padding-bottom: 0;
  line-height: 1.8;
}

.input-row input {
  width: 100%;
  padding: 0;
  font: inherit;
  line-height: inherit;
  color: var(--text);
  background: transparent;
  border: 0;
  border-bottom: 1px solid var(--rule);
  border-radius: 0;
}

.input-row input:focus {
  outline: none;
  border-bottom-color: var(--h2);
}

/* Chrome paints autofilled fields with its own background and text
   colour, which no background rule can override -- the sanctioned trick
   is an inset shadow thick enough to cover the paint, in the theme's own
   field tone, and the matching text fill. */
.input-row input:-webkit-autofill,
.input-row input:-webkit-autofill:hover,
.input-row input:-webkit-autofill:focus {
  box-shadow: 0 0 0 100rem var(--field) inset;
  -webkit-text-fill-color: var(--text);
  caret-color: var(--text);
}

.input-row input:disabled {
  color: var(--dim);
  border-bottom-style: dashed;
}

/* An action is a link you press: same underline, same row behaviour, no
   button furniture. */
button.action {
  display: block;
  width: auto;
  font: inherit;
  color: var(--text);
  background: none;
  border: 0;
  border-radius: calc(var(--radius) / 2);
  text-align: left;
  text-decoration: underline;
  text-decoration-thickness: 1px;
  text-underline-offset: 0.22em;
  cursor: pointer;

  /* A 44px tap height without touching the words: the padding grows to
     it and the text stays whatever size a reader chose. */
  padding-block: var(--tap-pad);
}

/* The consent pair's controls wear the x's own pill metrics, so the walk
   marks Cancel and Okay alike; the padding is cancelled by its margin, so
   the pair keeps its edges. Here only -- a Sow standing flush on a field's
   right edge must not lean past it. */
p.consent-acts button.action {
  padding: var(--tap-pad) 0.4rem;
  margin: 0 -0.4rem;
}

/* Settings is nothing but choices -- two themes, two cache states, two
   for auto-update, a row per model -- and every one of them padded out to
   44px made a column of boxes down the page: "don't have huge ass buttons
   in settings". The two he called good are both button.x, whose box is
   its line (Remove on a spore's page, Clear on this one), so the page's
   own actions take the same measure. The padding stays where an act
   stands alone among words and has to be found; a page that is a list of
   choices has the row's own padding around each already, and the mark is
   drawn by a shadow rather than by the box. */
.page.settings button.action {
  padding-block: 0;
}

/* A line that is nothing but acts is pills, not a row: nothing ever marks
   it whole, by pointer or by key. The consent's pair and the sheet's both
   wear it, and rows.js decides the same thing by the same shape rather
   than by this class -- a row whose every child is a control has no words
   of its own to mark. */
p.row.acts:hover,
p.row.acts.selected {
  background: none;
}

/* The blank line the design draws before sign-out. */
.gap {
  margin-top: var(--blank-line);
}

/* A line of text and one thing flush on the right: the text takes whatever
   room that leaves, so the trailing piece always sits on the row's right
   edge whatever the text's own length. The right edge is for controls only
   -- a fact rides inline after its words instead -- so the trailing piece
   is a control: the cache line on Settings wears this to stand its clear
   there. Not tied to .row: the held-bytes line on Settings wears the shape as
   plain text -- a fact beside its clear press, deliberately no row, so it
   takes no hover band and no selection. */
.trailing-x {
  display: grid;
  grid-template-columns: 1fr auto;
  column-gap: 0.7rem;
  align-items: baseline;
}

/* The held line gives its flavor before its words: squeezed, everything
   after the byte count is a fitted cut (format.js refitLines) ending in
   dots, and the count itself stays whole. */
#cache-held > span:first-child {
  display: flex;
  align-items: baseline;
  min-width: 0;
}

#cache-held .meta-label {
  flex: 1;
  min-width: 0;
  overflow: hidden;
  white-space: nowrap;

  /* The air a leading space cannot be: it would collapse at the flex
     edge, and the words would sit flush against the byte count. */
  margin-left: 0.65ch;
}

/* Every row on Settings is a short technical fact or choice -- a theme name,
   a byte count, on/off, a version number -- never prose, so the whole page
   speaks metadata: `.settings .rows` sits in the two-voices list above,
   and no face is stated here where it could drift from that list.

   And at the leading the account's own fields keep, not the one a row of
   ordinary text keeps: a row's padding was sized for text a rank larger, so
   this page's short facts came out set half again as far apart as the
   identical rows on Account -- the same voice, the same size, visibly airier.
   The line box is the pill now, which is the shape a field row already has.
   The held-bytes line is named alongside: no longer a row, same leading. */
.settings .rows li.row,
.settings .rows #cache-held {
  padding-block: 0;
  line-height: 1.8;
}

/* A choice is a word, not a paragraph: it loses the leading a lone plain row
   opens with, since a run of them is a list of options. "current" is a
   fact, not a control, so it rides right after the choice word in force --
   dim, one small gap -- the way the update mark rides its heading, never
   out on the right edge where the trailing controls stand. Every plain row
   in a list, not Settings' alone: the event's presses and the fact after
   "In your Google Calendar" are the same shape (node.js exportRow). */
ul.rows li.row.plain {
  display: flex;
  gap: 0.6rem;
  align-items: baseline;
  margin-top: 0;
}

/* A model name and its hint each broke mid-word where the row was too
   narrow for both -- so the row wraps whole words instead, and the hint
   drops beneath the name rather than squeezing beside it. The row's own
   gap already reads as the line gap once that happens. */
#model-rows li.row.plain {
  flex-wrap: wrap;
}

#model-rows button.action,
#model-rows .meta-label {
  white-space: nowrap;
}

/* ------------------------------------------------------------------ */

/* The pending queue: state, excerpt, outcome, and the x at the row's
   right edge -- the deliberate second target l reaches. */

/* One line, in the listing's own columns: the state where a saved row keeps
   its time, the capture's line, what became of it, and the x. It was two
   lines, and a queue that wraps beside a list that does not reads as a
   different kind of thing than it is. */
li.queue-item {
  /* The outcome column is its words, capped at a share of the row: an
     empty outcome is nothing at all -- fit-content, never a minmax whose
     definite max is a growth limit the track maximizes to beside a
     blank, which is what once cut "left unsown" to six glyphs. A refused
     row's own reason wraps to as many lines as it needs rather than
     being cut to this cap (.queue-item.refused .queue-detail, above), so
     the cap only bounds how wide a share it takes before wrapping,
     leaving the room the state, the kind and the line beside it need.

     The cap alone was not enough for the line beside it: a refused row
     spends the state, the kind and the outcome before the line is
     expanded into what is over, which at 300px was three dots and
     nothing else. The floor is what the outcome now competes with
     (--line-floor). */
  grid-template-columns:
    auto auto minmax(var(--line-floor), 1fr)
    fit-content(40%);
}

/* The losses a sign-out costs wear the queue's row -- the same blank where
   a stamp would be, the same line -- but the row is two columns and not
   four: nothing listed here has landed, so there is no kind to state and
   no outcome to report. It borrowed all four tracks and let the two it
   never fills collapse, which holds only while an empty track is worth
   nothing. The line's floor is worth 6ch, and the empty track took it off
   the line rather than leaving it: the same rows lost half their words to
   a column with nothing in it. Two columns, named. */
#loss-list li.queue-item {
  grid-template-columns: auto minmax(var(--line-floor), 1fr);
}

/* The blank a capture still waiting stands in that column: exactly a
   timestamp wide -- five characters of the stamp face the times beside it
   are set in -- so every line on this page starts on the edge the lists in
   Soil start theirs on, and moving between them moves nothing. */
.queue-state {
  min-width: 5ch;
}

/* The section heading and its aside share one line: the aside is a fitted
   cut (format.js refitLines), so it needs a bounded box to be cut to, and
   the heading never grows a second line however long the aside's words. */
h2.queue-section {
  display: flex;
  align-items: baseline;
}

h2.queue-section .dim {
  flex: 1;
  min-width: 0;
  overflow: hidden;
  white-space: nowrap;

  /* The air a leading space cannot be: it would collapse at the flex
     edge, and the aside would sit flush against the heading's word. */
  margin-left: 0.65ch;
}

/* The blank a row with nothing to add holds in the outcome's own column,
   so every section's rows keep the same four tracks a refusal's does. */
.queue-detail {
  overflow: hidden;
  white-space: nowrap;
}

/* A refusal wears the red every other refusal on every other page wears,
   and its reason wraps rather than being cut: it is the one thing a
   failure has to say, and five words of it answers nothing. Not the time:
   red is for the failure, and when it happened is not what went wrong.

   A word on its own can still be wider than the column at a narrow
   enough width -- a wrap breaks between words, not inside one, so
   without somewhere to give a long enough word overflows rather than
   wrapping. It breaks there too rather than widen the row past its
   screen. */
.queue-item.refused .queue-detail {
  overflow: visible;
  overflow-wrap: anywhere;
  white-space: normal;
  color: var(--fail);
}

/* Above the row-wide target its neighbouring link stretches over it: the x
   is a second control on a row that has one, and a press on it must reach
   it rather than opening the row. */

/* Fixed, not inherited: a shared control rendered inside different rows
   (a metadata line here, a queue row there) still has to look like one
   control, not whatever size and face its neighbours happen to wear. */
button.x {
  position: relative;
  z-index: 1;

  /* The word's own line, plus a character either side to press at. It
     padded out to 44px for a while, which drew a box around Remove half
     again as tall as the words in it -- "buttons don't need to be this
     huge", against the cache line's own Clear, which never took that
     padding (`#cache-held button.x.row`) and is the one he called good.
     A line is 29px here, past WCAG 2.5.8's 24px floor, and the 44px
     guidance is about a target's hit area rather than the box a reader
     has to look at. */
  padding: 0 0.4rem;
  line-height: inherit;

  /* The quiet red of loss: every x deletes something, and its word
     should say so before it is read. */
  color: var(--danger);
  background: none;
  border: 0;
  border-radius: calc(var(--radius) / 2);
  text-decoration: underline;
  text-decoration-thickness: 1px;
  text-underline-offset: 0.22em;
  cursor: pointer;
}

/* The cache line's clear is a row of its own -- the walk lands on the
   pill, never on the words beside it -- but it sits inside the line's
   grid, where the row box's negative margins would push it past the
   edge the fits sweep guards. Its padding is every x's now. */
#cache-held button.x.row {
  margin: 0;
}

/* The walk's mark: one control at a time, never the row around it, and
   the x and an action beside it wear the same pill whichever is held. */
button.x.selected,
button.x:hover,
button.action.selected,
button.action:hover {
  background: var(--select);
}

/* The cut's own press (format.js moreButton), escaping the row-wide
   target the same way the x above does: nested inside the fitted element
   a link owns, a press on it must reach it rather than opening the row or
   following the words its dots stand at the end of. Otherwise plain --
   the surrounding button reset already gives it the cut text's own face
   -- until the walk or a hand actually lands on it.

   Inline, not the browser's own inline-block, and its native widget
   chrome turned off (appearance): a button is a control before it is
   text, so a browser still boxes it -- taller than the glyphs beside it,
   at the line's own height rather than the font's -- however plainly it
   is told to sit among them. Its own line-height answers to the font
   instead of inheriting the row's, which is the one property that box
   is sized from: reads as a line of its own otherwise to anything
   measuring rects rather than eyes, the fits sweep's one-line check
   among them (test_fits.py one_line). The dots it replaces sat in the
   text's own line box, not beside it.

   The tap width grows the same way the consent's own pills do --
   padding, cancelled back by its margin so the fitted line's own box
   does not widen around it (p.consent-acts button.action). Not the
   height: an inline element's vertical padding has no margin to cancel
   it, and growing unevenly would read one row taller than the rest, the
   bug finding #12 already fixed once -- so three dots settle for a
   wider target rather than a taller one. */
button.more {
  display: inline;
  appearance: none;
  line-height: normal;
  position: relative;
  z-index: 1;
  padding-inline: 1rem;
  margin-inline: -1rem;
}

button.more.selected,
button.more:hover,
button.more:focus-visible {
  background: var(--select);
  border-radius: 0.15rem;
  box-shadow: 0 0 0 0.2rem var(--select);
  box-decoration-break: clone;
  outline: none;
}

/* One box, and it is the outer one. A row that paints its own band already
   says where the pointer is, so an action inside that band drew the same
   state twice -- a pill barely clearing the words, inside a bar around
   them. Every fold heading wore it: Details, Neighbors, Tags.

   The pill is for a control that shares its row, which is what `.acts`
   marks and why that row paints no band of its own: there the mark has to
   say *which* control, and only the pill can. Where the row is one thing,
   the row is what marks.

   Hover only. Under the walk the row takes the band and the action takes
   nothing, so there was never a second box to remove -- and a stop that
   does wear the mark must keep it. */
.row:not(.acts) > button.action:hover {
  background: none;
}

/* An x that leads its line rather than trailing a row -- a record page's
   clear -- keeps its word on the gutter every line's first glyph shares:
   the tap padding is pulled back rather than allowed to push the word off
   it. */
.row > button.x:first-child {
  margin-left: -0.4rem;
}

/* ------------------------------------------------------------------ */

/* Recipes: the row is the name, then what the recipe does -- the kind that
   wakes it, the arrow, the kinds it writes. The arrow is a fixed mark
   between two facts and never breaks (recipes.js arrow); the emitted
   kinds are cut to the row's own line like every other row's line,
   rather than growing the row underneath it. */
.shape {
  display: flex;
  min-width: 0;
  align-items: baseline;
  gap: 0.45em;
}

.shape > .arrow {
  flex: none;
  white-space: nowrap;
}

.shape > .shape-tail {
  min-width: 0;
  overflow: hidden;
  white-space: nowrap;
}

/* The reader's own press on the cut above (format.js expandLine) undoes
   it: the emitted kinds wrap to say the rest instead of being cut to it. */
.shape > .shape-tail[data-expanded] {
  overflow: visible;
  white-space: normal;
  overflow-wrap: anywhere;
}

.shape > .shape-tail:empty,
.shape:has(> .shape-tail:empty) > .arrow {
  display: none;
}

/* ------------------------------------------------------------------ */

/* Invites: each unused code is the full register link on a capture-shaped
   row; pressing the line copies and the row says so in its kind column. */

/* The line is a button wearing the excerpt's own one-line shape, so it
   sheds a button's chrome rather than adding to it. button.invite-copy,
   not the bare class, so this beats the browser's button styling without
   a selector-weight tie. */
button.invite-copy {
  background: none;
  border: 0;
  padding: 0;
  text-align: left;
  cursor: pointer;
}

.copied-mark {
  color: var(--live);
}

/* Used invites read as one table under its header row: the list owns the
   column tracks -- the day, the code, who it admitted -- and every row
   borrows them, so each value starts on the edge its neighbours' does
   however long one runs, and the columns flow one after another rather
   than pinning the last to the card's edge, which is for controls. The
   code column is capped at a share of the row and both written columns
   are fitted cuts (format.js refitLines), so a long value ends in dots on
   the one line. */
ul.rows.aligned {
  display: grid;
  grid-template-columns: auto fit-content(35%) 1fr;
  column-gap: 0.7rem;
}

/* A listing row reads down its columns: when it was saved, what kind of
   thing it is, then the thing itself. The first two are the width of their
   own words and the third takes what is left, so the fitted cut has a track
   that can shrink (minmax, not 1fr, or a long line pushes the columns apart
   instead of being cut to them) -- and never less than the floor, or what
   is left is nothing (--line-floor). The labels are fitted cuts too
   (listing.js labelCell), because a floor means a label can be the one
   with no room for its words, and a column that cannot say so in dots is
   a word clipped mid-letter instead.

   The list owns the tracks and every row borrows them, so the third column
   starts on one edge down the whole page: sized per row, a `web content`
   pushed its line a word right of the `link` above it, and the eye had no
   line to run down.

   One character between columns. Aligned columns need no more than that to
   be told apart -- the gap was doing the work the alignment now does. */
ul.rows.listing {
  display: grid;
  grid-template-columns: auto auto minmax(var(--line-floor), 1fr);
  column-gap: 1ch;
}

/* A listing whose rows carry two labels -- a neighbourhood's kind and
   relation -- caps each at just over a quarter of the row (enough for
   "event reasoning" at a phone's width), so the line, the one
   column a reader actually reads, keeps at least half of it whatever the
   labels say. `auto` sized them to their widest row, and "event reasoning"
   beside "derived from" left the line its six-glyph floor and nothing
   else: a row of three dots a reader cannot open (walk finding #1). A
   label past its quarter is a fitted line like any other and cuts with
   dots (listing.js labelCell). A single label -- Soil's kind column, the
   Thread page's -- keeps its full width, since there it is what tells the
   rows apart. */
ul.rows.listing:has(li.row > .kind + .kind) {
  grid-template-columns: fit-content(25%) fit-content(25%) minmax(
      var(--line-floor),
      1fr
    );
}

/* A neighbourhood states no time and a relation instead, in the same three
   tracks: the kind, the relation, the line. One shape for every listing
   in the app, which is what the developer asked for by name. */

/* An event's row reads down two columns: the hours it runs for, and what
   it is. There is no kind column because every row in this list is the
   same kind, and a column of the same word repeated is a column to scan
   past. The tracks are the list's, borrowed by every row, for the reason
   the listing above states -- so a whole day's "all day" and an hour's
   stamp leave their lines starting on one edge. */
ul.rows.dated {
  display: grid;
  grid-template-columns: auto minmax(var(--line-floor), 1fr);
  column-gap: 1ch;
}

/* The hours column is the width of a whole span, always, so every title
   down the list starts on one edge -- "event list first column should
   always have space for HH:mm-hh:mm so second column always starts at
   same horizontal level". Sized to the longest thing it can hold rather
   than to the longest it happens to hold: an evening with no end, or a
   whole day, would otherwise pull its own row's title left of the rest.

   The floor sits on the cell rather than on the track, and the cell
   wears the stamp face, because `ch` is one character of whatever font
   the element is set in -- 11 of them on the grid would have been 11
   proportional characters, which is a different and wrong width. Eleven
   is `19:00-23:00`. */
ul.rows.dated > li.row > :first-child {
  min-width: 11ch;
  font-family: var(--face-stamp);
  font-size: var(--size-meta);
}

/* The kind column is the app's own voice about the row, so it never wraps
   and never grows the row a second line: it is short by nature, and the
   line beside it is the half a reader is scanning.

   No floor under it. The column is the width of the longest kind the list
   actually holds and not one character more, so a page of `link` and
   `text` rows starts its lines four characters in rather than six -- and a
   list that later holds a `web content` moves them, which is the right thing
   for it to do. From the developer, on the gap the floor was leaving:
   "don't have as much spacing, but when longer node kinds are added then
   the visual representation has to be moved ... use as much space as
   possible, don't have more separation than needed, as long as each column
   starts at the level." The alignment that matters is down a list, and the
   grid is what holds that; the floor was buying alignment across two lists
   at the cost of the gap on every row of both. */
li.row.listing > .kind,
li.queue-item > .kind {
  /* A grid item's automatic minimum falls to zero once it declares
     overflow, so nothing above stopped a squeezed row from cutting this
     silently -- and the two lists must lose that room the same way, not
     each its own, or the same word "text" sizes to two different widths
     and the edge dbc6f954 was buying across them moves. Six characters is
     short of "web content", the longest kind there is, but is legible on
     its own where the row has no room for the rest, and is the floor this
     column wore before it was cut loose to size itself -- restored as a
     floor under the loss, not instead of it. */
  min-width: 6ch;
  overflow: hidden;
  white-space: nowrap;
}

ul.rows.aligned .kind {
  overflow: hidden;
  white-space: nowrap;
}

/* ------------------------------------------------------------------ */

/* The share sheet's preview, and status lines. */

/* The sheet is a form, not a list. Rows stack tight everywhere else
   because a list wants one texture; here the three parts are different
   things -- what was shared, the note about it, and the act -- and the
   list's rhythm reads as crowding. Each part gets its own air, and the
   jot takes back the width a three-letter key was holding. */
#preview {
  margin-bottom: 1.1rem;
}

/* One row, one key: nothing to align down a column, so the field starts
   right after the word rather than a track away. */
#share-form label.input-row {
  grid-template-columns: max-content 1fr;
}

/* The act sits at the right edge, under the thumb that is already there --
   the one place in this design where a control is furniture rather than a
   row of a list, because the sheet is a dialog and not a page. fit-content
   is what lets the auto margin push it: a block at width auto fills the
   card and has nothing to be pushed across. */
#share-form button[type="submit"],
#okay {
  width: fit-content;
  margin-left: auto;
}

/* The act's line: the clear at its left edge -- leaving the share is the
   quiet half of the row -- with the act keeping the right edge under the
   thumb. */
#sheet-acts {
  display: flex;
  align-items: center;
  margin-top: 1.1rem;
}

/* The consent a destructive act ends in wears the same line: cancel at
   the left where leaving lives, the act under the thumb at the right --
   the remove page and the sign-out alike, so agreeing to a loss looks
   the same wherever one is asked. Its colours are the button rule in
   the developer's own words: a default or positive act is the text's
   white, a cancel or any non-default act is the metadata's gray, and a
   destructive act is red and named by what it does -- never "Okay". */
button.action.quiet {
  color: var(--dim);
}

button.action.danger {
  color: var(--danger);
}

p.consent-acts {
  display: flex;
  align-items: center;
  margin-top: var(--acts-gap);
}

/* A saved address shown whole on a detail page: the machine's face, since
   an address is a token, in the prose colour rather than a heading's --
   underlined and pressable, because the address is also the way out to
   the page it names. */
.content a.external-link {
  color: var(--text);
  font-family: var(--face-stamp);
  font-size: var(--size-meta);
  overflow-wrap: anywhere;
}

/* A fenced block in the rebuilt prompt (node.js paintPrompt): a source's
   own words, quoted whole, wear the same token face an address does --
   and stay the prose colour, never the dim one, because what the model
   actually read is content and not metadata about it. */
.prompt-source {
  font-family: var(--face-stamp);
  white-space: pre-wrap;
}

/* The sheet is a page like the others: wordmark at one height, card top
   where every other page puts it. It hung 10vh lower once, to be read
   where the eye already is -- and paid at the other end, where a long
   share ran the act off the bottom. That room is bought below instead, by
   the card being made to fit, which is the same room without moving it.

   Exactly one screen, its middle row taking what the wordmark and the
   liveness word leave, so the card grows downwards into that row and the
   row is what stops it.

   svh answers for the browser's own chrome, not for a keyboard the OS
   draws on top of everything -- so it stays the full screen's height
   while the keyboard is up, and the card stayed that tall too, with the
   acts row exactly what the keyboard covered and nothing on the page able
   to scroll to it. sow.js keeps --keyboard-vh at the visual viewport's
   own height once the keyboard has made one shorter than the other, and
   the card is capped to whichever is shorter. */
body.sheet {
  grid-template-rows: auto minmax(0, 1fr) auto;
  height: min(100svh, var(--keyboard-vh, 100svh));
}

/* A column whose other rows -- the way back, the heading, the jot, the
   act, the verdict -- are each one line and stand, so what is left over
   is the preview's and it scrolls inside the card. Scrollable itself too,
   for the same reason at the whole card's own level: a keyboard shrinking
   the card below what its standing rows need leaves them nowhere to be
   scrolled to unless the card can be the thing that scrolls. */
body.sheet .page {
  display: flex;
  flex-direction: column;
  max-height: 100%;
  min-height: 0;
  overflow-y: auto;
}

/* Nothing gives but the words. A column short of room shrinks everything
   in it by default, and the heading is `overflow: hidden`: it went to
   nothing with no mark to say it had, and a sheet holding a long share
   drew no "Sow" at all while every assertion about fitting stayed true.
   Both levels are named because the form between them is
   `display: contents` -- its rows are the card's flex items, not it. */
body.sheet .page > *,
body.sheet .page > form > *,
body.sheet #preview > * {
  flex: none;
}

body.sheet #preview {
  display: flex;
  flex-direction: column;
  min-height: 0;
}

/* The picture is the item that may be cut, at both levels it stands in:
   it re-scales into whatever room is left, which is how a tall screenshot
   and a short screen agree. */
body.sheet #preview,
body.sheet #preview > p.file {
  flex: 0 1 auto;
}

/* Words are not. They are capped in their own lines instead (`#preview
   p:not(.file)` below), and a cap is the whole answer only if nothing
   else can squeeze them past it -- `overflow-y: auto` zeroes a flex
   item's content-based minimum, so a shrinkable paragraph goes to nothing
   however short its words are. That is what put a scrollbar on a two-line
   share the moment the keyboard took the room.

   The box holding them stops giving with them, or it would clip words
   that now have no scrollbar to reach. The card is what scrolls when a
   long share and a keyboard want the screen at once, which is what
   `body.sheet .page` is already for. */
body.sheet #preview:not(:has(> p.file)),
body.sheet #preview > p:not(.file) {
  flex: none;
}

/* A saved picture, wherever it is shown: on the sheet before it is saved, on
   the page a pending row opens, and on the node's own page afterwards. The
   frame is the picture's own edge: a full-width box would letterbox a
   portrait shot with dead stripes beside it and none above, so the box hugs
   the photo instead and sits centred, capped in both directions. The top
   margin puts it under a heading where the text it stands in for would sit.

   The height cap is what makes the picture worth looking at rather than a
   thumbnail in an empty card: as wide as the card allows, and never taller
   than a screen. A picture that fits the screen can be scrolled to and seen
   whole, which is the promise -- a taller one would be a thing you read in
   passes, and the way to see the rest of it is to press it and open the
   bytes. Landscape is what made the old 34vh indefensible: a third of a short
   viewport is a stamp with the rest of the card empty beside it. */
.capture-image {
  display: block;
  max-width: 100%;
  max-height: 85vh;

  /* A saved image can be a single pixel -- a probe capture, or a genuinely
     tiny screenshot -- and a box that follows its own intrinsic size then
     paints as a dot nobody can find, let alone press. The floor keeps
     every picture a box worth looking at; a replaced element with only
     one dimension given scales the other to match its own ratio, so a
     larger image is untouched by it. */
  min-height: 3rem;
  margin-top: 0.75rem;
  margin-inline: auto;
  border-radius: 0.75rem;
}

/* Except on the sheet, where the picture is not the point: the jot, the act
   and the verdict sit under it, and a picture that took the screen would put
   the button being waited for below the fold. */
body.sheet .capture-image {
  max-height: min(34vh, 100%);
  object-fit: contain;
}

/* The picture's row shrinks the same way a shared line of words does
   (`#preview > p` above), but a picture cannot re-wrap into whatever room
   that leaves it -- it has to be told the room is there. Column flex down
   through the row and its link gives the image a real height to read
   `max-height: 100%` against, so it scales to whatever the row was
   actually given rather than spilling past it; 34vh above stays the
   ceiling on a tall screen, this is the floor on a short one. */
body.sheet .row.file {
  display: flex;
  flex-direction: column;
  min-height: 0;
}

body.sheet .row.file > a {
  display: flex;
  flex-direction: column;
  align-items: center;
  min-height: 0;
}

/* In the row that opens the bytes the air above is the row's own, and two
   top margins would open a gap the same picture does not have on the sheet. */
.row.file .capture-image {
  margin-top: 0;
}

/* Words keep their own line breaks wherever they are shown whole: a
   three-line share is three lines on the sheet, on the record its row
   opens, and on the node's page after it lands. The rows themselves stay
   one line -- they are cut by measure, not wrapped. */
#preview p,
p.content {
  white-space: pre-wrap;
}

/* A blank line was two of a paragraph's own lines stacked on its margin:
   `pre-wrap` renders every literal newline at the text's own line height,
   and a paragraph split from a run of them (node.js textContent) carried
   both. Capped to one line, which is what a blank line still reads as
   once the words on either side of it are paragraphs of their own again
   (walk finding #48). */
p.content + p.content {
  margin-top: 1lh;
}

/* Shared words on the sheet. No `min-height: 0`, which is what let them be
   squeezed by anything else wanting room -- the keyboard above all -- so a
   two-line share got a scrollbar on a card that was merely short: "i can
   scroll that text - that's stupid. on the sowing page, only if i'm sharing
   a really long text should the shared text be scrollable".

   Without it a flex item will not shrink below its own content, and the cap
   below is what keeps that from being the old bug back again: a flex item's
   automatic minimum is clamped by its `max-height`, so these words shrink to
   the cap and no further. Short share, nothing to give and no scrollbar; long
   share, it gives down to the cap and scrolls inside it, which is what keeps
   Sow on the screen. */
#preview p {
  margin: 0.75rem 0 0;
  overflow-wrap: anywhere;
}

/* Only the shared words scroll in place, and only once they are genuinely
   long: a picture is already capped to a share of the screen
   (.capture-image) and never needs a scrollbar of its own to be seen
   whole, and giving its row one anyway was a phantom scrollbar that
   vanished and jumped the page the moment a finger left it.

   Long is measured in the words' own lines and not in a share of the
   viewport, which is the distinction the last attempt at this got wrong:
   55svh was more than the card could hold on some phones, so the page
   scrolled instead of the words. Twenty lines is more than anybody reads
   on a sheet they are about to press Sow on, and a share shorter than
   that keeps every line of itself on screen with no scrollbar anywhere. */
#preview p:not(.file) {
  max-height: 20lh;
  overflow-y: auto;
}

/* Said nothing, standing nowhere: every one of these appears by being
   written into, so an empty one may not hold the room it will need. The
   only rule of the four that is about the role rather than the shape. */
[role="alert"]:empty,
[role="status"]:empty {
  display: none;
}

/* The three below are about a line of its own -- the sheet's verdict, the
   liveness word, a form's refusal -- which is what every paragraph
   carrying one of these roles is. A status riding a row is not that: it
   is a mark beside the words it is about, and it takes the row's own
   placement and its own colour (.copied-mark). Unscoped, this margin fell
   on Account's copied mark, and a grid row aligned on its baselines then
   stood a whole line taller for as long as the word showed -- one fact
   reading as two rows. */
p[role="alert"],
p[role="status"] {
  margin: 1rem 0 0;
}

p[role="alert"] {
  color: var(--fail);
}

p[role="status"] {
  color: var(--dim);
}

/* ------------------------------------------------------------------ */

/* The update offer: settled under Settings, on the row renderVersion draws
   there, never a prompt of its own -- auto-update decides whether that row
   is ever pressed by a reader or by the app itself (register-sw.js). */

/* Beside the heading the update is reached through, off its words. The
   waiting count wears the same spacing on Land: both are facts said dim on
   the heading that leads to them. */
.update-mark,
.waiting-mark {
  margin-left: 0.6rem;
}

/* The count is a second target on a heading whose own link stretches over
   the whole row: positioned, it stands above the stretch the way a queue
   row's x does, so a press on it opens the Waiting page rather than the
   heading's own. It takes the room the heading leaves and no more -- a
   real width of its own is what the measured cut (queue.js) measures
   against, so the heading never wraps and never grows a glyph. */
.page h2:has(.waiting-mark) {
  display: flex;
  align-items: baseline;
}

.waiting-mark {
  position: relative;
  flex: 1;
  min-width: 0;
  overflow: hidden;
  white-space: nowrap;
}

/* Once the update is taken, the row's whole width is the download. */
.update-track {
  flex: 1;
  height: 0.4rem;
  border-radius: 0.2rem;
  background: var(--rule);
  overflow: hidden;
}

.update-track i {
  display: block;
  height: 100%;
  background: var(--live);
  transition: width 0.2s linear;
}

/* ------------------------------------------------------------------ */

/* Pull to refresh (pull.js): the one manual "check now", on every page.
   --pull rides in from the drag itself, so the row's own height is the
   distance pulled -- no separate spinner icon, the row growing in is the
   affordance. */
.pull-indicator {
  height: var(--pull, 0);
  overflow: hidden;
  color: var(--dim);
  text-align: center;
  transition: height 0.15s ease-out;
}

/* ------------------------------------------------------------------ */

/* Terminal: the same design on a plain screen. Not a dark variant of
   grove but the opposite argument -- one monospace face at one size, the
   hierarchy carried by blank lines and the marks a terminal actually has,
   and nothing on screen that is not information.

   Which is a way of setting the design, not a second one. Everything this
   theme differs by is a token above, re-cut here: the ornament to
   nothing, the face to the stamp, every rank to one size, and every
   vertical measure to a whole row of the grid. No rule below carries a
   colour or a measure, after a year of the two quietly coming apart. */
:root[data-theme="terminal"] {
  /* No ornament: the card is a flat panel, so the glass film's blur, its
     hairline, the drop shadow and the corner radius all go to nothing. Its
     neutral canvas and panel are the flat terminal surface, not grove's green
     field and film. */
  --ink: var(--terminal-ink);
  --surface: var(--terminal-surface);
  --surface-line: transparent;
  --surface-blur: none;
  --card-shadow: none;
  --radius: 0;

  /* One face for everything: this theme's two voices are one. */
  --face-text: var(--face-stamp);

  /* One row of the terminal's grid -- the unit every vertical distance
     below is counted in. */
  --line: 1.5rem;

  /* One size for every rank: this theme's hierarchy is blank lines and
     the heading marks, never scale, and saying so in the tokens is what
     keeps a page from having to be told again per element. A group is
     separated by a whole blank row and its content sits directly under
     its heading -- which is the terminal answer to grouping, and the one
     grove is imitating with space of its own. */
  --size-h1: 0.94rem;
  --size-h2: 0.94rem;
  --size-h3: 0.94rem;
  --size-text: 0.94rem;
  --size-meta: 0.94rem;
  --size-wordmark: 0.94rem;
  --size-link-state: 0.94rem;
  --size-back: 0.94rem;
  --group-gap: var(--line);
  --group-tight: 0;

  /* Terminal's own answer to a run of folds is the grid, not a fraction
     of a gap: every vertical distance here is a whole row, and half of
     one is the thing this theme exists not to draw. So a fold sits a row
     from the fold above it, which is where every other line sits. */
  --fold-gap: var(--line);
  --fold-inner-gap: 0;

  /* Every vertical distance is a whole row and every horizontal one a
     whole character: the card is one line of padding plus the character a
     row's bar reclaims, so the bar sits exactly one line from every edge
     with one character of air inside it. Grove's half-measures -- the
     paragraph's leading, the day list's hair of space, Back's lift off the
     heading -- read here as a broken row, so they go to a whole row or to
     nothing. The bar is the row's, not the link's, which is the one place
     the gutter moves. */
  --card-pad-y: var(--line);
  --card-pad-x: calc(var(--line) + 1ch);
  --row-gutter: 1ch;
  --row-pad-y: 0;
  --heading-row-pad: 0;
  --heading-gap: 1ch;
  --back-lift: 0;
  --back-gap: var(--line);
  --back-pad-x: var(--row-gutter);
  --back-link-pad-x: 0;
  --para-gap: var(--line);
  --acts-gap: var(--line);
  --menu-gap: var(--line);
  --day-gap: 0;
  --blank-line: var(--line);
  --title-air: var(--line);
}

[data-theme="terminal"] body::before {
  background: var(--ink);
}

/* One leading, and none of the shaping grove uses to tell ranks apart: the
   sizes are the tokens above, so what is left here is the tracking. The
   casing is not: grove's uppercase is undone by the page-wide lowercase
   below, and cancelling it here as well would only mean these selectors
   beat that rule by being more specific than it -- which is exactly how
   the headings came to be the one thing the theme did not lowercase. */
[data-theme="terminal"] .page,
[data-theme="terminal"] .page h1,
[data-theme="terminal"] .page h2,
[data-theme="terminal"] .page h3,
[data-theme="terminal"] .queue-state,
[data-theme="terminal"] p.back-row,
[data-theme="terminal"] .day .relative {
  line-height: 1.6;
  letter-spacing: 0;
}

/* Type only: the liveness colours are the tokens above, which this theme
   has already restated as its own. */
[data-theme="terminal"] .wordmark {
  font-family: var(--face-stamp);
  letter-spacing: 0;
  text-indent: 0;
}

/* The drawing and the motion are the two things here that carry no
   information. */
[data-theme="terminal"] .wordmark svg {
  display: none;
}

/* The frame's liveness word, said plainly: grove's small caps are a
   flourish, and the page-wide lowercase below never reaches this line,
   which stands under the card rather than on it. */
[data-theme="terminal"] #link-state {
  letter-spacing: 0;
  text-transform: lowercase;
}

/* No pill in terminal where the row is one control: Back is an ordinary
   row, a bar like every other row's. A row holding two links is the
   exception, and the whole of why `:has(a + a)` appears below: Back and
   the keys reference share it for layout and are two independent things,
   so a bar across both answers a pointer that is on one of them. The
   keyboard has said so since rows.js stopped marking the row; the mouse
   kept doing it, and the pill grove draws is the mark that fits one. */
[data-theme="terminal"] p.back-row a {
  display: block;
}

/* The keys link selected on its own -- Back left for its other link
   (rows.js) -- has no row-wide bar to ride on, so it gets the same pill
   grove gives it rather than going unmarked. */
[data-theme="terminal"] .back-row .keys-link.selected {
  background: var(--select);
  color: var(--text);
}

/* And no row-wide bar where the row's one control is the keys link.
   `:not(:has(a + a))` alone read "one link, so the link is the row",
   which is true of Back -- left-aligned and `display: block`, so it fills
   the line -- and false of the `?`, a single character at the right.
   The front page has only that: "hovered with my mouse on the top row on
   terminal theme, entire row was highlighted on front page". It gets the
   pill its own selected state already wears, which is the mark that fits
   one character. */
@media (hover: hover) {
  [data-theme="terminal"] p.back-row:not(:has(a + a), :has(.keys-link)):hover {
    background: var(--hover);
  }

  [data-theme="terminal"]
    p.back-row:not(:has(a + a), :has(.keys-link))
    a:hover {
    background: none;
    color: var(--text);
  }

  [data-theme="terminal"] p.back-row .keys-link:hover {
    background: var(--select);
    color: var(--text);
  }
}

[data-theme="terminal"] label.input-row,
[data-theme="terminal"] ul.fields {
  line-height: 1.6;
}

/* The ladder spelled in the marks a terminal has, each in its rank's own
   colour. The lowercase is not here: it is the whole page's, below. */
[data-theme="terminal"] .page h1,
[data-theme="terminal"] .page h2,
[data-theme="terminal"] .page h3 {
  font-weight: 400;
}

/* The marks carry no whitespace of their own: a heading is a line of
   parts and the gap between them is the measure above (--heading-gap,
   one character here), so the mark is separated from the words the same
   way the words are separated from whatever rides beside them. Written
   into the content instead, the space was trimmed by a flex heading on
   one page ("##to sow") and counted twice beside the gap on another
   ("##  2026-08-02"). */
[data-theme="terminal"] .page h1::before {
  content: "#";
}

[data-theme="terminal"] .page h2::before {
  content: "##";
}

[data-theme="terminal"] .page h3::before {
  content: "###";
}

[data-theme="terminal"] .day .relative::before {
  content: "(";
}

[data-theme="terminal"] .day .relative::after {
  content: ")";
}

/* Grove renders text as written; the lowercase is terminal's alone.
   Stated once, over the whole page, rather than as a list of the elements
   that happened to be remembered -- that list was the bug: a value row
   added later said "Soil" in a theme where nothing else is capitalised,
   and nothing was wrong except that its selector had never been written.
   A rule with no list cannot be forgotten.

   What was written rather than composed here is the exception, and it says
   so in the markup: `theirs` is the class for words somebody authored, and
   an input is their words being typed. Both are rendered exactly as given
   -- a capture is kept for decades, and a theme is no reason to alter it.
   A recipe's prompt wears it too, and is the case that made the rule wider
   than "the reader's own": authored prose the database stored. Everything
   else on a page is the app talking, and the app talks in lowercase here.

   A button is named because the platform will not let it be inherited to:
   form controls take the user agent's own `text-transform: none` rather
   than their ancestor's, so a rule on the page reaches every word on it
   except the ones on buttons. That is the whole reason the old list had
   `button.action` and `button.x` in it, and dropping them for a page-wide
   rule put "Remove" back in capitals until the sweep said so. An input is
   left where the platform puts it, because what is in one is being typed
   by the reader and is theirs by the same rule as the rest. */
[data-theme="terminal"] .page,
[data-theme="terminal"] .page button {
  text-transform: lowercase;
}

[data-theme="terminal"] .page .theirs,
[data-theme="terminal"] .page input {
  text-transform: none;
}

/* Inside a kept line, the machine's own token -- "Image" where a picture
   has no words, a media type, a kind -- is still the app talking: the
   exemption above is for what a person wrote, and the voice spans
   (format.js paintLine) already say which half is which. */
[data-theme="terminal"] .page .theirs .token {
  text-transform: lowercase;
}

/* A byte count's unit is not the app's own voice to recase: "41.5 kb"
   reads as a different, wrong unit, where "41.5 KB" is the one the figure
   actually means (format.js formatBytes). */
[data-theme="terminal"] .page .byte-count {
  text-transform: none;
}

/* Metadata's raw created_at stamp keeps the same exemption, for the same
   reason: "utc+02:00" is not the offset it was stored as (node.js
   createdAtRow). */
[data-theme="terminal"] .page .raw-stamp {
  text-transform: none;
}

/* The chevron drawn in the characters a terminal has. */
[data-theme="terminal"] .back-row svg {
  display: none;
}

[data-theme="terminal"] .back-link::before {
  content: "<- ";
}

/* And the fold's mark the same way: `+` for a section that will open, `-`
   for one that is open. Grove keeps the drawn chevron, which is the whole
   of the difference between the two themes -- one draws its marks and the
   other spells them, and a terminal that spells every other mark and draws
   this one is the theme forgetting itself. */
[data-theme="terminal"] .fold > button.action > svg {
  display: none;
}

[data-theme="terminal"] .fold > button.action::after {
  content: "+";
}

[data-theme="terminal"] .fold > button.action[aria-expanded="true"]::after {
  content: "-";
}
