/* OAR dashboard — the workstation, ported from the Reading Room and re-toned onto OOPS.
 *
 * THIS REPLACES `viewer.css`, which is retired. The Reading Room's `readingroom/ui/static/app.css`
 * carries a second register beside its finding-aid one — the archive WORKSTATION: a folder canvas,
 * floating windows, a message log, a media viewer, statistics and history panes. That register is
 * PORTED HERE, rule for rule, with every colour re-pointed at the OOPS tokens this dashboard already
 * declares in `tokens.css` — nothing here NAMES a colour, it only borrows one.
 *
 * THE MAPPING IS FIXED, NOT IMPROVISED. The Reading Room's `--accent` family becomes the OOPS `--key`
 * family (the open call's magenta, re-cut for a light ground); `--accent-soft`, which the source
 * reserves for dark-ground glyphs, becomes `--signal` (the system's own cyan voice); the Reading
 * Room's `--bg-*` surfaces become the OOPS `--board-*` plates, and `--bg-hover`/`--bg-active` both
 * become `--wash` — the register's own greys and `--font-mono`'s system stack are NOT reached for
 * here even where tokens.css still declares them, because this dashboard's cream ground and single
 * DM Mono face are the point of the OOPS layer, not an alternative to it. `--ink`, `--ink-muted`,
 * `--ink-faint`, `--line`, `--line-strong`, `--line-ink`, `--ok`, `--danger`, `--warn`, `--pending*`,
 * `--shadow-win`, `--mono`, `--display`, `--body` and `--sidebar-w` are declared under both registers
 * and need no re-pointing — they are kept exactly as they stood.
 *
 * NO CUSTOM PROPERTY IS DECLARED IN THIS FILE. `tokens.css` is the one place a colour is named, and a
 * `--x:` here would be a second such place. Two literals in the source had no forward mapping —
 * translucent overlays on top of an already-coloured surface, which this palette has no rgba token
 * for — and each is called out at its own rule with the substitution made and why.
 *
 * SCOPED SO IT CANNOT REACH THE OTHER EIGHT PAGES. This sheet is linked everywhere, and `app.css`
 * already owns the bare-element register for the finding aid (`a`, `button`, `input`, `.mono`,
 * `.faint`, `.eyebrow`, `.note`, `.danger`, `.ok`…). Every control plate the workstation needs is
 * therefore restated under `.ws` rather than reused from the shared one, and every bare element below
 * (`li`, `td`, `summary`, `svg`…) is qualified by a workstation class or id ancestor — never bare on
 * its own.
 *
 * WHAT WAS DELIBERATELY NOT PORTED, and why each is left behind:
 *   - the finding-aid shell (`.spine`, `.surface`) — OAR's `app.css` already owns the page frame
 *     (`.strip`, `.aid`); the workstation sits inside it, it does not bring a second one.
 *   - the mailbox (`.mail*`), the join wizard's `.door`, and `#archives` — correspondence and
 *     onboarding surfaces this dashboard does not carry.
 *   - the link EDITOR (`#win-link`, `.link-*`, `.edge*`) — choosing an object, a predicate and a
 *     line for the timeline is a form, and this app renders its forms on the SERVER so they work
 *     with no script and refuse in place. `/relationships` is that form.
 *     **The relationship WINDOW has come off this list** (checklist item 33): `#win-relationships`
 *     and the `.rel-*` rules below are here now, because the graph service it would have opened onto
 *     exists — `oar.graph`, `App.edges_at`, and each mount's `/api/graph`. They keep the Reading
 *     Room's names rather than taking this page's `.edges__*`, which is deliberate and argued at the
 *     rules themselves: two surfaces, two names, no shared rule to drift.
 *   - every `.code-*` rule and the syntax-colour block — the code-view TAB is rendered (disabled),
 *     but the panels it would open are not built, so their rules would be dead weight and an
 *     invitation to wire them up early.
 *   - the commit sheet (`.dialog`, `.sheet-*`) — the formal write act the Reading Room fronts with a
 *     review-and-acknowledge dialog; this dashboard's write half is still a stub (see
 *     `.ws [data-stub]` below), so there is nothing yet for a commit sheet to commit.
 *   - `body`/`html` rules — `app.css` already paints this dashboard's own ground.
 *
 * TWO RULES HAVE NO READING ROOM EQUIVALENT and were written fresh, in the same flat, drawn-edge
 * register: `.ws-scope` (the mono label saying which folder a view is scoped to) and
 * `.ws [data-stub]` (marking the write-half controls that are wired to a stub, so a person can see at
 * a glance which half of the toolbar is not built yet — given the pending treatment `button.pending`
 * already carries in the source, on the theory that "not built" and "not yet pushed" are the same
 * kind of incompleteness this palette already has one colour for).
 */

/* ─── the workstation shell: crumb bar, toolbar, sidebar, icon grid ──────────────────────────────── */
/* **IT SPANS BOTH COLUMNS OF THE FINDING AID, AND IT HAS TO BE SAID.** The workstation sits inside
   a `_section`, whose children are items of `.aid`'s two-column grid — gutter, then content. Without
   this the desktop auto-placed into the 248px gutter track and rendered as a sliver: a whole
   workstation in the margin, at every desktop width, with nothing erroring.
   `test_EVERY_GRID_ITEM_OF_EVERY_REAL_PAGE_LANDS_IN_A_COLUMN` caught it, which is what it is for.
   `.month`, `.grid` and `.oops` in app.css carry the same line for the same reason.

   The height is VIEWPORT-RELATIVE because a desktop is a desktop: the sidebar scrolls, the canvas
   scrolls, and the toolbar stays where a person left it. `min-height` rather than `height` so a
   short window does not clip the windows floating over it. */
.ws { grid-column: 1 / -1; display: flex; flex-direction: column;
      /* THE SPINE ABOVE AND THE DECK BELOW ARE BOTH SUBTRACTED. With `<main>` its own scroll
         container (item 54) a box of `100vh - titlebar` is taller than the space there is, so the
         workstation arrived with a permanent scrollbar showing 44px of nothing. */
      min-height: calc(100vh - var(--titlebar-h) - var(--deck-h)); background: var(--board-sunken); }
.ws-book {
  display: flex; align-items: flex-start; justify-content: space-between; gap: 16px;
  padding: 8px 14px 7px; background: var(--board-raised); border-bottom: 1px solid var(--line-strong);
}
/* **A CHAIN ADDRESS IS ONE WORD TO A BROWSER, AND `min-width: 0` DOES NOT BREAK WORDS.** Measured
   at 375px, `.ws-book .min0`, its `h1` and its `.ws-sub` all ran to x=450 — 75px past the screen —
   because the sub-line carries a 66-character `0x…` chain id with nothing in it a line may break
   at. The page itself did not scroll sideways, which is why this survived: `<main>` is a scroll
   container now (item 54) and absorbed it into its own horizontal scrollbar. That is the house rule
   working, and it is not a reason to ship a book header a phone cuts in half. The gutter and
   `.field__value` carry the identical line for the identical reason. */
.ws-book h1 { color: var(--key); font-size: 15px; font-weight: 600; overflow-wrap: anywhere; }
.ws-book .ws-sub { font-size: 11px; color: var(--ink-muted); margin-top: 2px;
                   overflow-wrap: anywhere; }
.ws-book .min0 { min-width: 0; }
/* the mono label saying which folder the current view is scoped to — no Reading Room equivalent */
.ws-scope { margin-left: auto; font-family: var(--mono); font-size: 11px; color: var(--ink-muted);
  text-align: right; white-space: nowrap; }

.crumbbar {
  display: flex; align-items: center; gap: 6px; padding: 0 14px; height: 30px;
  background: var(--key); color: var(--key-ink); font-size: 12.5px; white-space: nowrap; overflow-x: auto;
}
.crumbbar a, .crumbbar .crumb-here { color: var(--key-ink); display: inline-flex; align-items: center; gap: 6px; }
.crumbbar a:hover { text-decoration: underline; color: var(--key-ink); }
.crumbbar .crumb-here { font-weight: 600; }
.crumbbar .crumb-sep { color: var(--key-ink); opacity: .6; }
.crumbbar .sq { width: 12px; height: 12px; background: var(--key-ink); opacity: .85; display: inline-block; }

