/* ===========================================================================
   COMPONENTS

   Each of these is defined once and referenced by class everywhere. The test
   for the whole file: changing `--colour-primary` in one `:root` line
   restyles every screen, and no pixel value for any of it appears in
   JavaScript.

   TWO VOCABULARIES, AND WHICH ONE TO REACH FOR

   The layout rule is that **one column is where a thing is changed and the
   other is where it is read**. That is not a convention about columns; it is
   what the two component families mean:

     `.field-row` + `.field-rows`   a settings row: a label, one line on why,
                                    and the control that changes it, inside a
                                    bordered group under a head.

     `.side-label` `.side-stat`     the information vocabulary: a small-caps
     `.side-quote` `.side-tile`     heading, a key and its value, a quotation
                                    of prose this app did not write, and a
                                    figure meant to be compared.

   They look like duplicates of each other and are not. `.field-row` holds a
   control; `.side-stat` holds a fact. The names say "side" for historical
   reasons — the information vocabulary grows in a right-hand pane — but it is
   the *job* that picks one, not the column: an Identity section is `side-stat`
   rows inside `.field-row`s in the main column, because it is information,
   and that is correct.

   The one thing neither family does is put a control in the reading column. A
   switch there is a second copy of one that is already on the row the pane is
   about, and the pane is the half of the screen a reader is not expected to be
   careful in.

   CONTENTS
     1. buttons             .primary .ghost .icon-btn .save-btn .side-action
     2. switch              .switch
     3. chips and tags      .chip .chip-* .tag-* .pill
     4. rows and lists      .row-list .row .row-* .add-row
     5. sections and fields .level-section-head .field-rows .field-row
     6. scope grid          .scope-grid .scope
     7. form controls       input select textarea checkbox radio
     8. information         .side-label .side-stat .side-quote .side-tile
     9. over the top        .snackbar .key-bar .hint .hint-bubble .banner
    10. panels and tones    .form-panel .form-panel.is-danger .fold .tone-*
    11. progress            .meter .is-spinning .side-quote mark
    12. motion              .is-entering .is-leaving .is-highlighted .is-saved
                            dialog
    13. press               every control answers :active with a scale
    14. row heights         .row-list[data-lines] .field-rows[data-lines]
    15. the flash           .is-highlighted .is-saved, drawn bigger than
                            the thing
    16. views and tables    .tabs .sep hr .box .scroll-area .filter-bar
                            .table dialog.dialog .menu .empty .skeleton
                            .badge kbd .pager .toolbar
    17. hierarchy           the section and row gaps on the space ladder,
                            forced colours, 24px press targets
    18. moving and people   .drag-handle .dropzone .tree .avatar .card
                            .board .is-confirming .field .auth .menu-head
                            .add-row.is-open
    19. cards and keys      .card.is-selected, the wheel, .is-context-target
                            .key-bar.is-docked .shortcuts
    20. colours             .colour-field .colour-swatch .colour-hex
                            .colour-picker .field-warn
    21. reading and editing .drawer .prose .placeholder .doc-frame .tiles
        at length           .actions
   =========================================================================== */


/* ===========================================================================
   1. BUTTONS
   =========================================================================== */

/* The page's own action. Before this existed, every Save, Copy and Apply was a
   raw browser button sitting in a styled page. */
button.primary {
  display: inline-flex;
  align-items: center;
  gap: var(--space-2);
  height: var(--control-h);
  padding: 0 var(--control-x-lg);
  border: 0;
  border-radius: var(--radius-pill);
  background: var(--primary);
  color: var(--on-primary);
  font: inherit;
  font-size: var(--text-5);
  font-weight: 600;
  cursor: pointer;
  transition: background var(--motion-short) var(--easing);
}
button.primary:hover { background: var(--on-primary-cont); }
button.primary:focus-visible { outline: 2px solid var(--primary); outline-offset: 2px; }
button.primary:disabled { opacity: .45; cursor: default; }

/* Tonal: the same button at the weight a batch action deserves. "Enable all"
   is affirmative but it is not the page's primary action — Save is — so it
   takes the container colour rather than the full primary. */
button.primary.is-tonal { background: var(--secondary-cont); color: var(--on-secondary-cont); }
button.primary.is-tonal:hover { background: var(--surface-est); }

/* Destructive keeps the same shape and changes only the colour, so it reads as
   the same kind of control rather than as a different widget. */
button.primary.danger { background: var(--error); color: var(--surface); }
button.primary.danger:hover { background: color-mix(in srgb, var(--error) 80%, var(--ink)); }

/* The quiet button. Written as a bare class rather than `button.ghost` so an
   anchor can take it — an export endpoint that sets its own filename wants a
   real link, and a second near-identical block for `a.ghost` is exactly the
   duplication the one styling rule exists to prevent. */
.ghost {
  background: transparent;
  /* An anchor is inline and would sit on the text baseline beside the buttons;
     the buttons are already inline-flex boxes. */
  display: inline-flex;
  align-items: center;
  text-decoration: none;
  /* A boundary somebody has to find, so `--control-edge` (3:1 or better on
     every content surface) rather than `--edge`, a divider at 1.31:1; and
     the ink one rung up, `--outline`. Both since 2026-09-28. */
  border: 1px solid var(--control-edge);
  color: var(--outline);
  border-radius: var(--radius-pill);
  height: var(--control-h);
  padding: 0 var(--control-x);
  font: inherit;
  font-size: var(--text-5);
  cursor: pointer;
  transition: color var(--motion-short) var(--easing),
              border-color var(--motion-short) var(--easing);
}
.ghost:hover { color: var(--on); border-color: var(--outline); }
.ghost:disabled { opacity: .45; cursor: default; }
.ghost:focus-visible { outline: 2px solid var(--primary); outline-offset: 1px; }
.ghost.is-small { height: var(--control-h-sm); padding: 0 var(--control-x-sm); font-size: var(--text-3); }
.ghost.danger { color: var(--error); border-color: color-mix(in srgb, var(--error) 75%, transparent); }
.ghost.danger:hover { color: var(--error); border-color: var(--error); }

/* An icon alone. Close is the X at the top-right of the pane it closes,
   everywhere. Give it an aria-label: this is the one place an icon is the
   only label, and the builder deliberately marks every glyph aria-hidden. */
.icon-btn {
  display: grid;
  place-items: center;
  flex: 0 0 auto;
  width: var(--control-h-lg);
  height: var(--control-h-lg);
  border: 0;
  border-radius: var(--radius-pill);
  background: var(--surface-cont);
  color: var(--on-variant);
  cursor: pointer;
  transition: background var(--motion-short) var(--easing);
}
.icon-btn:hover { background: var(--surface-high); }
.icon-btn:focus-visible { outline: 2px solid var(--primary); outline-offset: 2px; }
.icon-btn.is-quiet {
  width: var(--control-h);
  height: var(--control-h);
  background: transparent;
  color: var(--dim);
}
.icon-btn.is-quiet:hover { background: var(--surface-cont); color: var(--on); }
.icon-btn.is-small { width: var(--control-h-sm); height: var(--control-h-sm); }
.icon-btn.is-small svg { width: var(--icon-size-sm); height: var(--icon-size-sm); }

/* The commit button, at the right of a header. Large, so the place a change is
   committed is the same size on every screen. */
.save-btn {
  display: inline-flex;
  align-items: center;
  gap: var(--space-2);
  height: var(--control-h-lg);
  padding: 0 var(--control-x-lg);
  border: 0;
  border-radius: var(--radius-pill);
  background: var(--primary);
  color: var(--on-primary);
  font: inherit;
  font-size: var(--text-6);
  font-weight: 500;
  cursor: pointer;
}
.save-btn:disabled { background: var(--surface-cont); color: var(--fainter); cursor: default; }
.save-btn:focus-visible { outline: 2px solid var(--primary); outline-offset: 2px; }

/* Quiet when nothing is pending, which is most of the time. It shares the
   tallest control's height so the state reads as part of the control group it
   describes rather than as text that happens to sit beside it. */
.save-state {
  display: inline-flex;
  align-items: center;
  height: var(--control-h-lg);
  font-size: var(--text-4);
  color: var(--faint);
  white-space: nowrap;
}
.save-state.is-dirty { gap: 6px; color: var(--warn); }
.save-state.is-dirty::before {
  content: "";
  width: 6px;
  height: 6px;
  border-radius: var(--radius-pill);
  background: var(--warn);
}

/* A full-width way OUT of a reading column, to a screen that column can only
   point at. The one exception to "no controls in the reading column", and it
   is not really a control. Nothing that opens a level of the same screen
   belongs here — that is a second copy of a gesture already on the row. */
.side-action {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: var(--space-2);
  width: 100%;
  height: var(--control-h-lg);
  margin-top: var(--space-3);
  border: 0;
  border-radius: var(--radius-pill);
  background: var(--secondary-cont);
  color: var(--on-secondary-cont);
  font: inherit;
  font-size: var(--text-6);
  font-weight: 500;
  cursor: pointer;
  transition: background var(--motion-short) var(--easing);
}
.side-action:hover { background: var(--surface-est); }
.side-action:focus-visible { outline: 2px solid var(--primary); outline-offset: 2px; }


/* ===========================================================================
   2. SWITCH

   A real focusable checkbox under a track. The input covers the whole control
   rather than collapsing to 0x0, so it keeps its own focus, its own click
   target and its own announcement — a div with a click handler would have
   needed all three rebuilt by hand and would have got one of them wrong.

   The entire on/off geometry is here, reacting to `:checked`. Script toggles
   nothing but the checkbox's own state.
   =========================================================================== */

.switch {
  position: relative;
  display: inline-block;
  width: 42px;
  height: 24px;
  flex: 0 0 auto;
}
.switch input {
  position: absolute;
  inset: 0;
  opacity: 0;
  margin: 0;
  width: 100%;
  height: 100%;
  cursor: pointer;
  z-index: 1;
}
.switch .slider {
  position: absolute;
  inset: 0;
  /* The off state is an *outlined* track — which is what keeps a row of
     switches legible when most of them are off. Filling it with the brightest
     surface in the palette makes an off switch read as nearly as lit as an on
     one, and a row of things waiting on setup looks like a row of live ones. */
  background: var(--surface-cont);
  border: 2px solid var(--field-border);
  border-radius: var(--radius-pill);
  cursor: pointer;
  transition: background var(--motion-short) var(--easing),
              border-color var(--motion-short) var(--easing);
}
/* The thumb grows when it turns on — which is the detail that makes the state
   readable without relying on the track colour alone, and so survives being
   looked at by somebody who cannot tell violet from grey. */
.switch .slider::before {
  content: "";
  position: absolute;
  width: 13px;
  height: 13px;
  left: 4px;
  top: 50%;
  border-radius: 50%;
  /* The off thumb is a quiet ink, not near-white: on is a filled track with a
     grown thumb, and an off thumb bright enough to be the loudest thing in the
     row was competing with it. */
  background: var(--dim);
  transform: translateY(-50%);
  transition: width var(--motion-short) var(--easing),
              height var(--motion-short) var(--easing),
              left var(--motion-short) var(--easing),
              background var(--motion-short) var(--easing);
}
.switch input:checked + .slider { background: var(--primary); border-color: var(--primary); }
.switch input:checked + .slider::before {
  width: 17px;
  height: 17px;
  left: 19px;
  background: var(--on-primary);
}

/* On a selected row the switch takes the sage. Sage means one thing everywhere
   — this is the thing you are looking at — and the switch is part of that row,
   not an exception to it. The on/off reading does not depend on the hue: the
   thumb still grows and the track still fills. */
.row.is-selected .switch input:checked + .slider { background: var(--accent-2); border-color: var(--accent-2); }
.row.is-selected .switch input:checked + .slider::before { background: var(--accent-2-cont); }

.switch input:focus-visible + .slider { outline: 2px solid var(--primary); outline-offset: 2px; }

/* Nothing to switch. Drawn inert rather than dimmed: `opacity` fades the
   outline out with everything else and leaves a bright thumb floating on a
   mid-purple pill, which reads as a switch somebody greyed out by accident
   rather than as one there is nothing behind. Explicit colours keep the
   outline, which is the part that says "this is a switch". */
.switch input:disabled { cursor: not-allowed; }
.switch input:disabled + .slider {
  cursor: not-allowed;
  background: var(--surface-low);
  border-color: var(--edge);
}
.switch input:disabled + .slider::before { background: var(--fainter); }
/* On, and not yours to turn off. It still has to read as on, so it keeps the
   fill and takes the container tone rather than the full primary. */
.switch input:disabled:checked + .slider { background: var(--primary-cont); border-color: var(--primary-cont); }
.switch input:disabled:checked + .slider::before { background: var(--surface-low); }

/* The dense variant, for a list forty rows deep rather than six. The density
   is the difference, not the meaning. */
.switch.switch-sm { width: 33px; height: 19px; }
.switch.switch-sm .slider { border-width: 2px; }
.switch.switch-sm .slider::before { width: 9px; height: 9px; left: 3px; }
.switch.switch-sm input:checked + .slider::before { width: 13px; height: 13px; left: 15px; }


/* ===========================================================================
   3. CHIPS AND TAGS

   One shape, several meanings, separated by a modifier rather than by a set of
   near-identical rules.
   =========================================================================== */

.chip {
  display: inline-flex;
  align-items: center;
  gap: 4px;
  max-width: 100%;
  padding: var(--chip-y) var(--chip-x);
  border: 1px solid var(--edge);
  border-radius: var(--radius-sm);
  background: var(--surface-cont);
  color: var(--outline);
  font-size: var(--text-3);
  line-height: 1.6;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.chip .icon { --icon-size: 13px; }

/* A granted permission, a code-ish value. */
.chip-grant { font-family: var(--mono); color: var(--on-secondary-cont); background: var(--secondary-cont); border-color: transparent; }
/* A variable name — something an operator has to go and fix. */
.chip-env { font-family: var(--mono); color: var(--dim); }
/* A classification, not a state: quieter than `.chip-env`. */
.chip-facet { color: var(--dim); font-style: italic; }
.chip-warn { color: var(--warn); border-color: color-mix(in srgb, var(--warn) 34%, transparent); background: color-mix(in srgb, var(--warn) 10%, var(--surface)); }
.chip-danger { color: var(--error); border-color: color-mix(in srgb, var(--error) 34%, transparent); background: color-mix(in srgb, var(--error) 10%, var(--surface)); }
/* Not a "success" colour — there is none in this palette. Sage here means the
   same thing it means everywhere: this is the live, current state. */
.chip-on { color: var(--on-accent-2-cont); background: var(--accent-2-cont); border-color: transparent; }

/* Tag colours are the tag's meaning, so the class carries the meaning rather
   than the colour. An unrecognised tag falls through to the plain chip. */
.tag-money { color: var(--tag-money); border-color: color-mix(in srgb, var(--tag-money) 34%, transparent); }
.tag-destructive { color: var(--tag-destructive); border-color: color-mix(in srgb, var(--tag-destructive) 34%, transparent); }
.tag-pii { color: var(--tag-pii); border-color: color-mix(in srgb, var(--tag-pii) 34%, transparent); }
.tag-beta { color: var(--tag-beta); border-color: color-mix(in srgb, var(--tag-beta) 34%, transparent); }
.tag-bulk { color: var(--tag-bulk); border-color: color-mix(in srgb, var(--tag-bulk) 34%, transparent); }

/* Violet-FILLED rather than outlined, which is the one place a chip is filled:
   it marks a row that is a different *kind* of thing from the rows under it,
   and an outline would read as one more tag. */
.chip-lead {
  color: var(--on-primary-cont);
  background: var(--primary-cont);
  border-color: transparent;
  font-weight: 600;
  letter-spacing: .05em;
  text-transform: uppercase;
  font-size: var(--text-1);
  padding: var(--chip-y-sm) var(--chip-x-sm);
}

/* The tiny uppercase mark that says what a row IS without competing with its
   name. It carries one state, because "this one writes" is the only thing
   about a kind worth stopping on. */
.chip-kind {
  font-size: var(--text-1);
  letter-spacing: .05em;
  text-transform: uppercase;
  color: var(--dim);
  padding: var(--chip-y-sm) var(--chip-x-sm);
}
.chip-kind.is-write { color: var(--warn); border-color: color-mix(in srgb, var(--warn) 30%, transparent); }

/* A monospaced value with a box round it — an id, a version, an env name. */
.pill {
  font-family: var(--mono);
  font-size: var(--text-5);
  color: var(--dim);
  background: var(--surface-cont);
  border: 1px solid var(--edge);
  border-radius: var(--radius-pill);
  padding: var(--chip-y) var(--chip-x);
}


/* ===========================================================================
   4. ROWS AND LISTS

   The shape every list on every screen borrows: a mark, a growing middle, and
   a fixed control on the right.

   Rows live in a `.row-list`, which is one bordered block with the rows ruled
   inside it. Separately outlined and spaced rows make three screens that hold
   the same kind of list look like three different apps. A list is read as a
   block and picked from one row at a time, and the block is what says where it
   stops.
   =========================================================================== */

/* One rule for both bordered groups, so a list of things and a list of
   settings cannot drift apart — which is exactly what they do when each gets
   its own. A third kind of list gets added to this selector, not given its own
   block. */
.row-list,
.field-rows {
  border: 1px solid var(--edge);
  border-radius: var(--radius);
  overflow: hidden;
}

.row {
  display: flex;
  align-items: center;
  gap: 13px;
  padding: var(--row-y) var(--row-x);
  border-bottom: 1px solid var(--rule);
  cursor: pointer;
  transition: background var(--motion-short) var(--easing);
}
.row:last-child { border-bottom: 0; }
.row:hover { background: var(--surface-low); }
.row:focus-visible { outline: 2px solid var(--primary); outline-offset: -2px; }

/* Sage means "this is the thing you are looking at", here as everywhere. An
   inset shadow rather than a border-left, so the row does not shift by a pixel
   when it becomes the selected one. */
.row.is-selected {
  background: color-mix(in srgb, var(--accent-2) 7%, transparent);
  box-shadow: inset 3px 0 0 var(--accent-2);
}

.row-mark {
  display: grid;
  place-items: center;
  flex: 0 0 auto;
  color: var(--primary);
}
.row-mark svg { width: 21px; height: 21px; }
.row.is-selected .row-mark { color: var(--accent-2); }
.row.is-off .row-mark, .row.is-unavailable .row-mark { color: var(--fainter); }

/* For a row with no glyph of its own: the first letter of its name, which is
   the one thing that is always different between two rows on that screen. */
.row-avatar {
  display: grid;
  place-items: center;
  flex: 0 0 auto;
  width: 34px;
  height: 34px;
  border-radius: 10px;
  background: var(--surface-cont);
  color: var(--dim);
  font-size: var(--text-7);
  font-weight: 500;
  text-transform: uppercase;
}
.row.is-selected .row-avatar { background: var(--accent-2-cont); color: var(--on-accent-2-cont); }
.row-avatar.is-small { width: 30px; height: 30px; font-size: var(--text-5); }

.row-body { flex: 1 1 auto; min-width: 0; }
.row-title {
  display: flex;
  align-items: baseline;
  gap: var(--space-2);
  flex-wrap: wrap;
  margin-bottom: 2px;
}
.row-name { font-size: var(--text-7); font-weight: 600; white-space: nowrap; }
.row-slug { font-family: var(--mono); font-size: var(--text-3); color: var(--faint); }
/* One line of what a thing says about itself, cut where the row ends. The
   whole paragraph is behind the hint marker on the name beside it. */
.row-desc {
  margin-top: 1px;
  font-size: var(--text-4);
  color: var(--dim);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
/* On the ladder rather than 3px: the chips' boxes sat a hair under the line
   above. Costs 1px, and a three-line row (81px) still fits `--row-h-3`. */
.row-chips { display: flex; flex-wrap: wrap; gap: var(--space-1); margin-top: var(--space-1); }

/* The answer this row ALREADY carries, stated above the control that would
   replace it. A chip says a standing answer exists; this says what it was and
   what the next press does to it — which is the difference between a person
   knowing and a person finding out afterwards. `.is-warn` is for the answer
   that is about to be overwritten rather than merely added to. It wraps, on
   purpose: it quotes somebody's own sentence, and a quotation cut off at the
   row's edge is the half of it nobody reads. */
.row-standing {
  display: flex;
  align-items: flex-start;
  gap: var(--space-2);
  margin-top: var(--space-2);
  padding: var(--space-2) var(--space-3);
  border-left: 2px solid var(--edge);
  border-radius: var(--radius-sm);
  background: var(--surface-cont);
  color: var(--on-variant);
  font-size: var(--text-4);
  line-height: 1.45;
}
.row-standing [data-icon] { flex: 0 0 auto; color: var(--dim); margin-top: 1px; }
.row-standing [data-icon] svg { width: var(--icon-size-sm); height: var(--icon-size-sm); }
.row-standing.is-warn {
  border-left-color: var(--warn);
  background: color-mix(in srgb, var(--warn) 7%, var(--surface));
}
.row-standing.is-warn [data-icon] { color: var(--warn); }
.row-aside {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  flex: 0 0 auto;
  margin-left: auto;
}
.row-count {
  text-align: right;
  min-width: 72px;
  font-size: var(--text-2);
  color: var(--faint);
}
.row-count b { display: block; font-size: var(--text-5); font-weight: 400; color: var(--on-variant); }

/* Something that could not load is dimmed rather than hidden: the operator
   needs to see that it exists and what it is waiting for. */
.row.is-unavailable .row-name,
.row.is-unavailable .row-desc { color: var(--dim); }

/* -------------------------------------------------------- the pinned row --
   The thing above the things. A parent is the FIRST ROW OF THE SAME LIST, not
   a card above it — it is one of these things, and a separate card would say
   it was a different screen with its own rules.

   Four marks, all geometry rather than copy: a violet left edge (the list's
   only one), a filled `.chip-lead`, a rule under it heavy enough to say
   everything below is inside it, and `.row.is-child` inset on the rows that
   belong to it. */
.row.is-pinned {
  background: color-mix(in srgb, var(--primary) 6%, transparent);
  box-shadow: inset 3px 0 0 var(--primary);
  border-bottom: 1px solid var(--edge);
  padding-top: var(--row-y-block);
  padding-bottom: var(--row-y-block);
}
.row.is-pinned:hover { background: color-mix(in srgb, var(--primary) 10%, transparent); }
.row.is-pinned .row-mark { color: var(--primary); }
.row.is-pinned .row-name { font-size: var(--text-8); }

/* Children, inset so the hierarchy is in the geometry and not only in the
   copy. Still an ordinary row in every other respect — the inset is the whole
   of what makes it a child. */
.row.is-child { padding-left: calc(var(--row-x) + 14px); }
.row.is-child .row-mark { color: var(--dim); }
.row.is-child.is-selected .row-mark { color: var(--accent-2); }

/* ----------------------------------------------------------- the add row --
   THE ONE ADD GESTURE, everywhere a list can be added to. A quiet trailing row
   inside the group's own border, because it IS the next row of the list —
   which is what makes "one more of these" read as ordinary rather than as a
   mode you entered. Present even when the list is empty.

   At rest a dashed ghost; clicked, it becomes the field in place, and Enter
   adds. It takes ONE value, then opens whatever needs more than one. */
.add-row {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  width: 100%;
  padding: var(--row-y) var(--row-x);
  border: 0;
  border-top: 1px dashed var(--rule);
  background: transparent;
  color: var(--faint);
  font: inherit;
  font-size: var(--text-5);
  text-align: left;
  cursor: pointer;
  transition: color var(--motion-short) var(--easing),
              background var(--motion-short) var(--easing);
}
.row-list > .add-row:first-child,
.field-rows > .add-row:first-child { border-top: 0; }
.add-row:hover { color: var(--on); background: var(--surface-low); }
.add-row:focus-visible { outline: 2px solid var(--primary); outline-offset: -2px; }
.add-row [data-icon] { display: grid; place-items: center; }
.add-row .icon { --icon-size: var(--icon-size-sm); }
/* Opened. The row is now the field, at the row's own inset. */
.add-row.is-open { cursor: default; background: var(--surface-low); }
.add-row.is-open input { flex: 1 1 auto; min-width: 0; }


/* ===========================================================================
   5. SECTIONS AND FIELDS

   Every section of every level is the same two things: a head, and one
   bordered group of rows under it. Head, box, head, box, the whole way down.
   =========================================================================== */

/* A head and the group under it are one section, so the gap between them is
   smaller than the gap between two sections. */
.level-section-head {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  margin-bottom: var(--space-2);
}
.level-section-head .side-label { padding: 0; }
.level-section-n { font-family: var(--mono); font-size: var(--text-2); color: var(--fainter); }

/* The gap after a section's box when the next section's head follows it inside
   the same block. Both bordered groups are one vocabulary, so the space after
   them is one rule too; naming only `.field-rows` lets a `.row-list` sit flush
   against the next heading, which reads as one long list with a label in the
   middle of it. */
.row-list + .level-section-head,
.field-rows + .level-section-head { margin-top: 26px; }

/* Sections are separated by space alone. A rule between them was right when
   each section was loose rows on a bare surface; now every section is a
   bordered group, and a line between two boxes is a third edge doing nothing.

   `~` with `:not([hidden])` on both ends asks the question the rule is trying
   to ask — is there a visible section above this one — instead of asking about
   whichever sibling happens to be adjacent in the markup. With `+`, a level
   that shows two of its five sections has the gap cancelled by the hidden
   blocks between them. */
.level-block { scroll-margin-top: var(--space-4); }
.level-block:not([hidden]) ~ .level-block:not([hidden]) { margin-top: 26px; }

/* What makes rows one group is the box round them. Every row of three lines
   or fewer is held to its list's height (section 14); a block row is whatever
   height its contents need. The gaps here are put on the ladder in section 17. */
.field-row {
  display: flex;
  align-items: center;
  gap: 16px;
  padding: var(--row-y) var(--row-x);
  border-bottom: 1px solid var(--rule);
}
.field-row:last-child { border-bottom: 0; }
/* A row that stacks holds a paragraph, a form or a card rather than a line, so
   it takes the taller inset — the line-height that keeps a one-line row from
   looking cramped is not enough air round a block. */
.field-row.is-block { display: block; padding: var(--row-y-block) var(--row-x); }
.field-row-body { min-width: 0; flex: 1 1 auto; }
.field-row-label { font-size: var(--text-6); color: var(--on-variant); }
/* The sentence under a label is not optional — a switch that does not say when
   it bites is a switch somebody flips twice. */
.field-row-hint {
  font-size: var(--text-3);
  color: var(--faint);
  margin-top: 1px;
  max-width: 66ch;
}
/* A block row's contents bring no margin of their own: the row's inset is the
   spacing, and a child with a margin on top of it reads as a second, wider
   row. */
.field-row > p:first-child { margin-top: 0; }
.field-row > :last-child { margin-bottom: 0; }
.field-row.is-block > .scope-grid { margin-top: 10px; }
/* A block row's label, then the block it labels: a textarea, a quote, a
   form. Flush, the box of a textarea touched the hint above it and the two
   read as one control. Costs `--space-2` in each block row that has a label. */
.field-row.is-block > :is(.field-row-body, .field-row-label, .field-row-hint) + :not(.field-row-hint, .scope-grid) {
  margin-top: var(--space-2);
}
/* A quote is already inside the group's box, so it draws none of its own — a
   box in a box reads as two things when it is one. Outside a group, where
   there is nothing round it, it keeps its edge. */
.field-row > .side-quote {
  margin: 0;
  width: 100%;
  border: 0;
  border-radius: 0;
  padding: 0;
  background: none;
}

/* A row that opens a level of its own, because the act is not "change a
   value": a thing that is a name, a token, a switch, a note and fifteen grants
   reads better on its own than scattered among the fields. The row summarises;
   the gear opens it.

   Everything else on a settings page is live. A folded control is a control
   somebody has to discover, and nothing is folded here to tidy the page. */
.field-row.is-grouped { cursor: pointer; transition: background var(--motion-short) var(--easing); }
.field-row.is-grouped:hover { background: var(--surface-low); }

/* The pencil that turns something read into something editable. On a section
   head it opens the whole section; on a row it opens that row. Which one it is
   is told by where it sits. */
.edit-control {
  display: grid;
  place-items: center;
  flex: 0 0 auto;
  width: var(--control-h-sm);
  height: var(--control-h-sm);
  border: 0;
  border-radius: var(--radius-pill);
  background: transparent;
  color: var(--faint);
  cursor: pointer;
  transition: color var(--motion-short) var(--easing),
              background var(--motion-short) var(--easing);
}
.edit-control:hover { color: var(--accent-2); background: var(--surface-cont); }
.edit-control:focus-visible { outline: 2px solid var(--primary); outline-offset: 1px; }
.edit-control svg { width: var(--icon-size-sm); height: var(--icon-size-sm); }


/* ===========================================================================
   6. SCOPE GRID

   A tick list laid across the width rather than down a column: fifteen grants
   in a column is a scroll, and the thing being read is which of them are on,
   which is a shape rather than a list. The checkbox is real and hidden — the
   chip is its label, so a click, a tab and a space all work without one line
   of script.
   =========================================================================== */

.scope-grid { display: flex; flex-wrap: wrap; gap: var(--space-2); }
.scope {
  display: inline-flex;
  align-items: center;
  gap: 5px;
  padding: var(--chip-y) var(--chip-x);
  border: 1px solid var(--edge);
  border-radius: var(--radius-pill);
  font-size: var(--text-5);
  color: var(--dim);
  cursor: pointer;
  user-select: none;
  transition: color var(--motion-short) var(--easing),
              border-color var(--motion-short) var(--easing);
}
.scope input { position: absolute; opacity: 0; width: 0; height: 0; }
/* The tick takes its space back rather than appearing beside the word and
   shoving it: the chip's width should not change when it is ticked. */
.scope .icon { --icon-size: 14px; opacity: 0; margin-left: -4px; transition: opacity var(--motion-short) var(--easing); }
.scope:has(input:checked) { border-color: var(--primary); background: var(--secondary-cont); color: var(--on-secondary-cont); }
.scope:has(input:checked) .icon { opacity: 1; margin-left: 0; }
.scope:has(input:focus-visible) { outline: 2px solid var(--primary); outline-offset: 1px; }
/* A grant somebody cannot change here. The same fade every other disabled
   control takes. */
.scope:has(input:disabled) { opacity: .45; cursor: not-allowed; }


/* ===========================================================================
   7. FORM CONTROLS
   =========================================================================== */

/* The `[type=...]` filters go through `:where()` so they add no specificity of
   their own. This is the *default* a field falls back to, and a component that
   has already drawn its own — a search pill, a single underlined line — has to
   be able to win with one class.

   Without `:where()` it cannot: `input[type="search"]` and `.my-search input`
   are both (0,1,1), this rule is further down the file, and ties go to the
   last one. Every search field then draws a bordered 37px box *inside* the
   32px pill that was supposed to contain it. */
input:where([type="text"], [type="number"], [type="search"],
            [type="password"], [type="email"], [type="date"]),
select, textarea {
  background: var(--field);
  border: 1px solid var(--field-border);
  border-radius: var(--radius-sm);
  color: var(--on);
  font: inherit;
  font-size: var(--text-7);
  padding: var(--field-y) var(--field-x);
  transition: border-color var(--motion-short) var(--easing),
              box-shadow var(--motion-short) var(--easing);
}
input:where([type="text"], [type="number"], [type="search"],
            [type="password"], [type="email"], [type="date"]):hover,
select:hover, textarea:hover { border-color: var(--field-border-hover); }
select { cursor: pointer; }
textarea { resize: vertical; }
input:focus-visible, select:focus-visible, textarea:focus-visible {
  outline: none;
  border-color: var(--primary);
  box-shadow: 0 0 0 1px var(--primary);
}
input::placeholder, textarea::placeholder { color: var(--faint); }

/* `:where()` for the same reason the rule above uses one: this is the default
   a disabled field falls back to, and it must not out-specify a component that
   has deliberately hidden its own input. `.switch input` is a real focusable
   control stretched over the thing that draws it, held at `opacity: 0` — and
   `input:disabled` is (0,1,1) like `.switch input`, further down the file, so
   it wins the tie and turns the browser's own checkbox back on at half
   opacity. That is a grey square in the middle of every disabled pill. */
input:where(:disabled), select:where(:disabled), textarea:where(:disabled) {
  opacity: .5;
  cursor: not-allowed;
}

/* A long-form field, at the width the prose will be read at. Monospaced
   because what is typed here is judged by its length as much as its shape. */
textarea.editor-area {
  min-height: 264px;
  font-family: var(--mono);
  font-size: var(--text-5);
  line-height: 1.6;
}

/* A long-form field that grows with its text, so the pane it sits in is the
   one thing that scrolls. A fixed box scrolling inside a scrolling pane made
   the screen hard to scroll at all: the wheel caught on the box halfway down
   and the rest of the page would not move. Six lines at the least. Where
   `field-sizing` is not supported it stays an ordinary textarea, which still
   works, so the inner scrollbar is only taken away where it is. Long text
   is still edited in a drawer (PATTERNS.md); this is for a field of a few
   paragraphs in a form. */
textarea.editor-area.is-grow {
  width: 100%;
  min-height: calc(6lh + 2 * var(--field-y));
  field-sizing: content;
}
@supports (field-sizing: content) {
  textarea.editor-area.is-grow { overflow-y: hidden; resize: none; }
}

/* A number field beside its own unit — "$" on the left, "%" on the right — has
   no room for a spinner, and nothing is nudged one step at a time. The
   keyboard still increments; the arrows just stop taking width from a figure
   that decides how much money something can spend. */
.num-field input[type="number"] { appearance: textfield; -moz-appearance: textfield; }
.num-field input[type="number"]::-webkit-outer-spin-button,
.num-field input[type="number"]::-webkit-inner-spin-button {
  appearance: none;
  -webkit-appearance: none;
  margin: 0;
}

/* ------------------------------------------------------------- checkbox --
   Drawn rather than accent-coloured, because the native control cannot show
   a third state. `indeterminate` is not decoration where a field holding null
   and one holding false resolve differently — the box has to say which.

   `.switch input` and `.scope input` are excluded: they are checkboxes too,
   but they are invisible by design and the track or the chip is the thing you
   see. */
input[type="checkbox"]:not(.switch input):not(.scope input) {
  appearance: none;
  -webkit-appearance: none;
  flex: 0 0 auto;
  width: 17px;
  height: 17px;
  margin: 0;
  border: 2px solid var(--outline);
  border-radius: 5px;
  background: transparent;
  cursor: pointer;
  display: inline-grid;
  place-items: center;
  transition: background var(--motion-short) var(--easing),
              border-color var(--motion-short) var(--easing);
}
input[type="checkbox"]:not(.switch input):not(.scope input):checked {
  background: var(--primary);
  border-color: var(--primary);
}
input[type="checkbox"]:not(.switch input):not(.scope input):checked::after {
  content: "";
  width: 9px;
  height: 5px;
  border: 2px solid var(--on-primary);
  border-top: 0;
  border-right: 0;
  transform: translateY(-1px) rotate(-45deg);
}
input[type="checkbox"]:not(.switch input):not(.scope input):indeterminate {
  background: var(--surface-est);
  border-color: var(--field-border-hover);
}
input[type="checkbox"]:not(.switch input):not(.scope input):indeterminate::after {
  content: "";
  width: 9px;
  height: 2px;
  background: var(--on-variant);
}
input[type="checkbox"]:not(.switch input):not(.scope input):focus-visible {
  outline: 2px solid var(--primary);
  outline-offset: 2px;
}

/* ---------------------------------------------------------------- radio -- */
input[type="radio"] {
  appearance: none;
  -webkit-appearance: none;
  flex: 0 0 auto;
  width: 17px;
  height: 17px;
  margin: 0;
  border: 2px solid var(--outline);
  border-radius: 50%;
  background: transparent;
  cursor: pointer;
  display: inline-grid;
  place-items: center;
  transition: border-color var(--motion-short) var(--easing);
}
input[type="radio"]:checked { border-color: var(--primary); }
input[type="radio"]:checked::after {
  content: "";
  width: 9px;
  height: 9px;
  border-radius: 50%;
  background: var(--primary);
}
input[type="radio"]:focus-visible { outline: 2px solid var(--primary); outline-offset: 2px; }

/* ------------------------------------------------------------ segmented --
   Two or three mutually exclusive words on one control. Real radios under the
   labels, so a tab and an arrow key work. */
.segmented {
  display: inline-flex;
  padding: 2px;
  background: var(--surface-cont);
  border-radius: var(--radius-pill);
}
.segmented-option {
  position: relative;
  display: inline-flex;
  align-items: center;
  gap: 5px;
  height: calc(var(--control-h) - 4px);
  padding: 0 var(--control-x);
  border-radius: var(--radius-pill);
  color: var(--dim);
  font-size: var(--text-5);
  cursor: pointer;
  user-select: none;
  white-space: nowrap;
  transition: background var(--motion-short) var(--easing),
              color var(--motion-short) var(--easing);
}
.segmented-option input { position: absolute; inset: 0; opacity: 0; margin: 0; cursor: pointer; }
.segmented-option:has(input:checked) { background: var(--primary-cont); color: var(--on-primary-cont); }
.segmented-option:has(input:focus-visible) { outline: 2px solid var(--primary); outline-offset: 1px; }

/* A search field: a pill with a glyph in it, not a bordered box. */
.search-field {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  height: var(--control-h);
  padding: 0 var(--control-x);
  background: var(--surface-cont);
  border-radius: var(--radius-pill);
  color: var(--faint);
}
.search-field [data-icon] { display: grid; place-items: center; flex: 0 0 auto; }
.search-field [data-icon] svg { width: var(--icon-size-sm); height: var(--icon-size-sm); }
.search-field input {
  flex: 1 1 auto;
  min-width: 0;
  background: transparent;
  border: 0;
  padding: 0;
  font-family: var(--mono);
  font-size: var(--text-3);
  color: var(--on);
}
.search-field input::placeholder { color: var(--fainter); }
.search-field input:focus { outline: none; }
.search-field:focus-within { outline: 2px solid var(--primary); outline-offset: 1px; }


/* ===========================================================================
   8. THE INFORMATION VOCABULARY

   A small-caps heading, a key and its value, a quotation of prose this app did
   not write, and a figure meant to be compared. Read rather than edited.

   Every pane reads in the same order: overview (description, then tiles, then
   stats), then anything quoted, then whatever is particular to that screen,
   and anything that deletes or removes last.
   =========================================================================== */

/* Section headings. Each carries the colour of what it is about: sage for what
   this thing *is*, violet for what you can change, blue for what a machine
   gets told. */
.side-label {
  padding: var(--space-4) 0 5px;
  font-size: var(--text-2);
  font-weight: 600;
  letter-spacing: .09em;
  text-transform: uppercase;
  color: var(--accent-2);
}
.side-label-alt  { color: var(--primary); }
.side-label-ctx  { color: var(--info); }
.side-label-warn { color: var(--warn); }

/* The inset moves from the label to the row, so an action sits on the label's
   own line rather than pushing it down. */
.side-label-row {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  padding: var(--space-4) 0 5px;
}
.side-label-row > .side-label { flex: 1 1 auto; padding: 0; }

/* What a thing IS, in the app's own words. Unboxed, directly under the
   overview label, because boxing it would say it came from somewhere else —
   which is exactly what `.side-quote` says, and why the two are not two
   versions of one thing. */
.side-desc {
  margin: 0 0 var(--space-3);
  font-size: var(--text-5);
  line-height: 1.5;
  color: var(--outline);
}

/* A key and its value on one line, the whole width of the column. */
.side-stat {
  display: flex;
  align-items: baseline;
  gap: var(--space-3);
  padding: 5px 0;
  border-bottom: 1px solid var(--rule);
}
.side-stat-k { flex: 1 1 auto; font-size: var(--text-4); color: var(--dim); }
.side-stat-v {
  flex: 0 0 auto;
  font-family: var(--mono);
  font-size: var(--text-4);
  color: var(--on);
  font-variant-numeric: tabular-nums;
}

/* A FIGURE somebody scans for and compares. A tile costs more space than a
   line of text and earns it by being compared with the tile beside it — so a
   fact that is read once and never compared is a `.side-stat`, not a tile. A
   tile round a sentence is a card with a sentence in it. */
.side-tiles {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(132px, 1fr));
  gap: var(--space-2);
}
.side-tile {
  padding: 9px 11px;
  border: 1px solid var(--edge);
  border-radius: var(--radius-sm);
  background: var(--surface);
}
.side-tile-v {
  font-family: var(--font-display);
  font-size: var(--text-12);
  font-weight: 500;
  letter-spacing: -.02em;
  color: var(--on);
  /* Figures in a row of tiles are compared down the column, and proportional
     digits make two four-digit numbers different widths. */
  font-variant-numeric: tabular-nums;
}
.side-tile-k { margin-top: 1px; font-size: var(--text-3); color: var(--dim); }
/* Figures, then facts: a stat straight after the tiles carries no top rule of
   its own, so without this its text sits against the tiles' bottom edge. */
.side-tiles + .side-stat { margin-top: var(--space-2); }
/* The one state a figure carries. Amber is "this is wrong", so it is the
   border and the figure together — a tile whose number is fine does not get a
   colour for being a number. */
.side-tile.is-warn { border-color: color-mix(in srgb, var(--warn) 34%, transparent); }
.side-tile.is-warn .side-tile-v { color: var(--warn); }

/* Prose this app did not write, shown as whoever wrote it typed it. The box is
   what says somebody else wrote it. */
.side-quote {
  background: var(--surface);
  border: 1px solid var(--edge);
  border-radius: var(--radius-sm);
  padding: var(--space-3);
}
.side-quote-head {
  display: flex;
  align-items: center;
  gap: 6px;
  font-size: var(--text-2);
  color: var(--faint);
  margin-bottom: 5px;
}
.side-quote-head [data-icon] { display: grid; place-items: center; }
.side-quote-head [data-icon] svg { width: 14px; height: 14px; }
/* Paragraphs and an indented list are how somebody wrote it, and collapsing
   them into one slab makes the longest text on the screen the hardest to read.
   `pre-wrap` changes how whitespace is drawn, not whether the string is
   interpreted — this is still safe to fill with textContent. */
.side-quote p {
  margin: 0;
  font-size: var(--text-4);
  line-height: 1.5;
  color: var(--outline);
  white-space: pre-wrap;
}
.side-quote-foot {
  margin-top: 9px;
  padding-top: 9px;
  border-top: 1px solid var(--rule);
  font-size: var(--text-3);
  color: var(--faint);
}

/* The information pieces carry no margin of their own above them, so two
   blocks met flush: a quote under a quote read as one box with a seam, and a
   description or a form under the last stat sat on its rule. Space between
   any two of them now, the stats themselves excepted, since their rules
   already divide them. Costs `--space-2` between two quotes and `--space-3`
   wherever else a block follows a stat, a tile row or a quote. */
.side-quote + .side-quote { margin-top: var(--space-2); }
:is(.side-stat, .side-tiles, .side-quote) + :is(.side-desc, .side-tiles, .form-panel, .banner, .row-list, .field-rows),
:is(.side-stat, .side-tiles) + .side-quote,
.side-quote + .side-stat { margin-top: var(--space-3); }

/* The gestures that take a thing away. A row rather than a stack, because
   there are at most two and the order they read in is the order they have to
   happen in. They wrap: a narrow column does not always fit two buttons and
   two hint markers on one line. */
.side-actions {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-3);
  margin-top: var(--space-2);
}