.toolbar {
  display: flex; align-items: center; gap: 6px; padding: 6px 10px; background: var(--board-sunken);
  border-bottom: 1px solid var(--line-strong); flex-wrap: wrap;
}
.toolbar .toolbar-sep { flex: 1; }
.toolbar .tray { display: inline-flex; gap: 4px; }
.toolbar .tray button { font-size: 11.5px; color: var(--ink-muted); }
.toolbar .tray button.on { color: var(--ink); background: var(--board-raised); box-shadow: 0 0 0 1px var(--line-ink); }
#staged-count { font-family: var(--mono); font-size: 11px; color: var(--pending); }

.ws-body { display: flex; flex: 1; min-height: 0; }
.sidebar { width: var(--sidebar-w); flex: none; background: var(--board-raised); border-right: 1px solid var(--line-strong); display: flex; flex-direction: column; }
.sidebar-head { background: var(--key); color: var(--key-ink); padding: 6px 12px; font-weight: 600; font-size: 12.5px; }
.sidebar ul { list-style: none; margin: 0; padding: 6px 0; overflow: auto; }
.sidebar li { display: flex; align-items: center; gap: 10px; padding: 6px 12px; cursor: default; font-size: 12.5px; color: var(--ink); }
.sidebar li:hover { background: var(--wash); }
/* accent-wash-strong -> --key-wash: this palette has no "strong" wash to reach for */
.sidebar li.on { background: var(--key-wash); }
.sidebar li.pending .sq { background: var(--pending); }
/* **A FOLDER IS THE SAME COLOUR IN BOTH PANES, AND IT WAS NOT.** The sidebar's marker was
 * `--signal` (teal) while the grid's folder glyph is `--key` (crimson) — the same object, named by
 * two different colours a few pixels apart, which is the seam a re-tokened sheet leaves when each
 * rule is mapped in isolation and nothing looks at the two together. Colour is what tells a folder
 * from a record here, so it has to mean one thing across the whole workstation. */
.sidebar .sq { width: 16px; height: 16px; background: var(--key); flex: none; }
.sidebar .empty { padding: 12px; font-size: 12px; color: var(--ink-faint); text-align: left; }

/* ONE WORKING SURFACE, NOT TWO. The sidebar is `--board-raised` and the canvas inherited the
 * marigold ground, so the tree and the grid read as two different documents pinned side by side
 * with a rule between them. They are one view of one folder: same paper, and the border does the
 * separating. */
.canvas { flex: 1; min-width: 0; overflow: auto; padding: 14px; position: relative;
          background: var(--board-raised); }
.icons { display: grid; grid-template-columns: repeat(auto-fill, 102px); gap: 8px 6px; align-content: start; }
.icon {
  display: flex; flex-direction: column; align-items: center; gap: 6px; text-align: center;
  padding: 10px 6px 8px; cursor: default; user-select: none; position: relative;
}
.icon:hover { background: var(--wash); }
/* **SELECTED IS A MARKED TILE, NOT A PAINTED ONE.** A solid key fill is right in the Reading Room,
 * where the accent is an electric blue; OOPS's key is a deep crimson and the same rule turned a
 * 102x72 tile into a slab of magenta with the name reversed out of it — the operator's screenshot.
 * The wash plus a drawn key edge says the same thing at a fraction of the weight, and keeps the
 * icon and the name in their own colours instead of flattening both to one. */
/* **A NAMED FOLDER LOOKS UNFINISHED, BECAUSE IT IS.** It is not in the archive until something is
 * filed into it and the session is stored, so it is drawn dashed and dimmed — enterable, and
 * obviously not yet a thing. Solid would be a claim the book cannot back.
 *
 * **IT IS `icon--pending` AND NOT `pending`, BECAUSE `pending` WAS ALREADY TAKEN.** `app.css`
 * gives a bare `.pending` `grid-column: 1 / -1` for the staging deck's list, so the first version
 * rendered a 102px tile as a 426px bar across the grid. Nothing errored — the tile simply came out
 * the wrong shape, which is how a class collision always presents. The second one today:
 * `data-chain` was already the workstation mount's. A single-word global in a shared sheet is a
 * name anybody can take by accident. */
/* **SCOPED `.ws …` BECAUSE A TILE IS A BUTTON.** `.ws button` sets the control plate's border and
 * is (0,1,1); a bare `.icon--pending` is (0,1,0) and loses — so the rule was present, correct, and
 * computed to `border-style: none`. Nothing errored and the tile was the right SIZE, which is why
 * the first look said "fixed". The inspector's computed value said otherwise.
 *
 * That is the third thing today that rendered wrongly with nothing raised: two class collisions and
 * now a specificity loss. The pattern is the same each time — CSS fails by being overridden, never
 * by complaining — so the check has to be the COMPUTED value, not the presence of the rule. */
.ws .icon--pending {
  /* `.ws button` draws its edge as a `box-shadow` ring and sets `border: none`. A dashed BORDER
   * alongside that ring would give a pending tile two edges, so the ring is dropped here and the
   * dash is the whole edge — which is also why the dash reads as "not finished" rather than as a
   * tile wearing an extra outline. */
  border: 1px dashed var(--line-strong); box-shadow: none;
}
.ws .icon--pending .glyph { opacity: .55; }
.ws .icon--pending .icon-name { color: var(--ink-muted); font-style: italic; }
.icon.selected { background: var(--key-wash); box-shadow: inset 0 0 0 2px var(--key); }
.icon.selected .icon-name { color: var(--ink); }
.icon.selected .glyph { color: var(--key); }
/* **THE BOX WAS 40px AND THE PICTURE WAS NOT.** These rules sized a span holding a text character,
 * so the character stayed at the body size and floated in a large empty square. The glyph is drawn
 * SVG now (`workstation.js:glyphFor`), which fills the box it is given — the sizing here is finally
 * the size of the thing a person sees. */
.icon .glyph { width: 34px; height: 34px; display: block; color: var(--signal); }
/* A folder is the thing you can go INTO, so it carries the key colour and a record stays quiet.
 * That is the one distinction the grid has to make at a glance, and colour makes it before shape
 * does. */
.icon.kind-folder .glyph { color: var(--key); }
.icon-name { font-size: 11px; line-height: 1.25; color: var(--ink); max-width: 100%; word-break: break-all;
  display: -webkit-box; -webkit-line-clamp: 3; -webkit-box-orient: vertical; overflow: hidden; }
.empty { color: var(--ink-faint); padding: 40px 0; text-align: center; }
#more { margin-top: 14px; }

/* ─── the view tab strip: the code-view tab is rendered, disabled — its panels are not ported ──── */
.viewbar { display: flex; gap: 0; padding: 0 10px; background: var(--board-sunken); border-bottom: 1px solid var(--line-strong); }
.viewbar button { background: transparent; box-shadow: none; color: var(--ink-muted); padding: 7px 12px; font-size: 12px; }
.viewbar button[aria-selected="true"] { color: var(--ink); background: var(--board-raised); box-shadow: 0 0 0 1px var(--line-strong); }
.viewbar button[disabled] { color: var(--ink-faint); cursor: not-allowed; }

/* ─── floating windows: white plates, key-coloured title bars, drawn edges ───────────────────────── */
.win {
  position: fixed; display: flex; flex-direction: column; background: var(--board-sunken); color: var(--ink);
  box-shadow: var(--shadow-win); z-index: 40; min-width: 320px; max-width: calc(100vw - 16px);
  /* **THE DECK IS 36px OF FIXED CHROME AT THE FOOT OF EVERY SIGNED-IN PAGE**, and a window sized
   * `100vh - 60px` could be dragged to a bottom that the deck then covered. The ceiling clears the
   * spine above and the deck below, so a window at full height is a window a person can read all
   * of. */
  max-height: calc(100vh - var(--titlebar-h) - var(--deck-h) - 12px);
  /* 320px RATHER THAN 260px, and the number comes from the content. The widest thing any of these
   * panels holds is a `blake3://` kuri — 64 hex characters plus the scheme — and the window with a
   * 96px key track and 10px of padding gives a value column of `width - 130`. At 260px that is
   * 130px, about 18 mono characters a line; at 320px it is 190px and a kuri wraps in two lines of
   * roughly 27. Below that the panel stops being readable and starts being a column of fragments. */
  resize: both; overflow: hidden;
}
.win.front { z-index: 60; }
.win.dragging { user-select: none; }
/* **`.deck__head` JOINS THIS RULE RATHER THAN COPYING IT** — checklist item 57. The deck's panels
   now carry a title row with their minimise control at the top right, which is exactly where
   `windows.js` puts minimise and close on every floating window. Restating these declarations under
   a second class would be two descriptions of one piece of chrome, and the day somebody changed the
   title bar's height the deck would be the one panel in the interface that did not follow. The
   `cursor: grab` is overridden immediately below, because a deck panel is not draggable and a grab
   cursor on something that cannot be moved is an affordance that lies. */