/* ===========================================================================
   9. THINGS THAT APPEAR OVER THE TOP
   =========================================================================== */

/* ------------------------------------------------------------- snackbar --
   One element, bottom centre, with an action slot that is only filled when
   there is something honest to put in it. A snackbar built without a revert
   closure renders no button at all rather than offering an undo that does
   nothing. */
.snackbar {
  position: fixed;
  bottom: var(--space-5);
  left: 50%;
  transform: translateX(-50%);
  display: flex;
  align-items: center;
  gap: var(--space-4);
  max-width: min(560px, calc(100vw - 2 * var(--space-5)));
  padding: var(--space-3) var(--space-4);
  background: var(--surface-est);
  color: var(--on);
  border: 1px solid var(--edge);
  border-radius: var(--radius-sm);
  font-size: var(--text-7);
  z-index: 20;
  box-shadow: var(--shadow-snackbar);
  animation: snack-in var(--motion-long) var(--easing) both;
}
.snackbar.is-error { border-color: var(--error); color: var(--error); }
.snackbar-text { flex: 1 1 auto; min-width: 0; }
.snackbar-undo {
  flex: 0 0 auto;
  background: transparent;
  border: 0;
  border-radius: var(--radius-sm);
  padding: 4px var(--space-2);
  color: var(--primary);
  font: inherit;
  font-size: var(--text-5);
  font-weight: 500;
  text-transform: uppercase;
  letter-spacing: .04em;
  cursor: pointer;
}
.snackbar-undo:hover { background: color-mix(in srgb, var(--primary) 16%, transparent); }
.snackbar-undo:focus-visible { outline: 2px solid var(--primary); outline-offset: 1px; }
.snackbar-undo:disabled { opacity: .5; cursor: default; }

@keyframes snack-in {
  from { opacity: 0; transform: translate(-50%, 8px); }
  to   { opacity: 1; transform: translate(-50%, 0); }
}

/* -------------------------------------------------------------- key bar --
   What the keys do for whatever has focus, shown only while someone is using
   the keyboard. Bottom LEFT, because the snackbar owns bottom centre and the
   two are often up at once. It is read, never clicked, so it takes no pointer
   events and cannot swallow a click on whatever it covers.

   `.key-bar-label` names what has focus; sage, because naming the thing you
   are at is sage's job. A key that cannot be used here is drawn `.is-off`
   rather than left out, so the strip keeps its shape as focus moves. */
.key-bar {
  position: fixed;
  left: var(--space-4);
  bottom: var(--space-4);
  z-index: 15;
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-2) var(--space-4);
  max-width: calc(100vw - 2 * var(--space-4));
  box-sizing: border-box;
  padding: var(--space-2) var(--space-3);
  background: var(--surface-est);
  color: var(--dim);
  border: 1px solid var(--edge);
  border-radius: var(--radius-sm);
  font-size: var(--text-3);
  pointer-events: none;
}
.key-bar-label { font-family: var(--mono); color: var(--accent-2); }
.key-bar-item { display: inline-flex; align-items: center; gap: var(--space-1); white-space: nowrap; }
.key-bar-item.is-off { opacity: .45; }
.key-bar kbd {
  padding: 0 var(--space-1);
  font-family: var(--mono);
  font-size: var(--text-2);
  color: var(--on);
  background: var(--surface-high);
  border: 1px solid var(--edge);
  border-radius: var(--radius-sm);
}

/* ---------------------------------------------------------------- hints --
   Explanatory prose, folded behind a marker beside the thing it explains. The
   marker is visible rather than the whole element being hoverable, so there is
   something to tab to and something to point at.

   The marker is styled here. The bubble is ONE node at the end of <body> — a
   popover, in the top layer — and everything about where it goes is decided by
   the script that opens it. Nothing here sets a position, and nothing here
   should: a second opinion about where the bubble goes is a second thing to
   keep in step. Putting it in the top layer is also what stops it being cut
   off by the pane it opens in. */
.hint {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  position: relative;
  flex: 0 0 auto;
  width: 14px;
  height: 14px;
  margin-left: 5px;
  border-radius: 50%;
  border: 1px solid var(--edge);
  background: transparent;
  color: var(--dim);
  font-size: var(--text-1);
  font-weight: 700;
  line-height: 1;
  cursor: help;
  vertical-align: middle;
  user-select: none;
}
.hint:hover, .hint:focus-visible {
  border-color: var(--primary);
  color: var(--primary);
  outline: none;
}
.hint-bubble {
  /* Fixed, at coordinates the script writes. `inset` and `margin` are reset
     because the UA stylesheet gives every `[popover]` `inset: 0; margin: auto`,
     which would centre it in the viewport and ignore both. */
  position: fixed;
  inset: auto;
  left: var(--hint-x, 0);
  top: var(--hint-y, 0);
  margin: 0;
  z-index: 60;
  width: max-content;
  max-width: 320px;
  padding: var(--field-y) var(--field-x);
  border-radius: 8px;
  border: 1px solid var(--edge);
  background: var(--surface-high);
  color: var(--on);
  font-size: var(--text-5);
  font-weight: 400;
  line-height: 1.45;
  text-align: left;
  white-space: normal;
  box-shadow: 0 8px 24px color-mix(in srgb, var(--shade) 45%, transparent);
  /* The accessible copy of this text is the marker's own `aria-label`, so
     nothing is lost by the bubble being invisible until it is placed. */
  opacity: 0;
  visibility: hidden;
  pointer-events: none;
  transition: opacity .12s ease;
}
/* Added only after the bubble has been measured and placed. Everything before
   that happens while it is transparent. */
.hint-bubble.is-open { opacity: 1; visibility: visible; }

/* ---------------------------------------------------------------- banner --
   A full-width message INSIDE the page, not over it. For a condition that
   persists — a bootstrap credential still accepted, a degraded backend — not
   for the result of an action, which is a snackbar. */
.banner {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  padding: var(--space-3) var(--space-4);
  border: 1px solid color-mix(in srgb, var(--warn) 34%, transparent);
  border-radius: var(--radius-sm);
  background: color-mix(in srgb, var(--warn) 8%, var(--surface));
  color: var(--warn);
  font-size: var(--text-5);
}
.banner.is-error {
  border-color: color-mix(in srgb, var(--error) 34%, transparent);
  background: color-mix(in srgb, var(--error) 8%, var(--surface));
  color: var(--error);
}
.banner-text { flex: 1 1 auto; min-width: 0; color: var(--on-variant); }
/* A banner stands above what it is about, so it keeps a gap below it: put at
   the top of a level, it sat flush on the first box under it and the two read
   as one bordered thing. Stacked banners keep a smaller one. Only in the
   columns that stack blocks: a flex panel spaces its own children with a
   gap, and a margin there would double it. Costs `--space-4` under the last
   banner. */
.banner + .banner { margin-top: var(--space-2); }
:is(.level, .level-body, .level-block, .tab-panel, .side-body, .drawer-body, .drawer-pane) > .banner + :not(.banner) {
  margin-top: var(--space-4);
}


/* ===========================================================================
   10. PANELS, FOLDS AND TONES
   =========================================================================== */

/* ------------------------------------------------------------ form panel --
   The part of a reading column somebody fills in, on a surface of its own so
   it reads as *the thing you change* against everything around it that you
   read. Sage, because sage is "the thing you are looking at or editing"
   (tokens, `--accent-2`), at a strength that tints without becoming a
   colour: the fields inside keep their own `--field` hole and border.

     <form class="form-panel">
       <div class="form-panel-head">Add Catherine</div>
       …fields…
       <div class="form-panel-foot"><button class="primary">Add</button></div>
     </form>

   One per thing, like one editor per thing. A second, different kind of
   decision -- saying no -- is the danger panel below, never a second form
   panel. */
.form-panel {
  display: flex;
  flex-direction: column;
  gap: var(--space-3);
  padding: var(--space-4);
  border: 1px solid color-mix(in srgb, var(--accent-2) 40%, var(--edge));
  border-radius: var(--radius-sm);
  background: color-mix(in srgb, var(--accent-2-cont) 65%, var(--surface));
}
/* Two panels -- the form and its danger panel -- are two boxes, apart. */
.form-panel + .form-panel { margin-top: var(--space-3); }
.form-panel-head {
  font-size: var(--text-6);
  font-weight: 600;
  color: var(--on);
  line-height: 1.5;
}
.form-panel-foot {
  display: flex;
  justify-content: flex-end;
  gap: var(--space-2);
  padding-top: var(--space-3);
  border-top: 1px solid var(--rule);
}

/* ----------------------------------------------------------- danger panel --
   Saying no, on its own box: a decline, a retire, a removal. It is not a
   quieter form panel -- it is a different kind of act, so it is drawn in the
   state colour that means *this undoes or refuses*, with the kit's filled
   destructive button (`button.primary.danger`) at its foot. The words above
   the button say what pressing it does; the colour is never the only sign. */
.form-panel.is-danger {
  border-color: color-mix(in srgb, var(--error) 45%, transparent);
  background: color-mix(in srgb, var(--error) 7%, var(--surface));
}
.form-panel.is-danger .form-panel-head { color: var(--error); }

/* ------------------------------------------------------------------ fold --
   Prose somebody may want, folded: a `<details class="fold">`. PATTERNS.md
   says what earns a fold; this is how one looks when it has earned it -- a
   32px line with the kit's small-caps label and a chevron, divided from the
   next by a rule, with no box round it and no padding to speak of. A stack
   of them reads as one list of things you can open.

     <details class="fold">
       <summary>How this screen works</summary>
       <div class="fold-body">…</div>
     </details>
*/
.fold { border-top: 1px solid var(--rule); }
.fold:last-of-type { border-bottom: 1px solid var(--rule); }
.fold > summary {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  min-height: var(--control-h);
  list-style: none;
  cursor: pointer;
  font-size: var(--text-3);
  font-weight: 600;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--dim);
}
.fold > summary::-webkit-details-marker { display: none; }
.fold > summary::after {
  content: "";
  margin-left: auto;
  width: 6px;
  height: 6px;
  border-right: 1.5px solid currentColor;
  border-bottom: 1.5px solid currentColor;
  transform: rotate(45deg) translateY(-2px);
  transition: transform var(--motion-short) var(--easing);
}
.fold[open] > summary::after { transform: rotate(-135deg) translateY(-1px); }
.fold > summary:hover { color: var(--on-variant); }
.fold > summary:focus-visible { outline: 2px solid var(--primary); outline-offset: 2px; }
.fold-body {
  padding: 0 0 var(--space-3);
  font-size: var(--text-5);
  line-height: 1.5;
  color: var(--on-variant);
}
.fold-body > * + * { margin-top: var(--space-2); }

/* ----------------------------------------------------------------- tones --
   A word inside a sentence that says what a thing *is* -- open, closed,
   waiting on you, not yours -- in the colour of that meaning, so a reader
   scanning a paragraph finds the state without reading every word. On the
   word itself, never on a badge beside it, and never colour alone: the word
   is the meaning and the colour is the finding aid.

   The four hues are the kit's own four meanings, spent the same way here as
   everywhere (PATTERNS.md, *Colour*):

     .tone-you     amber   waiting on the reader: needs you, still to decide
     .tone-them    blue    with somebody else: waiting on them, machine-read
     .tone-done    sage    settled: closed, filed, confirmed
     .tone-alert   pink    wrong or refused: blocked, failed, not yours

   [measured 2026-09-28] each is 8.2:1 or better on every content surface.
   Weight 600, like a marked word, so bold and colour say the same thing. */
.tone-you, .tone-them, .tone-done, .tone-alert { font-weight: 600; }
.tone-you   { color: var(--warn); }
.tone-them  { color: var(--info); }
.tone-done  { color: var(--accent-2); }
.tone-alert { color: var(--error); }


/* ===========================================================================
   11. PROGRESS
   =========================================================================== */

/* How far a long job has got: a native `<progress>`, so a screen reader hears
   the value and the maximum without an `aria-` attribute written by hand, and
   the page carries two numbers rather than an inline width. Sage fill, because
   it is the thing being watched; a quiet track rather than an outlined box,
   because a figure beside it says the number and this only shows the share.
   Never the only statement of progress: write "34 of 157" beside it. */
progress.meter {
  display: block;
  width: 100%;
  height: 6px;
  margin: var(--space-2) 0;
  border: 0;
  border-radius: 3px;
  background: var(--surface-cont);
  color: var(--accent-2);
  overflow: hidden;
  appearance: none;
  -webkit-appearance: none;
}
progress.meter::-webkit-progress-bar { background: var(--surface-cont); border-radius: 3px; }
progress.meter::-webkit-progress-value { background: var(--accent-2); border-radius: 3px; }
progress.meter::-moz-progress-bar { background: var(--accent-2); border-radius: 3px; }

/* A glyph that turns while something is happening now. On the glyph's host --
   a `data-icon` element -- never on a row, and never alone: the word beside it
   says what is happening. Reduced motion stops it (base.css), and a still
   glyph beside the word loses nothing. */
@keyframes kit-spin { to { transform: rotate(360deg); } }
.is-spinning > svg, svg.is-spinning {
  animation: kit-spin 1.2s linear infinite;
  transform-origin: 50% 50%;
}

/* A span marked inside quoted prose: the words a claim rests on, found in the
   text it came from. Amber's tint, because it asks to be looked at; the text
   keeps its own ink, so the mark is a finding aid and never the only sign --
   say what is marked beside it. */
.side-quote mark {
  background: color-mix(in srgb, var(--warn) 22%, transparent);
  color: inherit;
  border-radius: 2px;
  padding: 0 1px;
}


/* ===========================================================================
   12. MOTION
   =========================================================================== */

/* Four named motions, each a class the page puts on and takes off. They say
   what happened to a thing, so pick by that: it arrived, it is going, look
   here, or your change was kept. Durations and curves are tokens; reduced
   motion cuts every one of them to nothing (base.css).

   They move with `translate`, not `transform`, so a component that places
   itself with a transform keeps its place while it moves. */

/* It arrived. Set `--i` on each of several to have them arrive in turn. */
.is-entering {
  animation: enter var(--motion-enter) var(--ease-out) backwards;
  animation-delay: calc(var(--i, 0) * 30ms);
}
@keyframes enter { from { opacity: 0; translate: 0 10px; } }

/* It is going. It holds its last frame, so remove it when `animationend`
   fires rather than on a timer. */
.is-leaving { animation: leave var(--motion-leave) var(--ease-in) forwards; pointer-events: none; }
@keyframes leave { to { opacity: 0; translate: 0 10px; } }

/* Look here: a wash of violet that fades. For where a jump landed, or a thing
   somebody else just changed. */
.is-highlighted { animation: highlight 1s ease-out; }
@keyframes highlight { from { background-color: color-mix(in srgb, var(--primary) 12%, transparent); } }

/* Your change was kept: a ring that glows once, where the change was made. */
.is-saved { animation: saved 1s var(--ease-out); }
@keyframes saved {
  0%   { box-shadow: 0 0 0 0 color-mix(in srgb, var(--primary) 70%, transparent);
         background-color: color-mix(in srgb, var(--primary) 16%, transparent); }
  35%  { box-shadow: 0 0 0 6px color-mix(in srgb, var(--primary) 25%, transparent); }
  100% { box-shadow: 0 0 0 10px transparent; }
}

/* Every dialog arrives, and the page behind it dims rather than blinks. A
   dialog with a motion of its own outranks this by naming its class. */
dialog[open] { animation: enter var(--motion-enter) var(--ease-out); }
dialog[open]::backdrop { animation: fade var(--motion-enter) ease-out; }
@keyframes fade { from { opacity: 0; } }

/* ===========================================================================
   13. PRESS

   Every press answers on the frame it happens and lets go with a spring.
   The rules, so a new control can join without reading the rest:

     - Down: `scale` to a --press-scale-* rung over --motion-press, and a
       filled control brightens by --press-lit. Nothing moves the layout:
       `scale` is its own property, so a component placed with `transform`
       or `translate` keeps its place.
     - Up: back to 1 over --motion-release on --ease-spring, one overshoot.
     - Taps are not delayed (touch-action: manipulation) and draw no grey
       mobile tap flash; every focus ring stays exactly as it was drawn.
     - Disabled controls do not press. Reduced motion (base.css) cuts the
       durations to 1ms, so the state still changes and nothing moves.

   To add a control: put its selector in both lists below. That is all.
   =========================================================================== */

button.primary, .ghost, .icon-btn, .save-btn, .side-action, .add-row,
.edit-control, .snackbar-undo, .rail-add, .rail-action, .rail-avatar, .crumb,
.segmented-option, .subnav-item, .subnav-tab, .subnav-back, .scope, .rail-item .rail-pill, .colour-swatch,
input[type="checkbox"]:not(.switch input):not(.scope input), input[type="radio"] {
  -webkit-tap-highlight-color: transparent;
  touch-action: manipulation;
  transition:
    background-color var(--motion-short) var(--easing),
    color var(--motion-short) var(--easing),
    border-color var(--motion-short) var(--easing),
    filter var(--motion-release) var(--easing),
    scale var(--motion-release) var(--ease-spring);
}

/* Down. */
button.primary:active:not(:disabled), .ghost:active:not(:disabled),
.save-btn:active:not(:disabled), .edit-control:active, .snackbar-undo:active:not(:disabled),
.rail-add:active, .rail-avatar:active, .crumb:active, .segmented-option:active,
.subnav-item:active, .subnav-tab:active, .subnav-back:active, .scope:active:not(:has(input:disabled)),
.rail-item:active .rail-pill {
  scale: var(--press-scale);
}
.icon-btn:active:not(:disabled), .rail-action:active, .colour-swatch:active,
input[type="checkbox"]:not(.switch input):not(.scope input):active:not(:disabled),
input[type="radio"]:active:not(:disabled) {
  scale: var(--press-scale-sm);
}
.side-action:active, .add-row:active {
  scale: var(--press-scale-lg);
}
button.primary:active:not(:disabled), .save-btn:active:not(:disabled),
.side-action:active, .rail-add:active, .segmented-option:has(input:checked):active {
  filter: brightness(var(--press-lit));
}
/* The whole down list, fast. Kept as one rule so down is always quicker than up. */
button.primary:active, .ghost:active, .icon-btn:active, .save-btn:active,
.side-action:active, .add-row:active, .edit-control:active, .snackbar-undo:active,
.rail-add:active, .rail-action:active, .rail-avatar:active, .crumb:active,
.segmented-option:active, .subnav-item:active, .subnav-tab:active, .subnav-back:active, .scope:active, .rail-item:active .rail-pill,
.colour-swatch:active,
input[type="checkbox"]:not(.switch input):not(.scope input):active, input[type="radio"]:active {
  transition-duration: var(--motion-short), var(--motion-short), var(--motion-short),
                       var(--motion-press), var(--motion-press);
  transition-timing-function: var(--easing), var(--easing), var(--easing),
                              var(--ease-press), var(--ease-press);
}

/* The switch: the thumb squashes toward where it is about to go while held,
   and the track's own transition carries it there on release. */
.switch input:active:not(:disabled) + .slider::before { width: 18px; }
.switch input:checked:active:not(:disabled) + .slider::before { left: 15px; width: 21px; }
.switch.switch-sm input:active:not(:disabled) + .slider::before { width: 12px; }
.switch.switch-sm input:checked:active:not(:disabled) + .slider::before { left: 12px; width: 16px; }
.switch .slider::before {
  transition: width var(--motion-short) var(--ease-spring),
              height var(--motion-short) var(--ease-spring),
              left var(--motion-long) var(--ease-out),
              background var(--motion-short) var(--easing);
}
.switch { -webkit-tap-highlight-color: transparent; touch-action: manipulation; }

/* A row that opens something takes the press as a fill, not a squeeze: a
   whole row shrinking inside its border reads as the list jumping. */
.row[tabindex]:active, .field-row.is-grouped:active {
  background-color: var(--surface-cont);
  transition-duration: var(--motion-press);
}

/* A fold's line answers the press too; its chevron already turns (section 10). */
.fold > summary { -webkit-tap-highlight-color: transparent; transition: color var(--motion-short) var(--easing); }
.fold > summary:active { color: var(--on); }

/* ===========================================================================
   14. ROW HEIGHTS

   A row of three lines or fewer is always the same height as its neighbours.
   The list says what its rows are expected to hold, and every row takes that
   height as its minimum, centred; a row that needs more than three lines grows.

     data-lines="1"   one line: a name, with a mark, chips, a switch, an avatar,
                      or a field or button up to --control-h      --row-h-1 52px
     data-lines="2"   a name and a line under it (desc, hint)     --row-h-2 60px
     data-lines="3"   a name, a line, and a row of chips          --row-h-3 84px
     data-lines="auto"  no minimum: each row its own height, as before

   With no attribute a list is "1", so a switch row and a select row in the same
   group no longer differ by 11px. Pick the number from the tallest row the
   application expects to show in that list, not from the rows on screen now:
   a list whose rows sometimes carry a description is a "2" even when none does.

   Measured with the fonts loaded: a text-only row is 38.5px, a switch row 41, an
   avatar or select row 51–52, a two-line row 56–57, a three-line row 81.
   =========================================================================== */
.row-list, .field-rows { --row-min: var(--row-h-1); }
.row-list[data-lines="2"], .field-rows[data-lines="2"], .table[data-lines="2"] { --row-min: var(--row-h-2); }
.row-list[data-lines="3"], .field-rows[data-lines="3"], .table[data-lines="3"] { --row-min: var(--row-h-3); }
.row-list[data-lines="auto"], .field-rows[data-lines="auto"] { --row-min: 0px; }
.row-list > .row, .field-rows > .field-row:not(.is-block) { min-height: var(--row-min); }
/* The pinned row is the head of its list and keeps its taller inset on top. */
.row-list > .row.is-pinned { min-height: calc(var(--row-min) + 2 * (var(--row-y-block) - var(--row-y))); }


/* ===========================================================================
   15. THE FLASH — redraws section 12's .is-highlighted and .is-saved

   A flash is always --flash-out (6px) bigger than the thing on every side, with
   its corners rounded to the thing's radius plus that much, so it never hugs a
   label's top or a field's edges. It is a spread box-shadow in the flash colour
   under the thing's own background, so nothing moves and no wrapper is needed.

   A thing with no radius of its own (a label-and-field wrapper) is given
   --radius-sm while it flashes, through :where() so any component's radius wins.

   Its container needs --flash-out of room. Every pane and panel inset is wider.
   A thing that fills a clipped group (a row in a .row-list, a .field-row, a
   table row) is drawn INSIDE instead: the wash fills the row and the saved ring
   is an inset outline, because an outset flash there would be cut off.

   Replay one from script with DS.flash(el, 'is-saved' | 'is-highlighted').
   =========================================================================== */
:where(.is-highlighted, .is-saved) { border-radius: var(--radius-sm); }

.is-highlighted { animation: flash-highlight 1s ease-out; }
@keyframes flash-highlight {
  from {
    background-color: color-mix(in srgb, var(--primary) 12%, transparent);
    box-shadow: 0 0 0 var(--flash-out) color-mix(in srgb, var(--primary) 12%, transparent);
  }
  to { box-shadow: 0 0 0 var(--flash-out) transparent; }
}