.win-title, .deck__head {
  display: flex; align-items: center; gap: 10px; padding: 0 6px 0 10px; height: 26px; flex: none;
  background: var(--key); color: var(--key-ink); font-weight: 600; font-size: 12.5px; cursor: grab;
}
.deck__head { cursor: default; }
.deck__head-name { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.deck__head-ctl { display: inline-flex; gap: 2px; }
/* (0,3,0) against `.deck .icon-btn`'s (0,2,0): the bar's icon buttons are ink on cream, and one of
   those on the key-coloured head would be a control drawn in the ground it sits on. */
.deck .deck__head-ctl .icon-btn { color: var(--key-ink); }
.deck .deck__head-ctl .icon-btn:hover { background: var(--key-hover); }
.win-title .win-name { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.win-title .win-count { font-family: var(--mono); font-weight: 400; opacity: .9; }
.win-title .win-ctl { display: inline-flex; gap: 2px; }
.win-title .win-ctl button { color: var(--key-ink); }
/* source: `background: rgba(255, 255, 255, .18)` — a lightening overlay over the key title bar.
   This palette declares no translucent-white token, so the nearest declared one is the pressed key
   shade itself: the bar darkens under the control instead of lightening, which reads just as clearly
   as "hovering something on the title bar". */
.win-title .win-ctl button:hover { background: var(--key-hover); }
.win-body { padding: 10px; overflow: auto; flex: 1; min-height: 0; }
.win-actions { display: flex; align-items: center; gap: 6px; flex-wrap: wrap; margin: 8px 0 10px; }
#win-context { top: 46vh; right: 14px; width: min(460px, calc(100vw - 28px)); max-height: calc(54vh - 20px); }
#win-pending { top: 320px; left: max(220px, calc(50% - 300px)); width: min(600px, calc(100vw - 240px)); }
/* The relationships window sits on the LEFT, opposite the context panel. They are read together —
 * "what is this" and "what is it attached to" — and stacking both on the right would put one behind
 * the other at the default arrangement, which is the position a person meets before they have
 * dragged anything. */
#win-relationships { top: 46vh; left: 220px; width: min(460px, calc(100vw - 240px)); max-height: calc(54vh - 20px); }
/* **THE TWO STATUS PANELS ARE ANCHORED TO THE FOOT, AND THEY CLEAR THE DECK.** Checklist item 47.
 *
 * `#win-messages` was `top: 620px` — a coordinate that assumes a window 700px or taller and lands
 * below the fold on a laptop — and `#win-session` was `bottom: 20px`, which is UNDER the 36px deck
 * on every signed-in page. Both are now measured from the bottom edge, past the deck, so neither
 * position depends on how tall the viewport happens to be.
 *
 * `left: 220px` is not a magic number and is kept: `--sidebar-w` plus the surface's own edge, so
 * messages opens just clear of the file tree rather than on top of it. It only read as "stranded in
 * dead space" because the SURFACE was boxed into 1248px of a 1920px screen; with the workstation
 * given the screen (`main.aid:has(.ws)` in app.css) the same 220px is inside it. The width is
 * bounded by what is left after the sidebar so it can never be clipped by the right edge either. */
#win-messages { top: auto; bottom: calc(var(--deck-h) + 8px); left: 220px; width: min(340px, calc(100vw - 248px)); }
#win-session { bottom: calc(var(--deck-h) + 8px); right: 14px; width: min(420px, calc(100vw - 28px)); }
#viewer { top: 100px; left: 50%; transform: translateX(-50%); width: min(880px, calc(100vw - 48px)); z-index: 61; }
#viewer.dragging { transform: none; }
.win.win-wide { width: min(760px, 92vw); }
.win table { font-size: 12px; }
.win th { font-size: 11px; background: var(--key); color: var(--key-ink); border-bottom: none; font-weight: 500; }
.win td { padding: 4px 8px; }
.win .actions-row { display: flex; gap: 6px; align-items: center; margin-top: 8px; flex-wrap: wrap; }
.win-empty { padding: 16px 8px; color: var(--ink-faint); font-size: 12px; }
/* The relationships window's two lists. **`.rel-*` AND NOT `.edges__*`**, which is the page's name
 * for the same idea: `app.css` is loaded on every page including this one, so sharing the class
 * would put the finding aid's 15.5rem gutter rhythm inside a 460px panel — a collision that renders
 * and raises nothing, which is precisely the defect class this repo met four times in one day. Two
 * surfaces, two names, no shared rule to drift. */
.rel-list { list-style: none; margin: 0 0 6px; padding: 0; }
.rel-list li {
  display: flex; flex-wrap: wrap; align-items: baseline; gap: 6px;
  padding: 4px 2px; border-bottom: 1px solid var(--line); font-size: 12px;
}
.rel-list .rel-pred {
  font-family: var(--mono); font-size: 11px; color: var(--ink-faint);
  text-transform: uppercase; letter-spacing: .04em;
}
.rel-list .rel-what { font-family: var(--mono); font-size: 11.5px; overflow-wrap: anywhere; }
.rel-list .rel-empty { color: var(--ink-faint); border-bottom: none; }
.win details { margin-top: 10px; }
.win details > summary { cursor: pointer; font-size: 12px; color: var(--key); }

/* ─── fields and panels: a window's own field list, key/value blocks, and preview furniture ────────
 *
 * **EVERY SELECTOR HERE IS SCOPED TO `.ws`/`.win`, AND THAT IS A FIX RATHER THAN A TIDY-UP.** It
 * was `.fields` and `.field`, unscoped — and `app.css` declares `.fields` and `.field` too, for the
 * finding aid's own read-only profile list. Same names, same specificity (0,1,0), and `app.css` is
 * loaded AFTER this sheet (`render.page` links tokens, workstation, app in that order), so on every
 * page in this app the finding aid's rule won inside the workstation's floating windows.
 *
 * It rendered. It raised nothing. What it did was give every window field the PROFILE's two-column
 * template — `var(--gutter-w) minmax(0, 1fr)`, a 15.5rem label track — inside a 380px panel. The
 * measurement, taken on the live `#win-session` with `getComputedStyle`, was
 * `grid-template-columns: 248px 40px`: a 40px value column, a `root kuri` wrapping at about eight
 * characters a line, each such row 260px tall, and the window 1020px from title bar to foot to show
 * eight facts. That is exactly the operator's screenshot, and the cause was never the window's
 * width.
 *
 * This is the FIFTH time in this estate a CSS rule has failed by being overridden rather than by
 * being wrong, so the fix is the one the `.rel-*` block already argues for one screen up: two
 * surfaces, two scopes, no shared rule to drift. `.ws .field` and `.win .field` are (0,2,0) and beat
 * `app.css`'s (0,1,0) wherever the sheets are ordered — which is the point: this must not depend on
 * which `<link>` comes last. Both ancestors are named because a window is `position: fixed` and
 * lives OUTSIDE `.ws` in the box tree as well as, on some pages, outside it in the DOM. */
.ws .fields, .win .fields { display: flex; flex-direction: column; gap: 3px; list-style: none; margin: 0; padding: 0; }
.ws .field, .win .field { display: grid; grid-template-columns: 96px 1fr auto; align-items: center; gap: 8px; background: var(--board-well); padding: 4px 8px; min-height: 26px; font-size: 12px; border-bottom: none; }
.ws .field .k, .win .field .k { color: var(--ink-faint); font-size: 11px; }
.ws .field .v, .win .field .v { text-align: right; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-variant-numeric: tabular-nums; }
.ws .field .v.left, .win .field .v.left { text-align: left; }
.ws .field .v.wrap, .win .field .v.wrap { white-space: normal; overflow-wrap: anywhere; text-align: left; }
.ws .field .v.pending, .win .field .v.pending { color: var(--pending); }
.kv { display: grid; grid-template-columns: 112px 1fr; gap: 0 10px; padding: 4px 8px; background: var(--board-well); margin-bottom: 2px; font-size: 12px; }
.kv-k { color: var(--ink-faint); font-size: 11px; padding-top: 1px; }
.kv-v { min-width: 0; overflow-wrap: anywhere; font-variant-numeric: tabular-nums; }
.kv-v pre { margin: 0; font-size: 11px; white-space: pre-wrap; overflow-wrap: anywhere; color: var(--ink-muted); }
.panel-preview { margin: 8px 0; background: var(--board-raised); box-shadow: inset 0 0 0 1px var(--line-strong); display: flex; justify-content: center; }
.panel-preview img { max-width: 100%; max-height: 220px; display: block; }
.panel-preview audio { width: 100%; }
.panel-desc { margin: 8px 0 4px; color: var(--ink-muted); font-size: 12.5px; }
.panel-section { margin-top: 10px; }
.panel-section .eyebrow { margin-bottom: 4px; }

/* ─── the message log ─────────────────────────────────────────────────────────────────────────────── */
.log { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 2px; }
.log li { background: var(--board-well); padding: 5px 8px; font-size: 12px; overflow-wrap: anywhere; border-bottom: 1px solid var(--line-strong); }
.log li.danger { color: var(--danger); }
.log li.ok { color: var(--ok); }
.log li .t { color: var(--ink-faint); font-family: var(--mono); font-size: 10.5px; margin-right: 6px; }
.json { margin: 6px 0 0; padding: 8px; background: var(--board-raised); box-shadow: inset 0 0 0 1px var(--line-strong); font-size: 11px; white-space: pre-wrap; overflow-wrap: anywhere; max-height: 260px; overflow: auto; }

/* ─── the media viewer body ────────────────────────────────────────────────────────────────────────── */
.viewer-body { padding: 10px; display: flex; justify-content: center; align-items: center; background: var(--board-well); overflow: auto; }
.viewer-body img, .viewer-body video { max-width: 100%; max-height: calc(100vh - 220px); display: block; }
.viewer-body audio { width: 100%; }
.viewer-body .viewer-pdf { width: 100%; height: calc(100vh - 220px); border: none; background: var(--board-raised); }
.viewer-body .viewer-text { width: 100%; max-height: calc(100vh - 220px); margin: 0; font-size: 12px; white-space: pre-wrap; overflow-wrap: anywhere; }
.viewer-msg { text-align: center; padding: 28px 16px; }

/* ─── statistics + history windows ────────────────────────────────────────────────────────────────── */
.stat-cards { display: grid; grid-template-columns: repeat(auto-fit, minmax(120px, 1fr)); gap: 8px; margin-bottom: 10px; }
.stat-card { background: var(--board-well); box-shadow: 0 0 0 1px var(--line-strong); padding: 8px 10px; }
.stat-card .v { font-family: var(--mono); font-size: 16px; font-variant-numeric: tabular-nums; }
.stat-card .k { color: var(--ink-muted); font-size: 11px; text-transform: uppercase; letter-spacing: .04em; }
.stat-card .k.unknown { color: var(--warn); }
#stats-spark svg { width: 100%; height: 64px; display: block; }
#stats-spark .area { fill: var(--key-wash); }
#stats-spark .line { fill: none; stroke: var(--key); stroke-width: 1.5; }

.history { display: grid; grid-template-columns: minmax(240px, 300px) 1fr; gap: 10px; max-height: 70vh; }
.history-list { overflow: auto; border-right: 1px solid var(--line-strong); padding-right: 8px; }
.history-detail { overflow: auto; }
.history-foot { grid-column: 1 / -1; display: flex; gap: 10px; align-items: center; }
.hist-row { padding: 6px 6px; cursor: pointer; border-bottom: 1px solid var(--line-strong); }
.hist-row:hover, .hist-row.selected { background: var(--key-wash); }
.hist-row .seq { font-family: var(--mono); color: var(--ink-muted); font-size: 11px; }
.hist-row .title { font-size: 12.5px; }
.hist-row .meta { font-family: var(--mono); font-size: 11px; color: var(--ink-muted); display: flex; gap: 6px; flex-wrap: wrap; }
.delta-pos { color: var(--ok); }
.delta-neg { color: var(--danger); }
.hist-docs td.path a { word-break: break-all; }

/* ─── the busy loader: what a control does between the click and the answer ──────────────────────────
 * Ported in full, unscoped, as the source has it: `.spinner`, `.busy-node`, `.busy-label` and
 * `.is-busy` are the loader's own classes and only busy.js ever sets them, so nothing but the loader
 * can reach them. `button[aria-busy="true"]` is the one exception — a bare-element attribute selector
 * would otherwise style every busy button on every other page — so it is scoped under `.ws` here,
 * the one addition this port makes to the source rule. */
.spinner {
  display: inline-block; width: 10px; height: 10px; flex: none; vertical-align: -1px;
  border: 1.5px solid currentColor; border-right-color: transparent; border-bottom-color: transparent;
  animation: busy-spin .7s linear infinite;
}
@keyframes busy-spin { to { transform: rotate(360deg); } }
/* **ASKING AND SIGNING SPEAK IN `--signal`, WHICH THE STYLE BOOK ALREADY RESERVED FOR THIS.**
 * `docs/oops-interaction.md` §2 gives each of the five states of a write exactly ONE expression,
 * and `--signal` is named in the palette as *the system's own voice* — a request in flight is the
 * system speaking about itself, which is precisely what it was reserved for and what it had never
 * once been used for. The muted ink it replaces said nothing at all: a narrated wait drawn in the
 * same colour as a caption is a wait a person reads past.
 *
 * NO TOKEN IS ADDED AND NO COLOUR MOVES (§6). This names a value `tokens.css` already declares. */
.busy-node { display: inline-flex; align-items: center; gap: 6px; color: var(--signal); }
.busy-label { font-size: inherit; }
.ws button[aria-busy="true"] { display: inline-flex; align-items: center; gap: 6px; }
/* The narrated label on a busy control, wherever it is — the deck's store, a compose form's
  * submit, the workstation's toolbar. Scoped to the attribute `busy.js` sets rather than to a
  * class, because the label IS the attribute's meaning. */
button[aria-busy="true"] .busy-label { color: var(--signal); }
.is-busy { position: relative; }
.is-busy > * { opacity: .55; }
.is-busy::after {
  content: ''; position: absolute; top: 6px; right: 6px; width: 10px; height: 10px; z-index: 2;
  border: 1.5px solid var(--key); border-right-color: transparent; border-bottom-color: transparent;
  animation: busy-spin .7s linear infinite;
}
@media (prefers-reduced-motion: reduce) {
  .spinner, .is-busy::after { animation: busy-pulse 1.4s ease-in-out infinite; }
  @keyframes busy-pulse { 0%, 100% { opacity: 1; } 50% { opacity: .25; } }
}

/* ─── control plates: flat, drawn edges, scoped under .ws so the other eight pages keep their own
 * register (`app.css`'s `.button`, `.signin__go`, `.compose__control`…) untouched ────────────────── */
.ws button, .ws .btn {
  font: inherit; font-size: 12.5px; line-height: 1; color: var(--ink);
  background: var(--board-well); border: none; box-shadow: 0 0 0 1px var(--line-strong);
  padding: 6px 10px; cursor: pointer; text-decoration: none; display: inline-flex; align-items: center; gap: 6px;
  white-space: nowrap;
}
.ws button:hover, .ws .btn:hover { background: var(--wash); color: var(--ink); text-decoration: none; }
.ws button:active, .ws .btn:active { box-shadow: 0 0 0 1px var(--line-ink); }
.ws button.primary, .ws .btn.primary { background: var(--key); color: var(--key-ink); box-shadow: none; }
.ws button.primary:hover, .ws .btn.primary:hover { background: var(--key-hover); color: var(--key-ink); }
.ws button.secondary, .ws .btn.secondary { background: var(--board-raised); }
.ws button.pending, .ws .btn.pending { color: var(--pending); box-shadow: 0 0 0 1px var(--pending); background: var(--board-raised); }
.ws button.pending:hover, .ws .btn.pending:hover { background: var(--pending-wash); }
.ws button:disabled, .ws .btn.disabled { opacity: .45; cursor: not-allowed; pointer-events: none; }
/* source: `background: rgba(0, 0, 0, .12)` — a darkening overlay over whatever plate the icon-btn
   sits on. Nearest declared token for "a step darker than the surrounding surface" is the well
   plate itself, which is what every inset field and table head in this register already uses. */
.ws .icon-btn { background: transparent; box-shadow: none; width: 22px; height: 22px; padding: 0; justify-content: center; font-size: 15px; color: inherit; }
.ws .icon-btn:hover { background: var(--board-well); }
.ws input, .ws select, .ws textarea {
  font: inherit; font-size: 12.5px; color: var(--ink); background: var(--board-raised);
  border: none; box-shadow: inset 0 0 0 1px var(--line-strong); padding: 5px 8px;
}
.ws input:focus-visible, .ws select:focus-visible, .ws textarea:focus-visible { box-shadow: inset 0 0 0 1px var(--key); outline: none; }
.ws input.mono { font-family: var(--mono); font-size: 12px; }

/* ─── shared text plates, scoped under .ws or .win so they cannot reach app.css's own register ────── */
.ws .mono, .win .mono { font-family: var(--mono); font-variant-numeric: tabular-nums; }
.ws .faint, .win .faint { color: var(--ink-faint); }
.ws .eyebrow, .win .eyebrow { font-size: 11px; letter-spacing: .02em; color: var(--ink-muted); font-weight: 500; }
.ws .note, .win .note { font-size: 12px; color: var(--ink-muted); }
.ws .danger, .win .danger { color: var(--danger); }
.ws .ok, .win .ok { color: var(--ok); }

/* the write-half controls wired to a stub — no Reading Room equivalent, given the pending treatment
 * `button.pending` above already carries so a person sees at a glance which half of the toolbar is
 * not built yet. */
.ws [data-stub] { color: var(--pending); box-shadow: 0 0 0 1px var(--pending); background: var(--board-raised); }
.ws [data-stub]:hover { background: var(--pending-wash); }

/* ─── the staging deck: on every page, at the foot of it ──────────────────────────────────────────
 *
 * Operator direction, 2026-09-19: *"that staging and anchoring status bar is common for all
 * activities on the site — announcements adding, editing calendar item etc"*, and *"the anchoring
 * status can be shown at the bottom of the page like mnem reading room"*. So this is NOT a
 * workstation component; it is shell chrome, rendered by `render.page` beside `<main>` on every
 * signed-in page. It lives in this sheet rather than in `app.css` because it is the same register —
 * flat plates, drawn 1px edges, mono for every identifier — and because `app.css` is the finding
 * aid's own vocabulary, which this is deliberately not part of.
 *
 * **FIXED, NOT AN ITEM OF THE GRID, AND THAT IS WHY IT IS `position: fixed` RATHER THAN A ROW.**
 * `.aid` is the two-column reading measure and `test_EVERY_GRID_ITEM_OF_EVERY_REAL_PAGE_LANDS_IN_A_COLUMN`
 * measures everything that falls out of it; a bar placed inside would be a new grid item on all
 * nine pages, which is exactly how `div.ws` ended up 248px wide in the margin this afternoon. The
 * markup sits outside `<main>` and this pins it, the way the spine is pinned at the top.
 *
 * **IT IS QUIET AT ZERO AND LOUD WHEN IT IS HOLDING SOMETHING.** `data-staged` is written by
 * `session.js` off the count, so the difference between "nothing staged" and "three staged" is a
 * change in the bar's own edge rather than only a change of words — a person who has left work
 * unstored should be able to tell from the corner of their eye.
 *
 * **IT IS A SECTION OF THE PAGE NOW, NOT AN OVERLAY OVER IT** — checklist item 54, operator
 * 2026-09-20: *"instead of an overlay you can make it a bottom section — that way it does not hide
 * the items on the top section."* It was `position: fixed` with `padding-bottom: 4rem` reserved
 * under `<main>`, which was defensible while the deck was a 36px bar somebody chose to open. Item 49
 * is what broke it: the deck opens ITSELF now, the moment anything stages, and the thing it opens
 * over is whatever the person was just working on. No fixed reservation can be right either, because
 * the panel's height is however many rows are in it.
 *
 * So it is the last item of the body's flex column (`app.css` declares that column and carries the
 * argument), `flex: none` so it is exactly as tall as it needs to be, and the document above it
 * shrinks and scrolls in its own container. The 60vh ceiling stays: a session with forty staged
 * actions must not become the whole screen. */
.deck {
  flex: none; z-index: 40;
  background: var(--board-sunken); border-top: 1px solid var(--line-strong);
  font-family: var(--body); font-size: 12px; color: var(--ink);
  display: flex; flex-direction: column-reverse; max-height: 60vh;
}
.deck[data-staged="yes"] { border-top: 2px solid var(--pending); background: var(--pending-wash); }
.deck__bar { display: flex; align-items: center; gap: 8px; padding: 6px 12px; flex-wrap: wrap; }
.deck__gap { flex: 1; }
.deck__count { font-family: var(--mono); color: var(--ink-muted); white-space: nowrap; }
.deck[data-staged="yes"] .deck__count { color: var(--pending-strong); font-weight: 500; }
.deck__note { font-size: 11.5px; color: var(--ink-muted); }
.deck__note.danger { color: var(--danger); }
.deck__anchor { display: inline-flex; gap: 10px; font-family: var(--mono); font-size: 11px;
                color: var(--ink-faint); flex-wrap: wrap; }
.deck__fact { white-space: nowrap; }
.deck__fact.danger { color: var(--danger); }
.deck__fact.ok { color: var(--ok); }
/* The deck's own control plates, restated here for the same reason the workstation's are: `app.css`
 * owns the finding aid's button register, and the deck is not part of that register. */
.deck button {
  font: inherit; font-size: 12px; line-height: 1; color: var(--ink);
  background: var(--board-well); border: none; box-shadow: 0 0 0 1px var(--line-strong);
  padding: 5px 9px; cursor: pointer; display: inline-flex; align-items: center; gap: 6px;
  white-space: nowrap;
}
.deck button:hover { background: var(--wash); }
.deck button:disabled { opacity: .45; cursor: not-allowed; }

/* ─── the bar's hierarchy: what each control DOES, drawn at the weight it deserves ────────────────
 *
 * Checklist item 50, operator 2026-09-20 with a screenshot of the bar: *"the button needs to be
 * prominent — let it be bigger, so that the user knows to use it."* Every control in the row was
 * the same 12px label on the same `--board-well` plate, so **Store changes — the one act in this
 * app that anchors anything — looked identical to Abort session, which throws the work away.**
 * Somebody who has just staged something could not tell from the bar which of the two they wanted.
 *
 * **EVERY RULE BELOW STATES WHAT IT OUTRANKS**, which is this sheet's house rule and is why the
 * deck's `.deck__add` rules already do: `.deck button` is (0,1,1) and sets the plate for the whole
 * bar, so a bare `.deck__act--store` at (0,1,0) would LOSE and the primary would simply not draw,
 * with nothing in any console.
 *
 *   `.deck .deck__act--store`        (0,2,0)  beats  `.deck button`         (0,1,1)
 *   `.deck .deck__act--store:hover`  (0,3,0)  beats  `.deck button:hover`   (0,2,1)
 *   `.deck .deck__act--quiet`        (0,2,0)  beats  `.deck button`         (0,1,1)
 *
 * Verified on the live element with `getComputedStyle`, not read off this file: a CSS defect in this
 * app presents as the wrong shape and never as a failure, which has now cost five rounds. */
.deck .deck__act--store {
  background: var(--key); color: var(--key-ink); box-shadow: 0 0 0 1px var(--key);
  /* THE SAME METRICS AS `.deck__add` (item 59). The store must never look SMALLER than the verbs
   * that feed it — it is the act they exist to reach — so the two share a size and the key fill is
   * what says which one is the primary. */
  font-size: 15px; font-weight: 600; padding: 12px 20px;
  min-height: calc(var(--touch) + 6px);
}
.deck .deck__act--store:hover:not(:disabled) { background: var(--key-hover); color: var(--key-ink); }
/* DISABLED IS QUIET, NOT LOUD-AND-GREYED. With nothing staged the primary has nothing to do, and a
 * big key-coloured plate at 45% opacity reads as a broken button rather than as an inactive one. */
.deck .deck__act--store:disabled {
  background: var(--board-well); color: var(--ink-muted); box-shadow: 0 0 0 1px var(--line-strong);
  opacity: 1;
}
/* **THE COUNT SITS WITH THE PRIMARY BECAUSE IT IS ITS SUBJECT.** It used to trail the row in small
 * mono, three controls away from the button it is the reason for. It is loud only when there is
 * something to say — see `.deck[data-staged="yes"] .deck__count` above, which this does not
 * replace. */
.deck .deck__count { font-size: 12.5px; margin-right: var(--s2); }
/* **ABORT IS THE QUIETEST CONTROL IN THE BAR, AND IT IS AT THE FAR END.** It destroys work that
 * exists nowhere else. A destructive act should not be the easiest thing to hit, and it should not
 * be adjacent to the act a person actually means. The two-press confirm (`.deck button.armed`) is
 * what makes it loud at the moment it matters. */
/* **THE STORE BOX'S OWN BUTTONS ARE THE BAR'S, and they sit inside `.deck__panel` rather than
 * `.deck__bar`** — so the two rules above, scoped `.deck .deck__act--store`, already reach them at
 * (0,2,0). Written down because the class names suggest otherwise and the next reader will look for
 * a second pair of rules that deliberately does not exist: one treatment, two places, so the
 * confirm can never drift from the primary it is confirming. */
.deck .deck__act--quiet {
  background: transparent; box-shadow: none; color: var(--ink-faint); font-size: 11.5px;
}
.deck .deck__act--quiet:hover:not(:disabled) { background: var(--board-well); color: var(--ink); }
/* ─── the three verbs are the site's CALL TO ACTION, and they are sized like it ───────────────────
 *
 * Checklist item 59, operator 2026-09-20 with a screenshot of the bar on an empty session: *"I want
 * these buttons much much bigger — these are the CTA of the site."*
 *
 * **THEY WERE SECONDARY AND THAT WAS RIGHT ABOUT HALF THE TIME.** Item 50 sorted the bar into store
 * / adds / abort, which is the correct order the moment somebody HAS staged something. With an
 * empty session — which is every arrival, every fresh page, and the state a person meets first —
 * Store changes is DISABLED and Abort has nothing to abort, so the three verbs are the only live
 * controls on the bar and the entire reason to touch it. Drawn at 12px on the bar's own plate, the
 * one thing a person could do looked like the two things they could not.
 *
 * So size is shared at the TOP of the hierarchy and COLOUR is what separates the primary: the adds
 * and the store are the same generous target, and the store alone carries the key fill. The eye
 * still sorts three weights; what changed is that the bottom of that order is no longer the only
 * thing on screen that works.
 *
 *   `.deck .deck__add`  (0,2,0)  beats  `.deck button`  (0,1,1)
 *
 * The metrics are deliberately the store's, to the pixel, so the row reads as one band of actions
 * rather than two sizes of button that happen to be adjacent. */
.deck .deck__add {
  box-shadow: 0 0 0 1px var(--line-ink);
  font-size: 15px; font-weight: 600; padding: 12px 20px;
  min-height: calc(var(--touch) + 6px);
}
.deck .deck__add:hover { background: var(--key-wash); box-shadow: 0 0 0 1px var(--key); }
.deck .icon-btn { background: transparent; box-shadow: none; width: 22px; height: 22px;
                  padding: 0; justify-content: center; font-size: 13px; }
.deck .icon-btn:hover { background: var(--board-well); }
/* ARMED: the second press of the two-press abort. It has to look different from the first press or
 * the confirm is a confirmation nobody noticed being asked for. */
.deck button.armed { color: var(--danger); box-shadow: 0 0 0 1px var(--danger);
                     background: var(--board-raised); }
.deck__panel { border-top: 1px solid var(--line-strong); background: var(--board-raised);
               padding: 10px 12px; overflow: auto; }
.deck__panel table { width: 100%; border-collapse: collapse; font-size: 12px; }
.deck__panel th { font-size: 11px; background: var(--key); color: var(--key-ink);
                  font-weight: 500; text-align: left; padding: 4px 8px; }
.deck__panel td { padding: 4px 8px; border-bottom: 1px solid var(--line); }
.deck__panel td.mono { font-family: var(--mono); font-size: 11.5px; overflow-wrap: anywhere; }
.deck__empty { padding: 12px 8px; color: var(--ink-faint); font-size: 12px; max-width: 64ch; }
.deck__store { margin-top: 10px; display: flex; flex-direction: column; gap: 6px; }
.deck__hint { margin: 0; font-size: 12px; color: var(--ink-muted); max-width: 68ch; }
.deck__label { font-size: 11px; letter-spacing: .02em; color: var(--ink-muted); font-weight: 500; }
.deck__store textarea {
  font: inherit; font-family: var(--mono); font-size: 12px; width: 100%; resize: vertical;
  color: var(--ink); background: var(--board-raised); border: none;
  box-shadow: inset 0 0 0 1px var(--line-strong); padding: 6px 8px; line-height: 1.45;
}
.deck__store textarea:focus-visible { box-shadow: inset 0 0 0 1px var(--key); outline: none; }
.deck__row { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }

/* ─── the three adds, in the bar and in panels above it ───────────────────────────────────────────
 *
 * Checklist item 41, operator 2026-09-20: *"we can make these three buttons available across the
 * page — maybe at the footer… we don't need the workstation to be the place for uploading the
 * content."* The deck is the one element every signed-in page already has, so the adds live in it
 * and reach all nine pages without any page composing them.
 *
 * **EVERY SELECTOR HERE IS CHOSEN AGAINST THE ONE IT HAS TO BEAT, NOT HOPED FOR.** `.deck button`
 * is (0,1,1) and sets the plate for every control in the bar — so `.deck__add` alone, at (0,1,0),
 * would LOSE and the open state would simply not draw, with no error anywhere. A CSS defect in this
 * app presents as the wrong shape and never as a failure, which has now cost four separate rounds,
 * so each rule below states what it outranks:
 *
 *   `.deck .deck__add[aria-expanded="true"]`  (0,3,0)  beats  `.deck button`        (0,1,1)
 *   `.deck .deck__go`                          (0,2,0)  beats  `.deck button`        (0,1,1)
 *   `.deck .deck__go:hover`                    (0,3,0)  beats  `.deck button:hover`  (0,2,1)
 *   `textarea.rights__field`                   (0,1,1)  beats  `.rights__field`      (0,1,0)
 *
 * The `.rights__*` block is NOT scoped under `.deck`, and that is deliberate: it styles the same two
 * fields wherever they are rendered, and the workstation's own upload form (`pages._upload_form`)
 * carries them too. Nothing in `.deck` or `.ws` declares a rule for `input`, `select` or a textarea
 * outside `.deck__store`, so these meet no competitor in either place. */
.deck__adds { display: flex; align-items: center; gap: 6px; flex-wrap: wrap; }
/* OPEN IS A DIFFERENT PLATE, not merely a panel that appeared. A person who has three doors and no
 * mark on the one they opened has to read the panel to find out which it was. */
.deck .deck__add[aria-expanded="true"] { background: var(--key); color: var(--key-ink);
                                         box-shadow: 0 0 0 1px var(--key); }
.deck__add-panel { border-top: 1px solid var(--line-strong); background: var(--board-raised);
                   padding: 10px 12px; overflow: auto; }
.deck .deck__go { background: var(--key); color: var(--key-ink); box-shadow: 0 0 0 1px var(--key); }
.deck .deck__go:hover { background: var(--key-hover); }

/* The two rights fields and the add forms' own boxes — one register, shared by both doors. */
.rights { display: flex; flex-direction: column; gap: 6px; max-width: 62ch; }
.rights__label { font-size: 11px; letter-spacing: .02em; color: var(--ink-muted); font-weight: 500; }
.rights__opt { color: var(--ink-faint); font-weight: 400; }
.rights__field {
  font: inherit; font-size: 12px; width: 100%; color: var(--ink);
  background: var(--board-raised); border: none;
  box-shadow: inset 0 0 0 1px var(--line-strong); padding: 6px 8px; line-height: 1.45;
}
textarea.rights__field { font-family: var(--mono); resize: vertical; }
.rights__field:focus-visible { box-shadow: inset 0 0 0 1px var(--key); outline: none; }
.rights__check { display: flex; align-items: flex-start; gap: 8px; font-size: 11.5px;
                 color: var(--ink-muted); max-width: 64ch; }
.rights__check input { margin: 2px 0 0; flex: none; }
/* **THE JOURNAL'S SENTENCE IS INK, NOT ALARM.** That an entry is readable by the residency is an
 * ordinary fact about where it goes; a danger colour here would make an intended act feel like a
 * mistake, which is the reasoning `STORE_HINT` carries in the same deck. */
.rights__note { margin: 0; font-size: 11.5px; color: var(--ink-muted); max-width: 64ch; }

/* ─── save a link: the one window whose body is a FORM ────────────────────────────────────────────
 * Every other floating window is a surface `workstation.js` fills; this one arrives from the server
 * as real markup and posts to a real route (`pages._save_url_form` says why at length). So it needs
 * field styling the other windows never did.
 *
 * **SCOPED NAMES, NOT THE COMPOSING FORM'S.** `.compose__row` is a two-column grid sized for the
 * finding aid's gutter (`grid-template-columns: var(--gutter-w) minmax(0, 1fr)`), which inside a
 * 380px window leaves a label column wider than the box a URL is typed into. Reusing it and then
 * overriding it would be a rule fighting a rule — a specificity contest this app lost once today,
 * invisibly, and three more like it. `saveurl__*` collides with nothing in either sheet.
 *
 * **AND THE PLATE IS NOT RESTATED**, which is the same lesson from the other end: `.ws input` is
 * already the workstation's authority on how a field is drawn, and the honest version of "in the
 * app's register" is to let that rule win rather than to write the same declarations at a
 * specificity where they cannot. See the block below. */
#win-saveurl { top: 150px; left: max(220px, calc(50% - 190px)); width: min(380px, calc(100vw - 28px)); }
.saveurl { display: flex; flex-direction: column; gap: 4px; margin: 0; }
.saveurl__label {
  font-size: 11px; letter-spacing: .02em; color: var(--ink-muted); font-weight: 500;
  margin-top: 6px;
}
/* The "(optional)" beside a label is quieter than the label and is NOT a second line: somebody
 * scanning for the required box should find it by weight rather than by reading. */
.saveurl__opt { font-weight: 400; color: var(--ink-faint); }
/* **`.saveurl__field` HAS NO PLATE RULE OF ITS OWN, AND TWO DRAFTS OF ONE WERE DELETED RATHER THAN
 * KEPT.** Both were INERT, in two different ways, and both rendered perfectly:
 *
 * · the first restated the whole plate — font, raised ground, `border: none`, the inset 1px edge,
 *   the padding. `.ws input, .ws select, .ws textarea` (0,1,1) already says all of it and a bare
 *   `.saveurl__field` is (0,1,0), so every declaration LOST to a rule saying nearly the same thing.
 *   Deleting the entire selector changed nothing on screen.
 * · the second kept `width: 100%` at a specificity that DID win — and it too did nothing, because
 *   `.saveurl` is a flex COLUMN and a flex item stretches on the cross axis by default.
 *
 * Both were found by deleting the rule and watching the guard stay green, which is the only way this
 * class is ever found: the page does not look wrong, it is that a rule a reader would take as
 * load-bearing is not. So the plate is `.ws input`'s, the width is the flex column's, and the class
 * survives on the two elements below plus as the handle the markup and the guard share. */
.ws .saveurl__field--prose { resize: vertical; }
/* **`:invalid` IS QUALIFIED BY `:not(:placeholder-shown)` ON PURPOSE.** `:invalid` alone paints an
 * empty required field red before anybody has typed a character — an error somebody is told they
 * made about a box they have not reached yet. At (0,3,0) it outranks `.ws input` and its focus rule,
 * which is what a refusal has to do to be seen at all. */
.ws .saveurl__field:not(:placeholder-shown):invalid { box-shadow: inset 0 0 0 1px var(--danger); }
.saveurl__hint { margin: 8px 0 0; font-size: 12px; color: var(--ink-muted); line-height: 1.45; }
.saveurl__go { display: flex; justify-content: flex-end; margin-top: 10px; }

/* ─── down to a phone: the workstation, its windows and the deck at 375px ─────────────────────────
 *
 * Checklist item 47, operator 2026-09-20: *"it will be nice if it is properly responsive — from
 * extended monitor to a mobile screen."* **Nothing had ever tried this end.** The workstation is a
 * DESKTOP metaphor — a sidebar beside a canvas, panels floating over both — and a desktop metaphor
 * at 375px is not a smaller desktop. It is a stack.
 *
 * The breakpoint is 46rem, which is the one `app.css` already collapses the finding aid's two
 * columns at. A second, differently-placed breakpoint here would give a band of widths where the
 * page had stacked and the workstation had not, and that band would be nobody's screen and
 * everybody's bug.
 *
 * **EVERY OVERRIDE IS WRITTEN AT A SPECIFICITY THAT WINS ITS OWN FIGHT, AND THE FIGHT IS NAMED.**
 * The window placements above are ID selectors — `#win-session` is (1,0,0) — so restating them here
 * as `.win` would change nothing at all, silently, which is this app's signature defect. They are
 * restated by ID.
 *
 * **WHAT THIS CANNOT REACH IS THE INLINE POSITION `windows.js` WRITES.** A stored `left: 220px` from
 * a desktop session beats every rule in this file, because an element's own `style` beats a
 * stylesheet outright. That half is solved in `windows.js`, which refuses to apply a stored
 * arrangement below this same width and clears one it already applied — the two halves are one
 * decision, and neither works alone. */
@media (max-width: 46rem) {
  /* THE SIDEBAR GOES ABOVE THE CANVAS RATHER THAN BESIDE IT, and it is capped: a residency with
     forty folders would otherwise push the canvas entirely off the bottom of the first screen. */
  .ws-body { flex-direction: column; }
  .sidebar { width: auto; flex: none; border-right: none; border-bottom: 1px solid var(--line-strong); }
  .sidebar ul { max-height: 30vh; }
  /* THE BOOK HEADER STACKS. `.ws-scope` is `margin-left: auto` + `nowrap`, which at 375px pushed a
     lookup path off the right edge and took the page's own width with it — the one layout fault
     this register does not permit. */
  .ws-book { flex-direction: column; gap: 4px; }
  .ws-scope { margin-left: 0; text-align: left; white-space: normal; overflow-wrap: anywhere; }
  /* THE TOOLBAR IS A THUMB TARGET. It already wraps; what it did not do is give each control the
     44px the rest of this app treats as non-negotiable on a phone. */
  .toolbar { gap: 4px; padding: 6px; }
  /* **THE TRAY IS AN `inline-flex` THAT DOES NOT WRAP, AND IT IS THE LAST THING WIDER THAN A
     PHONE.** The toolbar itself wraps; the tray inside it is one row of eight window buttons, so at
     375px it ran to x=687 and `<main>` grew a horizontal scrollbar of its own. It was invisible to
     the page-level check for exactly the reason item 54 introduced: main is a scroll container now
     and absorbed it. Measured, not eyeballed — `SPAN.tray@687` against a 375px viewport. */
  .toolbar .tray { flex-wrap: wrap; }
  .ws button, .ws .btn { min-height: var(--touch); }
  .ws .icon-btn { min-height: 0; }
  .toolbar .toolbar-sep { display: none; }

  /* **A WINDOW BECOMES A BOTTOM SHEET.** Dragging a 320px panel around a 375px screen is not a
     feature, it is a way to lose it; and `max-width: calc(100vw - 16px)` left every window at
     whatever `left` its own rule named, which at 375px put `#win-messages` (left: 220px) 155px
     wide against the edge. So they span the width, sit above the deck, and take at most half the
     height — a person can read one and still see what they were looking at. `resize: none` because
     a resize handle on a full-width sheet only ever makes it worse. */
  /* **ANCHORED TO THE TOP, NOT TO THE BOTTOM, AND THE DECK IS WHY.** The first draft pinned these
     to `bottom: var(--deck-h)` — and `--deck-h` is the bar's COLLAPSED height. At 375px the bar
     wraps to three rows: measured live, 113px, so every sheet ran 69px underneath the deck and the
     bottom of a session panel was unreadable. A taller constant would be a number that is wrong
     again the day the bar gains a control, and CSS cannot ask an element how tall another one is.
     Anchoring below the toolbar instead makes the sheet's position independent of the deck
     entirely, and the deck is the fixed furniture at the foot of a phone anyway. */
  .win, #win-context, #win-pending, #win-relationships, #win-messages, #win-session,
  #win-saveurl, #viewer {
    left: 8px; right: 8px; top: 120px; bottom: auto;
    width: auto; min-width: 0; max-width: none; max-height: 50vh;
    transform: none; resize: none;
  }
  .win.win-wide { width: auto; }

  /* THE DECK'S BAR WRAPS INTO ROWS, and the primary takes the first one on its own: at 375px the
     store, the count, three adds, an abort and two icons cannot share a line, and the control the
     operator asked to be unmissable is the one that must not end up on row three. */
  .deck__bar { gap: 6px; padding: 6px 8px; }
  .deck .deck__act--store { flex: 1 0 100%; justify-content: center; }
  .deck__adds { flex: 1 1 100%; }
  .deck .deck__add { flex: 1 1 auto; justify-content: center; min-height: var(--touch); }
  /* THE ANCHOR LINE IS THE FIRST THING TO GO. Chain, block and final are three mono facts nobody
     came to a phone to read, and they were pushing the controls onto a fourth row. */
  .deck__anchor, .deck__gap { display: none; }
  .deck { max-height: 80vh; }
}

/* ─── the one orchestrated movement in this interface ─────────────────────────────────────────────
 *
 * `docs/oops-interaction.md` §4. The register is FLAT — no radii, no shadows — and scattered
 * animation would read as decoration bolted onto a considered thing. So there is exactly one moment
 * that moves, and it is the moment the application changes state from *reading* to *you have
 * unsigned work*: the deck rising as the session goes from empty to holding something (items 49 and
 * 54 together).
 *
 * **IT MOVES PAINT AND NOT LAYOUT.** The deck is a section of the body's flex column now, so its
 * space is already taken before the animation starts — `translateY` slides what is drawn, and
 * nothing above it reflows. An animation that grew the element would push the document up under the
 * reader's eye for 220ms, which is the opposite of what an item about not hiding the page is for.
 *
 * `both` so the first frame is the off-screen one: without it the deck paints in place and THEN
 * jumps down to start, which reads as a flicker rather than a rise.
 *
 * `session.js` adds the class on the transition and takes it off when the animation ends; it is
 * never on at rest, so nothing replays on a repaint. */
@keyframes deck-rise { from { transform: translateY(100%); } to { transform: translateY(0); } }
.deck.deck--rose { animation: deck-rise 220ms var(--ease) both; }
/* **REDUCED MOTION REMOVES THE RISE AND KEEPS THE STATE** (§4). The deck still opens, the count
 * still changes, the bar is still loud at `data-staged="yes"` — what goes is the travel. A person
 * who asked for less motion asked for that, not for less information. */
@media (prefers-reduced-motion: reduce) {
  .deck.deck--rose { animation: none; }
}

/* ─── the add panels use the width they are given ────────────────────────────────────────────────
 *
 * Checklist item 47, the too-wide end, in the deck: the operator's screenshot of the expanded
 * Add-link panel shows the form in a narrow left column with the full width of the screen empty to
 * its right. `.rights` is capped at 62ch, and that cap is RIGHT — a URL box two feet wide is not
 * easier to type into. What is wrong is that the panel then has nothing in the other two thirds.
 *
 * So at a desk the guidance moves out of the column and sits beside the form: the fields keep their
 * measure, the two hint paragraphs are read at theirs, and the panel is as tall as the shorter of
 * the two instead of the sum. Below 64rem — the width the spine's own nav collapses at — it is one
 * column again, in source order, which is the order somebody reads on a phone.
 *
 * **THE COLUMNS ARE ASSIGNED BY WHAT AN ELEMENT IS, NOT BY ITS POSITION.** `:not(.deck__hint)`
 * catches every field, the go row and the note without naming them, so a fourth add or a new field
 * lands in the form column by default rather than in whichever column a hand-written list forgot. */
@media (min-width: 64rem) {
  .deck__add-panel .rights {
    max-width: none; display: grid; align-content: start;
    grid-template-columns: minmax(0, 62ch) minmax(0, 46ch); gap: 6px 24px;
  }
  .deck__add-panel .rights > .deck__hint { grid-column: 2; margin: 0; }
  .deck__add-panel .rights > :not(.deck__hint) { grid-column: 1; }
}

/* ─── the tablet band: two bottom-anchored panels do not fit side by side ─────────────────────────
 *
 * Between the phone breakpoint and a desk, both status windows still open at their desktop
 * arrangement — and measured at 768×1024 they OVERLAP: messages runs x=220–560 and session
 * x=334–754, so the panel a person opened second covers the one they opened first, at the position
 * they meet before they have dragged anything.
 *
 * Narrowing either one is the wrong fix: messages would have to drop to 80px to clear it, and a
 * window too small to read is worse than one behind another. So in this band they stop sharing the
 * bottom edge — messages goes to the TOP of the canvas, under the toolbar, and session keeps the
 * foot. They are read at different moments anyway: messages is a log you glance at, session is a
 * reference you hold open.
 *
 * **THE UPPER BOUND IS 64rem AND THE LOWER IS THE PHONE BLOCK'S 46rem**, so this band is exactly
 * "wider than a phone, narrower than a desk" and neither rule can reach into the other's range.
 * `#win-messages` is (1,0,0) and this restates it by ID for that reason — a `.win` selector here
 * would lose to the ID rule above and change nothing at all, silently. */
@media (min-width: 46.01rem) and (max-width: 64rem) {
  #win-messages { top: 150px; bottom: auto; left: 220px; width: min(340px, calc(100vw - 248px)); }
}

/* ---- the link creator's panel — checklist item 74 ---------------------------------------------
 *
 * **NO PLATE RULES HERE, and that is deliberate** — the same lesson `.saveurl__field` records two
 * hundred lines up. `.ws input, .ws select` (0,1,1) already carries the font, the ground, the inset
 * edge and the padding, and these controls are inside `.ws`; a `.win-control` rule restating any of
 * it would lose the cascade and read as load-bearing anyway. What is left is the column, the label
 * and the subject line.
 *
 * It opens where the context panel's `Link…` is pressed from — centred rather than on either flank,
 * because both flanks are taken (context right, relationships left) and a creator that opened under
 * one of them would be the one window a person has to drag before they can use it. */
#win-link { top: 120px; left: max(220px, calc(50% - 210px)); width: min(420px, calc(100vw - 28px)); }
.link-form { display: flex; flex-direction: column; gap: 4px; }
.win-label {
  font-size: 11px; letter-spacing: .02em; color: var(--ink-muted); font-weight: 500;
  margin-top: 6px;
}
/* The subject is an address and can be long; it wraps rather than scrolls, because a path that
 * runs off the edge of the panel is a subject nobody can check before they commit to it. */
.link-subject {
  font-size: 11.5px; margin: 0; overflow-wrap: anywhere; color: var(--ink);
}

/* The per-edge withdraw control. Quiet by default and only itself on hover: a list of what a record
 * says should read as a list, with the way to change it available rather than advertised. */
.rel-list .rel-drop {
  margin-left: auto; font-size: 10.5px; padding: 1px 6px;
  color: var(--ink-faint); background: transparent; border: 1px solid transparent;
}
.rel-list .rel-drop:hover { color: var(--danger); border-color: var(--danger); }

/* ---- the link picker's browser — checklist item 75 --------------------------------------------
 *
 * **A FIXED-HEIGHT SCROLLER, and the height is the point.** The flat menu this replaces grew with
 * the book; a browser shows one folder and the panel stays the size it opened at however large ch21
 * becomes. The rows are dense because a picker is scanned, not read. */
.link-crumbs { font-size: 11px; margin: 2px 0 4px; color: var(--ink-muted); overflow-wrap: anywhere; }
.link-crumb { color: var(--key); }
.link-sep { color: var(--ink-faint); padding: 0 2px; }
.link-picker {
  height: 190px; overflow-y: auto; background: var(--bg-sunken);
  box-shadow: inset 0 0 0 1px var(--line); padding: 2px;
}
.link-row {
  display: flex; align-items: baseline; gap: 6px; padding: 2px 5px; cursor: pointer;
  font-size: 11.5px; font-family: var(--mono);
}
.link-row:hover { background: var(--bg-hover); }
.link-row--quiet { color: var(--ink-faint); cursor: default; }
.link-row--quiet:hover { background: transparent; }
.link-row--up { color: var(--ink-muted); }
.link-icon { color: var(--ink-faint); }
.link-name { overflow-wrap: anywhere; }
/* The type rides at the end, quiet: it is what tells a document from an announcement at a glance,
 * and it is never the thing being searched for. */
.link-ytype { margin-left: auto; color: var(--ink-faint); font-size: 10.5px; white-space: nowrap; }
/* **THE CHOSEN ROW IS MARKED IN THE LIST AND NAMED ABOVE IT.** A picker that only named it would
 * make somebody scroll to check they pressed the right one. */
.link-row--on { background: var(--key-wash); }
.link-row--on .link-name { color: var(--key-true); }
.link-chosen { font-size: 11.5px; margin: 0; overflow-wrap: anywhere; color: var(--ink); }
/* The feed's own door, beside the share control. */
.card__act { margin: 2px 0 0; }
.act-link {
  font-size: 11px; padding: 1px 6px; font-family: var(--mono);
  color: var(--key); background: transparent; border: 1px solid var(--key);
}
.act-link:hover { background: var(--key); color: var(--key-ink); }