.is-saved { animation: flash-saved 1s var(--ease-out); }
@keyframes flash-saved {
  0% {
    background-color: color-mix(in srgb, var(--primary) 16%, transparent);
    box-shadow: 0 0 0 var(--flash-out) color-mix(in srgb, var(--primary) 16%, transparent),
                0 0 0 calc(var(--flash-out) + 2px) color-mix(in srgb, var(--primary) 70%, transparent);
  }
  35% {
    box-shadow: 0 0 0 var(--flash-out) color-mix(in srgb, var(--primary) 10%, transparent),
                0 0 0 calc(var(--flash-out) + 6px) color-mix(in srgb, var(--primary) 25%, transparent);
  }
  100% {
    box-shadow: 0 0 0 var(--flash-out) transparent,
                0 0 0 calc(var(--flash-out) + 10px) transparent;
  }
}

/* Inside a clipped group: drawn inside the row. */
.row-list > .is-highlighted, .field-rows > .is-highlighted, .table tr.is-highlighted > td { animation-name: flash-highlight-in; border-radius: 0; }
.row-list > .is-saved, .field-rows > .is-saved { animation-name: flash-saved-in; border-radius: 0; }
.table tr.is-saved > td { animation: flash-highlight-in 1s var(--ease-out); }
@keyframes flash-highlight-in { from { background-color: color-mix(in srgb, var(--primary) 12%, transparent); } }
@keyframes flash-saved-in {
  0%   { background-color: color-mix(in srgb, var(--primary) 16%, transparent);
         outline: 2px solid color-mix(in srgb, var(--primary) 70%, transparent); outline-offset: -2px; }
  100% { outline: 2px solid transparent; outline-offset: -2px; }
}


/* ===========================================================================
   16. VIEWS, TABLES AND MENUS — every value is a token

   tabs · separators · box (surface levels) · scroll area · filters · table ·
   dialog · menu · empty state · skeleton · badge · kbd · pager · toolbar
   =========================================================================== */

/* ------------------------------------------------------------------ tabs --
   Views of ONE thing, switched in place. Violet, like the rail: the active tab
   is navigation, "this is on". The underline grows in from its centre. Use
   role="tablist" / "tab" / "tabpanel"; DS.tabs(el) does the keys. Not for
   screens (that is the rail) and not for filters (that is .segmented). */
.tabs {
  display: flex;
  align-items: flex-end;
  gap: var(--space-1);
  border-bottom: 1px solid var(--rule);
  overflow-x: auto;
  scrollbar-width: none;
}
.tab {
  position: relative;
  display: inline-flex;
  align-items: center;
  gap: var(--space-2);
  height: var(--control-h);
  padding: 0 var(--control-x);
  border: 0;
  border-radius: var(--radius-sm) var(--radius-sm) 0 0;
  background: transparent;
  color: var(--dim);
  font: inherit;
  font-size: var(--text-6);
  font-weight: 500;
  white-space: nowrap;
  cursor: pointer;
  -webkit-tap-highlight-color: transparent;
  touch-action: manipulation;
  transition: color var(--motion-short) var(--easing), background-color var(--motion-short) var(--easing);
}
.tab:hover { color: var(--on); background: var(--surface-low); }
.tab[aria-selected="true"] { color: var(--on); }
.tab::after {
  content: "";
  position: absolute;
  left: var(--space-2);
  right: var(--space-2);
  bottom: -1px;
  height: 2px;
  border-radius: 2px 2px 0 0;
  background: var(--primary);
  scale: 0 1;
  transition: scale var(--motion-long) var(--ease-out);
}
.tab[aria-selected="true"]::after { scale: 1 1; }
.tab:focus-visible { outline: 2px solid var(--primary); outline-offset: -2px; }
.tab:active { background: var(--surface-cont); transition-duration: var(--motion-press); }
.tab:disabled { color: var(--fainter); cursor: default; }
.tab-panel { padding-top: var(--space-4); }
.tab-panel:focus-visible { outline: 2px solid var(--primary); outline-offset: 2px; border-radius: var(--radius-sm); }

/* ------------------------------------------------------------ separators --
   The three lines, as elements. Picked by what they separate, as ever:
   .sep between two blocks inside one panel (--rule), .sep.is-edge where a
   panel's own parts meet (--edge), .sep-v between groups on a bar, and
   .sep-label to name what comes after the line. <hr> is a .sep. */
hr, .sep {
  border: 0;
  height: 1px;
  margin: var(--space-4) 0;
  background: var(--rule);
}
.sep.is-edge { background: var(--edge); }
.sep.is-tight { margin: var(--space-2) 0; }
.sep-v {
  flex: 0 0 auto;
  align-self: stretch;
  width: 1px;
  margin: var(--space-1) var(--space-1);
  background: var(--rule);
}
.sep-label {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  margin: var(--space-4) 0;
  font-size: var(--text-1);
  font-weight: 600;
  letter-spacing: .06em;
  text-transform: uppercase;
  color: var(--faint);
}
.sep-label::before, .sep-label::after { content: ""; flex: 1 1 auto; height: 1px; background: var(--rule); }
.sep-label.is-start::before { display: none; }

/* ------------------------------------------------------ box: surface levels --
   Four ways a region is set apart, from quietest to loudest. Pick by what the
   region IS, and never stack more than two:
     .box              a group: a line round it, the page behind it (--edge)
     .box.is-raised    a region above the page: a filled surface-low and edge
     .box.is-inset     a well below the page: code, logs, a preview (field)
     .box.is-overlay   over everything: menus, popovers (surface-cont + shadow)
   A group of rows is .row-list or .field-rows, which already are a .box. */
.box {
  border: 1px solid var(--edge);
  border-radius: var(--radius);
  padding: var(--space-4);
}
.box.is-raised { background: var(--surface-low); }
.box.is-inset { background: var(--field); border-color: var(--rule); }
.box.is-overlay { background: var(--surface-cont); box-shadow: var(--shadow-snackbar); }
.box.is-tight { padding: var(--space-3); }
.box-head {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  margin: calc(-1 * var(--space-4)) calc(-1 * var(--space-4)) var(--space-4);
  padding: var(--space-3) var(--space-4);
  border-bottom: 1px solid var(--rule);
  font-size: var(--text-7);
  font-weight: 600;
}

/* ------------------------------------------------------------ scroll area --
   A region that scrolls inside the page. The thin scrollbar is global
   (base.css); this adds the one thing it cannot say, that there is more,
   with a shadow at an edge only while content is hidden past it. Set
   --scroll-bg to the surface it sits on. overscroll-behavior keeps a wheel at
   the end of the list from scrolling the page behind it. */
.scroll-area {
  --scroll-bg: var(--surface);
  overflow: auto;
  overscroll-behavior: contain;
  scrollbar-gutter: stable;
  background:
    linear-gradient(var(--scroll-bg) 30%, transparent) top / 100% var(--space-5) no-repeat local,
    linear-gradient(transparent, var(--scroll-bg) 70%) bottom / 100% var(--space-5) no-repeat local,
    radial-gradient(farthest-side at 50% 0, color-mix(in srgb, var(--shade) 55%, transparent), transparent) top / 100% var(--space-3) no-repeat scroll,
    radial-gradient(farthest-side at 50% 100%, color-mix(in srgb, var(--shade) 55%, transparent), transparent) bottom / 100% var(--space-3) no-repeat scroll;
  background-color: var(--scroll-bg);
}
.scroll-area:focus-visible { outline: 2px solid var(--primary); outline-offset: 2px; }

/* ---------------------------------------------------------------- filters --
   Narrowing a list. One bar over the list, in this order: search, the filter
   chips a person toggles, dropdown filters, then the summary with Clear.
   Applied filters show as tokens that each remove one. A filter that is ON is
   violet ("this is on"), the same as a ticked scope. */
.filter-bar {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-2);
  margin-bottom: var(--space-3);
}
.filter-bar > .search-field { flex: 1 1 200px; max-width: 320px; }
.filter-chip {
  display: inline-flex;
  align-items: center;
  gap: var(--space-1);
  height: var(--control-h-sm);
  padding: 0 var(--control-x-sm);
  border: 1px solid var(--control-edge);
  border-radius: var(--radius-pill);
  background: transparent;
  color: var(--outline);
  font: inherit;
  font-size: var(--text-5);
  cursor: pointer;
  -webkit-tap-highlight-color: transparent;
  touch-action: manipulation;
  transition: background-color var(--motion-short) var(--easing), color var(--motion-short) var(--easing),
              border-color var(--motion-short) var(--easing), scale var(--motion-release) var(--ease-spring);
}
.filter-chip:hover { color: var(--on); border-color: var(--outline); }
.filter-chip[aria-pressed="true"] { background: var(--secondary-cont); border-color: var(--primary); color: var(--on-secondary-cont); }
.filter-chip:focus-visible { outline: 2px solid var(--primary); outline-offset: 1px; }
.filter-chip:active { scale: var(--press-scale); transition-duration: var(--motion-short), var(--motion-short), var(--motion-short), var(--motion-press); }
.filter-chip .icon { --icon-size: 14px; }
.filter-n { font-family: var(--mono); font-size: var(--text-2); color: var(--faint); }
.filter-chip[aria-pressed="true"] .filter-n { color: inherit; }
/* A dropdown filter: a ghost button with a caret and how many are picked. */
.ghost.has-caret { gap: var(--space-1); padding-right: var(--control-x-sm); }
.ghost.has-caret .icon { --icon-size: var(--icon-size-sm); }
.ghost.has-caret.is-set { color: var(--on-secondary-cont); border-color: var(--primary); background: var(--secondary-cont); }
.filter-tokens { display: flex; flex-wrap: wrap; gap: var(--space-1); margin-bottom: var(--space-3); }
.filter-token {
  display: inline-flex;
  align-items: center;
  gap: var(--space-1);
  height: 24px;
  padding: 0 2px 0 var(--chip-x);
  border-radius: var(--radius-pill);
  background: var(--secondary-cont);
  color: var(--on-secondary-cont);
  font-size: var(--text-3);
}
.filter-token-k { color: color-mix(in srgb, var(--on-secondary-cont) 72%, transparent); }
.filter-token button {
  display: grid;
  place-items: center;
  width: 20px;
  height: 20px;
  padding: 0;
  border: 0;
  border-radius: var(--radius-pill);
  background: transparent;
  color: inherit;
  cursor: pointer;
  transition: background-color var(--motion-short) var(--easing), scale var(--motion-release) var(--ease-spring);
}
.filter-token button:hover { background: color-mix(in srgb, var(--on-secondary-cont) 18%, transparent); }
.filter-token button:active { scale: var(--press-scale-sm); transition-duration: var(--motion-short), var(--motion-press); }
.filter-token button:focus-visible { outline: 2px solid var(--primary); outline-offset: 1px; }
.filter-token button .icon { --icon-size: 14px; }
.filter-summary { margin-left: auto; font-size: var(--text-4); color: var(--dim); white-space: nowrap; }
.search-field.has-clear .search-clear {
  display: grid; place-items: center; width: 20px; height: 20px; padding: 0; border: 0;
  border-radius: var(--radius-pill); background: transparent; color: var(--dim); cursor: pointer;
}
.search-field.has-clear .search-clear:hover { color: var(--on); background: var(--surface-high); }
.search-field.has-clear .search-clear:focus-visible { outline: 2px solid var(--primary); outline-offset: 1px; }
.search-field.has-clear .search-clear .icon { --icon-size: 14px; }
.search-field.has-clear input:placeholder-shown + .search-clear { visibility: hidden; }

/* ------------------------------------------------------------------ table --
   Records with the same fields, compared across columns. A list of things to
   open is a .row-list; a table is for reading across. Rows keep the list
   heights (data-lines), numbers align right in tabular figures, the head
   sticks, a sortable head is a button and its <th> carries aria-sort. */
.table-wrap {
  border: 1px solid var(--edge);
  border-radius: var(--radius);
  overflow: auto;
  overscroll-behavior: contain;
}
.table { --row-min: var(--row-h-1); width: 100%; border-collapse: separate; border-spacing: 0; font-size: var(--text-6); }
.table th {
  position: sticky;
  top: 0;
  z-index: 1;
  height: var(--control-h);
  padding: 0 var(--row-x);
  border-bottom: 1px solid var(--edge);
  background: var(--surface-low);
  color: var(--dim);
  font-size: var(--text-1);
  font-weight: 600;
  letter-spacing: .06em;
  text-transform: uppercase;
  text-align: left;
  white-space: nowrap;
}
.table td {
  height: var(--row-min);
  padding: var(--row-y) var(--row-x);
  border-bottom: 1px solid var(--rule);
  color: var(--on-variant);
  vertical-align: middle;
}
.table tbody tr:last-child td { border-bottom: 0; }
.table tbody tr { transition: background-color var(--motion-short) var(--easing); }
.table tbody tr:hover { background: var(--surface-low); }
.table tbody tr.is-selected { background: color-mix(in srgb, var(--accent-2) 7%, transparent); }
.table tbody tr.is-selected td:first-child { box-shadow: inset 3px 0 0 var(--accent-2); }
.table tbody tr[tabindex]:focus-visible { outline: 2px solid var(--primary); outline-offset: -2px; }
.table .is-num { text-align: right; font-variant-numeric: tabular-nums; }
.table .is-mono { font-family: var(--mono); font-size: var(--text-4); color: var(--outline); }
.table .is-name { color: var(--on); font-weight: 600; font-size: var(--text-7); }
.table th.is-num { text-align: right; }
.table-sort {
  display: inline-flex; align-items: center; gap: 2px; height: 24px; margin: 0 -4px; padding: 0 4px;
  border: 0; border-radius: var(--radius-sm); background: transparent; color: inherit;
  font: inherit; letter-spacing: inherit; text-transform: inherit; cursor: pointer;
}
.table-sort:hover { color: var(--on); background: var(--surface-cont); }
.table-sort:focus-visible { outline: 2px solid var(--primary); outline-offset: 0; }
.table-sort .icon { --icon-size: 14px; opacity: .35; transition: rotate var(--motion-short) var(--easing), opacity var(--motion-short) var(--easing); }
th[aria-sort] .table-sort { color: var(--on); }
th[aria-sort] .table-sort .icon { opacity: 1; }
th[aria-sort="ascending"] .table-sort .icon { rotate: 180deg; }

/* ---------------------------------------------------------------- dialog --
   A decision that has to be made before anything else. Native <dialog> with
   showModal(): it traps focus, Esc closes it, and it arrives with `enter`
   (section 12). Head, body, foot; the foot's last button is the action. */
dialog.dialog {
  width: min(480px, calc(100vw - 2 * var(--space-5)));
  max-height: calc(100vh - 2 * var(--space-6));
  padding: 0;
  border: 1px solid var(--edge);
  border-radius: var(--radius-lg);
  background: var(--surface-low);
  color: var(--on);
  box-shadow: var(--shadow-snackbar);
}
dialog.dialog::backdrop { background: color-mix(in srgb, var(--shade) 55%, transparent); }
.dialog-head { display: flex; align-items: center; gap: var(--space-3); padding: var(--space-4) var(--space-5) var(--space-2); }
.dialog-head h2 { flex: 1 1 auto; }
.dialog-body { padding: var(--space-2) var(--space-5) var(--space-4); color: var(--on-variant); font-size: var(--text-7); }
.dialog-foot {
  display: flex; justify-content: flex-end; gap: var(--space-2);
  padding: var(--space-3) var(--space-5) var(--space-4);
  border-top: 1px solid var(--rule);
}

/* ------------------------------------------------------------------ menu --
   A short list of actions on one thing, opened from a button. A popover in
   the top layer, so no pane clips it; DS.menu(button, menu) places it, moves
   focus with the arrows and hands focus back on close. Destructive items go
   last, after a separator. */
.menu {
  position: fixed;
  inset: auto;
  top: var(--menu-y, 0);
  left: var(--menu-x, 0);
  margin: 0;
  min-width: 200px;
  padding: var(--space-1);
  border: 1px solid var(--edge);
  border-radius: var(--radius-sm);
  background: var(--surface-cont);
  color: var(--on);
  box-shadow: var(--shadow-snackbar);
}
.menu:popover-open { animation: menu-in var(--motion-short) var(--ease-out); }
@keyframes menu-in { from { opacity: 0; translate: 0 -4px; } }
.menu-item {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  width: 100%;
  height: var(--control-h);
  padding: 0 var(--space-3);
  border: 0;
  border-radius: calc(var(--radius-sm) - 2px);
  background: transparent;
  color: var(--on-variant);
  font: inherit;
  font-size: var(--text-6);
  text-align: left;
  cursor: pointer;
}
.menu-item:hover, .menu-item:focus-visible { background: var(--surface-high); color: var(--on); outline: none; }
.menu-item:focus-visible { box-shadow: inset 0 0 0 2px var(--primary); }
.menu-item:active { background: var(--surface-est); transition: background-color var(--motion-press); }
.menu-item .icon { --icon-size: var(--icon-size-sm); color: var(--dim); }
.menu-item kbd { margin-left: auto; }
/* A choice of one (a theme): the tick says which, and holds its place when
   it is not this one, so the labels do not move as the pick does. */
.menu-item .menu-check { margin-left: auto; }
.menu-item .menu-check .icon { color: var(--primary); }
.menu-item[aria-checked="false"] .menu-check { visibility: hidden; }
.menu-item.is-danger, .menu-item.is-danger .icon { color: var(--error); }
.menu-item:disabled { color: var(--fainter); cursor: default; background: transparent; }
.menu-sep { height: 1px; margin: var(--space-1) calc(-1 * var(--space-1)); background: var(--rule); }
.menu-label { padding: var(--space-2) var(--space-3) var(--space-1); font-size: var(--text-1); font-weight: 600; letter-spacing: .06em; text-transform: uppercase; color: var(--faint); }

/* ----------------------------------------------------------- empty state --
   What a list says when it has nothing in it: what would be here, and the
   one way to make the first. Inside the list's own box, with the add row
   still under it. Never an illustration, never a joke. */
.empty {
  display: grid;
  justify-items: center;
  gap: var(--space-2);
  padding: var(--space-6) var(--space-5);
  text-align: center;
  color: var(--dim);
}
.empty > [data-icon] { color: var(--fainter); }
.empty > [data-icon] svg { width: var(--icon-size-lg); height: var(--icon-size-lg); }
.empty-title { font-size: var(--text-8); font-weight: 500; color: var(--on-variant); }
.empty-text { max-width: 44ch; font-size: var(--text-4); }
.empty .primary, .empty .ghost { margin-top: var(--space-2); }

/* ---------------------------------------------------------------- skeleton --
   The shape of what is loading, held still. It does not shimmer: nothing
   loops in this system but a glyph that turns (PATTERNS.md). Put aria-busy on
   the container and keep the skeleton rows at the list's own data-lines. */
.skeleton {
  display: block;
  height: .8em;
  width: var(--w, 100%);
  border-radius: var(--radius-sm);
  background: var(--surface-cont);
}
.skeleton.is-text { height: .7em; margin: .35em 0; }
.skeleton.is-avatar { width: 34px; height: 34px; border-radius: 10px; flex: 0 0 auto; }
.skeleton.is-control { width: 42px; height: 24px; border-radius: var(--radius-pill); flex: 0 0 auto; }

/* ------------------------------------------------------------------ badge --
   A count on a thing: unread, pending, errors. Always a number (or a word),
   never a bare dot, so it does not rely on colour. Violet for "yours to act
   on", quiet for a plain total, amber for "wants attention". */
.badge {
  display: inline-grid;
  place-items: center;
  min-width: 18px;
  height: 18px;
  padding: 0 5px;
  border-radius: var(--radius-pill);
  background: var(--primary);
  color: var(--on-primary);
  font-size: var(--text-2);
  font-weight: 600;
  font-variant-numeric: tabular-nums;
  line-height: 1;
}
.badge.is-quiet { background: var(--surface-high); color: var(--on-variant); }
.badge.is-warn { background: var(--warn); color: var(--surface); }
.badge.is-error { background: var(--error); color: var(--surface); }

/* -------------------------------------------------------------------- kbd --
   A key, wherever it is named in prose or a menu. The key bar draws its own. */
kbd {
  display: inline-grid;
  place-items: center;
  min-width: 20px;
  height: 20px;
  padding: 0 5px;
  border: 1px solid var(--edge);
  border-bottom-width: 2px;
  border-radius: calc(var(--radius-sm) / 2);
  background: var(--surface-cont);
  color: var(--outline);
  font-family: var(--mono);
  font-size: var(--text-2);
  line-height: 1;
}

/* ------------------------------------------------------------------ pager --
   Pages of a long list: where you are, in words, then the ways to move. The
   current page is aria-current="page" and filled; Previous and Next are
   disabled, not hidden, at the ends so nothing moves under the pointer. */
.pager { display: flex; align-items: center; gap: var(--space-1); }
.pager-info { margin-right: auto; font-size: var(--text-4); color: var(--dim); font-variant-numeric: tabular-nums; }
.pager-page {
  display: grid; place-items: center; min-width: var(--control-h); height: var(--control-h); padding: 0 var(--space-2);
  border: 0; border-radius: var(--radius-pill); background: transparent; color: var(--dim);
  font: inherit; font-size: var(--text-5); font-variant-numeric: tabular-nums; cursor: pointer;
  transition: background-color var(--motion-short) var(--easing), color var(--motion-short) var(--easing), scale var(--motion-release) var(--ease-spring);
}
.pager-page:hover { background: var(--surface-cont); color: var(--on); }
.pager-page[aria-current="page"] { background: var(--secondary-cont); color: var(--on-secondary-cont); font-weight: 600; }
.pager-page:focus-visible { outline: 2px solid var(--primary); outline-offset: 1px; }
.pager-page:active { scale: var(--press-scale); transition-duration: var(--motion-short), var(--motion-short), var(--motion-press); }
.pager-gap { color: var(--fainter); padding: 0 var(--space-1); }

/* ---------------------------------------------------------------- toolbar --
   A row of tools that act on one thing (an editor, a canvas, a selection),
   grouped by .sep-v. role="toolbar"; a toggle tool is an .icon-btn.is-quiet
   with aria-pressed, filled while on. */
.toolbar {
  display: inline-flex;
  align-items: center;
  gap: 2px;
  padding: var(--space-1);
  border: 1px solid var(--edge);
  border-radius: var(--radius-pill);
  background: var(--surface-low);
}
.icon-btn[aria-pressed="true"] { background: var(--secondary-cont); color: var(--on-secondary-cont); }
.icon-btn[aria-pressed="true"]:hover { background: var(--surface-est); }


/* ===========================================================================
   17. HIERARCHY AND HOUSEKEEPING
   =========================================================================== */

/* Onto the ladder: sections 4 and 5 space sections by a literal 26px and rows
   by 13 and 16. Section gaps are now --space-5, a row's inner gap --space-3 and a
   field row's --space-4, so the steps read as one scale:
     inside a row 2px · rows: a rule · head to its box space-2 ·
     box to the next head space-5 · region to region space-6 */
.row-list + .level-section-head,
.field-rows + .level-section-head { margin-top: var(--space-5); }
.level-block:not([hidden]) ~ .level-block:not([hidden]) { margin-top: var(--space-5); }
.row { gap: var(--space-3); }
.field-row { gap: var(--space-4); }
.level + .level, .region + .region { margin-top: var(--space-6); }

/* A section head names what is under it. Its label is the quietest heading on
   the screen and must still out-rank the rows: small caps, 600, tracked.
   .level-section-n counts it. Headings in prose stay sentence case. */
.level-section-head { min-height: 20px; }

/* Forced colours (Windows high contrast): the states this system draws with
   shadow and fill would disappear, so draw them with system colours. */
@media (forced-colors: active) {
  .row.is-selected, .table tbody tr.is-selected { outline: 2px solid Highlight; outline-offset: -2px; }
  .tab[aria-selected="true"]::after { background: Highlight; }
  .switch .slider { border-color: CanvasText; }
  .switch input:checked + .slider { background: Highlight; }
  .switch .slider::before { background: CanvasText; }
  .chip, .badge, .filter-token, .pager-page[aria-current="page"] { border: 1px solid CanvasText; }
  .is-highlighted, .is-saved { outline: 2px solid Highlight; outline-offset: var(--flash-out); }
}

/* A ghost button holding an icon or a badge spaces it like the primary does. */
.ghost { gap: var(--space-2); }

/* Press targets: the small remove and clear buttons draw at 20px but take a
   24px target (WCAG 2.2, 2.5.8), so a thumb that misses by two pixels still
   lands. */
.filter-token button, .search-field.has-clear .search-clear { position: relative; }
.filter-token button::before, .search-field.has-clear .search-clear::before { content: ""; position: absolute; inset: -2px; }

/* ===========================================================================
   18. MOVING, UPLOADING, PEOPLE AND SIGN-IN — every value a token

   drag and drop · upload · tree · avatar · card and board · confirmations ·
   field (label, hint, error) · sign-in · account menu · add in place
   =========================================================================== */

/* ------------------------------------------------------------ drag and drop --
   DS.sortable(list) makes the list's items movable. While one moves:
     .is-drag-source   the item's own place, held open as a dashed slot
     .drag-ghost       the lifted copy under the pointer
     .is-dropped       the landing: one spring, then still
   A handle (.drag-handle) is the grab point when a row also holds controls;
   without one the whole item is the grab point. On touch, a handle drags at
   once and anything else drags after a 250ms hold, so scrolling still works.
   Alt+↑/↓ moves the focused item; Alt+←/→ moves it to the next list. */
[data-sortable] > * { -webkit-user-select: none; user-select: none; }
.drag-handle {
  display: grid;
  place-items: center;
  flex: 0 0 auto;
  width: 24px;
  height: 24px;
  margin-left: calc(-1 * var(--space-1));
  border-radius: var(--radius-sm);
  color: var(--fainter);
  cursor: grab;
  touch-action: none;
  transition: color var(--motion-short) var(--easing), background-color var(--motion-short) var(--easing);
}
.drag-handle svg { width: var(--icon-size-sm); height: var(--icon-size-sm); }
:hover > .drag-handle, .drag-handle:hover { color: var(--dim); }
.drag-handle:hover { background: var(--surface-cont); }
.is-drag-source {
  opacity: 1;
  background: transparent !important;
  box-shadow: inset 0 0 0 1px var(--edge) !important;
  outline: 1px dashed var(--control-edge);
  outline-offset: -4px;
}
.is-drag-source > * { visibility: hidden; }
.drag-ghost {
  position: fixed;
  z-index: 50;
  margin: 0;
  pointer-events: none;
  background: var(--surface-low);
  border: 1px solid var(--control-edge);
  border-radius: var(--radius-sm);
  box-shadow: var(--shadow-snackbar);
  scale: 1.02;
  rotate: -.6deg;
  cursor: grabbing;
  transition: scale var(--motion-short) var(--ease-spring), rotate var(--motion-short) var(--ease-spring);
}
.drag-ghost.card { border-radius: var(--radius); }
html.is-dragging, html.is-dragging * { cursor: grabbing !important; }
[data-sortable].is-drop-target { outline: 2px dashed color-mix(in srgb, var(--primary) 55%, transparent); outline-offset: 2px; }
.is-dropped { animation: dropped var(--motion-release) var(--ease-spring); }
@keyframes dropped { from { scale: 1.03; } }

/* ------------------------------------------------------------------ upload --
   A drop zone and the files it took. The zone is a <label> round a hidden
   file input, so a click, Enter and Space all open the chooser without a line
   of script; DS.dropzone(el, onFiles) adds dropping. Each file is a row in a
   .row-list: its name, size and state, a meter while it goes, and Cancel or
   Remove. A file that failed says why, in words. */
.dropzone {
  display: grid;
  justify-items: center;
  gap: var(--space-2);
  padding: var(--space-5) var(--space-4);
  border: 1px dashed var(--control-edge);
  border-radius: var(--radius);
  color: var(--dim);
  text-align: center;
  cursor: pointer;
  -webkit-tap-highlight-color: transparent;
  transition: background-color var(--motion-short) var(--easing), border-color var(--motion-short) var(--easing),
              color var(--motion-short) var(--easing), scale var(--motion-release) var(--ease-spring);
}
.dropzone input[type="file"] { position: absolute; width: 1px; height: 1px; opacity: 0; pointer-events: none; }
.dropzone > [data-icon] svg { width: var(--icon-size-lg); height: var(--icon-size-lg); }
.dropzone:hover { border-color: var(--outline); color: var(--on-variant); background: var(--surface-low); }
.dropzone:focus-within { outline: 2px solid var(--primary); outline-offset: 2px; }
.dropzone:active { scale: var(--press-scale-lg); transition-duration: var(--motion-short), var(--motion-short), var(--motion-short), var(--motion-press); }
.dropzone.is-over {
  border-style: solid;
  border-color: var(--primary);
  background: color-mix(in srgb, var(--primary) 10%, transparent);
  color: var(--on);
  scale: 1.01;
}
.dropzone-title { font-size: var(--text-7); font-weight: 600; color: var(--on-variant); }
.dropzone-text { font-size: var(--text-3); color: var(--faint); }
.dropzone-title u { text-decoration-color: var(--primary); text-underline-offset: 3px; }
.dropzone.is-compact { grid-auto-flow: column; justify-content: start; padding: var(--space-3) var(--space-4); text-align: left; }
.file-row .row-body { display: grid; gap: 2px; }
.file-row progress.meter { margin: var(--space-1) 0 0; }
.file-row .file-state { font-size: var(--text-3); color: var(--faint); font-variant-numeric: tabular-nums; }
.file-row.is-error .row-mark, .file-row.is-error .file-state { color: var(--error); }
.file-row.is-done .row-mark { color: var(--accent-2); }

/* -------------------------------------------------------------------- tree --
   Parents and children more than one level deep. role="tree"; each item is an
   <li role="treeitem" aria-expanded> holding a .tree-row and, for a parent, a
   .tree-group of children. A guide line runs down each level at its chevron.
   Branches open and close with the height, not a jump. DS.tree(el) does the
   keys: ↑/↓ move, → opens or goes to the first child, ← closes or goes to the
   parent, Home/End, Enter selects. For ONE level of parent and children in a
   list, use .row.is-pinned and .row.is-child instead. */
.tree, .tree ul { list-style: none; margin: 0; padding: 0; }
.tree { --tree-indent: 20px; font-size: var(--text-6); }
.tree-row {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  min-height: var(--control-h);
  padding: 0 var(--space-2) 0 var(--space-1);
  border-radius: var(--radius-sm);
  color: var(--on-variant);
  cursor: pointer;
  transition: background-color var(--motion-short) var(--easing);
}
.tree-row:hover { background: var(--surface-low); }
.tree-row:active { background: var(--surface-cont); transition-duration: var(--motion-press); }
[role="treeitem"]:focus-visible { outline: none; }
[role="treeitem"]:focus-visible > .tree-row { outline: 2px solid var(--primary); outline-offset: -2px; }
[role="treeitem"][aria-selected="true"] > .tree-row {
  background: color-mix(in srgb, var(--accent-2) 9%, transparent);
  box-shadow: inset 3px 0 0 var(--accent-2);
  color: var(--on);
}
.tree-toggle {
  display: grid;
  place-items: center;
  flex: 0 0 auto;
  width: 20px;
  height: 20px;
  border-radius: var(--radius-sm);
  color: var(--dim);
  transition: rotate var(--motion-long) var(--ease-out);
}
.tree-toggle svg { width: var(--icon-size-sm); height: var(--icon-size-sm); }
[aria-expanded="false"] > .tree-row .tree-toggle { rotate: -90deg; }
.tree-leaf { width: 20px; flex: 0 0 auto; }
.tree-row [data-icon]:not(.tree-toggle) { color: var(--dim); }
.tree-row [data-icon]:not(.tree-toggle) svg { width: var(--icon-size-sm); height: var(--icon-size-sm); }
.tree-name { flex: 1 1 auto; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.tree-row .badge { flex: 0 0 auto; }
.tree-group {
  display: grid;
  grid-template-rows: 1fr;
  transition: grid-template-rows var(--motion-long) var(--ease-out);
}
[aria-expanded="false"] > .tree-group { grid-template-rows: 0fr; }
.tree-group > ul {
  min-height: 0;
  overflow: hidden;
  margin-left: calc(var(--space-1) + 10px);
  padding-left: calc(var(--tree-indent) - 10px);
  border-left: 1px solid var(--rule);
}

/* ------------------------------------------------------------------ avatar --
   Who, as a face or a letter. The letter is the first of the name; the whole
   name is the element's title or the accessible name of the button round it.
   Sizes are tokens. A stack overlaps by a third and says "+N" for the rest. */
.avatar {
  display: inline-grid;
  place-items: center;
  flex: 0 0 auto;
  width: var(--avatar);
  height: var(--avatar);
  border-radius: var(--radius-pill);
  background: var(--surface-est);
  color: var(--on);
  font-size: var(--text-5);
  font-weight: 500;
  text-transform: uppercase;
  overflow: hidden;
}
.avatar img { width: 100%; height: 100%; object-fit: cover; }
.avatar.is-sm { width: var(--avatar-sm); height: var(--avatar-sm); font-size: var(--text-2); }
.avatar.is-lg { width: var(--avatar-lg); height: var(--avatar-lg); font-size: var(--text-7); }
.avatar-stack { display: inline-flex; }
.avatar-stack > .avatar { box-shadow: 0 0 0 2px var(--avatar-ring, var(--surface)); }
.avatar-stack > .avatar + .avatar { margin-left: calc(var(--avatar-sm) / -3); }
.avatar-btn {
  display: inline-grid; place-items: center; padding: 0; border: 0; border-radius: var(--radius-pill);
  background: transparent; cursor: pointer; -webkit-tap-highlight-color: transparent;
  transition: scale var(--motion-release) var(--ease-spring);
}
.avatar-btn:focus-visible { outline: 2px solid var(--primary); outline-offset: 2px; }
.avatar-btn:active { scale: var(--press-scale-sm); transition-duration: var(--motion-press); }
.avatar-btn[aria-expanded="true"] .avatar { box-shadow: 0 0 0 2px var(--primary); }

/* ---------------------------------------------------------------- card --
   A thing that stands on its own and can move: a task on a board, a result in
   a grid. Not a settings group (that is .field-rows) and not a row in a list.
   A card that opens is one link or button, the .card-link, stretched over the
   whole card so the card is one target and the other controls on it still
   work. Its parts, top to bottom: title, a line of detail, meta (chips, an
   avatar, a date). */
.card {
  position: relative;
  display: grid;
  gap: var(--space-2);
  padding: var(--space-3);
  border: 1px solid var(--edge);
  border-radius: var(--radius);
  background: var(--surface-low);
  transition: border-color var(--motion-short) var(--easing), background-color var(--motion-short) var(--easing),
              scale var(--motion-release) var(--ease-spring);
}
.card:hover { border-color: var(--control-edge); }
.card:has(.card-link:active) { scale: var(--press-scale-lg); transition-duration: var(--motion-short), var(--motion-short), var(--motion-press); }
.card:has(.card-link:focus-visible) { outline: 2px solid var(--primary); outline-offset: 2px; }
.card.is-selected { border-color: var(--accent-2); box-shadow: inset 3px 0 0 var(--accent-2); }
.card-title { font-size: var(--text-7); font-weight: 600; color: var(--on); line-height: 1.35; }
.card-link { color: inherit; text-decoration: none; background: none; border: 0; padding: 0; font: inherit; text-align: left; cursor: pointer; }
.card-link:focus-visible { outline: none; }
.card-link::after { content: ""; position: absolute; inset: 0; border-radius: inherit; }
.card-text { font-size: var(--text-4); color: var(--dim); display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; }
.card-meta { display: flex; flex-wrap: wrap; align-items: center; gap: var(--space-1) var(--space-2); font-size: var(--text-3); color: var(--faint); }
.card-meta .avatar, .card-meta .chip, .card-meta button { position: relative; z-index: 1; }
.card-meta > :last-child:not(:first-child) { margin-left: auto; }
.card-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(220px, 1fr)); gap: var(--space-3); }

/* A board: columns of cards, each column a list that scrolls on its own. */
.board { display: flex; gap: var(--space-3); align-items: flex-start; overflow-x: auto; overscroll-behavior-x: contain; padding-bottom: var(--space-2); }
.board-col {
  display: flex;
  flex-direction: column;
  flex: 0 0 var(--board-col-w, 260px);
  max-height: 100%;
  border: 1px solid var(--rule);
  border-radius: var(--radius);
  background: var(--surface-pane);
}
.board-col-head { display: flex; align-items: center; gap: var(--space-2); padding: var(--space-3) var(--space-3) var(--space-2); font-size: var(--text-1); font-weight: 600; letter-spacing: .06em; text-transform: uppercase; color: var(--dim); }
.board-col-head .level-section-n { margin-left: auto; }
.board-col-body { display: grid; align-content: start; gap: var(--space-2); min-height: var(--row-h-2); padding: 0 var(--space-2) var(--space-2); overflow-y: auto; overscroll-behavior: contain; }
.board-col > .add-row { border-top: 1px dashed var(--rule); border-radius: 0 0 var(--radius) var(--radius); }

/* ---------------------------------------------------------- confirmations --
   Pick by what the act costs, lightest that is safe:
     it can be undone          do it, and offer Undo in a snackbar
     small and final           the two-step button: DS.confirm(button, opts)
     large and final           a dialog that names the thing and the result
     cannot be recovered       a dialog where the name must be typed
   The two-step button fills with the danger colour, says what a second press
   does, and a bar under its label runs down the time it waits; Esc, leaving
   it, or the time running out puts it back. */
.is-confirming {
  position: relative;
  overflow: hidden;
  background: var(--error) !important;
  border-color: var(--error) !important;
  color: var(--surface) !important;
  font-weight: 600;
}
.is-confirming::after {
  content: "";
  position: absolute;
  left: 0;
  right: 0;
  bottom: 0;
  height: 2px;
  background: color-mix(in srgb, var(--surface) 55%, transparent);
  transform-origin: left;
  animation: confirm-time var(--confirm-ms, 4000ms) linear forwards;
}
@keyframes confirm-time { to { transform: scaleX(0); } }
.confirm-type { display: grid; gap: var(--space-1); margin-top: var(--space-3); }
.confirm-type code { color: var(--on); }

/* --------------------------------------------------------------- field --
   A labelled control outside a settings group: in a form, a dialog, sign-in.
   Label, control, then either the hint or the error, never both. The error is
   words with an icon, tied to the control by aria-describedby, and the
   control takes aria-invalid="true". */
/* Label, control and hint a step apart, and fields two steps. At 4px the
   label sat on the box's border and the hint under it read as the next
   field's label; at 12px between fields a form with hints ran together.
   Costs 8px a field and 4px between fields. */
.field { display: grid; gap: var(--space-2); }
.field + .field { margin-top: var(--space-4); }
.field-label { display: flex; align-items: baseline; justify-content: space-between; gap: var(--space-2); font-size: var(--text-6); color: var(--on-variant); }
.field-label a, .field-label .ghost { font-size: var(--text-3); }
.field > input, .field > select, .field > textarea, .field-affix { width: 100%; }
.field-hint { font-size: var(--text-3); color: var(--faint); }
.field-error { display: flex; align-items: flex-start; gap: var(--space-1); font-size: var(--text-3); color: var(--error); }
.field-error svg { width: 14px; height: 14px; flex: 0 0 auto; margin-top: 1px; }
input[aria-invalid="true"], select[aria-invalid="true"], textarea[aria-invalid="true"] { border-color: var(--error) !important; }
.field-affix { position: relative; display: flex; }
.field-affix > input { flex: 1 1 auto; padding-right: 40px; }
.field-affix > button {
  position: absolute; right: 4px; top: 50%; translate: 0 -50%;
  display: grid; place-items: center; width: 28px; height: 28px; padding: 0; border: 0;
  border-radius: var(--radius-sm); background: transparent; color: var(--dim); cursor: pointer;
}
.field-affix > button:hover { color: var(--on); background: var(--surface-cont); }
.field-affix > button:focus-visible { outline: 2px solid var(--primary); outline-offset: 0; }
.field-affix > button svg { width: var(--icon-size-sm); height: var(--icon-size-sm); }
a { color: var(--primary); text-underline-offset: 2px; }
/* A row or a side tile that opens a page is a link, and reads as the row it is. */
a.row, a.side-tile { color: inherit; text-decoration: none; }
a:hover { color: var(--on-primary-cont); }
a:focus-visible { outline: 2px solid var(--primary); outline-offset: 2px; border-radius: 2px; }

/* ---------------------------------------------------------------- sign-in --
   The one screen with no shell: a raised panel centred on the pane, the
   product's name above it. Email, then password or a sign-in link. The
   primary button is full width because it is the only thing to do. */
.auth {
  display: grid;
  place-items: center;
  min-height: 100%;
  padding: var(--space-6) var(--space-4);
  background: var(--surface-pane);
}
.auth-panel {
  width: min(360px, 100%);
  padding: var(--space-5);
  border: 1px solid var(--edge);
  border-radius: var(--radius-lg);
  background: var(--surface-low);
}
.auth-brand { display: flex; align-items: center; gap: var(--space-2); margin-bottom: var(--space-4); font-weight: 600; font-size: var(--text-7); color: var(--on); }
.auth-brand [data-icon] { display: grid; place-items: center; width: 32px; height: 32px; border-radius: var(--radius-sm); background: var(--primary); color: var(--on-primary); }
.auth-panel h1 { font-size: var(--text-11); margin-bottom: var(--space-1); }
.auth-sub { font-size: var(--text-4); color: var(--dim); margin-bottom: var(--space-4); }
.auth-panel button.primary, .auth-panel .ghost.is-wide { width: 100%; justify-content: center; height: var(--control-h-lg); margin-top: var(--space-4); }
.auth-panel .ghost.is-wide { margin-top: 0; }
.auth-panel .sep-label { margin: var(--space-4) 0; }
.auth-foot { margin-top: var(--space-4); font-size: var(--text-3); color: var(--faint); text-align: center; }
.auth-sent { display: grid; justify-items: center; gap: var(--space-2); text-align: center; }
.auth-sent > [data-icon] { color: var(--accent-2); }
.auth-sent > [data-icon] svg { width: var(--icon-size-lg); height: var(--icon-size-lg); }

/* ------------------------------------------------------------ account menu --
   The menu from the signed-in person's avatar: who and where first (name,
   email, role), then their own things, then Sign out last. A .menu with a
   .menu-head; from the rail it opens to the right. */
.menu-head { display: flex; align-items: center; gap: var(--space-3); padding: var(--space-2) var(--space-3) var(--space-3); }
.menu-head-name { font-size: var(--text-7); font-weight: 600; color: var(--on); }
.menu-head-sub { font-size: var(--text-3); color: var(--faint); }
.menu-head .chip { margin-left: auto; }
.menu.is-account { min-width: 248px; }

/* ------------------------------------------------------------ add in place --
   The add row (section 4) opened: DS.addRow(button, opts) swaps it for a field
   in the same place at the same height. Enter adds and stays open for the
   next; Esc, or leaving it empty, closes it and focus goes back to the add row.
   The new row arrives with .is-entering. */
.add-row.is-open { cursor: text; color: var(--on); }
.add-row.is-open input {
  height: 24px; padding: 0; border: 0; background: transparent; box-shadow: none;
  color: var(--on); font: inherit; font-size: var(--text-6); outline: none;
}
.add-row.is-open input::placeholder { color: var(--fainter); }
.add-row.is-open:focus-within { box-shadow: inset 0 0 0 2px var(--primary); }
.add-row-hint { flex: 0 0 auto; font-size: var(--text-2); color: var(--faint); }
.add-row-hint kbd { height: 18px; min-width: 18px; }
.table tfoot .add-row { border-top: 1px dashed var(--rule); }

/* ===========================================================================
   19. CARD SELECTION, THE WHEEL, CONTEXT MENUS, THE KEY BAR
   =========================================================================== */

/* A selected card is outlined and its title (and icon) turn sage. No thick
   left edge: the outline goes all the way round, and the title's colour and
   the outline's weight together say "this one", not colour alone. */
.card.is-selected {
  border-color: var(--accent-2);
  box-shadow: 0 0 0 1px var(--accent-2);
}
.card.is-selected .card-title,
.card.is-selected .card-icon { color: var(--accent-2); }
.card-head { display: flex; align-items: center; gap: var(--space-2); }
.card-icon { display: grid; place-items: center; flex: 0 0 auto; color: var(--dim); transition: color var(--motion-short) var(--easing); }
.card-icon svg { width: var(--icon-size-sm); height: var(--icon-size-sm); }
.card:hover .card-icon { color: var(--on-variant); }
.card .card-title { transition: color var(--motion-short) var(--easing); }

/* The wheel. A vertical scroller keeps its vertical wheel to itself, but lets
   a sideways wheel (a tilt wheel, a thumb wheel, a trackpad swipe) pass on to
   the board or strip it sits in. Before this, `overscroll-behavior: contain`
   on a column stopped a sideways wheel over a column from moving the board.
   DS.wheel (always on) also turns an ordinary wheel into sideways scrolling
   over anything that only scrolls sideways: a board, a tab strip, a filter
   bar, or anything with data-wheel-x. */
.board-col-body, .scroll-area, .table-wrap { overscroll-behavior: auto; overscroll-behavior-y: contain; }
.board, .tabs, [data-wheel-x] { overscroll-behavior-x: contain; }
.filter-bar.is-scroll { flex-wrap: nowrap; overflow-x: auto; scrollbar-width: none; }

/* The thing a context menu is open on stays marked until the menu closes. */
.is-context-target { background-color: var(--surface-low); outline: 1px solid var(--control-edge); outline-offset: -1px; }
.card.is-context-target { outline-offset: 1px; }

/* The key bar, two more ways to show it (DS.keyBar options):
     dock: true     a strip across the bottom of the screen, always there,
                    saying "Tab to move" when nothing has focus
     .is-static     drawn in place, for a guide or a settings screen */
.key-bar.is-docked {
  left: 0;
  right: 0;
  bottom: 0;
  max-width: none;
  border-width: 1px 0 0;
  border-radius: 0;
  padding: var(--space-2) var(--space-4);
  background: var(--surface-rail);
}
.key-bar-idle { color: var(--faint); }
.key-bar.is-static { position: static; display: inline-flex; pointer-events: auto; }
.key-bar-item { gap: var(--space-2); }

/* The keyboard shortcuts sheet: every key the app knows, grouped, opened with
   ? from anywhere that is not a text field. DS.shortcuts() builds it. */
dialog.dialog.is-shortcuts { width: min(640px, calc(100vw - 2 * var(--space-5))); }
.shortcuts { display: grid; grid-template-columns: repeat(auto-fill, minmax(240px, 1fr)); gap: var(--space-4) var(--space-5); }
.shortcuts-group h3 { margin-bottom: var(--space-2); font-size: var(--text-1); font-weight: 600; letter-spacing: .06em; text-transform: uppercase; color: var(--dim); }
.shortcuts-group dl { margin: 0; display: grid; grid-template-columns: auto 1fr; gap: var(--space-1) var(--space-3); align-items: center; }
.shortcuts-group dt { display: flex; gap: 2px; justify-content: flex-end; white-space: nowrap; }
.shortcuts-group dd { margin: 0; font-size: var(--text-6); color: var(--on-variant); }

/* ===========================================================================
   20. COLOURS
   The four colours an app sets (tokens.css, THE FOUR COLOURS), a picker that
   opens on hex, and the warning under a colour that leaves something too
   faint to read. DS.colourSettings builds the rows; DS.colourField builds one
   field on its own.
   =========================================================================== */

.colour-field { display: inline-flex; align-items: center; gap: var(--space-2); flex: 0 0 auto; }

/* The swatch shows the picked colour, which is data rather than a palette
   colour: the script writes it as --swatch, the one colour it writes. The
   inset ring keeps a swatch the colour of the ground from vanishing into it. */
.colour-swatch {
  width: var(--control-h);
  height: var(--control-h);
  padding: 0;
  border: 1px solid var(--control-edge);
  border-radius: var(--radius-sm);
  background: var(--swatch, transparent);
  box-shadow: inset 0 0 0 2px var(--surface);
  cursor: pointer;
}
.colour-swatch:hover { border-color: var(--on-variant); }
.colour-swatch:focus-visible { outline: 2px solid var(--primary); outline-offset: 2px; }
.colour-swatch[aria-expanded="true"] { border-color: var(--primary); }
.colour-field.is-low .colour-swatch { border-color: var(--warn); }

/* Hex in the monospaced face, so #c39cdc and #8a8a8a are the same width and a
   mistyped digit stands out. Seven characters and the field's own inset. */
.colour-hex, .colour-picker-hex input {
  width: calc(7ch + 2 * var(--field-x) + 2px);
  font-family: var(--mono);
  font-size: var(--text-6);
}

/* The warning: words with an icon, in amber, under the hint of the colour it
   is about. A warning, not an error: the colour is kept, because a person may
   be on the way to a pair that reads. */
.field-warn {
  display: flex;
  align-items: flex-start;
  gap: var(--space-1);
  margin-top: var(--space-1);
  font-size: var(--text-3);
  line-height: 1.4;
  color: var(--warn);
}
.field-warn svg { width: 14px; height: 14px; flex: 0 0 auto; margin-top: 1px; }

/* The picker. A popover in the top layer, placed by the script like a menu.
   `display` only while open: the UA hides a closed popover with display, and
   a plain `display: grid` here would out-rank it and leave it showing. */
.colour-picker {
  position: fixed;
  inset: auto;
  left: var(--picker-x, 0);
  top: var(--picker-y, 0);
  margin: 0;
  width: var(--picker-w);
  padding: var(--space-3);
  border: 1px solid var(--edge);
  border-radius: var(--radius);
  background: var(--surface-cont);
  color: var(--on);
  box-shadow: var(--shadow-snackbar);
}
.colour-picker:popover-open {
  display: grid;
  gap: var(--space-3);
  animation: menu-in var(--motion-short) var(--ease-out);
}
.colour-picker-hex { display: flex; align-items: center; gap: var(--space-2); font-size: var(--text-5); color: var(--dim); }
.colour-picker-hex input { flex: 1 1 auto; min-width: 0; }

/* The plane and the strip draw the colour model itself: white, black and the
   pure hues are what is being chosen between, not colours of this palette. */
.colour-plane {
  position: relative;
  aspect-ratio: 3 / 2;
  border-radius: var(--radius-sm);
  cursor: crosshair;
  touch-action: none;
  background:
    linear-gradient(to top, #000, transparent),
    linear-gradient(to right, #fff, hsl(var(--picker-hue, 0) 100% 50%));
}
.colour-plane:focus-visible { outline: 2px solid var(--primary); outline-offset: 2px; }
.colour-plane-dot {
  position: absolute;
  left: calc(var(--picker-s, 0) * 100%);
  top: calc((1 - var(--picker-v, 0)) * 100%);
  width: var(--icon-size-sm);
  height: var(--icon-size-sm);
  translate: -50% -50%;
  border: 2px solid var(--on);
  border-radius: var(--radius-pill);
  box-shadow: 0 0 0 1px var(--shade), inset 0 0 0 1px var(--shade);
  pointer-events: none;
}
.colour-hue {
  -webkit-appearance: none;
  appearance: none;
  width: 100%;
  height: var(--space-3);
  margin: 0;
  border-radius: var(--radius-pill);
  cursor: pointer;
  background: linear-gradient(to right,
    hsl(0 100% 50%), hsl(60 100% 50%), hsl(120 100% 50%), hsl(180 100% 50%),
    hsl(240 100% 50%), hsl(300 100% 50%), hsl(360 100% 50%));
}
.colour-hue:focus-visible { outline: 2px solid var(--primary); outline-offset: 2px; }
.colour-hue::-webkit-slider-thumb {
  -webkit-appearance: none;
  width: var(--icon-size-sm);
  height: var(--icon-size-sm);
  border: 2px solid var(--on);
  border-radius: var(--radius-pill);
  background: hsl(var(--picker-hue, 0) 100% 50%);
  box-shadow: 0 0 0 1px var(--shade);
}
.colour-hue::-moz-range-thumb {
  width: var(--icon-size-sm);
  height: var(--icon-size-sm);
  box-sizing: border-box;
  border: 2px solid var(--on);
  border-radius: var(--radius-pill);
  background: hsl(var(--picker-hue, 0) 100% 50%);
  box-shadow: 0 0 0 1px var(--shade);
}
.colour-hue::-moz-range-track { background: transparent; }

/* ===========================================================================
   21. READING AND EDITING AT LENGTH
   drawer · prose · placeholder · doc frame · tiles in a main column · actions
   =========================================================================== */

/* ---------------------------------------------------------------- drawer --
   A sheet from the right, for editing one thing at length: a document, a
   brief, a procedure. A native <dialog> opened with showModal(), so it traps
   focus and the page under it waits; DS.drawer(dialog) plays it in and out
   and hands focus back.

     <dialog class="drawer">
       <div class="drawer-head">
         <div class="drawer-ident"><div class="drawer-title">…</div><div class="drawer-sub">…</div></div>
         …actions…
         <button class="icon-btn is-quiet" data-drawer-close aria-label="Close">…</button>
       </div>
       <div class="drawer-body">…</div>
       <div class="drawer-foot"><span class="drawer-note">…</span><button class="ghost">…</button><button class="primary">Save</button></div>
     </dialog>

   The body is the only part that scrolls, so the head and the foot, where
   the way out and the save are, never leave the screen. Not a dialog: a
   dialog is a decision that cannot wait, read in a breath; a drawer holds a
   page of text somebody works on for ten minutes. */
dialog.drawer {
  /* The UA centres a modal and caps it short of the viewport on every side;
     a sheet stands on the right edge, the whole height. */
  margin: 0 0 0 auto;
  width: min(var(--drawer-w), 100vw);
  max-width: 100vw;
  height: 100dvh;
  max-height: 100dvh;
  padding: 0;
  border: 0;
  border-left: 1px solid var(--edge);
  background: var(--surface-low);
  color: var(--on);
  box-shadow: var(--shadow-snackbar);
  overflow: hidden;
}
dialog.drawer.is-wide { width: min(var(--drawer-w-wide), 100vw); }
dialog.drawer[open] {
  display: flex;
  flex-direction: column;
  animation: drawer-in var(--motion-enter) var(--ease-out);
}
dialog.drawer::backdrop { background: color-mix(in srgb, var(--shade) 55%, transparent); }
/* It leaves the way it came. DS.drawer adds this and closes the dialog when
   the animation ends, so Esc slides it out rather than snapping it shut. */
dialog.drawer.is-closing { animation: drawer-out var(--motion-leave) var(--ease-in) forwards; }
dialog.drawer.is-closing::backdrop { animation: fade var(--motion-leave) var(--ease-in) reverse forwards; }
@keyframes drawer-in { from { opacity: 0; translate: var(--space-6) 0; } }
@keyframes drawer-out { to { opacity: 0; translate: var(--space-6) 0; } }

.drawer-head {
  flex: 0 0 auto;
  display: flex;
  align-items: center;
  gap: var(--space-3);
  padding: var(--pane-y) var(--pane-x);
  border-bottom: 1px solid var(--rule);
}
.drawer-ident { flex: 1 1 auto; min-width: 0; }
.drawer-title,
.drawer-sub { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.drawer-title { font-family: var(--font-display); font-size: var(--text-10); font-weight: 500; color: var(--on); }
.drawer-sub { margin-top: 1px; font-size: var(--text-4); color: var(--faint); }

.drawer-body {
  flex: 1 1 auto;
  min-height: 0;
  overflow: auto;
  overscroll-behavior: contain;
  padding: var(--pane-y) var(--pane-x) var(--space-5);
}

/* Held at the foot of the sheet, outside the scroll: the note on the left
   says what pressing Save does or when it was last kept, and the buttons sit
   on the right with the primary last, as a dialog's do. */
.drawer-foot {
  flex: 0 0 auto;
  display: flex;
  align-items: center;
  justify-content: flex-end;
  gap: var(--space-2);
  padding: var(--space-3) var(--pane-x);
  border-top: 1px solid var(--rule);
  background: var(--surface-low);
}
.drawer-note {
  flex: 1 1 auto;
  min-width: 0;
  margin-right: var(--space-2);
  font-size: var(--text-3);
  color: var(--faint);
}

/* Two equal columns: the text being edited and what it will look like. Each
   scrolls on its own and the body does not, so the two halves keep their
   place while one of them is scrolled. A split wants the wide drawer. */
.drawer-body:has(> .drawer-split) {
  display: flex;
  flex-direction: column;
  overflow: hidden;
  padding: 0;
}
.drawer-split {
  flex: 1 1 auto;
  min-height: 0;
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
}
.drawer-pane {
  min-width: 0;
  min-height: 0;
  display: flex;
  flex-direction: column;
  overflow: auto;
  overscroll-behavior: contain;
  padding: var(--pane-y) var(--pane-x) var(--space-5);
}
.drawer-pane + .drawer-pane { border-left: 1px solid var(--rule); }
/* A narrow window stacks the two, the editor first, each half the height. A
   viewport width, because a media query cannot read a token. */
@media (max-width: 900px) {
  .drawer-split { grid-template-columns: minmax(0, 1fr); grid-auto-rows: minmax(0, 1fr); }
  .drawer-pane + .drawer-pane { border-left: 0; border-top: 1px solid var(--rule); }
}

/* The editor fills what is left of its pane, or of a body that holds it
   directly, rather than being a 264px box with the sheet empty under it.
   Everything beside it keeps its own height: in a flex column a clipped list
   would otherwise be squashed to nothing. */
.drawer-pane > *,
.drawer-body:has(> textarea.editor-area) > * { flex: 0 0 auto; }
.drawer-body:has(> textarea.editor-area) { display: flex; flex-direction: column; }
:is(.drawer-pane, .drawer-body) > textarea.editor-area {
  flex: 1 1 auto;
  width: 100%;
  /* Filling, it needs no 264px floor; six lines keeps a stacked half of a
     short window from scrolling round its own editor. */
  min-height: calc(6lh + 2 * var(--field-y));
  resize: none;
}

/* ----------------------------------------------------------------- prose --
   Rendered markdown in a reading column: a client's document, a procedure, a
   plan. DS.markdown(src) makes the HTML; this makes it read. The column
   decides the measure, so nothing here caps the width.

   Headings are sized off the type ladder, and an h1 is never bigger than a
   level's own title, because the document sits inside a screen that already
   has one. Space above a heading grows with its rank and the space below
   stays small, so a heading reads as the start of what follows it rather
   than the end of what came before. */
.prose {
  font-size: var(--text-8);
  line-height: 1.6;
  color: var(--on-variant);
  overflow-wrap: anywhere;
}
.prose > :first-child { margin-top: 0; }
.prose > :last-child { margin-bottom: 0; }
.prose :is(h1, h2, h3, h4, h5, h6) {
  margin: var(--space-5) 0 var(--space-2);
  font-family: var(--font-display);
  font-weight: 500;
  line-height: 1.3;
  letter-spacing: -0.01em;
  color: var(--on);
}
.prose h1 { margin-top: var(--space-6); margin-bottom: var(--space-3); font-size: var(--text-11); }
.prose h2 { margin-top: var(--space-6); font-size: var(--text-10); }
.prose h3 { font-size: var(--text-9); }
.prose :is(h4, h5, h6) { margin-top: var(--space-4); font-size: var(--text-8); font-weight: 600; letter-spacing: 0; }
.prose p { margin: 0 0 var(--space-3); }
.prose :is(ul, ol) { margin: 0 0 var(--space-3); padding-left: var(--space-5); }
.prose li + li { margin-top: var(--space-1); }
.prose li::marker { color: var(--faint); }
.prose li > :is(ul, ol) { margin: var(--space-1) 0 0; }
.prose li > p { margin: var(--space-2) 0 0; }
.prose li > p:first-child { margin-top: 0; }
/* Somebody else's words, set in from an edge. The edge is a `--rule`, not an
   accent: it says "quoted", which is neither of the two meanings the accents
   carry. */
.prose blockquote {
  margin: 0 0 var(--space-3);
  padding: var(--space-1) 0 var(--space-1) var(--space-4);
  border-left: 3px solid var(--rule);
  color: var(--outline);
}
.prose blockquote > :last-child { margin-bottom: 0; }
.prose code {
  padding: 0 var(--space-1);
  border: 1px solid var(--rule);
  border-radius: calc(var(--radius-sm) / 2);
  background: var(--field);
  font-size: .9em;
  color: var(--outline);
}
/* A well, like `.box.is-inset`. It scrolls sideways only: a line of code is
   not wrapped, because a wrapped line of code reads as two lines. */
.prose pre {
  margin: 0 0 var(--space-3);
  padding: var(--space-3) var(--space-4);
  border: 1px solid var(--rule);
  border-radius: var(--radius-sm);
  background: var(--field);
  overflow-x: auto;
  overflow-y: hidden;
  overflow-wrap: normal;
}
.prose pre code {
  padding: 0;
  border: 0;
  background: none;
  font-size: var(--text-5);
  line-height: 1.6;
  white-space: pre;
}
/* `.table`'s look: rules between rows, the head in faint small caps. Block,
   so a wide table scrolls sideways inside the column instead of widening it,
   and as wide as its content: a block is otherwise the column's width, and
   its border ran on past the last column with nothing under it. */
.prose table {
  display: block;
  width: fit-content;
  max-width: 100%;
  margin: 0 0 var(--space-3);
  overflow-x: auto;
  border: 1px solid var(--edge);
  border-radius: var(--radius);
  border-collapse: separate;
  border-spacing: 0;
  font-size: var(--text-6);
  line-height: 1.5;
}
.prose th {
  padding: var(--space-2) var(--row-x);
  border-bottom: 1px solid var(--edge);
  background: var(--surface-low);
  color: var(--dim);
  font-size: var(--text-1);
  font-weight: 600;
  letter-spacing: .06em;
  text-transform: uppercase;
  text-align: left;
  white-space: nowrap;
}
.prose td {
  padding: var(--space-2) var(--row-x);
  border-bottom: 1px solid var(--rule);
  vertical-align: top;
}
.prose tr:last-child > td { border-bottom: 0; }
.prose hr { margin: var(--space-5) 0; }
.prose strong { font-weight: 600; color: var(--on); }
.prose em { font-style: italic; }

/* ----------------------------------------------------------- placeholder --
   A feature that is not built yet, framed where it will be. Dashed, because
   it is a space kept rather than a thing, and it names the card that builds
   it, so nobody reads it as broken or as a list with nothing in it. Never a
   control inside: there is nothing to press yet.

     <div class="placeholder">
       <span class="placeholder-mark" data-icon="monitoring"></span>
       <div class="placeholder-title">Performance</div>
       <div class="placeholder-text">Opens, clicks and revenue for each send.</div>
       <span class="placeholder-card">MEM-17</span>
     </div> */
.placeholder {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: var(--space-2);
  padding: var(--space-6) var(--space-5);
  border: 1px dashed var(--edge);
  border-radius: var(--radius);
  text-align: center;
}
/* A chart's room: three tall rows, so the page keeps its shape when the
   chart arrives. */
.placeholder.is-tall { min-height: calc(3 * var(--row-h-3)); }
.placeholder-mark { color: var(--fainter); }
.placeholder-mark .icon { --icon-size: var(--icon-size-lg); }
.placeholder-title { font-size: var(--text-7); font-weight: 600; color: var(--on-variant); }
.placeholder-text { max-width: 52ch; font-size: var(--text-4); line-height: 1.5; color: var(--dim); }
.placeholder-card {
  margin-top: var(--space-1);
  padding: var(--chip-y) var(--chip-x);
  border: 1px solid var(--edge);
  border-radius: var(--radius-pill);
  background: var(--surface-cont);
  font-family: var(--mono);
  font-size: var(--text-3);
  color: var(--dim);
}

/* ------------------------------------------------------------- doc frame --
   A rendered document shown inside the page, an email most of all: an inset
   well holding an <iframe>, with a bar above it for what it is and how wide
   to show it. The paper inside is white in both themes (`--doc-paper`),
   because the document was designed on white.

     <div class="doc-frame">
       <div class="doc-frame-bar"><span>Preview</span><div class="segmented">…</div></div>
       <iframe title="…" srcdoc="…" sandbox></iframe>
     </div>

   `.is-narrow` shows it at a phone's width, centred in the well. The page
   sets the frame's height, to its content when it can read it. */
.doc-frame {
  border: 1px solid var(--rule);
  border-radius: var(--radius);
  background: var(--field);
  overflow: hidden;
}
.doc-frame-bar {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  min-height: calc(var(--control-h) + 2 * var(--space-2));
  padding: var(--space-2) var(--space-2) var(--space-2) var(--space-4);
  border-bottom: 1px solid var(--rule);
  font-size: var(--text-4);
  color: var(--dim);
}
.doc-frame-bar > :first-child { flex: 1 1 auto; min-width: 0; }
.doc-frame iframe {
  display: block;
  width: 100%;
  min-height: calc(5 * var(--row-h-3));
  border: 0;
  background: var(--doc-paper);
  /* Drawn in the scheme the email is: matched, the frame's canvas stays
     clear and the white under it shows, where a dark page would have the
     browser paint a dark canvas behind a light email. */
  color-scheme: light;
}
.doc-frame.is-narrow iframe {
  /* The phone's width is the email's, inside the frame's border. */
  box-sizing: content-box;
  width: min(var(--doc-narrow), calc(100% - 2 * var(--space-4) - 2px));
  margin: var(--space-4) auto;
  border: 1px solid var(--rule);
  border-radius: var(--radius-sm);
}

/* ---------------------------------------------------- tiles in a column --
   The figures of a thing, across the main column: `.side-tile`s in a grid
   that fits as many as the width holds. Wider than the side pane's and
   further apart, because the main column has the room, and a figure read
   across a screen wants it. */
.tiles {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(160px, 1fr));
  gap: var(--space-3);
}
.tiles > .side-tile { padding: var(--space-3) var(--space-4); }

/* --------------------------------------------------------------- actions --
   A row of buttons under what they act on: a section's Edit, a form's Save
   beside Cancel. They wrap rather than squeeze, and keep a step from the
   block above, which buttons dropped under a paragraph used to sit on. A
   note in the row says what they do, in a hint's faint small type. */
.actions {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-2);
  margin-top: var(--space-3);
}
.actions-note { font-size: var(--text-3); color: var(--faint); }
