/* =============================================================================
   fractal control room.

   Echofold's black canvas, glass navigation, soft geometry and teal/rose light
   are adapted here for a dense operational interface. The fonts are vendored so
   the control room keeps its identity when the network is unavailable.
   ========================================================================== */

@font-face {
  font-family: 'Geist Sans';
  src: url('/fonts/geist-sans.woff2') format('woff2');
  font-style: normal;
  font-weight: 100 900;
  font-display: swap;
}

@font-face {
  font-family: 'Geist Mono';
  src: url('/fonts/geist-mono.woff2') format('woff2');
  font-style: normal;
  font-weight: 100 900;
  font-display: swap;
}

@font-face {
  font-family: 'Orbitron';
  src: url('/fonts/orbitron.woff2') format('woff2');
  font-style: normal;
  font-weight: 400 900;
  font-display: swap;
}

:root {
  color-scheme: dark;

  --void: #000000;
  --rack: #090b0b;
  --rack-2: #111414;
  --rack-3: #171b1a;

  --ink: #e7e9e8;
  --ink-dim: #a5adab;
  /* Raised from #68716f on 24 August 2026. The old value was mathematically
     incapable of passing 4.5:1 anywhere in this app: 4.18 against pure black,
     3.93 on --rack, 3.69 on --rack-2, 3.46 on --rack-3, and a probe sweep of
     25 screens found 945 of 1,973 rendered controls painted in exactly it.
     #838a88 clears 4.5 on every surface here (4.93 on --rack-3, the worst
     case), which closed about sixty failing pairs without touching a rule. */
  --ink-faint: #838a88;

  /* One accent, and it is teal. --violet was #009d89 against --teal's #00b39c:
     two names six units apart, 1.28:1 from each other, both cyan-green, and
     nothing said which a new rule should pick. It carried .tag-crit, the
     selected card outline, .chip.is-on and the coordinator's corner ticks while
     --teal carried .btn-go, every focus ring, --status-ready and the terminal
     cursor, so "critical", "selected" and "coordinator" were indistinguishable
     from "ready", "go" and focus. Its 36 uses are now --teal and the name is
     gone; web/depmap.css records that it was #a874ff before it was recoloured,
     which is how one token ended up with two jobs and no hue of its own. */
  --green: #55c895;
  --blue: #66a1b0;
  --amber: #e6a05d;
  --red: #e56f72;
  --teal: #00b39c;
  --rose: #e7a4b4;
  /* Two project accents that exist only to be told apart from the other six.
     'plum' pointed at --violet (teal) and 'slate' at --grey, which is the ink
     body text is written in, so two of the eight named project colours were
     the same hue as each other and a third was indistinguishable from a label.
     Nothing else in the room may use these: they are an identity, not a state. */
  --plum: #b48ad6;
  --slate: #7d8ea0;

  /* ---- surfaces ------------------------------------------------------------
     One idea, a raised panel, was drawn six ways: .col, .pane, .dlg, .kcol,
     .rail-block/.dock-panel and .ticket, between them four start colours, four
     end colours, five alpha pairs, one gradient at 155deg instead of 145, and
     the only one that actually floats (.ticket) was the only flat fill. Nothing
     named any of it, so a seventh panel written tomorrow would have been a
     seventh value. This is the .rail-block value, which is the median of the
     five gradients and already covered the most selectors.

     Below it, the plates. Five opaque bar colours for what is a bar or a
     floating box over a panel, two of them differing only in the third decimal
     of an alpha. --surface-bar is the value the command bar and the deck bar
     already shared; --surface-float is the toast and the scope menu rounded
     together; --surface-inset is a pane's own head and foot; --surface-chip is
     the topbar pill plate, which was a literal repeated behind eight selectors
     and is the only one of the four that is meant to be seen through. */
  --surface-panel: rgba(19, 23, 22, 0.94);
  --surface-bar: rgba(14, 17, 16, 0.89);
  --surface-float: rgba(17, 20, 20, 0.96);
  --surface-inset: rgba(8, 11, 10, 0.94);
  --surface-chip: rgba(10, 13, 12, 0.7);
  /* The accent under a pointer. It was written out as a literal four times, in
     two blocks, and one of them (.btn-go:not(:disabled):hover) added
     `filter: brightness(1.12)` ON TOP of the other, so the rendered plate was
     approximately #2CE0C7: neither of the two values written down, and a
     different teal from the one the rail's ADD button hovered to eight inches
     away. A hover colour is a colour and gets a name. */
  --teal-hot: #27c8b2;
  /* Ink that sits ON a filled colour, which is the one place this room writes a
     near-black. Four decisions were spelled out as eleven literals: seven of
     one value on the teal, two of another on the amber, and one each of two
     more, one of which differed from its neighbour by three units of red. The
     comment beside the first of them recorded that it had already replaced two
     hand-picked blacks without ever being given a name. */
  --on-accent: #001c18;   /* on --teal */
  --on-warn: #241500;     /* on --amber */
  --on-alarm: #1b0403;    /* on --red */

  /* The accent's alpha ladder. 56 raw rgba(0, 157, 137, x) and
     rgba(0, 179, 156, x) were written across 38 distinct alphas, while the
     white ladder beside them was six values each used exactly once as a token.
     Every one of the 38 rounds onto one of these five, and a sixth step is a
     new decision rather than a nudge. */
  --accent-wash: rgba(0, 179, 156, 0.08);      /* a tinted plate */
  --accent-tint: rgba(0, 179, 156, 0.14);      /* a lit plate */
  --accent-glow: rgba(0, 179, 156, 0.2);       /* a glow, or a soft ring */
  --accent-line: rgba(0, 179, 156, 0.42);      /* an accented border */
  --accent-line-hot: rgba(0, 179, 156, 0.66);  /* that border, hovered or focused */

  /* And the same five steps for every other hue, which is the half of this that
     was never done. --red was written freehand 20 times across 11 alphas, --amber
     11 times across 10, --rose 7 across 6, --ink-dim 3 across 3, and nothing said
     which a new rule should pick: .38 and .40 and .42 were one decision written
     three ways. They cannot be tokens, because the ladder is a function of a hue
     rather than a set of names, so it is a FORM and every use takes it:

       color-mix(in srgb, var(--hue) 8|14|20|42|66%, transparent)

     8 a tinted plate, 14 a lit one, 20 a glow or a soft ring, 42 a coloured
     border, 66 that border hot. Every one of the 42 raw alphas rounded onto one
     of the five, and a sixth step is a new decision rather than a nudge. */

  /* Elevation. 53 box-shadow declarations resolved to 46 distinct values and
     two of them used a token: five floating surfaces, five shadows. A glow is
     not elevation and keeps its own colour; these three are the drop. */
  /* Hairlines before boxes. A panel is bounded by --rule and sits on a ground
     a shade below it; a 32px pool of black under it as well was the second of
     two answers to the same question, and it is what made eleven panels on one
     screen read as eleven objects stacked on a table rather than one surface
     divided up. Only a dialog floats over something, so only --shadow-3 keeps
     a real drop. --glow-teal is gone with the rest of the decorative light. */
  --shadow-1: 0 1px 2px rgba(0, 0, 0, 0.3);      /* a control lifting under the pointer */
  --shadow-2: 0 1px 3px rgba(0, 0, 0, 0.34);     /* a panel, a column, a card */
  --shadow-3: 0 16px 48px rgba(0, 0, 0, 0.55);   /* a dialog or anything floating over one */

  /* ---- the design system --------------------------------------------------
     Everything above is a colour. Everything below is a MEASUREMENT, and it is
     here because there was no scale. A sweep of this file on 24 August 2026
     counted 13 distinct border-radius values across 65 declarations (57 of them
     literals bypassing the two tokens that already existed), 29 rendered
     control heights, 18 font sizes including six half-pixel steps, and 28
     freehand white border alphas of which nine were a token written out
     longhand. That is why the room read as "not standard" rather than as any
     one broken rule: every component was styled where it was written, so
     nothing agreed with anything.

     Each token names the values it collapsed. Nothing new may invent a
     fourteenth radius or a thirtieth height: pick one of these, or change the
     token and move everything with it. */

  /* Radius. The value chosen for each band is the one that band already used
     most, so this was convergence rather than a redesign: nothing moved by
     more than 4px and most controls moved by 1 or 2. */
  /* Three steps, an order of magnitude tighter than the 8/12/16 this room
     opened with. A control room is dense: at 26px tall, an 8px corner spends a
     third of the control's height being round, and fifteen of them in a row
     read as a bag of lozenges rather than a bar of controls.

     --radius-pill is gone rather than reduced. It was carrying two different
     ideas: a capsule (a chip, a tab, a track) and a circle (a status dot, the
     busy spinner). The capsule was the busiest thing in the room and is now a
     control corner like everything else; the circle is a shape, not a size, and
     the two rules that need one say 50%. */
  --radius-sm: 2px;      /* controls: buttons, inputs, selects, chips, tabs, tracks */
  --radius: 3px;         /* cards and inner surfaces */
  --radius-lg: 4px;      /* panels, dialogs, floating boxes */

  /* Control height. The height lives on the token and padding is horizontal
     only, so a font-size change can never move a control's box again, which is
     how 18, 19, 20, 22, 24, 25, 26, 30, 31, 32 and 36 happened in the first
     place. */
  --ctl-h-sm: 22px;      /* dense in-panel controls */
  --ctl-h: 26px;         /* the default */
  --ctl-h-lg: 30px;      /* topbar, command bar and dialog controls */
  --pill-h: 18px;        /* a read-only state chip on a pane header */

  /* A status dot. There were three sizes in one 288px rail: .perf-led at 8px,
     .lane-led at 7 and .project-clock-dot at 6, and the two LISTS are stacked
     directly on top of each other, so the dot column and the text beside it
     both visibly stepped 2px to the right as the eye crossed from QUEUE into
     PERFORMANCE. 8px is the largest of the three because a 6px dot with a 1px
     border has two pixels of fill left to carry a colour. */
  --dot: 8px;

  /* Panel chrome bars. .rail-head carried two different heights under the same
     selector 2,614 lines apart (26px and 34px); .pane-head was 31, .kcol-head
     38, .pane-foot 22. Four bars doing one job. */
  --bar-h: 34px;
  --bar-h-sm: 22px;

  /* Type. Pass 1 declared six steps and 117 of the 136 font-size declarations
     ignored them, writing the same integer as a literal instead: 57 x 10px,
     29 x 11px, 10 x 12px, 9 x 9px. Every one of those literals except three was
     EXACTLY a token value, so the scale was not wrong, it was unused. The four
     steps below --fs-lg are new names for sizes that were already in the file
     (15, 16, 17, 26, 34) and had nowhere to come from; naming them is what
     makes the next size a choice from ten rather than from infinity. */
  --fs-micro: 9px;       /* uppercase micro labels */
  --fs-xs: 10px;
  --fs-sm: 11px;
  --fs-md: 12px;
  --fs-base: 13px;       /* body */
  --fs-lg: 14px;         /* the reading face: prose, notes, an imported comment */
  --fs-xl: 16px;         /* a heading: the wordmark, a dialog title, an h2 in a ticket */
  --fs-2xl: 18px;        /* the subject of a panel, and nothing else */
  /* Two reading faces, which is the size a number somebody stares at is set
     in. --fs-face-lg is the headline of a block, and the one block that has one
     is TIMER, which is named after the project clock it holds. --fs-face is the
     smaller step; the bay's empty-state plus is what reads it.

     Which face got which step was wrong once, and it is worth writing down.
     "The face a panel is named after" was the rule, the panel was called TIMER
     and it held two clocks, so the larger step landed on the recording
     countdown: 34px, the largest text anywhere in the room, on a
     screen-recording stopwatch. That countdown left on 2 September 2026. */
  --fs-face: 26px;
  --fs-face-lg: 34px;

  /* Tracking. 41 uppercase rules carried fifteen letter-spacings between them
     (0.035 through 0.22em) for what is two type roles. --track-label is the
     value 13 of them already used. The 0.22em on the password field is a third
     and it is documented where it is written: it is a length, not a label. */
  --track-label: 0.1em;
  --track-brand: 0.16em;
  /* And one for the two clock faces, because a face drawing digits at 26 and
     34px is tracked for the digits and not for a label. */
  --track-face: 0.035em;

  /* Spacing. Every integer from 1px to 12px was in use, plus 14, 16, 18, 20. */
  --sp-h: 2px;
  --sp-1: 4px;
  --sp-1h: 6px;
  --sp-2: 8px;
  --sp-2h: 10px;
  --sp-3: 12px;
  --sp-4: 16px;
  --sp-5: 20px;
  /* The one horizontal inset a panel's header and its body must share. They
     disagreed by 3px in five panel types on both screens, which is the kind of
     misalignment a reader sees before they can name it. Pass 1 settled it at
     9px, which is the one number on this list that was on no grid at all; it
     reads off the scale now, so a panel inset and a card gap are the same
     decision rather than two. */
  --pad-panel: var(--sp-2h);
  /* And the one a full-width chrome bar takes. .topbar inset its content 16px,
     .deck-bar 12 and .commandbar 14, so the leftmost and rightmost control of
     three bars stacked directly above one another formed a ragged edge down
     both sides of the app. */
  --pad-bar: var(--sp-3);
  /* What a select has to keep clear on its right for the chevron below. */
  --chev-pad: 24px;

  /* Borders. Three, and nothing else. --edge is what a control or a card is
     bounded by; --rule is an internal divider that carries no meaning; --edge-hot
     is hover, focus and armed. WCAG asks 3:1 for a control boundary, which on
     this ground needs alpha 0.35, and every border in the file was between a
     third and a half of that. 0.18 is a deliberate part-way step: the fill of
     every field here differs from its panel as well, so the border is not the
     only boundary, and 0.35 white on a glass interface is a different app. */
  --rule: rgba(255, 255, 255, 0.09);
  --edge: rgba(255, 255, 255, 0.18);
  /* Hover, focus and armed, and it has to sit ABOVE --edge-control or a hot
     control draws a fainter edge than a resting one. It was 0.3 against
     --edge-control's 0.34 and had exactly one consumer, so nothing showed it.
     Thirteen hover rules picked their border from six other values instead,
     three of them from --accent-glow, which the ladder labels "a glow, or a soft
     ring" and which is half the strength of --accent-line, the documented
     accented border. */
  --edge-hot: rgba(255, 255, 255, 0.55);
  /* And one more, for the boundary WCAG 1.4.11 actually asks 3:1 of. 81 of the
     136 rendered controls carried --edge, which composites to 1.72:1 on a
     dialog, 1.60 on the rail and 1.67 on the topbar; --edge-hot, the strongest
     edge here and hover-only, tops out at 2.67. It is for the border of an
     input, a select, a textarea or a button; --edge stays for cards, panels and
     dividers, where 1.4.11 does not apply and this much white would be a
     different application.

     Raised from 0.34, which cleared the bar it was aimed at by 0.06 and only on
     the lightest ground in the room: measured 3.11:1 on --rack-2, 3.12 on
     --rack-3 and 2.89 on --void, and --void is what sits behind the secrets
     rail, the timer's RESET and every field in NEW SESSION and NEW PROJECT, so
     the twenty controls that most needed it were the twenty that failed. 0.40
     measures 3.66:1 on --void and 3.5 on the panel grounds: it clears
     everywhere with room to spare, on one edit that lands on all of them. */
  --edge-control: rgba(255, 255, 255, 0.4);
  --wash-1: rgba(255, 255, 255, 0.025);   /* the quiet background wash */
  --wash-2: rgba(255, 255, 255, 0.055);   /* the lit one */
  /* A recessed field was painted five different blacks: 0.2, 0.27, 0.28, 0.3
     and 0.34, so a field in the rail, a field in a dialog and a field in the
     ticket were three shades of one idea. */
  --field-bg: rgba(0, 0, 0, 0.3);
  --well-bg: rgba(0, 0, 0, 0.2);

  /* Stacking. Seven ad-hoc integers with no scale (3, 4, 30, 40, 50, 55, 60),
     one of which was silently trapped inside a backdrop-filter's stacking
     context and painted under the cards it was supposed to float over. */
  --z-base: 1;
  --z-chrome: 30;
  --z-beam: 40;
  --z-float: 50;
  --z-panel: 55;
  --z-toast: 60;

  /* State dimming. There is none left. Pass 1 collapsed thirteen ad-hoc
     opacities onto two tokens and kept them for "transient" states; measured,
     the transient states were the worst of the lot, because an opacity
     multiplies a token that is already the dimmest ink in the file. A card
     being dragged sat at 0.7, which took its id, priority and labels to 3.27:1
     while the operator was looking straight at it; a card a filter had pushed
     back sat at 0.55 and measured 2.44:1; the eight "not enough data" rows in
     the performance panel sat at 0.7 and measured 3.33:1; an exited pane 0.72
     and 3.40:1; a snoozed brief item 0.58 and 2.60:1. All of them are declared
     colour pairs now, so every one of them can be measured. A recessive state
     is a colour. Opacity is for something that is genuinely mid-animation, and
     nothing in this file is. */

  /* A state that has receded is said in --ink-faint, which is that token's job
     on a card ground and clears 4.5:1 on every surface here. It had an alias of
     its own, --ink-recessive, declared inside the legacy block below because
     web/depmap.css was thought to read it; that sheet does not, and this one
     read it twice, so a live rule here took its ink from a name whose whole
     purpose was to be a shim. One name. */

  /* Dialogs. Three width rules produced three dialog widths at one viewport. */
  --dlg-w: min(520px, 92vw);
  --dlg-w-wide: min(880px, 94vw);
  --dlg-pad: 20px;

  /* ---- legacy names -------------------------------------------------------
     Kept defined, not kept in use. web/depmap.css is not this file's to edit and
     it reads all three: --rule-hot and --grey through a var() fallback, so
     deleting those would drop that sheet onto its own fallbacks, which are the
     previous palette; and --violet in eight places with NO fallback at all,
     including `stroke` and `fill` on the critical path. An unresolved var() in
     a paint property is invalid at computed-value time and falls back to the
     initial value, which for stroke and fill is black: deleting this name would
     have painted the critical path black on a black canvas. It resolves to the
     one accent now, six units from the #009d89 it used to be. Nothing in this
     file references any of the three. */
  --rule-hot: var(--edge);
  --grey: var(--ink-dim);
  --violet: var(--teal);

  --state-starting: var(--ink-dim);
  --state-idle: var(--green);
  --state-working: var(--blue);
  --state-awaiting-input: var(--amber);
  /* Starting and exited were both --ink-dim, so a pane coming up and a pane that
     had died were the same colour on the two things a glance across a seven-pane
     grid actually reads: the state pill and the pane's own edge bar. The only
     difference was an opacity and a grayscale filter on the terminal body, and
     neither touches the pill or the edge. Dead is red-shifted; it is not --red,
     which is reserved for an error the operator has to act on. */
  --state-exited: #b08486;
  --state-error: var(--red);

  /* The wordmark and every panel heading, and nothing that is READ as a number.
     Orbitron draws a slashed zero and has no `zero` feature to turn it off,
     which was checked: `font-feature-settings: 'zero' 0` renders identically.
     The project clock's resting value is 00:00:00, so the largest readout in
     the room spent most of its life looking struck through, eight glyphs of
     which six carried a diagonal. Both clock faces are --mono now, which is the
     family every other number in this room is already set in and draws an
     unslashed zero; the display face keeps the identity work. */
  /* The wordmark, and only the wordmark. One rule reads this; the four others
     that used to were a micro-label, two column captions and a dialog title,
     none of which is a logo. */
  --display: 'Orbitron', 'Geist Sans', system-ui, sans-serif;
  --ui: 'Geist Sans', 'Segoe UI Variable Text', 'Segoe UI', system-ui, sans-serif;
  --mono: 'Geist Mono', 'Cascadia Mono', Consolas, ui-monospace, monospace;
  /* Three families, three names. --sans was a fourth: `--sans: var(--ui)`,
     byte-identical by definition, read by exactly one rule (.safety-word) while
     --ui was read by fourteen. A second name for a value that already has one
     is how two rules that mean the same thing come to look different. */
  /* ---- the widths the layout changes at ------------------------------------
     There were eight, and not one was named anywhere, so a control could vanish
     at a width that corresponded to nothing the layout did. Seven now: 1320px
     is gone, because between it and about 1600 the topbar wrapped on its own
     into four and five rows and the break belongs at the same width the tally
     goes. A custom property cannot be read inside a media query, so they are
     written out at every use and the ladder is written down once, here. Each
     rung sheds one more thing, in the order of how well what is left already
     says the same thing:

       1500px  the fleet tally goes (the panes say it), and the topbar's
               elastic gap becomes a deliberate row break: brand and status
               above, readouts and actions below
       1180px  the RAIL goes, the dock drops under the board, and the gutters
               tighten. #btn-rail goes with it: a toggle for a column that
               cannot exist at this width is a control that lies
       1100px  the link keeps its dot and loses its word, and the ticket panel
               stops floating and becomes a sheet
       1000px  the gauge bars and the attention sentence go; both keep a number
        820px  the STAGE restacks into one column and the gutters tighten again
        700px  the usage strip goes
        560px  the command bar drops to two columns

     Plus two heights: 640px, where the ticket becomes a sheet, and 560px, where
     the topbar goes back to one compact row.

     An eighth width in a @media in this file is a new rung on this ladder and
     needs a line here saying what it sheds. */

  /* ---- the nine board statuses ---------------------------------------------
     These lived in a second :root 2,200 lines down and a third 2,000 lines
     after that, and the third existed only to undo a mistake in the second:
     --status-review was declared as the old --violet, which was the same hue as
     --status-ready, so two adjacent board columns were one colour, and the
     later block repainted it rose rather than fixing the line. Both lines
     survived. One block, corrected where it is written. */
  --status-backlog: var(--ink-dim);
  --status-ready: var(--teal);
  --status-doing: var(--blue);
  --status-review: var(--rose);
  /* --status-blocked was a tenth name and it was --ink-faint, the same value as
     --status-soft-blocked beside it and the same value the room writes every
     caption nobody is meant to read in. It had one consumer, the card whose
     status is 'blocked', and src/work/board.mjs puts exactly those cards in the
     HARD BLOCK column, so the name was describing the state a second time in a
     colour that contradicted the column heading above it. Gone: that card reads
     --status-hard-blocked, which is what it is. Soft keeps the faint ink, which
     is right for it: held, but alive, and quiet. */
  --status-soft-blocked: var(--ink-faint);
  /* Hard-blocked and dead were both --red, and they are separate columns on the
     same board: the two failure states an operator most has to tell apart were
     the only pair in the nine with no differentiator at all. Red now means dead,
     which is the end of a task; hard-blocked is red pulled towards the faint
     ink, because it is a task that is still alive and cannot move. */
  --status-hard-blocked: color-mix(in srgb, var(--red) 60%, var(--ink-faint));
  /* Urgent is finished work held by one human act, so it is composed rather than
     given a colour of its own: green because it IS done bar that act, barred
     because it is held. The obvious choice was amber, and the rule below is why
     it was not taken. What the composition needed was a bar that can be seen:
     the old hatch was --ink-dim at 22% over #55c895, which measures 1.03:1
     against the plain edge beside it inside a 4px strip, so the whole
     distinction was invisible and urgent and done were simply the same colour. */
  --status-urgent: var(--green);
  --status-done: var(--green);
  --status-dead: var(--red);

  --dock-w: 390px;
  --kcol-w: 272px;
  /* The one measurement that decides how many of the nine columns a person can
     see, and it was the only layout number in the file still written as a raw
     literal: 224px on .kcol and 244px inside the 820px media query, so the
     narrowest viewport was given the WIDEST columns. Measured, the board
     scrolled sideways at every width tested (2,096px of content in 1,260 at
     1680 wide; 2,276 in 808 at 820), and four of nine columns sat behind that
     scroll. One name, and the narrow rung goes DOWN. */
  --kcol-min: 224px;
  --kcol-min-tight: 200px;
  /* Barring, for a 4px colour strip and for nothing else. This was one token
     doing two incompatible jobs: in six of its eight uses it painted a 4px-period
     gradient BEHIND 10px type, which cut a SOFT BLOCK chip from 8.84:1 to 6.22
     and read as a rendering fault rather than as texture. Type-bearing surfaces
     carry "held" on a dashed boundary now, which is a channel of its own and
     crosses no glyph. Black rather than ink, because this has to read against
     --green, --red and --ink-faint alike. */
  --hatch-edge: repeating-linear-gradient(135deg, rgba(0, 0, 0, 0.55) 0 3px, transparent 3px 6px);

  --rail-w: 310px;
  --coord-w: clamp(340px, 25vw, 480px);
  --head-h: 58px;
  --term-size: 12.5px;


  /* Ink on a WASHED plate, as opposed to a filled one. --on-accent and the two
     beside it are for a plate filled with the colour at full strength; these
     three are for the 8% wash an error, a warning and an armed chip sit on,
     where the ink has to be a tint of the same hue rather than a near-black.
     Three literals across five rules, and no name until the room grew a second
     theme and every one of them turned out to be a light-on-dark value. */
  --on-wash-alarm: #ffd7d5;
  --on-wash-warn: #ffe7cd;
  --on-wash-accent: #6bd6c7;

  /* Three more that were literals for the same reason. The scrim dims the page
     behind a dialog, --on-selection is the ink a selected run of text is read
     in, and --wordmark-ink is the opaque colour under the clipped gradient on
     the wordmark, which is what forced-colors mode falls back to. */
  --scrim: rgba(0, 0, 0, 0.72);
  --on-selection: #ffffff;
  --wordmark-ink: #f1f3f2;

  /* One millisecond of grain, cheap enough to leave on the chrome for ever. */
  --grain: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='120' height='120'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.85' numOctaves='3'/%3E%3C/filter%3E%3Crect width='120' height='120' filter='url(%23n)' opacity='0.5'/%3E%3C/svg%3E");
}

/* =============================================================================
   Quiet Control Light.

   The room in daylight. :root above is the dark theme and stays the reference
   the rest of this sheet is written against; this block redeclares only the
   BASE colours, because every derived name (--status-*, --state-*, --grey,
   --rule-hot) is already var() onto one of them and custom
   properties resolve at use. Redefining twenty-eight values here retints
   about four hundred declarations without touching a rule.

   Two things are not a straight inversion, and both are contrast:

   1. The accents move. #00b39c is 7.4:1 on --rack and 2.65:1 on white, so the
      brand teal cannot be text in daylight; the same is true of --green,
      --amber, --red and --rose, which are all light-on-dark values. Each was
      darkened along its own hue until the WORST of the four grounds cleared
      4.5:1, which is the rule styles.css:43 already applies to --ink-faint.
      Measured on the lightest ground each is: teal 5.14, green 5.13, amber
      5.90, red 5.43, rose 5.13, blue 5.75, ink-faint 5.15. The brand #00b39c
      survives in the dark theme and on the filled plates, which take white.

   2. The ink-on-a-filled-colour trio inverts outright. --on-accent and friends
      are the one place the dark room writes a near-black; on a light theme the
      plates they sit on are dark, so all three become white.
   ========================================================================== */
html[data-fractal-embedded='true'] #btn-theme { display: none; }

html[data-theme='light'] {
  color-scheme: light;

  /* The ladder still rises, it just rises towards white: --void is the stage
     behind the panels and --rack-3 is the topmost plate, so a raised thing is
     lighter than what it sits on exactly as it is in the dark. */
  --void: #f0f1ee;
  --rack: #f7f7f5;
  --rack-2: #fcfcfb;
  --rack-3: #ffffff;

  --ink: #171a19;
  --ink-dim: #4e5754;
  --ink-faint: #646e6b;

  --green: #287a56;
  --blue: #2f6e7e;
  --amber: #8a5a18;
  --red: #c0392f;
  --teal: #007a6a;
  --rose: #9a5870;
  --plum: #6e4a9e;
  --slate: #4a5a6b;

  /* White on every one of these clears 4.5:1: teal 5.26, green 5.24, amber
     5.90, red 5.43. */
  --on-accent: #ffffff;
  --on-warn: #ffffff;
  --on-alarm: #ffffff;
  /* Hover deepens rather than brightens: on a light ground the lit state is
     the darker one, which is the opposite of what --teal-hot does above. */
  --teal-hot: #00695b;

  --surface-panel: rgba(255, 255, 255, 0.96);
  --surface-bar: rgba(250, 250, 248, 0.92);
  --surface-float: rgba(255, 255, 255, 0.98);
  --surface-inset: rgba(244, 245, 242, 0.94);
  --surface-chip: rgba(255, 255, 255, 0.72);

  /* The same five-step ladder, on the daylight teal. */
  --accent-wash: rgba(0, 122, 106, 0.07);
  --accent-tint: rgba(0, 122, 106, 0.12);
  --accent-glow: rgba(0, 122, 106, 0.18);
  --accent-line: rgba(0, 122, 106, 0.38);
  --accent-line-hot: rgba(0, 122, 106, 0.62);

  /* Lines and washes invert from white-over-black to ink-over-white, at the
     alphas the design system uses for a hairline rather than the dark room's,
     which are tuned to survive a black ground and read as pencil on white. */
  --rule: rgba(23, 26, 25, 0.1);
  --edge: rgba(23, 26, 25, 0.18);
  --edge-hot: rgba(23, 26, 25, 0.45);
  --edge-control: rgba(23, 26, 25, 0.32);
  --wash-1: rgba(23, 26, 25, 0.025);
  --wash-2: rgba(23, 26, 25, 0.055);

  /* A field is the one thing that goes lighter than its surround in daylight,
     the way it goes darker in the dark: a well is recessed either way. */
  --field-bg: #ffffff;
  --well-bg: rgba(23, 26, 25, 0.04);

  /* A black drop on a white ground is a smudge. These are the design system's
     raised/floating elevations, which are ink at four to twenty per cent with
     a negative spread rather than a wide black pool. */
  --shadow-1: 0 1px 2px rgba(23, 26, 25, 0.05);
  --shadow-2: 0 1px 3px rgba(23, 26, 25, 0.07);
  --shadow-3: 0 12px 40px -12px rgba(23, 26, 25, 0.22);

  --hatch-edge: repeating-linear-gradient(135deg, rgba(23, 26, 25, 0.24) 0 3px, transparent 3px 6px);
  --state-exited: #995b5d;

  /* On a white ground the washed plates are pale, so the ink on them inverts
     with everything else: 7.76, 7.31 and 5.84 on the worst of the four grounds. */
  --on-wash-alarm: #8f2018;
  --on-wash-warn: #6d4611;
  --on-wash-accent: #00695b;

  /* A 72% black scrim on a light theme reads as the lights going out rather
     than a dialog opening; 32% ink is the same job at daylight strength. And a
     selection is a pale teal here, not a saturated one, so its ink is the --ink
     it would have been anyway rather than white. */
  --scrim: rgba(23, 26, 25, 0.32);
  --on-selection: #171a19;
  --wordmark-ink: #171a19;
}

/* The swap, and only the swap, is animated. The class is added by app.js for
   the length of the crossfade, so this never runs on load and never blankets
   the transitions the components declare for themselves. */
html[data-theme-switching] * {
  transition: background-color 260ms ease, border-color 260ms ease, color 260ms ease,
    fill 260ms ease, stroke 260ms ease, box-shadow 260ms ease !important;
  transition-delay: 0ms !important;
}

@media (prefers-reduced-motion: reduce) {
  html[data-theme-switching] * { transition: none !important; }
}

html[data-density='s'] { --term-size: 11px; --coord-w: clamp(320px, 22vw, 420px); }
html[data-density='l'] { --term-size: 14.5px; --coord-w: clamp(380px, 28vw, 560px); }

/* Every ::-webkit-scrollbar rule this file used to carry was dead:
   `scrollbar-color` on body is the standard property, it inherits, and Chrome
   ignores the legacy pseudo-elements on any scroller that has it. Eight
   scrollers declared 8px or 9px and all of them rendered 15, which cost the
   performance block 15px of right inset against 9 on the left and made a card
   in a scrolling column 15px narrower than the card beside it.

   The standard properties are here, on everything, because scrollbar-width does
   NOT inherit (scrollbar-color does, which is what made this look as though it
   was already handled). */
* {
  box-sizing: border-box;
  scrollbar-width: thin;
}

/* The UA rule for [hidden] is display:none at the weakest possible specificity,
   so any class in here that sets a display quietly un-hides its own element. */
[hidden] { display: none !important; }
.fractal-connection-status {
  position: fixed; top: 0; left: 0; right: 0; z-index: var(--z-toast);
  padding: var(--sp-4); text-align: center; color: var(--ink, #111);
  background: var(--rack, #090b0b); border-bottom: 1px solid currentColor;
}

/* Native groups keep their names without adding browser-default borders or
   preventing the existing flex and grid layouts from shrinking. */
fieldset {
  min-inline-size: 0;
  margin: 0;
  padding: 0;
  border: 0;
}

output.dlg-sub { display: block; }

html, body {
  height: 100%;
  margin: 0;
}

body {
  background: var(--void);
  color: var(--ink);
  font-family: var(--ui);
  font-size: var(--fs-base);
  overflow: hidden;
  font-stretch: normal;
  -webkit-font-smoothing: antialiased;
  text-rendering: optimizeLegibility;
  scrollbar-width: thin;
  scrollbar-color: var(--accent-line) var(--wash-1);
}

/* One corner for every control, so a component rule only has to say so when it
   deliberately differs. This used to be `border-radius: 0` in a block 3,300
   lines further down, which existed purely to be undone by .btn, .seg button
   and the shared field rule. */
button, input, select, textarea {
  font: inherit;
  color: inherit;
  border-radius: var(--radius-sm);
}

code {
  font-family: var(--mono);
  font-size: var(--fs-md);
  color: var(--ink-dim);
  background: var(--rack-3);
  padding: var(--sp-h) var(--sp-1);
}

/* ---- shell ---------------------------------------------------------------- */

.shell {
  position: relative;
  display: grid;
  /* The column is pinned rather than left to `auto`, which would size to the
     topbar's max-content and drag the terminals off the right of a small
     screen. Everything inside is then responsible for its own overflow. */
  grid-template-columns: minmax(0, 1fr);
  /* The head row is `auto` rather than a fixed `--head-h`, and the topbar keeps
     that height as a MINIMUM instead. On a wide screen nothing changes. On a
     narrow one the bar is allowed to wrap onto a second row and the grid follows
     it, which is the whole reason its controls no longer vanish. */
  grid-template-rows: auto minmax(0, 1fr) auto;
  height: 100dvh;
  isolation: isolate;
  gap: var(--sp-2h);
  padding: var(--sp-2h);
  background:
    radial-gradient(circle at 12% -12%, var(--accent-glow), transparent 30rem),
    radial-gradient(circle at 88% 16%, color-mix(in srgb, var(--rose) 8%, transparent), transparent 34rem),
    linear-gradient(var(--wash-1) 1px, transparent 1px),
    linear-gradient(90deg, var(--wash-1) 1px, transparent 1px),
    var(--void);
  background-size: auto, auto, 36px 36px, 36px 36px, auto;
}

/* The attention beam. It runs along the very top edge of the app whenever any
   session is waiting on a human, so a glance at a wall-mounted screen from the
   other side of the room still reads. */
.shell::before {
  content: '';
  position: absolute;
  z-index: var(--z-beam);
  pointer-events: none;
  background-size: 34% 100%;
  background-repeat: no-repeat;
  opacity: 0;
  transition: opacity 240ms ease;
  inset: 0 22% auto;
  height: 2px;
  border-radius: var(--radius-sm);
  background-image: linear-gradient(90deg, transparent, var(--amber), var(--rose), transparent);
}

body[data-attention='1'] .shell::before {
  opacity: 1;
  animation: beam 2.8s linear infinite;
}

@keyframes beam {
  from { background-position: -34% 0; }
  to { background-position: 134% 0; }
}

.textured {
  position: relative;
}

.textured::after {
  content: '';
  position: absolute;
  inset: 0;
  pointer-events: none;
  background-image: var(--grain);
  mix-blend-mode: overlay;
  border-radius: inherit;
  opacity: 0.018;
}

/* ---- topbar --------------------------------------------------------------- */

.topbar {
  display: flex;
  align-items: center;
  min-height: var(--head-h);
  position: relative;
  z-index: var(--z-chrome);
  gap: var(--sp-2h);
  /* --pad-bar. The three full-width bars are the same width and stacked
     directly above one another, and they inset their content by 16, 12 and 14px
     respectively, so the leftmost and rightmost control of each formed a ragged
     vertical line down both sides of the app. */
  padding: 0 var(--pad-bar);
  /* NOT `overflow-x: auto` with the scrollbar hidden, which is what this was.
     A bar that scrolls with no scrollbar and no affordance does not degrade: it
     silently takes `new session`, `sound`, `rail` and the whole usage strip out
     of reach below about 1280px, and nothing on screen says they are still
     there. Wrapping is the honest failure mode: the bar gets taller and every
     control stays clickable. */
  flex-wrap: wrap;
  align-content: center;
  row-gap: var(--sp-1h);
  overflow: visible;
  /* --radius-lg, not --radius-pill. The three full-width bars sit on the same
     two vertical lines down the app and inset their content by the same
     --pad-bar, and two of them turned their corners at 16px while this one
     asked for 999, which on a bar that wraps to 88px tall resolves to a 44px
     corner, so the top bar of the app curved and the two under it did not. The
     token block reserves --radius-pill for "pills, chips, tabs, tracks, dots",
     and a bar is none of the five. */
  background: var(--surface-bar);
  backdrop-filter: blur(18px) saturate(125%);
  -webkit-backdrop-filter: blur(18px) saturate(125%);
}

.brand {
  display: flex;
  flex: 0 0 auto;
  align-items: center;
  gap: var(--sp-2);
  margin-right: var(--sp-h);
}

/* An <img>, not a tinted box around a letter. The mark carries its own
   colours on either ground, so it takes no border, no plate and no glow: the
   three things that used to make it agree with the theme are exactly the three
   the logo rules say never to apply to it. */
.brand-mark {
  display: block;
  width: 30px;
  height: 30px;
  flex: 0 0 30px;
  object-fit: contain;
}

/* The word became the page's only <h1> on 24 August 2026, so that the heading
   list does not open on "performance" with nothing above it. The UA gives an h1
   0.67em of margin top and bottom, which on a baseline-aligned flex row pushed
   the mark and the sub-title 10px apart from it; the box has to stay exactly the
   box the span was. */
.brand-name {
  margin: 0;
  color: var(--wordmark-ink);
  font-family: var(--display);
  font-size: var(--fs-xl);
  font-weight: 800;
  letter-spacing: var(--track-label);
}

.gauge-label,
.rail-head h2,
/* The stage's column caption became an h2 on 24 August 2026 so the control
   screen has a heading of its own: the rail named all five of its blocks with an
   h2 and the two columns beside it named theirs with a span, so the heading list
   jumped from the brand straight into the rail. It has to keep the exact box the
   span had, hence the margin reset below. */
.col-rule > h2,
.col-rule span,
.pane-kind,
.pane-warn,
.pill,
.attention-text,
.bay-label,
.lane-name,
.security-name,
.card-meta,
.field > span {
  /* The family and the weight belong here too. Without them this rule set the
     SIZE and the CASE of every micro-label in the room and left the face to
     whatever each one inherited, so the four in the topbar rendered in three
     different combinations: the column captions in Geist Mono at 600, the
     slots label in Geist Sans at 400, the lane names in Sans at 600. A label
     is the design system's one mono role, so it is mono here, once. */
  font-family: var(--mono);
  font-size: var(--fs-xs);
  font-weight: 600;
  letter-spacing: var(--track-label);
  text-transform: uppercase;
}

.topbar-gap { flex: 1 1 auto; }

.usage-strip {
  display: flex;
  align-items: center;
  gap: var(--sp-1h);
  min-width: 0;
}

.usage-pool {
  display: grid;
  grid-template-columns: auto auto;
  column-gap: var(--sp-1);
  align-items: center;
  /* Fixed, not minimum. Each pill sized itself to its own label, so the three
     meters measured 156, 156 and 145px and the right-hand pool's 91 per cent
     drew shorter than the left-hand pool's 86. A row of meters exists to be
     compared down its length; comparing them needs one baseline length.

     184 rather than the 174 it was: the meters moved off the room's only 9px
     size onto the 10px every other readout uses, and the longest name,
     "Claude Code · Max 5h", then measured 120px against 48 for the figure and
     8 of right padding, which is 4px more than the old column had. The cost of
     getting that wrong is not cosmetic: the name ellipsises at the window, so
     the 5h pool and the 1w pool both render as "Claude Code · Max " and the
     one thing three meters exist to let you compare is the thing that is
     cut. */
  width: 184px;
  /* No plate and no border: this is read, never clicked, and the 2px track
     below the numbers already carries the whole of what it says. Padding stays
     asymmetric so the three meters keep their shared baseline length. */
  padding: var(--sp-1) var(--sp-2) var(--sp-1) 0;
}

.usage-pool b { min-width: 0; color: var(--ink-dim); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }

.usage-pool em { color: var(--green); font-style: normal; text-align: right; }

/* Staleness is a fact about the READING, not about the headroom, and it was
   painted on the figure: measured live, the three pools read "88% left" green,
   "66% left" green and "91% left" AMBER, so the warning colour landed on the
   pool with the most room left. It is on the pool's name now, which is what has
   gone stale. The figure keeps one job, which is to say how much is left. */
.usage-pool[data-state='stale'] b { color: var(--amber); }

.usage-pool[data-state='unavailable'] em, .usage-empty { color: var(--ink-faint); }

.usage-pool i { grid-column: 1 / -1; height: 2px; margin-top: var(--sp-h); background: var(--rule); }

.usage-pool i::after { content: ''; display: block; width: var(--remaining, 0%); height: 100%; background: var(--green); }

/* The figure and the fill say how much is left, so they are coloured by how
   much is left. Measured before this: 97%, 48% and 2% all drew rgb(85,200,149)
   and only the fill's width moved, so a pool three per cent from the wall was
   painted as safely as one at ninety-seven. The bands are usageHeadroom()'s, in
   web/app.js, so the word and the colour cannot drift apart. */
.usage-pool[data-headroom='low'] em { color: var(--amber); }

.usage-pool[data-headroom='low'] i::after { background: var(--amber); }

.usage-pool[data-headroom='out'] em { color: var(--red); }

.usage-pool[data-headroom='out'] i::after { background: var(--red); }

/* "We do not know" was drawn as a 0px green fill in a plain track, which is the
   same picture as "you have none left" and the more alarming of the two
   readings. A barred track says there is no reading rather than no headroom. */
.usage-pool[data-state='unavailable'] i,
.usage-pool[data-headroom='unknown'] i {
  background-image: repeating-linear-gradient(135deg, var(--edge) 0 2px, transparent 2px 4px);
}

.usage-pool[data-headroom='unknown'] em { color: var(--ink-faint); }

.usage-cost { display: flex; color: var(--ink-dim); font: var(--fs-xs)/1 var(--mono); white-space: nowrap; }

.access-warning {
  flex: 0 0 auto;
  border-left: 2px solid var(--amber);
  background: color-mix(in srgb, var(--amber) 8%, transparent);
  color: var(--amber);
  padding: var(--sp-1) var(--sp-2) var(--sp-h);
  font-size: var(--fs-xs);
  font-weight: 700;
  letter-spacing: var(--track-label);
  line-height: 1;
  white-space: nowrap;
}

.production-safety {
  gap: var(--sp-1h);
  flex: 0 0 auto;
  border: 1px solid var(--red);
  background: color-mix(in srgb, var(--red) 16%, var(--rack));
  color: var(--red);
  padding: 0 var(--sp-2);
}

.production-safety:hover { background: color-mix(in srgb, var(--red) 24%, var(--rack)); }

.production-safety[aria-pressed='true'] {
  border-color: var(--green);
  background: color-mix(in srgb, var(--green) 10%, var(--rack));
  color: var(--green);
}
/* Amber's rule holds here too: this pill is not waiting on an answer from you,
   it is reporting that it does not have one. --rose, the same colour REVIEW
   uses, means the same thing it means there: a person has to decide. */
.production-safety[data-effective='mixed'],
.production-safety[data-effective='unsupported'] {
  border-color: var(--rose);
  background: color-mix(in srgb, var(--rose) 16%, var(--rack));
  color: var(--rose);
}

.safety-dot { width: 7px; height: 7px; border-radius: 50%; background: currentColor; }

.link {
  display: flex;
  align-items: center;
  gap: var(--sp-1h);
  padding: var(--sp-1) var(--sp-2) var(--sp-1) 0;
}

.link-dot {
  width: 7px;
  height: 7px;
  border-radius: 50%;
  background: var(--ink-dim);
}

.link-label {
  font-family: var(--mono);
  color: var(--ink-dim);
  font-size: var(--fs-xs);
}

.link[data-link='live'] .link-dot { background: var(--green); }

.link[data-link='connecting'] .link-dot { background: var(--amber); animation: blink 1s steps(2) infinite; }

.link[data-link='offline'] .link-dot { background: var(--red); }

.link[data-link='offline'] .link-label { color: var(--red); }

/* ---- the socket has gone -----------------------------------------------------
   A 7px dot changed colour and nothing else did. Every number on the board kept
   claiming to be current, and SEND, BROADCAST and NEW SESSION stayed lit, so a
   click bought a toast that vanished in 5.2 seconds. web/app.js now writes the
   state onto <body>, which is what lets a whole screen answer for it.

   Deliberately NOT a dim: this pass spent its budget removing opacity-as-state,
   and half-erasing a board somebody is trying to read during an outage is the
   worst moment to start again. The pill becomes the alarm it always should have
   been, and the two bars that carry live counts grow a red rule so the staleness
   is stated where the stale numbers are. */
body[data-link='offline'] .link {
  border-color: var(--red);
  background: color-mix(in srgb, var(--red) 14%, transparent);
}

body[data-link='offline'] .deck-bar,
body[data-link='offline'] .commandbar {
  border-top: 1px solid var(--red);
}

body[data-link='offline'] .deck-stamp::after {
  content: ' · offline, these counts have stopped moving';
  color: var(--red);
}

/* Every filled primary in the room, unfilled, for as long as the socket is
   down. web/app.js locks exactly three controls by id (SEND, BROADCAST and NEW
   SESSION, the three whose entire effect is a message crossing the socket) and
   deliberately leaves the other sixteen mutating controls reachable, because
   MOVE TO, REQUEUE, KILL and SAVE fail loudly at the API and a real refusal is
   worth more than a greyed button. That decision is right and this rule is what
   it was missing: sixteen controls kept a full teal plate through an outage, so
   the only thing on screen that said the room could not be written to was a 7px
   dot. The plate is the promise; it is withdrawn, and the control stays live so
   the API can still answer for itself. Teal on the border, so the primary is
   still identifiably the primary: exactly the treatment .btn-go:disabled takes
   in the block at the end of this file, for the same reason. */
body[data-link='offline'] .btn-go,
body[data-link='offline'] .mini-go {
  border-color: color-mix(in srgb, var(--teal) 42%, transparent);
  background: transparent;
  color: var(--ink-dim);
  box-shadow: none;
}

/* The one exception this block used to carry went with the countdown that
   earned it: that clock was this tab's own arithmetic against localStorage and
   had never asked the daemon anything, so an outage was not its business.

   The project clock is the opposite of that. START, PAUSE and RESET are each a
   POST to /api/project-clock, so a clock the daemon cannot be told about must
   not look pressable, and #project-clock-toggle takes the withdrawn plate above
   with every other primary in the room. RESET stays reachable for the reason
   the sixteen other mutating controls do: a real refusal from the API says more
   than a greyed button. */

@keyframes blink { 50% { opacity: 0.25; } }

.attention {
  display: flex;
  align-items: center;
  gap: var(--sp-2);
  padding: var(--sp-1) var(--sp-3) var(--sp-1) var(--sp-2);
  border: 1px solid var(--amber);
  background:
    repeating-linear-gradient(135deg, color-mix(in srgb, var(--amber) 14%, transparent) 0 5px, transparent 5px 10px),
    color-mix(in srgb, var(--amber) 8%, transparent);
  color: var(--amber);
  cursor: pointer;
  animation: attention-throb 1.6s ease-in-out infinite;
}

.attention:hover { background-color: color-mix(in srgb, var(--amber) 20%, transparent); }

.attention-count {
  font-family: var(--mono);
  font-size: var(--fs-lg);
  font-weight: 700;
  line-height: 1;
}

.operator-brief-button,
.mailbox-button {
  gap: var(--sp-2);
  padding: 0 var(--sp-2h);
  border: 1px solid var(--edge-control);
  background: var(--rack-2);
  color: var(--ink-dim);
}

.operator-brief-button:hover,
.mailbox-button:hover {
  color: var(--ink);
  border-color: var(--accent-line-hot);
  background: var(--accent-wash);
}

.operator-brief-button[data-alert='true'],
.mailbox-button[data-alert='true'] {
  border-color: var(--amber);
  color: var(--amber);
  background: color-mix(in srgb, var(--amber) 10%, var(--rack));
}

@keyframes attention-throb {
  50% { box-shadow: 0 0 0 3px color-mix(in srgb, var(--amber) 14%, transparent); }
}

.gauges {
  display: flex;
  align-items: center;
  gap: var(--sp-4);
  min-width: 0;
  overflow: hidden;
}

.gauge {
  display: flex;
  align-items: center;
  gap: var(--sp-2);
}

/* --ink-dim, like PROJECT and LIVE. Four uppercase micro-captions doing one job
   in one 30px row were split across two inks with a 1.5x contrast gap and
   nothing said which got which: measured, PROJECT and LIVE read 8.42:1 and
   SLOTS read 5.62. This is the same class of defect the operator reported on
   the secrets scope select. */
.gauge-label { color: var(--ink-dim); }

.gauge-value {
  font-family: var(--mono);
  color: var(--ink);
}

.gauge-bar {
  display: block;
  width: 74px;
  height: 5px;
  background: var(--rack-3);
  border: 1px solid var(--edge);
}

.gauge-bar i {
  display: block;
  height: 100%;
  width: 0;
  background: var(--teal);
  transition: width 320ms ease;
}

.tally {
  display: flex;
  gap: var(--sp-2h);
}

.tally-item {
  display: flex;
  align-items: center;
  gap: var(--sp-1);
  font-family: var(--mono);
  color: var(--ink-dim);
}

.tally-item i {
  width: 6px;
  height: 6px;
  border-radius: 50%;
  background: currentColor;
}

/* An empty fleet, said rather than left blank. The dot is hollow because there
   is no state to colour it with, and --ink-faint is 5.2:1 here, so "0 sessions"
   is still readable rather than a gap the eye slides over. */
.tally-item.is-none { color: var(--ink-faint); }

.tally-item.is-none i { background: transparent; box-shadow: inset 0 0 0 1px currentColor; }

.topbar-actions {
  display: flex;
  align-items: center;
  gap: var(--sp-2);
  margin-left: auto;
}

/* ---- buttons -------------------------------------------------------------- */

/* --edge-control, not --edge, on every control below. WCAG 1.4.11 asks 3:1 of a
   control's boundary and --edge composites to between 1.60 and 1.72:1 on every
   ground in this room. It is written into each control's own block rather than
   into one shared group at the bottom of the file, because such a group is
   (0,1,0) and so are .btn-go, .mini-go and .mini-warn: written later, it would
   quietly take the accent border off all three. */

/* The height comes off the token and the padding is horizontal only. A button
   that sizes itself from its own font is a button that disagrees with the
   select beside it, which is how this file ended up rendering 29 distinct
   control heights. */
/* ---- the one button --------------------------------------------------------

   Measured on the control screen: 35 buttons rendering SEVENTEEN different
   treatments. .btn was already the answer (11px, 600, uppercase, --track-label)
   and six components wrote their own version of it beside it: 9px mono on the
   coordinator's APPLY, 10px/800 on Production Safety, 13px/400/sentence case on
   the mailbox and brief buttons and on the project +. Three type sizes, three
   weights and two cases, for one object.

   The bar already fixed the BOX — one rule 4,300 lines below gives every
   control in the topbar the same 30px band — and nobody ever wrote the matching
   rule for the TYPE. This is it, in the same shape as the one field rule above:
   everything a button shares, stated once.

   Not the border, deliberately. A shared border here would be (0,1,0), exactly
   like .btn-go and .mini-go, and being later in the file it would quietly strip
   the accent edge off every primary in the app. Each component declares its own
   boundary, which is the rule the guard at 'a control that is live reads as
   live' holds down.

   Size is the one thing a component still chooses, from the same three the
   fields use: --ctl-h-sm dense, --ctl-h default, --ctl-h-lg a bar or a dialog.
   .mini takes --fs-xs with it, because a size modifier gets smaller text and
   never a smaller box.                                                        */
.btn,
.mini,
.project-add,
.operator-brief-button,
.mailbox-button,
.production-safety,
.coordinator-settings-submit {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  border-radius: var(--radius-sm);
  font-family: var(--ui);
  font-size: var(--fs-sm);
  font-weight: 600;
  letter-spacing: var(--track-label);
  text-transform: uppercase;
  line-height: 1;
  white-space: nowrap;
  cursor: pointer;
}

.btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  height: var(--ctl-h);
  padding: 0 var(--sp-3);
  border: 1px solid var(--edge-control);
  font-size: var(--fs-sm);
  letter-spacing: var(--track-label);
  text-transform: uppercase;
  cursor: pointer;
  background: var(--wash-2);
  color: var(--ink-dim);
  font-weight: 600;
  transition: color 160ms ease, border-color 160ms ease, background 160ms ease, transform 160ms ease, box-shadow 160ms ease;
}

.btn:not(:disabled):hover { color: var(--ink); border-color: var(--edge-hot); }

.btn:not(:disabled):active { transform: translateY(1px); }

.btn-quiet { background: transparent; }
/* Off used to be the state you could not read, which is backwards: off is the
   state that needs noticing. Both states stay above 4.5:1 and the difference is
   carried on the border and the fill. */
.btn-quiet[aria-pressed='false'] { color: var(--ink-dim); }

.btn-quiet[aria-pressed='true'] { color: var(--ink); border-color: var(--edge-hot); background: var(--wash-2); }
/* A size modifier makes the TEXT smaller, never the box: shrinking the box is
   what put an 18px ADD button on a row with a 24px select. */
.btn-tiny { padding: 0 var(--sp-2); font-size: var(--fs-xs); }

.btn-warn:not(:disabled):hover { color: var(--amber); border-color: var(--amber); }
/* #12060f was a purple-tinted black left over from the old violet. One
   ink-on-accent value now, and it is the best of the three that were here.

   The secrets rail's ADD wears this class rather than being redrawn under
   #secret-save, which is where it lived: ten declarations at ID specificity,
   written because the markup was believed to be out of this file's reach. It
   was one word away. That copy also cost the button both of its states, because
   an ID outranks .btn:disabled and button[aria-busy]: measured in Chrome, ADD
   came back rgb(0,179,156) on rgb(0,28,24) in all three of resting, busy and
   off, so pressing it disabled the control and changed nothing an operator
   could see. */
.btn-go {
  font-weight: 600;
  border-color: var(--teal);
  background: var(--teal);
  color: var(--on-accent);
}

/* One rule for one hover, and the plate lives with the component. There were
   two: this one, which declared nothing but `filter: brightness(1.12)`, and a
   `.btn-go:hover` 3,700 lines below it that declared the fill. Both applied, so
   the rendered plate was the written #27c8b2 multiplied by 1.12, roughly
   #2CE0C7: neither of the two values anybody had written down, and a third teal
   again from the one the secrets rail's ADD button hovered to on the same
   screen. One selector now: that ADD carries .btn-go in the markup, so it
   arrives here on its own rather than being named a second time. */
.btn-go:not(:disabled):hover {
  border-color: var(--teal-hot);
  background: var(--teal-hot);
  color: var(--on-accent);
  box-shadow: var(--shadow-1);
}

/* ---- one field --------------------------------------------------------------
   Ten field variants and no field component. What a text box looks like was
   restated in every rule that drew one, and then a 16-selector group 3,000 lines
   BELOW them all reached back and repainted border, corner and fill on top:
   roughly fifteen border and background declarations written in the component
   rules never rendered once. Reading `.field input { background-color: var(--rack) }`
   told you a dialog field was #090b0b; it drew --field-bg. That group was the
   mechanism behind the measured "26 rules styling an input, 20 styling a
   select", and it is why the operator's own example, the secrets scope select,
   could be the wrong colour with three rules in the file claiming otherwise.

   It is one rule, and it is here with the controls rather than after everything
   it governs. A component below now declares only what genuinely DIFFERS: its
   box, its type size and its horizontal inset. Three sizes and no others:

     dense       --ctl-h-sm / --fs-sm / --sp-1h   in-panel, in a dense row
     default     --ctl-h    / --fs-sm / --sp-1h   the rail and the ticket
     bar, dialog --ctl-h-lg / --fs-md / --sp-2    a chrome bar or a dialog

   Before this there were 3 heights, 5 type sizes, 2 families and 4 left
   paddings among ten controls that are all the same object.

   --mono for all of them: eight of the ten already were, and the two that were
   not (the broadcast target and the ticket's move-to select) are the two that
   read as though they had come from another application.

   background-COLOR, not the shorthand: the shorthand resets background-image
   and would wipe the chevron the shared select rule at the end of this file
   draws. Every rule below sets the colour longhand for the same reason. */
.target,
.cmd-input,
.broadcast select,
.field input,
.field textarea,
.field select,
.detail-input input,
.ticket select,
.mailbox-head-actions select,
.secret-add input,
.secret-add-foot select,
.ticket-edit input,
.ticket-edit select,
.coordinator-setting input,
.activity-note-form textarea {
  border: 1px solid var(--edge-control);
  border-radius: var(--radius-sm);
  background-color: var(--field-bg);
  color: var(--ink);
  font-family: var(--mono);
  transition: border-color 160ms ease, background-color 160ms ease, box-shadow 160ms ease;
}

/* One focus treatment for every text field in the room, and it is written out
   in full on purpose: there were five. .field's was a 0.66 border plus a 3px
   0.1 shadow, .secret-add's a solid teal border plus a 2px 0.14, .ticket-edit's
   a 1px outline at offset 0, .coordinator-setting's a border swap keyed off
   :focus rather than :focus-visible (so it was the only control here that lit
   up under a mouse click) and .detail-input's a border colour and nothing else.
   This is the soft cue; the keyboard ring is the one that has to be findable,
   and no field may delete it. */
.cmd-input:focus,
.scope-input:focus,
.secret-add input:focus,
.secret-add-foot select:focus,
.field input:focus,
.field textarea:focus,
.field select:focus,
.ticket-edit input:focus,
.ticket-edit select:focus,
.coordinator-setting input:focus,
.activity-note-form textarea:focus,
.detail-input input:focus,
.ticket select:focus,
.broadcast select:focus,
.mailbox-head-actions select:focus,
.scope:focus-within {
  border-color: var(--accent-line-hot);
  box-shadow: 0 0 0 3px var(--accent-wash);
}

/* The .scope wrapper is the field; its input is borderless inside it, so the
   soft cue belongs to the wrapper and would otherwise be drawn twice. */
.scope-input:focus { box-shadow: none; }

.seg {
  display: flex;
  border: 1px solid var(--edge-control);
  gap: var(--sp-h);
  padding: var(--sp-h);
  border-radius: var(--radius-sm);
  background: var(--field-bg);
}

/* --ink-dim, for the reason given on .pane-acts button. S, M, L, CONTROL and
   BOARD measured 5.62:1 while SOUND and NEW SESSION, 30px to their right in the
   same bar, measured 8.42. .is-on already carries the difference on a filled
   teal plate, which is a channel of its own and does not need the off state to
   be hard to read. */
.seg button {
  flex: 0 0 auto;
  min-width: 26px;
  border: 0;
  background: transparent;
  color: var(--ink-dim);
  font-size: var(--fs-sm);
  text-transform: uppercase;
  /* Without this a word breaks inside its own segment and the button reports
     content wider than the box it was given. */
  white-space: nowrap;
  cursor: pointer;
  height: calc(var(--ctl-h-lg) - 6px);
  display: inline-flex;
  align-items: center;
  justify-content: center;
  padding: 0 var(--sp-2);
  border-radius: var(--radius-sm);
  font-weight: 600;
  letter-spacing: var(--track-label);
}

.seg button + button {
  border-left: 0;
}

.seg button:hover {
  background: var(--wash-2);
  color: var(--ink);
}

.seg button.is-on {
  background: var(--teal);
  color: var(--on-accent);
  box-shadow: var(--shadow-1);
}

.seg-wide button { flex: 1; padding: 0 var(--sp-2h); }

/* Profiles wrap: six of them do not fit on one row, and a config can add more.
   These buttons carry whole profile names ("Claude Code · Kimi K2.7 Code"), so
   they need their text to wrap INSIDE the cell, which is the opposite of what a
   compact segmented control wants.

   And in the new-session dialog this is not a segmented toggle at all, it is a
   radio group in a form column. The 3px of wrapper padding a toggle needs put
   its buttons 4px inside the left and right edge of every field in the same
   dialog, so the form had two left margins; and because two of the six labels
   wrap, its rows measured 23, 36 and 36px, which is a ragged plate block as well
   as an offset one.

   So: a grid rather than a wrapping flex row, and it is the ROW HEIGHTS that
   make the difference. In a wrapping flex row nothing levels one line against
   the next, so a wrapped label made one row 30px and the one under it 40.
   `grid-auto-rows: 1fr` sizes every row to the tallest cell in the block, so a
   long name grows the whole plate rather than one row of it. The wrapper gives
   up its own box here and each cell becomes a control in its own right. */
.seg-wrap {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(108px, 1fr));
  grid-auto-rows: 1fr;
  height: auto;
  padding: 0;
  border: 0;
  background: transparent;
  gap: var(--sp-1);
}

.seg-wrap button {
  min-width: 0;
  height: auto;
  min-height: var(--ctl-h-lg);
  padding: var(--sp-1h) var(--sp-2h);
  border: 1px solid var(--edge-control);
  white-space: normal;
  overflow-wrap: anywhere;
}

/* A profile whose credentials are missing. Still selectable, because seeing why
   it cannot run is more useful than a chip that silently does nothing, and the
   reason appears under the row the moment it is chosen. */
.seg button[data-unhealthy] { position: relative; color: var(--amber); }

/* In the tile's own corner rather than in the label's text flow. As a text node
   it wrapped independently of the words it belonged to: on a 116x53 profile tile
   with `overflow-wrap: anywhere`, CODEX broke as "CODEX · API / KEY !" and the
   Grok tile put the marker beside the orphaned "N" of "SUBSCRIPTIO / N". Seven
   tiles in a clean four-across grid, and the only thing breaking the block was a
   one-character suffix on two of them. */
.seg button[data-unhealthy]::after {
  content: '!';
  position: absolute;
  top: var(--sp-1);
  right: var(--sp-1h);
  line-height: 1;
  font-weight: 700;
}

/* Choosing an unhealthy profile used to repaint it in the healthy teal plate,
   because `.seg button.is-on` and `.seg button[data-unhealthy]` are both
   (0,2,0) and the selected rule is written later. The warning therefore
   disappeared at the exact moment it mattered: the profile that cannot start is
   the one you have just selected. Written after .is-on for the same reason. */
.seg button[data-unhealthy].is-on {
  border-color: var(--amber);
  background: var(--amber);
  color: var(--on-warn);
}

/* ---- stage ---------------------------------------------------------------- */

.stage {
  display: grid;
  grid-template-columns: var(--coord-w) minmax(0, 1fr) var(--rail-w);  min-height: 0;
  gap: var(--sp-2h);
  background: transparent;
}

body[data-rail='closed'] .stage { grid-template-columns: var(--coord-w) minmax(0, 1fr); }
body[data-rail='closed'] .rail { display: none; }

/* ---- the one panel ---------------------------------------------------------

   Ten containers, and before this rule they drew themselves seven different
   ways on a single screen: .col and .kcol bordered at --edge with a drop, .pane
   the same but turning the CARD corner instead of the panel one, the three
   chrome bars each restating the box under a different plate, .rail-block with
   no boundary at all, .seg turning a card corner on what is a control group,
   and .topbar painted in --wash-2 while the two bars either side of it used the
   bar plate.

   The boundary and the corner are the same decision for all of them and are
   stated once, here. What a container still chooses for itself is its PLATE,
   because that genuinely differs: --surface-panel for a panel, --surface-bar
   for a chrome band, --surface-float for something over the top of them.

   No drop shadow. The hairline is the boundary; a pool of black underneath as
   well is the second of two answers to one question, and eleven of them on one
   screen is what made the room read as a stack of loose objects. Only .dlg
   keeps a real drop, because a dialog is the only thing here that floats.   */
.col,
.pane,
.kcol,
.ticket,
.rail-block,
.dock-panel,
.dlg,
.topbar,
.commandbar,
.deck-bar,
.empty {
  border: 1px solid var(--edge);
  border-radius: var(--radius-lg);
}

.col {
  min-width: 0;
  min-height: 0;
  display: flex;
  flex-direction: column;
  overflow: hidden;
  background: var(--surface-panel);
  backdrop-filter: blur(14px);
  -webkit-backdrop-filter: blur(14px);
}

.col-rule {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: var(--sp-1) var(--sp-2);
  /* A minimum, not a height. This rule carries the coordinator's workers and
     polling controls, whose inputs are 22px tall inside what used to be a 20px
     box, so both were clipped at every width and the row overflowed its own
     column by 14px whatever the screen was doing. The minimum is --bar-h now,
     which is what .rail-head and .kcol-head are: this was 31px against their 34,
     and it inset its heading 12px against their 9, so COORDINATOR and
     PERFORMANCE, two captions at the same level on the same screen, started on
     neither the same line nor the same left edge. */
  border-bottom: 1px solid var(--rule);
  min-height: var(--bar-h);
  padding: 0 var(--pad-panel);
  background: var(--wash-1);
  color: var(--teal);
}

/* The workers column has no caption bar of its own, so its panes started 31px
   above the coordinator's and the top edge of the stage was stepped: the first
   thing the eye lands on when the control screen opens. The bar is drawn empty
   rather than captioned, because the word would have to come from CSS content,
   which a screen reader would then announce as text nobody wrote. */
.col-workers::before {
  content: '';
  display: block;
  flex: 0 0 auto;
  min-height: var(--bar-h);
  border-bottom: 1px solid var(--rule);
  background: var(--wash-1);
}

/* A UA h2 brings 0.83em of margin top and bottom and a 1.5em font size, both of
   which would push this 20px rule open. The caption is the same box it was. */
.col-rule > h2 { margin: 0; }

.col-rule::after {
  content: '';
  flex: 1;
  height: 1px;
  background: linear-gradient(90deg, var(--accent-line), color-mix(in srgb, var(--rose) 14%, transparent), transparent);
}

.coordinator-slot {
  flex: 1;
  display: flex;
  /* A column, because the slot holds one pane PER PROJECT now. Side by side in
     a 380px rail each would be 180px wide and neither would be readable. */
  flex-direction: column;
  gap: var(--sp-2);
  padding: var(--sp-2);
  /* An empty-state card sized for a comfortable window spills out of a 380px
     one. Let it shrink and scroll rather than paint over what is below it. */
  min-height: 0;
  overflow: auto;
}

.coordinator-slot .pane {
  flex: 1 1 0;
  min-height: 160px;
}

.col-workers { overflow: hidden; }

/* Full screen has no second column to line up with, and the empty bar would be
   34px taken off the pane that was expanded to get more of them. */
.stage[data-expanded] .col-workers::before { display: none; }

/* An empty grid is still a flex item, so it kept the bay from filling the
   column it was the only thing in. */
#worker-grid:empty { display: none; }

.worker-grid {
  flex: 1;
  min-height: 0;
  overflow-y: auto;
  display: grid;
  /* min() rather than a bare 330px: on a column narrower than one pane the
     track would otherwise refuse to shrink and push the grid off the screen. */
  grid-template-columns: repeat(auto-fit, minmax(min(330px, 100%), 1fr));
  grid-auto-rows: minmax(186px, 1fr);
  gap: var(--sp-2);
  padding: var(--sp-2);
  background: transparent;
}

/* Full screen. Rather than reparenting the pane (which would tear down and
   rebuild its terminal), the columns that do not hold it are simply hidden, so
   xterm never loses its buffer or its scroll position. */
.stage[data-expanded] .rail { display: none; }

.stage[data-expanded-role='worker'] .col-coordinator { display: none; }

.stage[data-expanded-role='coordinator'] .col-workers { display: none; }

.stage[data-expanded] { grid-template-columns: minmax(0, 1fr); }

.stage[data-expanded] .worker-grid { grid-template-columns: minmax(0, 1fr); grid-auto-rows: minmax(0, 1fr); }

.stage[data-expanded] .pane:not(.is-expanded),
.stage[data-expanded] .bay { display: none; }

/* ---- pane ----------------------------------------------------------------- */

.pane {
  position: relative;
  display: grid;
  grid-template-rows: auto minmax(0, 1fr) auto;
  min-width: 0;
  min-height: 0;
  /* A dense fleet across a laptop screen leaves narrow panes, which is not enough for a
     full header. The queries at the end of this block drop chrome in order of
     what a fleet operator can most afford to lose. */
  container-type: inline-size;
  overflow: hidden;
  background: var(--surface-panel);
  transition: border-color 160ms ease, box-shadow 160ms ease, transform 160ms ease;
}

.pane-edge {
  position: absolute;
  left: 0;
  top: 0;
  bottom: 0;
  width: 2px;
  background: var(--state-idle);
  z-index: 3;
}

/* "Where do my keystrokes go" must stay answerable even on a pane that is
   glowing amber for attention, so the focused pane carries a border and a ring
   of its own colour; the block that draws it is under the visual system below.
   The `outline: none` that used to sit on .pane deleted the shared keyboard
   ring from every pane in the fleet and replaced it with nothing. */
.pane.is-focused .pane-head {
  background: linear-gradient(90deg, var(--accent-tint), transparent 68%), var(--surface-inset);
}

.pane.is-focused .pane-id::before { content: '\203A'; margin-right: var(--sp-1); color: currentColor; }

.pane-head {
  display: flex;
  align-items: center;
  gap: var(--sp-1h);
  border-bottom: 1px solid var(--rule);
  overflow: hidden;
  height: var(--bar-h);
  /* One inset, both ends, and the same one the footer takes. The header insetted
     its right edge 6px and the footer 10, so on the coordinator pane the
     header's content edge landed at 474 and the footer's at 470: 4px of rag on
     the top and bottom of one box. The 6px was bought to fit KILL, which is the
     clipping bug below rather than a reason for two insets. */
  padding: 0 var(--pad-panel) 0 var(--sp-3);
  border-bottom-color: var(--rule);
  background: var(--surface-inset);
}

.pane-head > * { flex: 0 0 auto; }

.pane-id {
  font-family: var(--mono);
  font-size: var(--fs-md);
  font-weight: 700;
  color: var(--ink);
}

.pane-kind { color: var(--ink-faint); }

/* The identity run is the shrinking group, and the path gives up first.
   .pane-head is `overflow: hidden` and every child but this one was
   `flex: 0 0 auto`, so a header wider than its pane did not shrink, it
   OVERFLOWED, and what sits at the overflowing end is .pane-acts. Measured with
   two sessions in a 1440x900 frame: header scrollWidth 442 against clientWidth
   350, FULL laid out at 735-781 and KILL at 783-826 against a right edge of
   740, and document.elementFromPoint at the centre of each returned the header
   rather than the button. RAW COPY measured 0x0. Only COPY was reachable, and
   the one that was not is the destructive one.

   A shrink factor rather than a container query, because the trigger is a
   shared worktree plus a known safety mode, both of which are the normal case
   at any width. 8 against the 1 on the two chips beside it: the path is the
   thing this room can most afford to ellipsise, and the footer says it again. */
.pane-cwd {
  flex: 0 8 auto;
  min-width: 0;
  font-family: var(--mono);
  font-size: var(--fs-sm);
  color: var(--ink-dim);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

.pane-warn {
  padding: var(--sp-h) var(--sp-1);
  color: var(--amber);
  border: 1px solid color-mix(in srgb, var(--amber) 42%, transparent);
  white-space: nowrap;
}

/* Both chips give ground before the actions do, for the reason .pane-cwd gives
   above: they are readouts and .pane-acts holds KILL. */
.pane-warn,
.pane-safety {
  flex: 0 1 auto;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
}

.pane-safety {
  padding: var(--sp-h) var(--sp-1);
  border: 1px solid currentColor;
  color: var(--ink-faint);
  font-size: var(--fs-micro);
  font-weight: 700;
  letter-spacing: var(--track-label);
  text-transform: uppercase;
  white-space: nowrap;
}

.pane-safety[data-mode='autonomous'] { color: var(--red); }

.pane-safety[data-mode='governed'] { color: var(--green); }
/* Neither of these is "an agent has stopped to ask you something", which is
   what amber means everywhere else in this room, and one screenshot of an idle
   fleet had six amber objects on it with nothing awaiting input. Not knowing is
   not an alert: it is the absence of an answer, so it is said in the inert ink.
   Unsupported is a real condition a person has to decide about, and --rose is
   the colour this app already uses for exactly that (REVIEW). */
.pane-safety[data-mode='unknown'] { color: var(--ink-dim); }

.pane-safety[data-mode='unsupported'] { color: var(--rose); }

.pane-gap { flex: 1 1 4px; min-width: 0; }

/* The kind and the path both survive in the footer, so a cramped pane gives
   them up rather than clipping the state pill, which is the one thing a glance
   across a fleet is actually reading. */
@container (max-width: 420px) {
  .pane-kind { display: none; }
}

@container (max-width: 330px) {
  .pane-cwd { display: none; }
}

@container (max-width: 470px) {
  .pane-acts button[data-act='copy-verbatim'] { display: none; }
}

/* The last rung, and the two chips are the last things shed because both keep a
   title attribute when they go, which is the rule the block above states: each
   thing shed keeps a dot, a colour or a tooltip. Below this width the shrink
   they were given has taken them to 18px, which is their own padding and no
   letters at all, and the header still overflowed by 37px with KILL at the
   overflowing end. A chip with nothing in it is not worth a control. */
/* Qualified by .pane-head, which is what makes it stick: the shared pill group
   at the end of this file gives both chips `display: inline-flex` at (0,1,0) and
   is written later, and a container query adds no specificity of its own. */
@container (max-width: 380px) {
  .pane-head .pane-warn,
  .pane-head .pane-safety { display: none; }
}

.pane-acts {
  display: flex;
  gap: var(--sp-h);
}

/* --ink-dim, not --ink-faint. .mini was moved off the faint ink with the note
   that "a live control is not secondary text", and four control families were
   left behind on it: this one, .seg button, .chip and .bay. Measured on the
   live fleet, COPY, FULL and KILL read 5.25 to 5.63:1 while SOUND, NEW SESSION
   and RESET, the same job in the same rows, read 8.42. A 1.5x gap with nothing
   in the file explaining it, on a row that holds the destructive control. */
.pane-acts button {
  border: 1px solid var(--edge-control);
  background: transparent;
  color: var(--ink-dim);
  font-size: var(--fs-xs);
  letter-spacing: var(--track-label);
  text-transform: uppercase;
  cursor: pointer;
  padding: 0 var(--sp-2);
}

.pane-acts button:hover {
  color: var(--ink);
  border-color: var(--accent-line);
  background: var(--accent-wash);
}

.pane-acts button[data-act='kill']:hover { color: var(--red); border-color: var(--red); }

.pane-acts button[data-armed='1'] { color: var(--red); border-color: var(--red); background: color-mix(in srgb, var(--red) 8%, transparent); }

.pane-term {
  position: relative;
  min-width: 0;
  min-height: 0;
  overflow: hidden;
  padding: var(--sp-1h) var(--sp-h) var(--sp-h) var(--sp-2);
  background: var(--rack-2);
}

.pane-foot {
  display: flex;
  align-items: center;
  gap: var(--sp-2h);
  border-top: 1px solid var(--rule);
  font-family: var(--mono);
  font-size: var(--fs-xs);
  color: var(--ink-faint);
  overflow: hidden;
  height: var(--bar-h-sm);
  /* The same inset the header takes, said in the same token. */
  padding: 0 var(--pad-panel) 0 var(--sp-3);
  border-top-color: var(--rule);
  background: var(--surface-inset);
}

/* The task's title is the only human-readable thing in the footer and it was the
   only thing paying for the footer being full. `flex: 1 1 auto` here against a
   bare `white-space: nowrap` on the pid and token stats meant the title absorbed
   every pixel of shrink and the stats gave up none: measured across six panes at
   3432px, titles of 16.3, 22.3 and 34.3px beside stats of 288, 294 and 306. At
   362px of pane the pane's name was one glyph and an ellipsis.

   A 40% floor on the name and an ellipsis on the stats. The pid is the
   disposable half: it is a number you copy, not a thing you read across a
   fleet. */
.pane-title {
  flex: 1 1 40%;
  min-width: 0;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  color: var(--ink-dim);
}

.pane-stats {
  flex: 0 1 auto;
  min-width: 0;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

/* ---- state ---------------------------------------------------------------- */

.pill {
  padding: var(--sp-h) var(--sp-1h);
  border: 1px solid currentColor;
  color: var(--state-idle);
  white-space: nowrap;
  line-height: 1.3;
}

.pill[data-state='starting'] { color: var(--state-starting); }

.pill[data-state='idle'] { color: var(--state-idle); }

.pill[data-state='working'] { color: var(--state-working); }

.pill[data-state='exited'] { color: var(--state-exited); }

.pill[data-state='error'] { color: var(--state-error); }

.pane[data-state='starting'] { color: var(--state-starting); }

.pane[data-state='idle'] { color: var(--state-idle); }

.pane[data-state='working'] { color: var(--state-working); }

.pane[data-state='exited'] { color: var(--state-exited); }

.pane[data-state='error'] { color: var(--state-error); }

.pane[data-state='awaiting-input'] {
  color: var(--state-awaiting-input);
  box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--amber) 42%, transparent);
}

.pane[data-state] .pane-edge { background: currentColor; }
/* A dead pane recedes on its own chrome, not on a multiplier over the whole
   box. 0.72 took .pane-kind and .pane-stats, both --ink-faint, to 3.40:1 at
   10px; the header and footer read at full strength now and the state is
   carried by the pill, the edge bar and the terminal's own grey. */
.pane[data-state='exited'] .pane-head,
.pane[data-state='exited'] .pane-foot { background: var(--rack-2); }

.pane[data-state='exited'] .pane-term { filter: grayscale(0.55); }

/* working: a slow travelling tick down the edge bar, so a busy pane is legible
   without reading a word of its output. */
.pane[data-state='working'] .pane-edge {
  background:
    linear-gradient(180deg, transparent, var(--state-working) 45%, transparent 55%) 0 0 / 100% 220% no-repeat,
    color-mix(in srgb, var(--state-working) 20%, transparent);
  animation: edge-run 2.4s linear infinite;
}

@keyframes edge-run {
  from { background-position: 0 -120%, 0 0; }
  to { background-position: 0 120%, 0 0; }
}

/* awaiting-input: the loudest thing in the room, and the only thing that
   pulses. Amber is used nowhere else. */
.pill[data-state='awaiting-input'] {
  color: var(--on-warn);
  border-color: var(--amber);
  font-weight: 700;
  /* Plain amber. Two rules still painted a 4px-period hatch BEHIND their own
     10px type, which is the exact defect the --hatch-edge note says was taken
     off six of its eight uses; these two escaped because they inline their own
     gradient instead of reading the token. Composited, the ink measured 8.08:1
     on the plain plate and 5.27 on the darkened stripe, a 1.53x swing repeating
     every four pixels inside a single letter. Neither chip needs it: this is the
     only filled pill in the room and the owner chip carries a 700 weight and an
     amber border of its own. */
  background: var(--amber);
}

.pane[data-state='awaiting-input'] .pane-head {
  background: linear-gradient(90deg, color-mix(in srgb, var(--amber) 14%, transparent), transparent 60%), var(--rack);
}

.pane[data-state='awaiting-input'] .pane-edge {
  width: 3px;
  animation: edge-pulse 1.3s ease-in-out infinite;
}

@keyframes edge-pulse {
  0%, 100% { opacity: 1; box-shadow: 0 0 14px 0 color-mix(in srgb, var(--amber) 66%, transparent); }
  50% { opacity: 0.42; box-shadow: 0 0 4px 0 color-mix(in srgb, var(--amber) 20%, transparent); }
}

/* ---- coordinator pane ----------------------------------------------------- */

.pane.is-coordinator {
  flex: 1;
  min-width: 0;
  border: 1px solid var(--accent-line);
  border-color: var(--accent-line);
  background: var(--rack-2);
  box-shadow: var(--shadow-2);
}

.pane.is-coordinator .pane-head {
  background: linear-gradient(90deg, var(--accent-tint), color-mix(in srgb, var(--rose) 8%, transparent) 58%, transparent), var(--surface-inset);
}

.pane.is-coordinator .pane-id { color: var(--teal); }

/* Corner ticks, drafting-table style. Only the coordinator gets them: on all
   eleven panes it would be noise, on one it says "this is the one you talk to". */
.pane.is-coordinator::before,
.pane.is-coordinator::after {
  content: '';
  position: absolute;
  width: 9px;
  height: 9px;
  border: 1px solid var(--teal);
  opacity: 0.65;
  pointer-events: none;
  z-index: 4;
  display: none;
}

.pane.is-coordinator::before { top: -1px; right: -1px; border-width: 1px 1px 0 0; }

.pane.is-coordinator::after { bottom: -1px; left: -1px; border-width: 0 0 1px 1px; }

/* ---- bay and empty states ------------------------------------------------- */

/* A slim bar under the grid rather than a tile inside it: a tile would take a
   whole grid row, and a row is 30% of the height a live pane could have had. */
.bay {
  display: flex;
  flex: 0 0 auto;
  align-items: center;
  justify-content: center;
  gap: var(--sp-2h);
  min-height: 40px;
  border-top: 1px dashed var(--edge);
  /* A control, so --ink-dim: see .pane-acts button. This is the + NEW SESSION
     tile and it measured 5.47:1 beside the 8.42 of the buttons in the bar
     above it. */
  color: var(--ink-dim);
  cursor: pointer;
  transition: color 140ms ease, border-color 140ms ease, background 140ms ease;
  margin: 0 var(--sp-2) var(--sp-2);
  border: 1px dashed var(--edge);
  border-radius: var(--radius);
  background: var(--wash-1);
}

.bay:hover {
  color: var(--teal);
  border-color: var(--accent-line);
  background: var(--accent-wash);
}
/* `min-height: 0` so the empty new-session panel can be squeezed instead of
   pushing the command bar off a short window. It has nothing in it to lose. */
.bay.is-empty { flex: 1 1 auto; flex-direction: column; min-height: 0; overflow: hidden; border: 1px dashed var(--edge); margin: var(--sp-3); }

.bay-plus { font-family: var(--mono); font-size: var(--fs-xl); line-height: 1; }

.bay.is-empty .bay-plus { font-size: var(--fs-face); }
/* "3 of 6 slots free" had no reason to be dimmed at all: 0.75 over --ink-faint
   measured 3.63:1 at 10px. It is the caption of the control it sits in. */
.bay-free { font-family: var(--mono); font-size: var(--fs-xs); color: inherit; }

/* At zero free the bay stops being a control and becomes the only thing on the
   screen that states slot capacity: it used to be removed outright the moment
   the last slot filled, so the fact and its button vanished together and a full
   fleet looked like a fleet with no bay at all. Solid rather than dashed,
   because a dashed edge in this room means "space for something" and there is
   none. The shared [aria-disabled='true'] block already takes the pointer away
   and quiets the ink; this is the part that says which kind of quiet it is. */
.bay[data-full='1'] {
  border-style: solid;
  border-color: var(--rule);
  background: var(--wash-1);
  cursor: default;
}

.empty {
  margin: auto;
  max-width: 320px;
  padding: var(--sp-4);
  text-align: center;
  /* One border declaration. The shorthand named --edge and the longhand under it
     named --accent-glow, so the rule stated two boundary colours and drew the
     second. --accent-line is the accented border; --accent-glow is the ring. */
  border: 1px dashed var(--accent-line);
  color: var(--ink-faint);
  background: var(--accent-wash);
}

/* An empty state that a filter caused has to carry the way out of it, so the
   panel has to be able to hold a control. */
.empty .btn { margin-top: var(--sp-3); }

/* A failure is not an invitation. The dashed teal-and-rose card above says
   "there is room here for something": exactly right for a project with no
   tasks in it yet, and exactly wrong for "GET /api/board said: ECONNREFUSED",
   which was drawn identically until now. The language is .toast[data-kind=
   'error'] and .rail-empty.is-error's, so the same news looks the same
   wherever the room prints it: a red rule down the left, no invitation in the
   fill, and the words at full --ink because a failure must never be the
   dimmest thing in its panel. Left-aligned, because a stack trace centred is a
   stack trace nobody can scan. */
.empty.is-error {
  text-align: left;
  border-style: solid;
  border-color: color-mix(in srgb, var(--red) 42%, transparent);
  border-left-width: 2px;
  border-left-color: var(--red);
  background: color-mix(in srgb, var(--red) 8%, transparent);
  color: var(--ink);
}

.empty.is-error .empty-title { color: var(--red); }

.empty-title {
  margin: 0 0 var(--sp-1h);
  font-size: var(--fs-sm);
  letter-spacing: var(--track-brand);
  text-transform: uppercase;
  font-family: var(--mono);
  color: var(--ink);
}

.empty-body { margin: 0; font-size: var(--fs-md); line-height: 1.5; }

.empty-coordinator { align-self: center; }

/* ---- rail ----------------------------------------------------------------- */

/* Four blocks now, and their minimum heights already add up to more than a
   laptop's rail. Clipping the bottom one silently (which is what overflow:hidden
   did) means the project overview simply is not there on a 900px screen, so the
   rail scrolls instead. Sideways it still must not: the blocks manage their own
   overflow. */
/* .dock stacks the visually identical .dock-panel at 10px and this stacked
   .rail-block at 8, so the app's two vertical panel stacks breathed differently
   while sharing one appearance rule. And the scrollbar took its gutter off one
   side only, so every block in the rail stopped 10px short of the shell's right
   edge while the bars above and below it stopped 17 and 15. */
.rail {
  gap: var(--sp-2);
  scrollbar-gutter: stable both-edges;
  overflow-x: hidden;
  overflow-y: auto;
  border: 0;
  border-radius: 0;
  background: transparent;
  box-shadow: none;
  backdrop-filter: none;
}

.rail-block {
  display: flex;
  flex-direction: column;
  min-height: 0;
  background: var(--rack);
}

/* Every scroller in the rail and the dock ends mid-glyph. Measured at 1440x900,
   #rail overflowed by 479px and #perf-body by 261 with a row height of 41.5, so
   the fold landed 71% of the way through the fourth row: a 34px display face cut
   in half reads as a rendering fault rather than as "there is more". The mask
   fades the last 12px, which is the difference between a cut row and a continued
   one.

   Unconditional, because CSS cannot ask whether a box is scrolling. On a panel
   with room to spare the fade falls on empty ground and cannot be seen, so the
   only cost is a control that happens to sit in the last 12px of a full panel.

   A mask clips everything the box paints, INCLUDING an outline drawn outside the
   border box, so every one of these bodies has to keep at least 4px of padding:
   the shared ring is 2px at 2px of offset and all three carry 8 or 10. Do not
   take the padding off one of them without taking the mask off with it. */
@supports (mask-image: linear-gradient(#000, transparent)) {
  .rail-body,
  .dock-body,
  .kcol-body {
    mask-image: linear-gradient(to bottom, #000 calc(100% - 12px), transparent);
  }
}

/* The performance panel is first and never collapsed: it is the one thing the
   operator asked to see the moment the page loads. The queue below it is now
   six one-line lanes rather than eleven slot rows, so it needs far less.

   The minimum is the load-bearing number. Four blocks cannot all fit a 1000px
   window, so the rail scrolls and something ends up below the fold; 190px is
   three full metric rows plus the header, and since perf.mjs sorts worst first
   those three rows are the alerts. Whatever else gets squeezed, the thing that
   is actually wrong is on screen without scrolling. */
#rail-perf { flex: 0 1 auto; max-height: 40%; min-height: 190px; }

#rail-queue { flex: 0 0 auto; max-height: 30%; }

/* Two scanners and the refresh footer, whole, with nothing behind a
   scrollbar: a panel whose job is to say "the vulnerability database is a
   fortnight old" must not put that line below the fold. 187 is the panel's
   own content height, measured in Chrome at 1440x900 with the rail overfull
   (which pins every block to its minimum) against the contract sample with
   both scanners current: 2px of border, 34 of head, 16 of body inset, rows of
   45 and 44, and a 43px footer. The cap is the secrets rail's, because the
   two sit together and a taller one would push the timer off a 900px laptop.

   A red word grows it, the way a pending approval grows the secrets rail: an
   outdated or missing binary carries its install line, and a fix behind a
   scrollbar is a fix nobody runs. 246 is the same measurement against the
   contract sample as written: Trivy outdated, so its row keeps its signatures
   line and adds the four-line command, which is the longer of the two. One
   scanner missing measures 232, because a binary that is not there has no
   signatures line to draw; both missing, the fresh-box case, measures 266 and
   takes the rest from the cap.

   Every number above was measured against the Unix line. Since 2 September
   2026 the daemon resolves `install` for the platform it runs on, and Windows
   has no one-line curl pipe to give: measured in Chrome against this
   stylesheet at the rail's own 310px, OpenGrep's download wraps to 8 lines
   (112px) where the curl pipe took 3 (42px), and Trivy through choco takes 2
   (28px) where its pipe took 4 (56px). The fresh Windows box, both missing,
   comes to about 308, which is inside the 40% cap on a 900px laptop. The
   numbers below are floors for a rail under pressure rather than the height of
   the block, so they stand. */
#rail-security { flex: 0 1 auto; min-height: 187px; max-height: 30%; }

#rail-security[data-overall='outdated'],
#rail-security[data-overall='missing'] { min-height: 246px; max-height: 40%; }
/* Two faces now: the project clock the daemon owns, and the recording
   countdown under it. 268 buys the elapsed face its own block without taking a
   pixel from the project overview, whose 282 is the number that decides whether
   the rail scrolls on a 900px laptop. */
/* The secrets rail had no rule of its own, so it took whatever the overview's
   `flex: 1 0 282px` left it. Measured on a 940px window: 61px, against 190 to
   282 for every neighbour, clipping 83px of 108 and putting two grants behind a
   scrollbar in a 25px slot.

   Retuned on 2 September 2026, when the panel stopped listing grants and started
   listing ITEMS with a footer control under them. It is still the same 232, and
   that is arithmetic rather than a coincidence: an item row is the padding
   (6 + 6), a title line (--fs-sm at the 1.4 .broker-title now declares, 15.4),
   the row gap (2), a meta line (--fs-xs at 1.4, 14) and its 1px divider, which
   is 44.4. Three of them, less the divider the last row drops, is 132.2; the
   header is 34, the block's own border 2, the body's padding 16 and the footer
   its 1px rule plus 8 + 26 + 8, which is 43. 227.2, and 232 is that rounded up
   with 4px of slack for a wrapped meta line.

   A pending approval is the exception and it grows the block, because a worker
   is BLOCKED while one sits there. That is the one thing in this panel with
   somebody waiting on the other end of it, so it must never be the thing that
   ends up below the fold, and neither may it be what pushes the item list out:
   304 is 232 plus one whole approval row (44.4 plus the acts row's 4 + 22),
   so the approval arrives with its own room rather than taking a row's. */
#rail-broker { flex: 0 1 auto; min-height: 232px; max-height: 30%; }

#rail-broker[data-asking='1'] { min-height: 304px; max-height: 40%; }

/* Where a password is typed. It sat under the grants in the rail until 2
   September 2026 and is the bottom band of #dlg-secrets now, because what it
   collects grew from two fields to a kind's worth of them. The secret fields are
   real password inputs, so the browser hides them from the screen, from autofill
   history and from a screen share, and nothing reads any of them back. */
.secret-add {
  display: flex;
  flex-direction: column;
  gap: var(--sp-2);
  border-top: 1px solid var(--rule);
  background: var(--well-bg);
  /* And it scrolls rather than being clipped. .dlg-secrets is overflow: hidden
     at min(88vh, 920px), and .secrets-list stops giving ground at its 132px
     floor, so once the list is on that floor this form's height is whatever its
     fields add up to and the dialog simply cut the rest off. Measured in Chrome
     on 2 September 2026 against the real markup: on a 620px-tall window with
     kind `env` chosen the dialog overflowed by 53px, ADD sat 40px below the
     dialog's bottom edge, and walking every ancestor of that button returned no
     user-scrollable box at all, so a mouse-only operator could fill the form in
     and never reach the control that submits it. min-height: 0 because a flex
     item's floor is its content, and without it overflow-y has nothing to do. */
  min-height: 0;
  overflow-y: auto;
}

/* The inset is the dialog's, not the rail's. `.dlg form` is (0,1,1) and would
   otherwise put --dlg-pad on all four sides of this one over the top of
   anything .secret-add said for itself, which is the same shadowing
   .dlg .mailbox-compose was written to answer. --sp-3 down the vertical,
   because this band already carries a rule and a fill to separate it. */
.dlg .secret-add { padding: var(--sp-3) var(--dlg-pad); }

/* A field is its caption over its control, the way .field is in the other
   dialogs. It used to be a 42px caption COLUMN, which worked while the two
   captions in this form were NAME and VALUE and stops working the moment one of
   them is "secret access key": at --fs-micro with --track-label, 42px is about
   six characters. .secret-add-field is the same box for the two rows whose
   control is a group of buttons rather than an input, which cannot be a
   <label> at all. */
.secret-field,
.secret-add-field {
  display: grid;
  gap: var(--sp-h);
}

/* A field caption is sized by the control it labels, and that is the whole
   rule: --fs-micro in --ink-faint beside a 22 or 26px control, --fs-xs in
   --ink-dim above a 30px one. The two ends of it are 800 lines apart (this and
   .field > span in the dialogs) and were reported as looking wrong in both
   places, so it is written down here rather than left to be inferred from two
   rules that did not know about each other. */
.secret-field > span,
.secret-add-cap {
  color: var(--ink-faint);
  font-size: var(--fs-micro);
  letter-spacing: var(--track-label);
  text-transform: uppercase;
}

/* Required and optional, in the slot .field > span em already uses for them in
   the three creation dialogs. A form that names its optional fields and says
   nothing about the ones it will be refused without is telling an operator half
   of what it knows, and here the cost of that is a spent password. */
.secret-field > span em,
.secret-add-cap em {
  font-style: normal;
  letter-spacing: 0;
  text-transform: none;
}

/* Two columns in an 880px dialog. One column put six http fields in a 640px
   ribbon of empty space; three made "secret access key" wrap its caption. */
.secret-fields {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: var(--sp-2) var(--sp-3);
  align-items: end;
}

/* The env kind's secret field is a list, so it takes the whole width. */
.secret-vars {
  grid-column: 1 / -1;
  display: grid;
  gap: var(--sp-1);
}

.secret-var {
  display: flex;
  gap: var(--sp-1h);
}

/* The NAME is public and the value is not, and the value is the longer paste,
   so the row is split a third to two thirds rather than in half. */
.secret-var-name { flex: 1 1 30%; }

.secret-var-value { flex: 1 1 70%; }

/* The form's own caption line, and the way back out of ROTATE. */
.secret-add-head {
  display: flex;
  align-items: center;
  gap: var(--sp-2);
}

/* Two modes, one form. ADD collects a kind and everything that kind needs;
   ROTATE shows nothing but the same kind's secret fields, because the alias,
   the scope and the derived grants all survive a rotation and re-asking for
   them would be inviting an operator to change one by accident.

   .secret-add-scope is named rather than given the class, because it is the one
   element in this form whose exact class attribute is pinned by
   test/web-markup.test.mjs: the printed caption IS the accessible name of the
   select beside it, which is the label-in-name defect that put it there. */
.secret-add[data-mode='rotate'] .secret-when-add,
.secret-add[data-mode='rotate'] .secret-add-scope,
.secret-add[data-mode='add'] .secret-when-rotate { display: none; }

.secret-add input,
.secret-add-foot select {
  min-width: 0;
  height: var(--ctl-h);
  padding: 0 var(--sp-1h);
  font-size: var(--fs-sm);
}

/* Monospace and wide letter-spacing on the dots, so a long paste reads as a
   length rather than as an indistinguishable smear.

   Scoped to typed characters. Applied unconditionally it also tracked the
   placeholder, and the placeholder of the day, "paste the password or token",
   at 11px mono plus 0.22em is 244px inside a 213px content box, so the empty
   field read "paste the password or to" with no ellipsis to say it had been
   cut. Every sealed field says "write-only" now, which fits either way; the
   scoping stays, because the next placeholder is not guaranteed to. */
.secret-add input[type='password']:not(:placeholder-shown) { letter-spacing: 0.22em; }

/* The form's settings row, and the reason it reads as one rather than as a
   fourth field: the fields above print their captions OVER their controls and
   this row prints its caption beside them. It used to be a 50px hanging indent
   instead, which put the select on the right edge NAME and VALUE agreed on
   while leaving the left one blank, so the form read as two labelled rows
   followed by a floating third. The word is in the markup now, because a
   caption column drawn by a caption cannot drift from the grid it imitates. */
.secret-add-foot {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--sp-1h);
}

.secret-add-scope {
  /* The caption column of this row, and only of this row. Both numbers moved
     with the form into the dialog on 2 September 2026 and this one did not:
     44px is still the width of the word plus its own 6px gap, which is what
     puts the select's left edge where the fields above it start. */
  flex: 0 0 44px;
  color: var(--ink-faint);
  font-size: var(--fs-micro);
  letter-spacing: var(--track-label);
  text-transform: uppercase;
}

/* Bounded rather than greedy. `flex: 1 1 120px` was written against a 260px
   rail block; in an 880px dialog it grew a two-option select to roughly 500px
   and pushed ADD and REVOKE against the far edge. */
.secret-add-foot select {
  flex: 0 1 200px;
  padding-right: var(--chev-pad);
}

/* The line that reports on the two fields above it, at the size of the fields
   it reports on. It was --fs-micro, the smallest step in the room, which is the
   caption size, not the message size. */
.secret-add-state {
  flex-basis: 100%;
  min-width: 0;
  color: var(--ink-faint);
  font-family: var(--mono);
  font-size: var(--fs-sm);
  line-height: 1.45;
  overflow-wrap: anywhere;
}

.secret-add-state:empty { display: none; }

/* One element carried four meanings in exactly one colour: a hint, progress, a
   success and a hard refusal, all --ink-faint, so "Secret alias must be 1-80
   letters" was painted identically to the neutral caption 42px to its left and
   the only way to tell a rejection from a label was to read it. Every dialog in
   this room already answers a refusal in red on a tinted plate; this is that
   language, in the space one rail line has. --red on the rail ground is 6.61:1
   and --green is 7.9:1, so the tone is a channel, not a decoration. */
.secret-add-state[data-tone='bad'] {
  padding: var(--sp-1) var(--sp-1h);
  border-left: 2px solid var(--red);
  border-radius: var(--radius-sm);
  /* The same literal .dlg-error carries, so the two refusal plates in this app
     are byte-identical rather than merely similar. */
  background: color-mix(in srgb, var(--red) 8%, transparent);
  color: var(--red);
}

.secret-add-state[data-tone='good'] { color: var(--green); }

.secret-add-state[data-tone='busy'] { color: var(--ink-dim); }

/* 172 is this block's flex basis and also its minimum, remeasured on 2
   September 2026 against what the panel holds now that the recording countdown
   has left it. Adding the declared boxes up: the caption line (11), the face
   with its margins (8 + 38.76 + 6, from --fs-face-lg at 1.14), the meta line
   (12) and the button row (10 + 26) come to 112, plus 16px of body padding, 34
   of header and 2 of border, which is 164. The 8px on top is what a meta line
   needs if it ever runs to a second row.

   It was 268 for a panel holding two faces, a progress track and a divider. A
   block that keeps 96px it has nothing left to put in takes that space off the
   four panels underneath it, which is the same defect as being overfull with
   the sign reversed. */
#rail-timer { flex: 0 1 172px; min-height: 172px; }

#rail-overview { flex: 1 0 282px; min-height: 282px; max-height: none; }

#rail-overview .rail-body { overflow-y: hidden; }

/* A heading and the rows under it must share a left edge. This bar was 11px in
   and the body under it 8px, in five panel types on both screens, which is the
   3px a reader sees before they can name it. */
.rail-head {
  display: flex;
  align-items: center;
  gap: var(--sp-2);
  height: var(--bar-h);
  padding: 0 var(--pad-panel);
  border-bottom: 1px solid var(--rule);
  border-bottom-color: var(--rule);
  background: var(--wash-1);
}

.rail-head h2 {
  margin: 0;
  font-weight: 600;
  color: var(--teal);
}

.rail-note {
  flex: 1;
  font-family: var(--mono);
  font-size: var(--fs-xs);
  color: var(--ink-faint);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

/* scrollbar-gutter, the way .rail already does for itself. A block that happens
   to scroll took its 15px of scrollbar out of the content box, so PERFORMANCE
   and the SECRETS grant list stopped 10px short of the QUEUE, the secrets form
   and PROJECT OVERVIEW: measured at 1600 wide, a right edge of 1559 against
   1569, and both edges inside the SECRETS panel at once. */
.rail-body {
  flex: 1;
  min-height: 0;
  overflow-y: auto;
  scrollbar-gutter: stable;
  padding: var(--sp-2) var(--pad-panel);
}

/* ---- a rail the operator arranges ------------------------------------------
   Which of these panels matters, and in what order, changes with what the fleet
   is doing. A merge afternoon wants the queue at the top; a recording wants the
   timer there; a day with the secrets rail quiet wants it folded down to its
   heading. None of those is an answer a stylesheet can take once, so every
   block carries a grip and a fold and the browser remembers both.

   Both are drawn at rest rather than on hover. The rule this room already keeps
   is that a control says what it is without a pointer on it, and a reorder grip
   that appears only under the mouse is a control a keyboard operator and a
   touch operator never find at all. */
.rail-acts {
  display: flex;
  flex: 0 0 auto;
  align-items: center;
  gap: var(--sp-h);
  /* auto, so the pair sits at the right edge whether or not the header has a
     .rail-note between it and the h2. #rail-timer has none, and without this
     its grip sat glued to the word TIMER while the other four were 200px away. */
  margin-left: auto;
}

/* The box .project-add already draws: a 22px square on the control boundary.
   Deliberately not .mini, which is a text pill and would wrap uppercase
   tracking and 8px of horizontal padding around a glyph. */
.rail-grip,
.rail-fold {
  display: flex;
  flex: 0 0 auto;
  align-items: center;
  justify-content: center;
  width: var(--ctl-h-sm);
  height: var(--ctl-h-sm);
  padding: 0;
  border: 1px solid var(--edge-control);
  border-radius: var(--radius-sm);
  background: transparent;
  color: var(--ink-dim);
}

.rail-grip:not(:disabled):hover,
.rail-fold:not(:disabled):hover { color: var(--ink); border-color: var(--edge-hot); }

.rail-fold { cursor: pointer; }

/* grab rather than pointer, because this one is carried rather than pressed and
   the cursor is the only thing that says so before somebody tries it. */
.rail-grip { cursor: grab; }

.rail-grip:active { cursor: grabbing; }

/* Six dots, drawn rather than typed. Geist ships no braille block and no dotted
   handle, so a character here would fall back to whatever the machine had and
   the same control would be a different mark on every box. A gradient is
   currentColor, so it inherits the hover above for nothing. */
.rail-grip::before {
  content: '';
  width: 8px;
  height: 12px;
  background-image: radial-gradient(circle at center, currentColor 0 1px, transparent 1px);
  background-size: 4px 4px;
}

/* The caret .lane-caret already draws, turned the same way, so the two things
   in this column that fold agree about which direction shut is. */
.rail-caret {
  display: block;
  font-size: var(--fs-xs);
  line-height: 1;
  transition: transform 140ms ease;
}

.rail-block[data-folded='1'] .rail-caret { transform: rotate(-90deg); }

/* Everything below the header, not the body alone: the secrets block has two
   children under its head, the grant list and the add-a-secret form, and a rule
   about .rail-body would have folded the list away and left the form. */
.rail-block[data-folded='1'] > :not(.rail-head) { display: none; }

/* A rule under nothing reads as a stray line rather than as the bottom of a
   heading. The colour goes and the 1px stays, because taking the width off
   moves the header and a folding panel that also twitches looks broken. */
.rail-block[data-folded='1'] .rail-head { border-bottom-color: transparent; }

/* And the block stops asking for the room its contents wanted. Written against
   the ids because each of those five carries its own basis and minimum at
   (1,0,0), which no class can outrank: #rail-overview alone would keep growing
   to fill the whole rail with one heading. A sixth panel joins this list, or it
   folds to a 282px header. */
#rail-perf[data-folded='1'],
#rail-queue[data-folded='1'],
#rail-security[data-folded='1'],
#rail-security[data-overall='outdated'][data-folded='1'],
#rail-security[data-overall='missing'][data-folded='1'],
#rail-broker[data-folded='1'],
#rail-broker[data-asking='1'][data-folded='1'],
#rail-timer[data-folded='1'],
#rail-overview[data-folded='1'] {
  flex: 0 0 auto;
  min-height: 0;
  max-height: none;
}

/* The panel being carried. It marks the block itself rather than a gap it might
   land in, because the reorder happens live: by the time the pointer is over a
   new position the row is already in it, so there is no gap to draw. */
.rail-block[data-dragging='1'] {
  border-color: var(--accent-line);
  box-shadow: var(--shadow-3);
}

/* Only while a panel is actually being carried. A transition left on the whole
   time would animate every layout this column does for its own reasons: a lane
   opening, a grant arriving, the queue rail growing a row, the perf panel
   changing height as its worst metric changes. */
.rail[data-sorting='1'] .rail-block { transition: transform 180ms cubic-bezier(0.2, 0.8, 0.2, 1); }

/* And the one under the cursor is the exception, because it IS the cursor: a
   transition here is 180ms of the panel trailing behind the pointer, which is
   the whole complaint the pointer drag was written to answer. */
.rail[data-sorting='1'] .rail-block[data-dragging='1'] {
  position: relative;
  z-index: var(--z-float);
  transition: none;
  cursor: grabbing;
}

/* Nothing in the carried panel's CONTENT may take the press: a button under the
   cursor lights up and a lane row arms itself while the panel is being moved
   over them.

   The header is deliberately not in this rule. It was, as a blanket rule over
   every descendant, and the grip that started the drag is one of those
   descendants: the button holding the gesture was given pointer-events: none by
   the very state it had just entered. */
.rail[data-sorting='1'] .rail-block[data-dragging='1'] .rail-body,
.rail[data-sorting='1'] .rail-block[data-dragging='1'] .broker-foot { pointer-events: none; }

/* preventDefault on the press stops the selection starting; this stops one
   already in progress being extended down the column by the drag. */
.rail[data-sorting='1'] { user-select: none; }

/* ---- the secrets rail ------------------------------------------------------
   A pending approval, an item and a hand-written grant read as the same kind of
   row on purpose: they are the same fact at three moments. The approval carries
   the controls and a countdown, because a worker is blocked while it sits
   there. */
.broker-ask,
.broker-item,
.broker-grant {
  display: flex;
  flex-direction: column;
  gap: var(--sp-h);
  padding: var(--sp-1h) 0 var(--sp-1h);
  border-bottom: 1px solid var(--rule);
}

.broker-ask:last-child,
.broker-item:last-child,
.broker-grant:last-child { border-bottom: 0; }

/* An unanswered call is the one thing in this rail that is COSTING something
   right now, so it is the only row that gets a colour. */
.broker-ask { border-left: 2px solid var(--amber); padding-left: var(--sp-1h); }

/* The row's own title line and meta line, and the two line-heights #rail-broker
   is measured against. They were unset, so the block's height depended on the
   metrics of whichever face happened to load, which is not a number a sizing
   comment can honestly quote. */
.broker-title {
  font-family: var(--mono);
  font-size: var(--fs-sm);
  line-height: 1.4;
  color: var(--ink);
  overflow-wrap: anywhere;
}

.broker-meta {
  font-family: var(--mono);
  font-size: var(--fs-xs);
  line-height: 1.4;
  color: var(--ink-faint);
  overflow-wrap: anywhere;
}

/* The alias and its kind sit on one line, so the row stays the two lines
   #rail-broker is measured against. */
.broker-line {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--sp-1h);
}

/* The item's kind, printed beside its alias. It is a WORD off the payload, so
   the chip is one shape whatever the daemon's catalogue grows next.

   Not the .pill group: that group is an 18px box, and 18 against the title's
   15.4 would make every item row 2.6px taller than the block is measured for.
   9px at 1.4 inside a 1px border is 14.6, which fits inside the line it is on. */
.broker-kind {
  flex: 0 0 auto;
  padding: 0 var(--sp-1);
  border: 1px solid var(--edge-control);
  border-radius: var(--radius-sm);
  color: var(--ink-dim);
  font-family: var(--mono);
  font-size: var(--fs-micro);
  line-height: 1.4;
  letter-spacing: var(--track-label);
  text-transform: uppercase;
}

/* The health the daemon reported, in the same three-way language the security
   rail uses for a scanner: current is quiet, something the operator has to act
   on within the week is amber, something that has already stopped working is
   red. Every one of these words is copied off the payload and compared with
   nothing here. */
.broker-item[data-health='expiring'] .broker-health { color: var(--amber); }

.broker-item[data-health='expired'] .broker-health,
.broker-item[data-health='revoked'] .broker-health { color: var(--red); }

/* A per-use item is the one that will stop a worker later, which is the whole
   reason the approvals sit above this list. */
.broker-item[data-approval='per-use'] .broker-title::after {
  content: ' ?';
  color: var(--ink-faint);
}

/* Hand-written grants, under their own heading and after the items, because a
   custom grant is the escape hatch rather than the normal case: it has no item
   behind it, so nothing in this room can rotate it or revoke it. */
/* No rule of its own: the item above it already draws one, and with no items
   above it this heading is the first thing in the body, where a line would be
   drawing the top of a panel that already has one. */
.broker-heading {
  display: block;
  margin-top: var(--sp-2);
  color: var(--ink-faint);
}

/* The one control the panel has left, in the footer slot the add form used to
   fill. A rail footer takes the rail's own inset, or its button starts 1px
   inside every row above it. */
.broker-foot {
  display: flex;
  gap: var(--sp-1h);
  padding: var(--sp-2) var(--pad-panel);
  border-top: 1px solid var(--rule);
  background: var(--well-bg);
}

.broker-acts {
  display: flex;
  align-items: center;
  gap: var(--sp-1h);
  margin-top: var(--sp-1);
}

.broker-clock {
  flex: 1;
  font-family: var(--mono);
  font-size: var(--fs-xs);
  color: var(--ink-dim);
}

.broker-grant[data-approval='per-use'] .broker-title::after {
  content: ' ?';
  color: var(--ink-faint);
}

/* ---- the security rail -----------------------------------------------------
   One row per scanner, and under it one line for the signatures it reads.
   Every colour on this panel is a word the daemon sent (`status` on a row,
   `overall` on the block), mapped here and nowhere else: web/app.js copies the
   words onto data attributes and does no comparing of its own, so a threshold
   that moves in src/security moves here for free. current is quiet, because
   two rows of green chips on an ordinary morning pull the eye to the two things
   with nothing to report; unknown is fainter still, because it is the absence
   of an answer and the daemon does not count it against the box. */
.security-tool {
  display: flex;
  flex-direction: column;
  gap: var(--sp-h);
  padding: var(--sp-1h) 0;
  border-bottom: 1px solid var(--rule);
}

.security-tool:last-child { border-bottom: 0; }

/* A binary that is not on PATH is the one row in this rail where the gate is
   measuring less than it claims, so it is the only row that gets an edge: the
   same 2px the secrets rail gives a call that is blocking a worker. */
.security-tool[data-posture='missing'] { border-left: 2px solid var(--red); padding-left: var(--sp-1h); }

.security-top,
.security-sigs {
  display: flex;
  align-items: center;
  gap: var(--sp-2);
  min-width: 0;
}

/* The face comes from the label group with .lane-name; only the ink is its
   own, because a scanner's name is the one thing on the row read first. */
.security-name { color: var(--ink); }

.security-version {
  flex: 1 1 auto;
  min-width: 0;
  font-family: var(--mono);
  font-size: var(--fs-xs);
  color: var(--ink-dim);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

.security-sigs {
  font-family: var(--mono);
  font-size: var(--fs-xs);
  color: var(--ink-faint);
}

.security-sigs-source {
  flex: 1 1 auto;
  min-width: 0;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

.security-sigs-age { flex: 0 0 auto; white-space: nowrap; }

/* The signature line's status is a word rather than a second chip: two boxed
   chips per scanner is four boxes in a 46px row, and the lane rail already
   spells a secondary state this way with .lane-flag. */
.security-sigs-status {
  flex: 0 0 auto;
  letter-spacing: var(--track-label);
  text-transform: uppercase;
}

/* The posture ladder, written once over both the chip and the word so the two
   cannot drift apart. The descendant selector is (0,2,0), which is what it
   takes to outrank .pill's own green default at (0,1,0).

   Every word the daemon can send has an arm here, and a word with no arm is
   the failure this comment exists to stop: `incomplete` (signatures, added
   2 September 2026, for a rule set that did not land) matched nothing for a
   day and fell through to .security-sigs's own --ink-faint, which is the
   value `unknown` carries two lines up and which app.js and this file both
   define as "could not be checked, not a security problem". A gate loading
   zero rules rendered indistinguishably from a release check that was
   rate-limited. It sits in the amber arm because the contract folds it into
   an amber `overall`, and because a refresh is the remedy for both. */
.security-tool [data-posture='current'] { color: var(--ink-dim); }

.security-tool [data-posture='unknown'] { color: var(--ink-faint); }

.security-tool [data-posture='stale'],
.security-tool [data-posture='incomplete'] { color: var(--amber); }

.security-tool [data-posture='outdated'],
.security-tool [data-posture='missing'] { color: var(--red); }

/* The fix, on screen, in the words `queue doctor` prints under a missing
   binary. user-select: all, so one click takes the whole command to the
   clipboard rather than the word under the pointer. */
.security-install {
  margin: var(--sp-1) 0 0;
  font-family: var(--mono);
  font-size: var(--fs-xs);
  line-height: 1.4;
  color: var(--ink-dim);
  overflow-wrap: anywhere;
  user-select: all;
}

/* Outside the scrolling body, like the secrets form under the grants: the one
   control in the panel stays reachable however many install lines the body
   grows, and a button rebuilt on every poll would drop the focus of whoever
   had just pressed it. */
.security-foot {
  display: flex;
  align-items: center;
  gap: var(--sp-2);
  padding: var(--sp-2) var(--pad-panel);
  border-top: 1px solid var(--rule);
}

.security-foot-note {
  flex: 1;
  min-width: 0;
  font-family: var(--mono);
  font-size: var(--fs-xs);
  color: var(--ink-faint);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

/* A refresh that failed, or half failed, is news the footer carries in the
   same two colours the rest of the panel uses for the same two degrees. */
.security-foot-note[data-tone='warn'] { color: var(--amber); }

.security-foot-note[data-tone='error'] { color: var(--red); }

/* The head carries the daemon's overall word, coloured the way the queue's
   trouble and the performance severity colour theirs: current is quiet, stale
   is amber, outdated and missing are red. The unconfirmed pair is written
   FIRST, so that at equal specificity a red posture still wins over the amber
   of a poll that merely failed: a missing scanner is worse news than a missed
   poll and must not be repainted as the lesser one. */
#rail-security[data-unconfirmed] .rail-head { border-bottom-color: var(--amber); }

#rail-security[data-unconfirmed] .rail-note { color: var(--amber); }

#rail-security[data-overall='stale'] .rail-note { color: var(--amber); }

#rail-security[data-overall='outdated'] .rail-note,
#rail-security[data-overall='missing'] .rail-note { color: var(--red); }

.lane {
  padding: var(--sp-h) 0 var(--sp-1);
  border-bottom: 1px solid var(--rule);
}

.lane[data-open='1'] { padding-bottom: var(--sp-2); }

.lane:last-child { border-bottom: 0; }

/* The lane header is a real <button>, not a div with a click handler: Tab has to
   reach it, Enter and Space have to work it, and aria-expanded only means
   anything on something that already has a button's role. Everything below
   strips the UA chrome back to the row it used to be. */
.lane-top {
  display: flex;
  align-items: baseline;
  gap: var(--sp-2);
  width: 100%;
  /* The horizontal inset is .perf-row's, not its own. These two lists sit
     directly on top of each other in the rail and were indented 2px and 4px, so
     the dot column and the label column both stepped right as the eye crossed
     from QUEUE into PERFORMANCE. Same dot, same inset, same gap: one left edge
     down the whole rail. */
  padding: var(--sp-h) var(--sp-1);
  border: 0;
  background: transparent;
  color: inherit;
  font: inherit;
  text-align: left;
  cursor: pointer;
}

.lane-top:hover .lane-name { color: var(--teal); }

/* A collapsed lane has to carry its own trouble, so the dot says what the hidden
   slot rows would have said: dark is idle, teal is working, amber is a queue
   waiting behind it, red is a dead lease or a lane that cannot be read. */
.lane-led {
  flex: 0 0 auto;
  align-self: center;
  width: var(--dot);
  height: var(--dot);
  border-radius: 50%;
  background: var(--rack-3);
  border: 1px solid var(--edge);
}

.lane[data-held='1'] .lane-led { background: var(--teal); border-color: var(--teal); }

.lane[data-trouble='warn'] .lane-led { background: var(--amber); border-color: var(--amber); }

.lane[data-trouble='error'] .lane-led { background: var(--red); border-color: var(--red); }

.lane-name { color: var(--ink); font-weight: 600; }

.lane-flag {
  font-family: var(--mono);
  font-size: var(--fs-xs);
  letter-spacing: var(--track-label);
  text-transform: uppercase;
  color: var(--ink-faint);
}

.lane-flag[data-tone='warn'] { color: var(--amber); }

.lane-flag[data-tone='error'] { color: var(--red); }

.lane-count {
  margin-left: auto;
  font-family: var(--mono);
  font-size: var(--fs-xs);
  color: var(--ink-faint);
}

.lane-caret {
  display: inline-block;
  font-size: var(--fs-xs);
  line-height: 1;
  color: var(--ink-faint);
  transition: transform 140ms ease;
}

.lane[data-open='0'] .lane-caret { transform: rotate(-90deg); }

.lane[data-open='0'] .lane-body { display: none; }

.lane-error {
  margin: var(--sp-1h) 0 0;
  font-family: var(--mono);
  font-size: var(--fs-xs);
  line-height: 1.4;
  color: var(--red);
}

#rail-queue[data-trouble='warn'] .rail-note { color: var(--amber); }

#rail-queue[data-trouble='error'] .rail-note { color: var(--red); }

.slot {
  display: grid;
  grid-template-columns: var(--dot) minmax(0, 1fr);
  gap: var(--sp-2);
  align-items: start;
  margin-top: var(--sp-1h);
  font-family: var(--mono);
  font-size: var(--fs-sm);
}

.slot-led {
  width: var(--dot);
  height: var(--dot);
  margin-top: var(--sp-h);
  border-radius: 50%;
  background: var(--rack-3);
  border: 1px solid var(--edge);
}

.slot.is-held .slot-led { background: var(--teal); border-color: var(--teal); }

.slot.is-expired .slot-led { background: var(--red); border-color: var(--red); }

.slot-key { color: var(--ink); }

.slot.is-free .slot-key { color: var(--ink-faint); }

.slot-meta { color: var(--ink-faint); font-size: var(--fs-xs); }

.slot-ttl {
  height: 3px;
  margin-top: var(--sp-1);
  background: var(--rack-3);
}

.slot-ttl i {
  display: block;
  height: 100%;
  background: var(--teal);
}

.slot.is-expired .slot-ttl i { background: var(--red); }

.waiting {
  margin: var(--sp-1h) 0 0;
  padding: var(--sp-1) 0 0 var(--sp-4);
  border-top: 1px dashed var(--rule);
  list-style: none;
  font-family: var(--mono);
  font-size: var(--fs-xs);
  color: var(--ink-dim);
}

.waiting li { display: flex; gap: var(--sp-2); padding: var(--sp-h) 0; }

.waiting li span:last-child { margin-left: auto; color: var(--ink-faint); }

/* ---- the performance panel ------------------------------------------------ */

/* Ten metrics, worst first, and the whole design goal is that a regression is
   visible before a single number has been read. An alert is a red block with a
   red bar and the loudest type on the panel; a steady row is one dim line that
   gets out of the way. Colour comes off `severity` (perf.mjs's contract), so
   adding a metric or a status cannot silently paint something green. */

.perf-row {
  display: grid;
  grid-template-columns: var(--dot) minmax(0, 1fr);
  gap: var(--sp-2);
  align-items: start;
  padding: var(--sp-1) var(--sp-1) var(--sp-1h) var(--sp-1);
  border-bottom: 1px solid var(--rule);
}

.perf-row:last-child { border-bottom: 0; }

.perf-led {
  width: var(--dot);
  height: var(--dot);
  margin-top: var(--sp-1);
  border-radius: 50%;
  background: var(--rack-3);
  border: 1px solid var(--edge);
}

.perf-top { display: flex; align-items: baseline; gap: var(--sp-2); }

.perf-label { color: var(--ink-dim); font-size: var(--fs-md); }

.perf-verdict {
  margin-left: auto;
  font-family: var(--mono);
  font-size: var(--fs-sm);
  color: var(--ink-faint);
  white-space: nowrap;
}

.perf-meta {
  display: flex;
  align-items: baseline;
  gap: var(--sp-1h);
  margin-top: var(--sp-h);
  font-family: var(--mono);
  font-size: var(--fs-xs);
  color: var(--ink-faint);
}

/* The median, not the last run: perf.mjs is emphatic about the difference, and
   one 22ms scan that indexed nothing must never be the number on this panel.

   min-width, because the explanation beside it started wherever the number
   happened to end: measured down one list, "was 118ms · 22 v 89 runs" and its
   four neighbours began at x 1346.8, 1353.4, 1360.0, 1365.0 and 1366.6, a
   19.8px rag down the left edge of the only column on the panel that is read as
   prose. 5ch holds "215 MB/s" and everything shorter; a longer reading pushes
   its own line out rather than being clipped, which is the right way round for
   a number. */
.perf-meta b {
  flex: 0 0 auto;
  min-width: 5ch;
  color: var(--ink-dim);
  font-weight: 600;
  font-size: var(--fs-sm);
}

.perf-meta span { min-width: 0; overflow: hidden; white-space: nowrap; text-overflow: ellipsis; }

/* Only ratio metrics get a bar, and it means one thing: how much worse than the
   baseline. Free disk space is a level, not a comparison, so it has none. */
.perf-bar {
  height: 3px;
  margin-top: var(--sp-1);
  background: var(--rack-3);
}

.perf-bar i { display: block; height: 100%; background: var(--amber); }

/* The row is reserved, the track is not drawn. See renderPerf: one stride down
   the list, and no bar on a row that has no comparison to make. */
.perf-bar.is-empty { background: transparent; }

.perf-row[data-tone='alert'] { background: color-mix(in srgb, var(--red) 8%, transparent); }

.perf-row[data-tone='alert'] .perf-led { background: var(--red); border-color: var(--red); }

.perf-row[data-tone='alert'] .perf-label { color: var(--ink); font-weight: 600; }

.perf-row[data-tone='alert'] .perf-verdict { color: var(--red); font-weight: 700; font-size: var(--fs-md); }

.perf-row[data-tone='alert'] .perf-bar i { background: var(--red); }

.perf-row[data-tone='watch'] .perf-led { background: var(--amber); border-color: var(--amber); }

.perf-row[data-tone='watch'] .perf-verdict { color: var(--amber); font-weight: 600; }

.perf-row[data-tone='better'] .perf-led { background: var(--green); border-color: var(--green); }

.perf-row[data-tone='better'] .perf-verdict { color: var(--green); }

/* Too few runs is a statement about the sample, not about the box, so it is
   painted as neither good nor bad and says so in words. */
/* Eight of the nine rows on this panel say "not enough data" on a normal
   morning, and 0.7 over --ink-faint composited to 3.33:1 across the label, the
   verdict and the meta line. The row already says it in words; the dim was
   doing nothing the text was not, and it was doing it to the panel the operator
   asked to see first. The LED stays grey, which is where "no verdict" belongs. */
.perf-row[data-tone='unknown'] .perf-label,
.perf-row[data-tone='unknown'] .perf-verdict,
.perf-row[data-tone='unknown'] .perf-meta,
.perf-row[data-tone='steady'] .perf-label { color: var(--ink-faint); }

#rail-perf[data-severity='1'] .rail-note { color: var(--amber); }

#rail-perf[data-severity='2'] .rail-note { color: var(--red); }

/* The daemon stopped answering. Keep the last honest reading on screen, dimmed,
   because an empty box is no use on the morning something is already wrong. */
#rail-perf[data-stale] .rail-head { border-bottom-color: var(--amber); }

#rail-perf[data-stale] .rail-note { color: var(--amber); }

#rail-perf[data-stale] .rail-note::after { content: " (stale)"; }

.board-col { margin-bottom: var(--sp-2h); }

.board-col-head {
  display: flex;
  align-items: center;
  gap: var(--sp-1h);
  width: 100%;
  padding: var(--sp-h) 0;
  border: 0;
  background: transparent;
  color: var(--ink-dim);
  font-size: var(--fs-xs);
  letter-spacing: var(--track-label);
  text-transform: uppercase;
  cursor: pointer;
}

.board-col-head::after {
  content: '';
  flex: 1;
  height: 1px;
  background: var(--rule);
}

.board-col-count { font-family: var(--mono); color: var(--ink-faint); }

.board-col[data-open='0'] .board-cards { display: none; }

.board-col-caret { transition: transform 140ms ease; }

.board-col[data-open='0'] .board-col-caret { transform: rotate(-90deg); }

.card {
  position: relative;
  border: 1px solid var(--rule);
  border-left: 2px solid var(--edge);
  margin-top: var(--sp-1h);
  padding: var(--sp-2) var(--sp-2) var(--sp-2) var(--sp-2h);
  border-color: var(--rule);
  border-radius: var(--radius);
  background: var(--wash-2);
  transition: border-color 160ms ease, background 160ms ease, transform 160ms ease, box-shadow 160ms ease;
}

.card:hover {
  border-color: var(--accent-line);
  background: var(--accent-wash);
  box-shadow: var(--shadow-1);
  transform: translateY(-1px);
}

.card[data-priority='high'] { border-left-color: var(--amber); }

.card[data-priority='highest'] { border-left-color: var(--red); }

.card[data-priority='low'], .card[data-priority='lowest'] { border-left-color: var(--edge); }

.card-top { display: flex; align-items: baseline; gap: var(--sp-1h); }

.card-key {
  font-family: var(--mono);
  font-size: var(--fs-sm);
  font-weight: 700;
  color: var(--teal);
  text-decoration: none;
}

.card-key:hover { text-decoration: underline; }

.card-claim {
  margin-left: auto;
  padding: var(--sp-h) var(--sp-1h);
  border: 1px solid var(--edge-control);
  color: var(--ink-faint);
  font-size: var(--fs-xs);
  letter-spacing: var(--track-label);
  text-transform: uppercase;
  cursor: pointer;
  opacity: 0;
  transition: opacity 120ms ease;
  background: var(--well-bg);
}

.card:hover .card-claim, .card-claim:focus-visible { opacity: 1; }

.card-claim:hover { color: var(--teal); border-color: var(--teal); }

.card-summary {
  margin: var(--sp-h) 0 var(--sp-1);
  font-size: var(--fs-md);
  line-height: 1.35;
  color: var(--ink);
  display: -webkit-box;
  -webkit-line-clamp: 3;
  -webkit-box-orient: vertical;
  overflow: hidden;
}

.card-meta {
  display: flex;
  flex-wrap: wrap;
  gap: var(--sp-1h);
  color: var(--ink-faint);
}

.card-meta b { font-weight: 600; color: var(--ink-dim); }

/* ---- empty, loading and failed are three different answers -----------------
   One class carried all three at eleven call sites: "No lanes configured", and
   "Local board is loading", and "GET /api/dlq said: ECONNREFUSED", in the same
   12px grey. A daemon that had fallen over looked exactly like a panel with
   nothing in it yet. The base is unchanged, so an empty panel still reads the
   way it always did; the other two now say which they are. */
.rail-empty {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: var(--sp-2);
  padding: var(--sp-3) var(--sp-1);
  font-size: var(--fs-md);
  line-height: 1.5;
  color: var(--ink-faint);
}

.rail-empty-text { margin: 0; }

/* In progress: a rule that is going somewhere. No opacity anywhere near it, so
   the words stay at the same strength they had before the wait started. */
.rail-empty.is-loading { padding-left: var(--sp-2h); border-left: 2px solid var(--rule); }

.rail-empty.is-loading .rail-empty-text::after {
  content: '';
  display: block;
  width: 44px;
  height: 2px;
  margin-top: var(--sp-1h);
  background: linear-gradient(90deg, transparent, var(--teal), transparent);
  animation: rail-wait 1.5s linear infinite;
}

@keyframes rail-wait {
  from { transform: translateX(-44px); }
  to { transform: translateX(120px); }
}

@media (prefers-reduced-motion: reduce) {
  .rail-empty.is-loading .rail-empty-text::after { animation: none; background: var(--rule); }
}

/* Failed: the same left-border language .toast[data-kind='error'] uses, and the
   text at full --ink, because a failure is the one thing in a panel that must
   not be the dimmest thing in it. */
.rail-empty.is-error {
  padding-left: var(--sp-2h);
  border-left: 2px solid var(--red);
  color: var(--ink);
}

/* ---- command bar ---------------------------------------------------------- */

.commandbar {
  display: grid;
  grid-template-columns: auto minmax(0, 1fr) auto auto auto;
  align-items: center;
  position: relative;
  gap: var(--sp-2h);
  /* Symmetrical. It was 10px above the row and 8px below, so the bar's one row
     of 30px controls sat 11px from the top edge and 9px from the bottom while
     the deck bar directly above it was 8 and 8. If a bar needs to be taller it
     takes the height, not the asymmetry. */
  padding: var(--sp-2) var(--pad-bar);
  overflow: hidden;
  background: var(--surface-bar);
  backdrop-filter: blur(18px);
  -webkit-backdrop-filter: blur(18px);
}

/* Three control heights sat in one row of this bar and the tallest of them was
   32px, which is a height that appears nowhere in the token set: the input sized
   itself from its own padding while the buttons beside it read theirs off
   --ctl-h. The bar is one 30px band. */
.target {
  display: flex;
  align-items: center;
  height: var(--ctl-h-lg);
  gap: var(--sp-1h);
  padding: 0 var(--sp-2);
}

.target-arrow { color: var(--ink-faint); }

.target-id {
  font-family: var(--mono);
  font-size: var(--fs-md);
  font-weight: 700;
}

.cmd-input {
  width: 100%;
  height: var(--ctl-h-lg);
  padding: 0 var(--sp-2);
  font-size: var(--fs-md);
}

.check {
  display: flex;
  align-items: center;
  gap: var(--sp-1);
  font-size: var(--fs-sm);
  color: var(--ink-faint);
  cursor: pointer;
}

/* The one control in the room the browser was drawing rather than this file.
   Two declarations against the five every other control gets, so it rendered
   the only 13px box and the only square corner on the screen, 30px from a 30px
   SEND button in the same row. It takes the field spec now: --edge-control for
   the boundary, --field-bg for the recessed fill, --radius-sm for the corner.

   16px, off the spacing scale rather than the control-height one, because the
   height scale sizes controls that hold type and this holds a mark. It is the
   only square in the file and it says so here.

   The tick is an SVG rather than the clipped gradients the select chevron uses
   at the end of this file. That comment rejects SVG because a data-URI is a
   separate document and `currentColor` cannot reach it, so the mark stops
   agreeing with its own control; here the plate is --teal whenever the mark is
   drawn at all, so the ink is --on-accent unconditionally and there is nothing
   for it to stop agreeing with. accent-color stays as the fallback for a
   renderer that ignores appearance:none. */
.check input {
  appearance: none;
  -webkit-appearance: none;
  accent-color: var(--teal);
  flex: 0 0 auto;
  width: var(--sp-4);
  height: var(--sp-4);
  margin: 0;
  border: 1px solid var(--edge-control);
  border-radius: var(--radius-sm);
  background-color: var(--field-bg);
  cursor: pointer;
  transition: border-color 160ms ease, background-color 160ms ease;
}

.check input:checked {
  border-color: var(--teal);
  background-color: var(--teal);
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 14 14'%3E%3Cpath d='M3 7.4 5.6 10 11 4.3' fill='none' stroke='%23001c18' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E");
  background-position: center;
  background-size: 100% 100%;
  background-repeat: no-repeat;
}

.check:hover input:not(:checked) { border-color: var(--edge-hot); }

.broadcast {
  display: flex;
  gap: var(--sp-1h);
}

/* A select holds a committed value, exactly as a filled input does. Four of
   the seven select rules in this file painted theirs in --ink-dim while all
   twenty-six input rules used --ink unanimously, which is why every dropdown in
   the room read as disabled next to the field beside it. Only the chevron is
   allowed to be quieter than the text. */
/* No height here. The shared bar group at the end of this file lists this
   selector at the same specificity and is written later, so the --ctl-h
   declared here never applied once: the file said 26px and the screen showed
   30, which is correct (it is a bar control) and was said in the wrong place. */
.broadcast select {
  padding: 0 var(--chev-pad) 0 var(--sp-2);
  font-size: var(--fs-md);
}

.hint {
  grid-column: 1 / -1;
  margin: 0;
  font-size: var(--fs-xs);
  color: var(--ink-faint);
  letter-spacing: var(--track-label);
}

/* ---- dialog and toasts ---------------------------------------------------- */

/* Anchored near the top rather than centred. A <dialog> re-centres on any
   content change, and .dlg-error is inserted into the form flow, so the moment
   a dialog reported a refusal it grew ~120px and every field jumped ~59px
   upward, out from under the pointer that was still resting on the field that
   caused it. The wide dialogs below keep `auto`: they are already near full
   height and their bodies scroll. */
.dlg {
  margin-block: clamp(24px, 12vh, 160px) auto;
  width: var(--dlg-w);
  padding: 0;
  color: var(--ink);
  overflow: hidden;
  border-color: var(--edge);
  background: var(--surface-panel);
  box-shadow: var(--shadow-3);
}

.dlg::backdrop {
  background: var(--scrim);
  backdrop-filter: blur(12px);
  -webkit-backdrop-filter: blur(12px);
}

.dlg form { padding: var(--dlg-pad); }

/* A dialog's heading was 13px over 12px body copy, which is not a step: one
   pixel of size, and everything else about the difference was uppercase,
   tracking and a gradient that made it transparent. --fs-xl over --fs-md is a
   step a reader can see. */
.dlg h2 {
  margin: 0;
  font-size: var(--fs-xl);
  letter-spacing: var(--track-brand);
  text-transform: uppercase;
  color: var(--ink);
  font-family: var(--ui);
  font-weight: 600;
}

/* No measure inside the 520px dialog. 46ch at 12px resolves to 370px against a
   478px field grid, so the top half of the dialog and the bottom half did not
   share a right edge. The narrow measure was set for the 880px dialogs and it is
   scoped to them below.

   And no `text-wrap: balance` either, which was the other half of that cause and
   was left behind when the measure went: measured after the fix, the three lines
   of the new-project intro still ran 318.8, 336.3 and 325.1px inside a 478px
   column, 70% of it, with 142px of dead space beside every one of them. Balance
   is a headline rule. It evens the lines of two or three words; on body copy it
   just holds the paragraph narrow. */
.dlg-sub {
  margin: var(--sp-1h) 0 var(--sp-4);
  font-size: var(--fs-md);
  color: var(--ink-dim);
  line-height: 1.5;
}

.field {
  display: block;
  margin-bottom: var(--sp-3);
}

.field > span {
  display: block;
  margin-bottom: var(--sp-1);
  color: var(--ink-dim);
}

.field > span em { color: var(--ink-faint); font-style: normal; text-transform: none; letter-spacing: 0; }

.field input,
.field textarea,
.field select {
  width: 100%;
  font-size: var(--fs-md);
}

/* An explicit height on the box, not a padding that a select resolves
   differently: #project-colour rendered 32px against #project-repos at 30 on
   the same row with the same declarations, so every field row holding a select
   had a stepped bottom edge and pushed the rows under it off the rhythm. */
.field input,
.field select {
  height: var(--ctl-h-lg);
  padding: 0 var(--sp-2);
  line-height: 1;
}

.field select { padding-right: var(--chev-pad); }

/* ---- the colour field draws a colour ---------------------------------------

   #project-colour is the only field in the new-project dialog whose entire
   purpose is visual, and it showed none: the word "moss" in 12px mono, the same
   object as the three text boxes beside it. The value it sets paints the focus
   selector's left edge, a board accent and a card rule everywhere else in the
   app, so the one place the decision is MADE was the one place it had no
   consequence you could see.

   :has(option:checked) rather than a class the script writes, so the dot
   follows the selection with nothing to keep in sync and no new listener. The
   eight hues are the same eight .project-focus-select takes; a test asserts the
   two mappings agree, because a swatch that disagrees with the accent it
   promises is worse than no swatch. */
.colour-pick {
  position: relative;
  --swatch: 12px;
}

.colour-swatch {
  position: absolute;
  top: 50%;
  left: var(--sp-2);
  translate: 0 -50%;
  width: var(--swatch);
  height: var(--swatch);
  border: 1px solid var(--edge-control);
  border-radius: 50%;
  background: var(--project-accent, var(--green));
  /* The select underneath still has to receive the click that opens it. */
  pointer-events: none;
}

/* The swatch, its 8px inset and the 8px the text would have had anyway. */
.colour-pick #project-colour { padding-left: calc(var(--sp-2) * 2 + var(--swatch)); }

.colour-pick:has(option[value='moss']:checked) { --project-accent: var(--green); }

.colour-pick:has(option[value='amber']:checked) { --project-accent: var(--amber); }

.colour-pick:has(option[value='slate']:checked) { --project-accent: var(--slate); }

.colour-pick:has(option[value='plum']:checked) { --project-accent: var(--plum); }

.colour-pick:has(option[value='teal']:checked) { --project-accent: var(--teal); }

.colour-pick:has(option[value='rust']:checked) { --project-accent: var(--red); }

.colour-pick:has(option[value='indigo']:checked) { --project-accent: var(--blue); }

.colour-pick:has(option[value='sand']:checked) { --project-accent: var(--rose); }

.field textarea { padding: var(--sp-1h) var(--sp-2); resize: vertical; }

.dlg-error {
  /* A row rather than a block, because a failure that can be retried carries
     its own retry: the mailbox's REFRESH used to live 340px away in the dialog
     header, which is not where somebody reading a red banner is looking. */
  display: flex;
  align-items: center;
  gap: var(--sp-2h);
  margin: 0 0 var(--sp-3);
  padding: var(--sp-1h) var(--sp-2);
  border-left: 2px solid var(--red);
  background: color-mix(in srgb, var(--red) 8%, transparent);
  font-family: var(--mono);
  font-size: var(--fs-sm);
  color: var(--red);
}

.dlg-error[hidden] { display: none; }

/* The amber sibling of the banner above, for a condition that has not failed
   yet but will. #new-profile-note carries the reason START is going to fail
   ("...auth.json does not exist, so this profile has no credentials yet") and
   it wore class="hint": 10px --ink-faint with no border, no icon and no role,
   in a dialog whose #new-error twenty pixels below is red mono on a red plate
   with a 2px bar. The blocking condition was the quieter of the two. */
#new-profile-note {
  display: flex;
  align-items: center;
  gap: var(--sp-2h);
  margin: 0 0 var(--sp-3);
  padding: var(--sp-1h) var(--sp-2);
  border-left: 2px solid var(--amber);
  background: color-mix(in srgb, var(--amber) 8%, transparent);
  font-family: var(--mono);
  font-size: var(--fs-sm);
  line-height: 1.45;
  letter-spacing: 0;
  color: var(--amber);
}

.dlg-error .btn { flex: 0 0 auto; margin-left: auto; }

.dlg-actions {
  display: flex;
  justify-content: flex-end;
  gap: var(--sp-2);
  margin-top: var(--sp-4);
}

.dlg-actions .btn { min-width: 96px; height: var(--ctl-h-lg); }

.dlg-clipboard {
  margin-block: auto;
  max-width: calc(100vw - 24px);
  max-height: calc(100dvh - 24px);
  overflow: auto;
}

.clipboard-copy-text {
  display: block;
  width: 100%;
  height: min(40vh, 320px);
  min-height: 80px;
  resize: vertical;
  font-family: var(--mono);
}

.dlg-operator-brief {
  margin-block: auto;
  width: var(--dlg-w-wide);
  max-height: min(86vh, 900px);
  overflow: hidden;
}

.operator-brief-head {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: var(--sp-4);
  padding: var(--dlg-pad) var(--dlg-pad) 0;
  border-bottom: 1px solid var(--rule);
}

.dlg-operator-brief .dlg-sub,
.dlg-mailbox .dlg-sub { max-width: 62ch; }

.operator-brief-head .dlg-sub { margin-bottom: var(--sp-3); }

.dlg-operator-brief > .dlg-error { margin: var(--sp-3) var(--dlg-pad) 0; }

.operator-brief-body {
  max-height: calc(min(86vh, 900px) - 150px);
  padding: var(--sp-3) var(--dlg-pad);
  overflow: auto;
  scrollbar-color: var(--edge) transparent;
}

.operator-brief-section {
  border-top: 1px solid var(--rule);
}

.operator-brief-section:last-child { border-bottom: 1px solid var(--rule); }

/* The caret is drawn here rather than left to the UA. Chrome renders a
   <summary>'s ::marker only while the element is display: list-item, and both
   summaries in this dialog were given a layout mode instead (flex on the
   section, grid on the item), which deletes the marker with no warning. The
   item rule below still carries `list-style-position: inside` from the attempt
   to keep it, so the intent was already here and simply could not fire.

   What it cost: five section headers and every one of the item rows read as
   inert labels. "COMING NEXT 349" is the worst of them, a heading with a
   number beside it and nothing to say the 349 are one click away.

   Two glyphs rather than one rotated by a transform, because the room already
   spells a caret this way in the lane rail and a rotated triangle sits on a
   different optical centre from an upright one. */
.operator-brief-section > summary {
  display: flex;
  align-items: center;
  gap: var(--sp-1h);
  padding: var(--sp-2) var(--sp-h);
  color: var(--ink-dim);
  font-size: var(--fs-xs);
  font-weight: 700;
  letter-spacing: var(--track-label);
  text-transform: uppercase;
  cursor: pointer;
}

.operator-brief-section > summary::before {
  content: '\25B8';
  color: var(--ink-faint);
  font-size: var(--fs-sm);
  line-height: 1;
  letter-spacing: 0;
}

.operator-brief-section[open] > summary::before { content: '\25BE'; }

/* Was justify-content: space-between on two children. The caret is a third
   child now, so the count is pushed right by itself and the label stays put. */
.operator-brief-section > summary b { margin-left: auto; color: var(--ink-faint); font-family: var(--mono); }

.operator-brief-list { display: grid; gap: var(--sp-1h); padding: 0 0 var(--sp-2h); }

.operator-brief-item {
  border: 1px solid var(--rule);
  border-left: 2px solid var(--teal);
  background: var(--rack);
}

.operator-brief-item[data-severity='warning'] { border-left-color: var(--amber); }

.operator-brief-item[data-severity='critical'] { border-left-color: var(--red); }
/* Snoozed, said on the rule down its left edge. 0.58 put the item's meta line
   at 2.60:1 and its body at 3.52:1, and the item already carries a border
   colour that says which kind of thing it is. */
.operator-brief-item[data-presentation='snoozed'] { border-left-color: var(--rule); }

.operator-brief-item[data-presentation='snoozed'] .operator-brief-item-title,
.operator-brief-item[data-presentation='snoozed'] .operator-brief-item-line { color: var(--ink-faint); }

.operator-brief-item[data-presentation='new'] summary,
.operator-brief-item[data-presentation='changed'] summary { background: color-mix(in srgb, var(--teal) 8%, transparent); }

/* A fourth track for the caret the ::marker cannot draw here either, so the
   row that opens looks like a row that opens. `auto` and not a fixed width:
   the two glyphs are the same advance in this face, and a track that sizes to
   them cannot drift if the face is ever swapped. */
.operator-brief-item > summary {
  display: grid;
  grid-template-columns: minmax(130px, 0.42fr) minmax(0, 1fr) auto auto;
  gap: var(--sp-2h);
  align-items: baseline;
  padding: var(--sp-2) var(--sp-2h);
  cursor: pointer;
  list-style-position: inside;
}

.operator-brief-item > summary::after {
  content: '\25B8';
  color: var(--ink-faint);
  font-size: var(--fs-sm);
  line-height: 1;
}

.operator-brief-item[open] > summary::after { content: '\25BE'; }

.operator-brief-item-title { font-size: var(--fs-sm); font-weight: 700; color: var(--ink); }

.operator-brief-item-line { font-size: var(--fs-sm); line-height: 1.4; color: var(--ink-dim); }

.operator-brief-item-meta { font: var(--fs-xs)/1 var(--mono); color: var(--ink-faint); white-space: nowrap; }

.operator-brief-item-detail { padding: 0 var(--sp-2h) var(--sp-2); border-top: 1px solid var(--rule); }

.operator-brief-item-detail p { margin: var(--sp-2) 0; color: var(--ink-dim); font-size: var(--fs-sm); }

.operator-brief-item-controls { display: flex; gap: var(--sp-1); flex-wrap: wrap; }

.operator-brief-empty { margin: var(--sp-h) 0 var(--sp-2); color: var(--ink-faint); font-size: var(--fs-sm); }

.operator-brief-actions {
  align-items: center;
  margin: 0;
  padding: var(--sp-3) var(--dlg-pad);
  border-top: 1px solid var(--rule);
}

.operator-brief-actions p { flex: 1 1 auto; margin: 0; color: var(--ink-faint); font-size: var(--fs-xs); }

.dlg-mailbox {
  margin-block: auto;
  width: var(--dlg-w-wide);
  max-height: min(88vh, 920px);
  overflow: hidden;
}

.mailbox-head {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: var(--sp-4);
  padding: var(--dlg-pad) var(--dlg-pad) 0;
  border-bottom: 1px solid var(--rule);
}

.mailbox-head .dlg-sub { margin-bottom: var(--sp-3); }

.mailbox-head-actions { display: flex; align-items: center; gap: var(--sp-1h); }

.mailbox-head-actions select {
  min-width: 185px;
  height: var(--ctl-h-lg);
  padding: 0 var(--chev-pad) 0 var(--sp-2);
  font-size: var(--fs-md);
}

.dlg-mailbox > .dlg-error { margin: var(--sp-3) var(--dlg-pad) 0; }

/* The three columns are told to bottom-align and landed on three different
   bottom edges 4px apart, because a two-line caption ("message to coordinator")
   grows its own grid item and the item is what gets aligned, not the control
   inside it. One caption line height across the row, and the 1px hand-nudge
   that used to admit the problem is gone. */
.dlg .mailbox-compose {
  display: grid;
  grid-template-columns: 130px minmax(0, 1fr) auto;
  gap: var(--sp-2h);
  align-items: end;
  padding: var(--sp-3) var(--dlg-pad);
  border-bottom: 1px solid var(--rule);
  background: color-mix(in srgb, var(--teal) 8%, transparent);
}

.mailbox-compose .field { margin: 0; }

.mailbox-compose .field > span {
  display: block;
  min-height: 14px;
  overflow: hidden;
  white-space: nowrap;
  text-overflow: ellipsis;
}

.mailbox-thread {
  display: grid;
  gap: var(--sp-2);
  max-height: calc(min(88vh, 920px) - 190px);
  padding: var(--sp-3) var(--dlg-pad);
  overflow: auto;
  scrollbar-color: var(--edge) transparent;
}

.mailbox-message {
  border: 1px solid var(--rule);
  border-left: 2px solid var(--teal);
  background: var(--rack);
}

.mailbox-message[data-kind='blocker'] { border-left-color: var(--red); }

.mailbox-message[data-direction='coordinator-to-worker'] { border-left-color: var(--teal); }

.mailbox-message-head {
  display: flex;
  align-items: baseline;
  gap: var(--sp-2);
  padding: var(--sp-2) var(--sp-2h);
  border-bottom: 1px solid var(--rule);
}

.mailbox-message-title { color: var(--ink); font: 700 var(--fs-sm)/1.2 var(--mono); }

.mailbox-message-meta { flex: 1 1 auto; color: var(--ink-faint); font: var(--fs-xs)/1.2 var(--mono); }

.mailbox-state {
  padding: var(--sp-h) var(--sp-1h);
  border: 1px solid var(--edge);
  color: var(--ink-dim);
  font: 700 var(--fs-micro)/1 var(--mono);
  text-transform: uppercase;
}

.mailbox-state[data-state='unread'],
.mailbox-state[data-state='queued'],
.mailbox-state[data-state='pending'] { border-color: var(--amber); color: var(--amber); }

.mailbox-state[data-state='delivered'],
.mailbox-state[data-state='promoted'] { border-color: var(--green); color: var(--green); }

.mailbox-state[data-state='cancelled'],
.mailbox-state[data-state='failed'],
.mailbox-state[data-state='expired'] { border-color: var(--red); color: var(--red); }

.mailbox-message-body {
  margin: 0;
  padding: var(--sp-2h);
  color: var(--ink-dim);
  font: var(--fs-sm)/1.5 var(--mono);
  white-space: pre-wrap;
  overflow-wrap: anywhere;
}

.mailbox-message-actions {
  display: flex;
  align-items: flex-end;
  gap: var(--sp-1h);
  padding: 0 var(--sp-2h) var(--sp-2h);
}

.mailbox-reply-field { flex: 1 1 auto; margin: 0; }
/* The thread's own empty and loading states. Left-aligned like every other
   .rail-empty in the room, with the block itself pushed off the top rule.
   .mailbox-empty was deleted with it: the dialog had a centred grey paragraph
   nothing else in the app used, which was half of why an offline inbox read as
   two applications arguing. */
.mailbox-thread .rail-empty { padding: var(--sp-4) var(--sp-1); }

.mailbox-foot { margin: 0; padding: var(--sp-3) var(--dlg-pad); border-top: 1px solid var(--rule); color: var(--ink-faint); font-size: var(--fs-xs); }

/* ---- the secrets manager ---------------------------------------------------
   The wide dialog treatment #dlg-mailbox already has, for the same reason: this
   one holds a list and a form rather than three fields, and --dlg-w's 520px put
   the two-column field grid on one column. margin-block: auto rather than the
   narrow dialogs' clamp, because at 88vh it is already near full height and has
   nowhere to be pushed. */
.dlg-secrets {
  margin-block: auto;
  width: var(--dlg-w-wide);
  max-height: min(88vh, 920px);
  overflow: hidden;
}

/* On the [open] state, and that is load-bearing: a UA sets `dialog:not([open])
   { display: none }`, an author rule beats a UA rule whatever its specificity,
   and a bare `display: flex` here would leave the manager on screen from the
   first paint of the page. */
.dlg-secrets[open] {
  display: flex;
  flex-direction: column;
}

.secrets-head {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: var(--sp-4);
  padding: var(--dlg-pad) var(--dlg-pad) 0;
  border-bottom: 1px solid var(--rule);
}

/* The assurance is the longest sentence in the room and the one nobody may
   paraphrase, so it gets a measure and the space to be read in. */
.secrets-head .dlg-sub { margin-bottom: var(--sp-3); max-width: 62ch; }

.secrets-head-actions {
  display: flex;
  align-items: center;
  gap: var(--sp-1h);
}

/* .scope is sized for the board's filter bar, where it is the widest thing on
   the row. Here it is one of two controls in a dialog header.

   Two classes, and that is load-bearing rather than decorative: `.scope` is
   declared 285 lines BELOW this block, so a single-class rule here would have
   lost on source order and the 420px basis would have rendered while this file
   said 240. */
.scope.secrets-search { flex: 0 1 240px; min-width: 0; }

/* The list takes whatever the head and the form leave, with a floor of three
   whole rows: the same three the rail block is measured against, so a short
   window shrinks the list rather than cutting a row through its own glyphs. */
.secrets-list {
  flex: 1 1 auto;
  min-height: 132px;
  padding: var(--sp-2) var(--dlg-pad);
  overflow-y: auto;
  scrollbar-gutter: stable;
  scrollbar-color: var(--edge) transparent;
}

/* A row is a button because selecting one is what offers ROTATE and REVOKE.
   Four columns: the alias, its kind, what it is scoped to and how it is, in
   that order, because that is the order the questions are asked in. */
.secret-row {
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto minmax(0, 12ch) minmax(0, 10ch);
  align-items: center;
  gap: var(--sp-2);
  width: 100%;
  padding: var(--sp-1h);
  border: 1px solid transparent;
  border-bottom-color: var(--rule);
  background: transparent;
  text-align: left;
  cursor: pointer;
}

.secret-row:hover { border-color: var(--accent-line); background: var(--wash-1); }

/* The selected row is the one the form below is about, so it carries the accent
   the form's verb is about to spend. */
.secret-row[aria-pressed='true'] {
  border-color: var(--accent-line-hot);
  background: var(--accent-wash);
}

.secret-row-alias {
  min-width: 0;
  color: var(--ink);
  font-family: var(--mono);
  font-size: var(--fs-md);
  overflow-wrap: anywhere;
}

.secret-row-scope,
.secret-row-health {
  min-width: 0;
  color: var(--ink-faint);
  font-family: var(--mono);
  font-size: var(--fs-xs);
  overflow-wrap: anywhere;
}

/* The same three-way language the rail rows use, said once more here because a
   manager row and a rail row are two components, not one. */
.secret-row[data-health='expiring'] .secret-row-health { color: var(--amber); }

.secret-row[data-health='expired'] .secret-row-health,
.secret-row[data-health='revoked'] .secret-row-health { color: var(--red); }

/* A per-use item asks a person every single time it is used, which is the one
   fact on this row that costs somebody an interruption later. */
.secret-row[data-approval='per-use'] .secret-row-alias::after {
  content: ' ?';
  color: var(--ink-faint);
}

/* An armed destructive control, in the language .mini[data-armed='1'] already
   speaks. REVOKE rests amber, because it is destructive before it is pressed,
   and goes red once it is one press from taking an item and every grant derived
   from it out of service. */
.btn[data-armed='1']:not(:disabled) {
  color: var(--red);
  border-color: var(--red);
  background: color-mix(in srgb, var(--red) 8%, transparent);
}

/* The stack has a ceiling and it scrolls to it. Error and warning toasts are
   sticky by design (web/app.js returns early for those kinds, so no timer ever
   removes one), and the column that held them had `max-height: none` and
   `overflow-y: visible`: measured in a 1440x900 frame at 74px per toast, the
   oldest one's top was y=32 at ten, y=-48 at eleven and y=-128 at twelve, off
   the top of the viewport with no way to scroll to it and no way to close it.
   The docblock on toast() says an operator should never have to reproduce a
   failure to reread its message; eleven failures did exactly that.

   The pointer comes back on the container only when the stack is holding
   something that will not leave on its own, which is the only time there is
   anything to scroll to. The rest of the time it stays away, so a five-second
   status message can still never swallow a click meant for the board under it,
   which is what the note on .toast[data-kind] below is protecting.

   `margin-top: auto` rather than `justify-content: flex-end`: it bottom-anchors
   the stack while there is room, and resolves to zero once there is not, so the
   overflow can never end up above the scroll origin where nothing can reach
   it. */
.toasts {
  position: fixed;
  right: 16px;
  bottom: 74px;
  z-index: var(--z-toast);
  display: flex;
  flex-direction: column;
  gap: var(--sp-1h);
  max-height: calc(100dvh - 120px);
  overflow-y: auto;
  overscroll-behavior: contain;
  pointer-events: none;
}

.toasts > :first-child { margin-top: auto; }

.toasts:has(.toast[data-kind='error']),
.toasts:has(.toast[data-kind='warn']) { pointer-events: auto; }

.toast {
  display: flex;
  align-items: flex-start;
  gap: var(--sp-2);
  min-width: 220px;
  max-width: 380px;
  /* A refusal is often several lines of advice about what to do instead. It may
     be long; it may not be endless, and a toast taller than this has pushed the
     ones under it off the screen. */
  max-height: 40vh;
  overflow-y: auto;
  padding: var(--sp-2) var(--sp-3);
  border: 1px solid var(--edge);
  /* Written AFTER the four-side border, and it used to be written before a
     `border-color: var(--edge)` in a second .toast block 2,000 lines down. A
     four-side shorthand overwrites one side, so the default toast's accent bar
     had quietly become a lopsided grey stripe that read as a rendering slip. */
  border-left: 2px solid var(--teal);
  font-family: var(--mono);
  font-size: var(--fs-sm);
  color: var(--ink);
  animation: toast-in 180ms ease both;
  border-radius: var(--radius);
  background: var(--surface-float);
  box-shadow: var(--shadow-3);
  backdrop-filter: blur(14px);
}

.toast-text { margin: 0; flex: 1 1 auto; min-width: 0; overflow-wrap: anywhere; }

/* .toasts takes the pointer away from the whole stack so a toast can never
   swallow a click meant for the board underneath it. A message that will not
   leave on its own has to take it back, or it can be neither dismissed nor
   selected and copied. */
.toast[data-kind='error'],
.toast[data-kind='warn'] { pointer-events: auto; user-select: text; }

.toast-close {
  flex: 0 0 auto;
  width: 18px;
  height: 18px;
  padding: 0;
  border: 0;
  border-radius: var(--radius-sm);
  background: transparent;
  color: var(--ink-dim);
  font-family: var(--mono);
  font-size: var(--fs-base);
  line-height: 1;
  cursor: pointer;
}

.toast-close:hover { background: var(--wash-2); color: var(--ink); }

/* A kind you can read from the block rather than from a 2px stripe most of the
   width away from the words. #ffd7d5 on the plain plate is 13.94:1 against
   --ink's 15.06, a factor of 0.93 and no perceptible lightness step at all, so
   an error and a status message were the same object with a different edge.
   The plate carries it now and the ink stays where it was. */
/* border-color BEFORE border-left-color in both, and that ordering is the whole
   point of the note above: a four-side shorthand overwrites one side, so
   writing the accent bar first hands it straight back to the neutral edge. */
.toast[data-kind='error'] {
  border-color: color-mix(in srgb, var(--red) 40%, var(--edge));
  border-left-color: var(--red);
  background: color-mix(in srgb, var(--red) 12%, var(--surface-float));
  color: var(--on-wash-alarm);
}

.toast[data-kind='warn'] {
  border-color: color-mix(in srgb, var(--amber) 34%, var(--edge));
  border-left-color: var(--amber);
  background: color-mix(in srgb, var(--amber) 10%, var(--surface-float));
  color: var(--on-wash-warn);
}

.toast.is-going { animation: toast-out 200ms ease both; }

@keyframes toast-in { from { opacity: 0; transform: translateX(14px); } }
@keyframes toast-out { to { opacity: 0; transform: translateX(14px); } }

/* ---- xterm ---------------------------------------------------------------- */

.pane-term .xterm { height: 100%; }

.pane-term .xterm-viewport { background: transparent; }

/* xterm draws its own scrollbar and tints it from the theme foreground, which
   comes out as a bright white slab against this palette. */
.pane-term .xterm-scrollable-element > .scrollbar > .slider { background: color-mix(in srgb, var(--ink-dim) 20%, transparent); }

.pane-term .xterm-scrollable-element > .scrollbar > .slider:hover { background: color-mix(in srgb, var(--ink-dim) 42%, transparent); }

/* ---- the board screen ----------------------------------------------------- */

/* Both screens are mounted at once and one of them is display:none, which is
   the same trick the full-screen pane already uses on whole columns: xterm keeps
   its buffer, the socket keeps its attachments, and switching costs a repaint
   rather than eleven terminal rebuilds.

   One palette rule carries over and matters more here than anywhere: amber is
   awaiting-input and nothing else. A board is a wall of chips and if any of them
   were amber the one that means "an agent has stopped to ask you something"
   would stop being findable. Blocked reads as hatching, dead reads as red. */

.deck { display: none; }

.hint-board { display: none; }

body[data-screen='board'] .stage { display: none; }
body[data-screen='board'] .hint-control { display: none; }
body[data-screen='board'] .hint-board { display: block; }

/* Two rows, not three. The third used to hold the selected-task strip, which
   took a third of the board from the moment you looked at anything and could
   only be put away by clicking the card again. It floats now, out at body
   level, so the deck gets that height back and the cards under an open ticket
   stay clickable. */
body[data-screen='board'] .deck {
  display: grid;
  grid-template-rows: auto minmax(0, 1fr);  min-height: 0;
  gap: var(--sp-2h);
  background: transparent;
}

.screens { position: relative; }

.screens-badge {
  display: inline-block;
  min-width: 15px;
  margin-left: var(--sp-1h);
  padding: 0 var(--sp-1);
  background: var(--red);
  color: var(--on-alarm);
  font-family: var(--mono);
  font-size: var(--fs-xs);
  font-style: normal;
  font-weight: 700;
  line-height: 14px;
  text-align: center;
  border-radius: var(--radius-sm);
}

/* ---- the filter bar ------------------------------------------------------- */

.deck-bar {
  display: flex;
  align-items: center;
  position: relative;
  z-index: var(--z-chrome);
  gap: var(--sp-3);
  padding: var(--sp-2) var(--pad-bar);
  background: var(--surface-bar);
  backdrop-filter: blur(16px);
  -webkit-backdrop-filter: blur(16px);
}

/* The filter bar carried three control heights (30, 26, 22) and three corner
   radii (12, 999, 8) in one band, which is the thing the topbar group at the
   end of this file forbids in so many words. Two of the three were this rule:
   --radius is reserved by the token block for "cards and inner surfaces" and
   this is a field, so it takes the field's corner like every other input in the
   room; and --ctl-h-lg is the bar height the TOPBAR takes, while REFRESH and
   NEW TASK 12px to the right of it are --ctl-h. The chips stay --ctl-h-sm and
   stay pills: they are the one readout on the bar, and the block above says
   why. Two idioms on a 26px band, not three on a 30px one. */
.scope {
  position: relative;
  display: flex;
  align-items: center;
  gap: var(--sp-1h);
  flex: 0 1 420px;
  padding: var(--sp-1) var(--sp-2);
  border: 1px solid var(--edge-control);
  min-height: var(--ctl-h);
  border-radius: var(--radius-sm);
  background: var(--field-bg);
}

.scope:focus-within { border-color: var(--teal); }

.scope-mark {
  font-family: var(--mono);
  font-size: var(--fs-base);
  font-weight: 700;
  color: var(--teal);
}

/* The wrapper is the field, so the wrapper carries the boundary and the input
   inside it carries none. It is the one field in the room outside the shared
   spec, and it is written out here rather than added to that list because a
   rule excluded from a shared spec should say why.

   Stretched, not centred. The input sized itself from its own 12px type and
   rendered 16px tall inside a 30px wrapper, so seven pixels above and below the
   text of the board's main search field did nothing when clicked. The wrapper
   is the field; the input is all of its inside. */
.scope-input {
  flex: 1;
  align-self: stretch;
  min-width: 0;
  border: 0;
  background: transparent;
  font-family: var(--mono);
  font-size: var(--fs-md);
  color: var(--ink);
}

input::placeholder,
textarea::placeholder { color: var(--ink-faint); }

.scope-clear {
  border: 0;
  background: transparent;
  color: var(--ink-faint);
  font-size: var(--fs-xs);
  letter-spacing: var(--track-label);
  text-transform: uppercase;
  cursor: pointer;
}

.scope-clear:hover { color: var(--red); }

.scope-menu {
  position: absolute;
  z-index: var(--z-float);
  max-height: 46vh;
  border: 1px solid var(--edge);
  top: calc(100% + 8px);
  left: 0;
  right: 0;
  overflow: hidden auto;
  border-color: var(--edge);
  border-radius: var(--radius);
  background: var(--surface-float);
  box-shadow: var(--shadow-3);
  backdrop-filter: blur(18px);
}

.scope-item {
  display: flex;
  align-items: baseline;
  gap: var(--sp-2);
  padding: var(--sp-1) var(--sp-2);
  border: 0;
  border-left: 2px solid transparent;
  background: transparent;
  text-align: left;
  cursor: pointer;
  margin: var(--sp-h);
  width: calc(100% - 6px);
  border-radius: var(--radius-sm);
}

.scope-item.is-on { background: var(--rack-2); border-left-color: var(--teal); }

.scope-item:hover { background: var(--rack-2); }

/* Two rows that are not choices: the line saying nothing matched, and the line
   counting what the forty-row cap left off. Both are divs so the arrow keys
   cannot land on them; this is what stops them LOOKING like choices too. No
   pointer, no hover, and the amber that the rest of the room uses for "read
   this" rather than the teal it uses for "click this". */
.scope-item.is-none,
.scope-item.is-note {
  cursor: default;
  font-family: var(--mono);
  font-size: var(--fs-sm);
  color: var(--ink-dim);
}

.scope-item.is-none { color: var(--amber); }

.scope-item.is-none:hover,
.scope-item.is-note:hover { background: transparent; }

.scope-item b {
  font-family: var(--mono);
  font-size: var(--fs-sm);
  font-weight: 700;
  color: var(--teal);
}

.scope-item span {
  flex: 1;
  font-size: var(--fs-sm);
  color: var(--ink-faint);
}

.scope-item i {
  font-family: var(--mono);
  font-size: var(--fs-sm);
  font-style: normal;
  color: var(--ink-dim);
}

/* No `overflow: hidden`. It wraps, so there was nothing to clip, and the box was
   exactly as tall as the 22px chips inside it: the shared focus ring is 2px at
   2px of offset and needs 4px of clearance, so a Tab onto any of the six filter
   chips drew a ring whose top and bottom strokes were both cut away entirely
   (ring box 97-127 against a host box of 101-123). It was the one place on the
   board where a keyboard operator could not see where they were. */
.chips {
  display: flex;
  flex-wrap: wrap;
  gap: var(--sp-1h);
  flex: 1 1 auto;
  min-width: 0;
}

.chip {
  display: flex;
  /* Centre, not baseline: baseline alignment inside a box with a declared
     height parks both children at the TOP of it and leaves 6px of dead space
     under them. */
  align-items: center;
  gap: var(--sp-1h);
  border: 1px solid var(--edge-control);
  /* A live filter, so --ink-dim: see .pane-acts button. Measured 5.21:1 against
     the 8.42 of REFRESH and NEW TASK on the same bar. */
  color: var(--ink-dim);
  font-size: var(--fs-xs);
  letter-spacing: var(--track-label);
  text-transform: uppercase;
  cursor: pointer;
  /* A filter chip is clickable, so it takes the clickable pill height. It was
     26px, which is the default control height, on a bar of 22px readouts; and
     stripping its vertical padding to put it on one left edge left it sizing
     from its own 10px type at 16. One idiom, two heights, and the difference
     means something: --pill-h reads, --ctl-h-sm is pressed. */
  height: var(--ctl-h-sm);
  padding: 0 var(--sp-2);
  border-radius: var(--radius-sm);
  background: var(--wash-1);
  transition: border-color 160ms ease, background 160ms ease, color 160ms ease, transform 160ms ease;
}

.chip b {
  font-family: var(--mono);
  font-size: var(--fs-md);
  font-weight: 700;
  letter-spacing: 0;
  color: var(--ink);
}

.chip:hover {
  border-color: var(--accent-line);
  background: var(--accent-wash);
  color: var(--ink);
  transform: translateY(-1px);
}

.chip.is-on {
  border-color: var(--accent-line-hot);
  background: var(--accent-tint);
  color: var(--on-wash-accent);
}

/* ---- zero ------------------------------------------------------------------

   ONE answer for the number nought, everywhere it is printed as a count.

   There were three, measured on a single screen: the deck chip's "0 held" at
   --ink-faint (a designed zero state), the ticket panel's "runs 0" at --ink-dim
   and byte-identical to "links 1" (no zero state at all), and the project
   overview's "0 needs you" in GREEN beside "0 dead" in RED while its six
   non-zero neighbours were all --ink (two alarm colours on two counts that both
   mean none). So the same fact read as de-emphasised, neutral and alarming
   depending on which panel it landed in.

   The rule: a zero recedes to --ink-faint and drops any semantic colour,
   because a count of nothing is never the alarming case. It is a colour and a
   weight only. The CONTROL keeps its border and its handler: a chip whose count
   is zero is still a live filter, and clicking it is how an operator asks "show
   me the held ones" before any exist. It used to be painted byte-for-byte as
   the disabled block at the end of this file paints a dead control (--rule
   border, transparent fill, --ink-faint ink), which spent the room's one signal
   for "this cannot be clicked" on seven controls that can; measured, a zero
   chip's border read 1.24:1 against a live chip's 2.9.

   web/app.js writes the class from the count at all three sites. */
.chip.is-zero { color: var(--ink-faint); }

.chip.is-zero b,
.overview-count.is-zero b,
.ticket-tab-count.is-zero { color: var(--ink-faint); font-weight: 400; }

.chip[data-tone='alarm'] b { color: var(--red); }

.chip[data-tone='alarm'] { border-color: color-mix(in srgb, var(--red) 42%, transparent); }

.deck-actions {
  display: flex;
  align-items: center;
  gap: var(--sp-2);
  margin-left: auto;
}

.deck-stamp {
  font-family: var(--mono);
  font-size: var(--fs-xs);
  color: var(--ink-faint);
}

/* ---- deck body ------------------------------------------------------------ */

.deck-body {
  display: grid;
  grid-template-columns: minmax(0, 1fr) var(--dock-w);  min-height: 0;
  /* `auto`, not `visible`. The kanban and the dock panels scroll internally, so
     this scroller normally never appears. It only engages on a window too short
     to honour a child's own minimum height, where the alternative was content
     spilling silently past the bottom of the deck. A scrollbar that shows up
     once in a while beats a row of cards nobody can see. */
  overflow: auto;
  gap: var(--sp-2h);
  background: transparent;
}

/* One panel at full width, in the same idiom as a full-screened pane: hide the
   others rather than reflow anything, because the dependency map is a wide
   drawing that a 390px dock can only ever show a corner of. */
.deck-body[data-dock='map'],
.deck-body[data-dock='dlq'],
.deck-body[data-view='dlq'] { grid-template-columns: minmax(0, 1fr); }

.deck-body[data-dock] .kanban,
.deck-body[data-view='dlq'] .kanban,
.deck-body[data-view='dlq'] #panel-map { display: none; }

.deck-body[data-dock='map'] #panel-dlq,
.deck-body[data-dock='dlq'] #panel-map { display: none; }

.kanban {
  display: flex;
  min-width: 0;
  min-height: 0;
  overflow-x: auto;
  overflow-y: hidden;
  gap: var(--sp-2h);
  padding: 0;
  background: transparent;
}

/* Grow into the room there is, shrink to a floor, and only then scroll. Five
   fixed columns on a laptop puts "done" permanently off the right, and a column
   nobody can see is a column nobody drags to. */
.kcol {
  display: flex;
  flex: 1 1 var(--kcol-w);
  flex-direction: column;
  min-height: 0;
  min-width: var(--kcol-min);
  overflow: hidden;
  background: var(--surface-panel);
}

.kcol-head {
  display: flex;
  align-items: center;
  gap: var(--sp-2);
  height: var(--bar-h);
  padding: 0 var(--pad-panel);
  border-bottom: 1px solid var(--rule);
  color: var(--kcol, var(--ink-dim));
  border-bottom-color: var(--rule);
  background: linear-gradient(90deg, color-mix(in srgb, var(--kcol, var(--teal)) 8%, transparent), transparent 62%);
}

.kcol-name {
  font-size: var(--fs-xs);
  letter-spacing: var(--track-label);
  text-transform: uppercase;
  font-weight: 600;
}

.kcol-count {
  margin-left: auto;
  font-family: var(--mono);
  font-size: var(--fs-sm);
  color: var(--ink-dim);
}

.kcol-wip {
  font-family: var(--mono);
  font-size: var(--fs-xs);
  color: var(--ink-faint);
}

/* URGENT is composed rather than given a hue of its own: green because the work
   IS done bar one human act, barred because it is held. The token block says so
   in full and the card carries it at .tcard-edge. The COLUMN HEADING never got
   the other half. Measured on the live board, the nine heads read backlog
   9.17:1, ready 7.93, doing 7.29, review 10.37 and dead 6.85 against --void,
   every pair separated, and then urgent rgb(85,200,149) against done
   rgb(85,200,149): 1.00:1, byte-identical, the most alarming column and the
   finished one indistinguishable at a glance across the board.

   The bar is the boundary rather than a hatch, for the reason --hatch-edge
   gives: this surface carries type, and a 4px-period gradient behind a 10px
   uppercase word reads as a rendering fault. A dashed rule under the heading is
   a channel of its own and crosses no glyph. */
.kcol[data-column='urgent'] .kcol-head {
  border-bottom-style: dashed;
  border-bottom-color: color-mix(in srgb, var(--status-urgent) 66%, transparent);
}

/* Over the WIP limit is a statement about the fleet, not about one card, so it
   is said in the column header where it cannot be scrolled away from. */
.kcol.is-breach .kcol-head {
  background-image: repeating-linear-gradient(135deg, color-mix(in srgb, var(--red) 14%, transparent) 0 5px, transparent 5px 10px);
  border-bottom-color: var(--red);
}

.kcol.is-breach .kcol-wip { color: var(--red); font-weight: 700; }

.kcol-body {
  flex: 1;
  min-height: 0;
  overflow-y: auto;
  padding: var(--pad-panel);
}

.kcol.is-over .kcol-body {
  box-shadow: inset 0 0 0 1px var(--teal);
  background: var(--accent-wash);
}

/* The same highlight, in the room's colour for no. Every column used to light
   the teal one for every card, so dragging a hard-blocked ticket onto DOING got
   the identical affirmative edge and then a 409 several lines long. Hatched as
   well as red, because the two states have to be told apart at the edge of
   vision by somebody whose eye is on the card, not on the column. The drop is
   still allowed to happen and the server still answers it: see dropVerdict in
   web/app.js for why the client warns but never refuses. */
.kcol.is-over-refused .kcol-body {
  box-shadow: inset 0 0 0 1px var(--red);
  background-color: color-mix(in srgb, var(--red) 8%, transparent);
  background-image: repeating-linear-gradient(135deg, color-mix(in srgb, var(--red) 14%, transparent) 0 6px, transparent 6px 12px);
}

/* One line, two meanings, and they are not the same news. "nothing in review"
   is the board telling the truth about itself; "3 hidden by the filter" is the
   board withholding three cards you asked for, which is a fact about YOUR state
   and not the project's, so it is the brighter of the two. Before this a column
   emptied by a filter and a column that was simply empty were both a blank box
   about a thousand pixels tall with textContent exactly ''. */
.kcol-hidden {
  padding: var(--sp-2) var(--sp-h);
  font-family: var(--mono);
  font-size: var(--fs-xs);
  color: var(--ink-faint);
}

.kcol-hidden[data-kind='filtered'] { color: var(--ink-dim); }

/* ---- task card ------------------------------------------------------------ */

.tcard {
  position: relative;
  border: 1px solid var(--rule);
  cursor: grab;
  --card: var(--status-backlog);
  margin-bottom: var(--sp-2);
  padding: var(--sp-2h) var(--sp-2h) var(--sp-2h) var(--sp-4);
  overflow: hidden;
  border-color: var(--rule);
  border-radius: var(--radius);
  background: var(--wash-2);
  box-shadow: var(--shadow-1);
  transition: border-color 160ms ease, background 160ms ease, box-shadow 160ms ease, transform 160ms ease, opacity 160ms ease;
  /* Every card on the board is in the DOM and laid out, including the ones far
     off the bottom of their column. On a 388-task board the tallest column
     holds all 388 cards over 46,838px of scrollHeight inside a 1,044px
     viewport: 97.8% of it is laid out where nobody can see it, and relaying it
     out costs roughly 0.03ms per card on every invalidation.

     These two lines let the engine skip layout and paint for a card outside the
     viewport, and contain-intrinsic-size gives it a placeholder height so the
     scrollbar and the scroll position stay honest while it is skipped.

     Measured A/B in the control room at 388 cards, 15 samples per arm:
     full-board forced layout 12.4ms with it off, 2.3ms with it on (81% off),
     and back to 2.8ms when the override is removed. Checked at the same time:
     scrollIntoView still reaches a card 300 rows down (scrollTop 0 -> 35,628),
     its text is rendered when it arrives, and cards stay draggable.

     110px is the MEDIAN rendered card height at the default density, measured on
     the seeded board: 76.2, 92.4, 110.4 x6 and 112.4 x2. The 114 that was here
     matched no card on the board and was 37.8px taller than the shortest, so a
     column's scroll height and its scrollbar were wrong until every card in it
     had been rendered once. `auto` in front of it means the engine only uses the
     estimate until then, and the card's real height afterwards. */
  content-visibility: auto;
  contain-intrinsic-size: auto 110px;
}

.tcard[data-status='backlog'] { --card: var(--status-backlog); }

.tcard[data-status='ready'] { --card: var(--status-ready); }

.tcard[data-status='doing'] { --card: var(--status-doing); }

.tcard[data-status='review'] { --card: var(--status-review); }

.tcard[data-status='blocked'] { --card: var(--status-hard-blocked); }

.tcard[data-status='urgent'] { --card: var(--status-urgent); }

.tcard[data-status='done'] { --card: var(--status-done); }

.tcard[data-status='dead'] { --card: var(--status-dead); }

/* THE SAME EIGHT DECISIONS, on the pill in the head of the ticket that card
   opens. It was being handed the SESSION vocabulary instead: app.js folded the
   eight board statuses onto starting/idle/working/error and --state-idle is
   --green, so backlog, ready, blocked, review and done all measured
   rgb(85,200,149) and only doing and done were right, done by accident. A red
   hard-blocked card therefore opened a green pill 8px from its own red HARD
   BLOCK pill.

   Written as a second mapping rather than folded into the group above because
   the card carries the hue on --card, which paints an edge, and the pill
   carries it on its own ink and border. test/web-markup.test.mjs reads both
   and asserts they are one decision, which is the only thing that stops two
   selector shapes drifting the way --status-review already drifted once. */
.pill[data-status='backlog'] { color: var(--status-backlog); }

.pill[data-status='ready'] { color: var(--status-ready); }

.pill[data-status='doing'] { color: var(--status-doing); }

.pill[data-status='review'] { color: var(--status-review); }

.pill[data-status='blocked'] { color: var(--status-hard-blocked); }

/* Two declarations, because --status-urgent IS --status-done, deliberately:
   urgent is finished work held by one human act, so the token block composes it
   rather than giving it a hue of its own, and the column head and the card edge
   carry the other half as a dashed bar and a hatch. A pill is too small for
   either and its whole boundary is one stroke, so the stroke is the bar.
   Without it, NEEDS YOU and DONE open the same green pill, which is the exact
   collision the column headings were fixed for. */
.pill[data-status='urgent'] { color: var(--status-urgent); border-style: dashed; }

.pill[data-status='done'] { color: var(--status-done); }

.pill[data-status='dead'] { color: var(--status-dead); }

/* Hatched like blocked, because both are held, but green rather than faint:
   this one is held at the END. Without the hatch an urgent card is
   indistinguishable from a done one, which is the whole distinction. */
.tcard[data-status='urgent'] .tcard-edge { background-image: var(--hatch-edge); }

.tcard-edge {
  position: absolute;
  left: 0;
  top: 0;
  bottom: 0;  background: var(--card);
  width: 4px;
  border-radius: var(--radius) 0 0 var(--radius);
}

/* Soft means a worker can still ask for the human action, so it stays textured
   and calm. Hard means no worker may take it, and is deliberately red. */
.tcard[data-blocked='1'] {
  background: linear-gradient(90deg, color-mix(in srgb, var(--ink-dim) 8%, transparent), transparent 45%), var(--wash-1);
}

.tcard[data-blocked='1'] .tcard-edge { background-image: var(--hatch-edge); }

.tcard[data-block-kind='soft'] { --card: var(--status-soft-blocked); }

.tcard[data-block-kind='hard'] {
  --card: var(--status-hard-blocked);
  border-color: color-mix(in srgb, var(--red) 42%, transparent);
  background: color-mix(in srgb, var(--red) 14%, transparent);
  box-shadow: var(--shadow-1);
}

.tcard[data-block-kind='hard'] .tcard-edge { background-image: none; }

.tcard[data-status='done'],
.tcard[data-status='dead'] { --ink: var(--ink-dim); border-color: var(--rule); }

.tcard:hover {
  border-color: color-mix(in srgb, var(--card) 42%, var(--edge));
  background: color-mix(in srgb, var(--card) 8%, var(--wash-1));
  box-shadow: var(--shadow-2);
  transform: translateY(-1px);
}

.tcard.is-selected {
  border-color: var(--teal);
  box-shadow: 0 0 0 1px var(--accent-glow), var(--shadow-2);
}
/* The board's primary gesture used to take the card being dragged to 0.7,
   which made the thing under the pointer the least readable object on screen:
   its id, priority and labels measured 3.27:1. The drag is already said by the
   grabbing cursor and the shadow; it is said here by the accent border, which is
   the same language selection uses because a dragged card IS the selected one. */
.tcard.is-dragging {
  cursor: grabbing;
  border-color: var(--teal);
  box-shadow: var(--shadow-2);
  transform: none;
}

/* And a card a filter has pushed into the background. 0.55 took the same three
   fields to 2.44:1 while the title survived at 5.32, so everything identifying
   the card went and the card stayed. A flatter border and a recessive ink
   instead: both measurable, both above 4.5. */
.tcard.is-dim { border-color: var(--rule); box-shadow: none; }

.tcard.is-dim .tcard-title { color: var(--ink-dim); }

.tcard.is-dim .tcard-edge { opacity: 0.45; }

.tcard-top {
  display: flex;
  /* Centred in a fixed row rather than baseline-aligned. The owner pill is 16px
     and the id beside it is 14px, and a shared baseline is computed from the
     TALLEST item, so a card with an assignee pushed its own id down 1px and its
     title down 2px. Cards in adjacent columns did not share a first line: on
     the measured board T-0015 had its id at y=217 and its title at y=235 while
     T-0001, top-aligned with it, had them at 218 and 237. A fixed row height
     means the pill's presence cannot move anything under it. */
  align-items: center;
  gap: var(--sp-1h);
  min-height: 16px;
}

.tcard-id {
  font-family: var(--mono);
  font-size: var(--fs-sm);
  font-weight: 700;
  color: var(--card);
}

.tcard-pri {
  font-family: var(--mono);
  font-size: var(--fs-xs);
  color: var(--ink-faint);
}

.tcard[data-priority='1'] .tcard-pri,
.tcard[data-priority='2'] .tcard-pri { color: var(--ink); font-weight: 700; }

/* An explicit height. This was the one pill on the board with none, so it sized
   from its own padding to 16px while the four other bordered pills on the same
   screen rendered 18, 22 and 26: one visual idiom at four heights. It is a
   readout of who holds the card, so it takes --pill-h. */
.tcard-owner {
  display: inline-flex;
  align-items: center;
  height: var(--pill-h);
  margin-left: auto;
  padding: 0 var(--sp-1h);
  border: 1px solid var(--edge-control);
  background: transparent;
  font-family: var(--mono);
  font-size: var(--fs-xs);
  color: var(--ink-dim);
  white-space: nowrap;
  cursor: pointer;
}

/* The one place amber is allowed on this screen, and it is earned: this card's
   agent has stopped to ask a question, which no other view of the board says. */
.tcard-owner[data-state='awaiting-input'] {
  border-color: var(--amber);
  color: var(--on-warn);
  font-weight: 700;
  /* Plain amber, for the reason .pill[data-state='awaiting-input'] gives: the
     hatch it used to carry crossed its own glyphs. */
  background: var(--amber);
}

.tcard-owner[data-state='working'] { border-color: color-mix(in srgb, var(--blue) 42%, transparent); color: var(--blue); }

.tcard-owner[data-state='person'] { border-color: var(--accent-line); color: var(--teal); cursor: default; }

.tcard-owner[data-state='exited'],
.tcard-owner[data-state='error'],
/* Held by a session that no longer exists: nothing will ever move this card,
   and nothing else on the screen would say so. */
.tcard-owner[data-state='gone'] { border-color: color-mix(in srgb, var(--red) 42%, transparent); color: var(--red); }

.tcard-owner[data-state='gone'] { border-style: dashed; }

.tcard-title {
  margin: var(--sp-1) 0 var(--sp-1);
  font-size: var(--fs-md);
  line-height: 1.35;
  color: var(--ink);
  display: -webkit-box;
  -webkit-line-clamp: 3;
  -webkit-box-orient: vertical;
  overflow: hidden;
}

.tcard-meta {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--sp-1);
  font-size: var(--fs-xs);
  letter-spacing: var(--track-label);
  text-transform: uppercase;
  color: var(--ink-faint);
}

.tcard-meta .tag {
  padding: 0 var(--sp-1);
  border: 1px solid var(--rule);
  color: var(--ink-faint);
}

.tcard-meta .tag-block {
  border-color: var(--edge);
  border-style: dashed;
  color: var(--ink-dim);
}

.tcard-meta .tag-hard-block {
  border-color: color-mix(in srgb, var(--red) 66%, transparent);
  color: var(--red);
  background-color: color-mix(in srgb, var(--red) 8%, transparent);
  background-image: none;
}

.pill-block[data-block-kind='hard'] {
  border-color: color-mix(in srgb, var(--red) 66%, transparent);
  color: var(--red);
}

.pill-block[data-block-kind='soft'] {
  border-color: var(--edge);
  border-style: dashed;
  color: var(--ink-dim);
}

.tcard-meta .tag-crit {
  border-color: color-mix(in srgb, var(--rose) 42%, transparent);
  color: var(--rose);
}

.tcard-meta .tag-hold { border-color: var(--amber); border-style: dashed; color: var(--amber); }

.tcard-meta .tag-cycle { border-color: var(--red); color: var(--red); }

.tcard-meta .tag-attempts { color: var(--ink-dim); }

.tcard-meta .tag-key { font-family: var(--mono); letter-spacing: 0; }

/* "+3", for the labels a 110px card has no room for. Borderless on purpose: it
   is not a label, it is the card saying it is holding some. The names are in
   its title attribute, which is where a card that cannot show a thing puts it. */
.tcard-meta .tag-more { border-color: transparent; font-family: var(--mono); letter-spacing: 0; }

/* A refusal is the API's own sentence, kept on the card that was dragged rather
   than only in a toast: the toast is gone in five seconds and the question
   "why did that not move" outlives it. */
.tcard-refusal {
  margin-top: var(--sp-1h);
  padding: var(--sp-1) var(--sp-1h);
  border-left: 2px solid var(--red);
  background: color-mix(in srgb, var(--red) 8%, transparent);
  font-family: var(--mono);
  font-size: var(--fs-xs);
  line-height: 1.4;
  color: var(--on-wash-alarm);
}

.tcard.is-refused { animation: shake 240ms ease; }

@keyframes shake {
  25% { transform: translateX(-3px); }
  75% { transform: translateX(3px); }
}

/* ---- dock ----------------------------------------------------------------- */

.dock {
  display: flex;
  flex-direction: column;
  min-width: 0;
  min-height: 0;
  gap: var(--sp-2);
  background: transparent;
}

.dock-panel {
  display: flex;
  flex: 1 1 50%;
  flex-direction: column;
  min-height: 0;
  background: var(--rack);
}

.deck-body[data-dock] .dock-panel { flex: 1 1 auto; }

/* --sp-2 vertically, the same declaration .rail-body carries. The dock and the
   rail share one header component and one appearance and disagreed about their
   body padding by 2px, so the first row of content sat lower in the dock than in
   the rail. The gap-level version of this fault is recorded on .rail above; this
   is the other half of it. */
.dock-body {
  flex: 1;
  min-height: 0;
  overflow: auto;
  padding: var(--sp-2) var(--pad-panel);
}

/* Both halves, and the second is the load-bearing one. `.depmap { display:
   block }` is an author rule, and the global `[hidden] { display: none
   !important }` above it does win on !important, but only once the attribute is
   actually there: renderMap() used to hide the map with `ui.depmap.hidden =
   true`, which does nothing at all on an <svg>. The attribute rule is restated
   here so the map's own display rule cannot outlive it if the guard ever moves. */
.depmap { display: block; }

.depmap[hidden] { display: none; }

/* ---- dead letters --------------------------------------------------------- */

/* Symmetrical. 6px above the content and 8px below put the 1px rule between two
   dead letters 8px under one card and 6px over the next, so the divider was not
   in the middle of its own gap. */
.dead {
  padding: var(--sp-2) 0;
  border-bottom: 1px solid var(--rule);
}

.dead:last-child { border-bottom: 0; }

.dead[data-status='discarded'],
.dead[data-status='requeued'] { padding-left: var(--sp-1h); border-left: 2px solid var(--rule); }

.dead[data-status='requeued'] { border-left-color: var(--green); }

.dead-top {
  display: flex;
  align-items: baseline;
  /* Four fields on one line (id, kind, reason, age) with reasons as long as
     `not-retryable`. In a narrow dock the age fell off the right-hand edge, and
     the age is the field that says whether an entry is worth reading. */
  flex-wrap: wrap;
  gap: var(--sp-h) var(--sp-2);
  font-family: var(--mono);
  font-size: var(--fs-sm);
}

.dead-id { font-weight: 700; color: var(--red); }

.dead[data-status='requeued'] .dead-id { color: var(--teal); }

.dead[data-status='discarded'] .dead-id { color: var(--ink-faint); }

.dead-kind {
  padding: 0 var(--sp-1);
  border: 1px solid var(--edge);
  border-radius: var(--radius-sm);
  background: var(--wash-1);
  font-size: var(--fs-xs);
  letter-spacing: var(--track-label);
  text-transform: uppercase;
  color: var(--ink-dim);
}

.dead-reason { color: var(--ink-dim); }

.dead-age { margin-left: auto; color: var(--ink-faint); font-size: var(--fs-xs); }

.dead-msg {
  margin: var(--sp-1) 0 0;
  font-family: var(--mono);
  font-size: var(--fs-xs);
  line-height: 1.45;
  color: var(--ink-dim);
  word-break: normal;
  overflow-wrap: anywhere;
}

.dead-foot {
  display: flex;
  align-items: center;
  /* The attempts, ticket and lane line plus its two buttons do not fit a narrow
     dock column on one row, and the requeue/discard buttons were the half that
     went over the edge. Wrapping keeps both reachable. */
  flex-wrap: wrap;
  gap: var(--sp-1) var(--sp-1h);
  margin-top: var(--sp-1h);
  font-family: var(--mono);
  font-size: var(--fs-xs);
  color: var(--ink-faint);
}

.dead-acts { display: flex; flex-wrap: wrap; gap: var(--sp-1); margin-left: auto; }

.mini {
  height: var(--ctl-h-sm);
  /* The ticket head's one horizontal padding, shared with .ticket-tab. */
  padding: 0 var(--sp-2);
  border: 1px solid var(--edge-control);
  /* The wash belongs here rather than in a shared group at the bottom of the
     file. `.dead-kind, .mini { background: var(--wash-1) }` lived down there,
     and it is (0,1,0) exactly as .mini-go and .mini-warn are, so being written
     later it overwrote BOTH of their plates: every REQUEUE, DISCARD, FAIL,
     SUPERSEDE, ASSIGN, REVIVE and APPROVE in the app rendered the same
     rgba(255,255,255,0.025) grey, and the plate the block below says was added
     so a red-green-deficient operator could tell a destructive action from a go
     one never drew at all. This rule is what `background: transparent` was
     pretending to be. */
  background: var(--wash-1);
  /* Was --ink-faint, which on this panel measured about 4.0:1 for 10px
     uppercase text with 0.1em tracking. A live control is not secondary text. */
  color: var(--ink-dim);
  font-size: var(--fs-xs);
  letter-spacing: var(--track-label);
  text-transform: uppercase;
  cursor: pointer;
}

/* A destructive action must be identifiable without a mouse on it. FAIL and
   SUPERSEDE carry .mini-warn in the markup, but the only rule for it was a
   :hover, so at rest all seven buttons in the acts row rendered byte-identical
   grey pills and a keyboard or touch operator never saw the warning at all. */
/* Hue alone was the whole signal: FAIL and SUPERSEDE had the same 22px box, the
   same fill and the same weight as LINK and UNASSIGN, and their borders measured
   1.92:1 against 1.68 on a ground where neither clears 3:1, so an operator with
   a red-green deficiency had no channel at all. A destructive action gets a
   plate, which is shape and fill, with the hue reinforcing rather than carrying. */
.mini-go {
  color: var(--teal);
  border-color: color-mix(in srgb, var(--teal) 66%, transparent);
  background: var(--accent-wash);
}

.mini-warn {
  color: var(--red);
  border-color: color-mix(in srgb, var(--red) 66%, transparent);
  background: color-mix(in srgb, var(--red) 14%, transparent);
}

.mini:not(:disabled):hover { color: var(--ink); border-color: var(--edge-hot); }

.mini-go:not(:disabled):hover { color: var(--teal); border-color: var(--teal); }

.mini-warn:not(:disabled):hover { color: var(--red); border-color: var(--red); }

.mini[data-armed='1'] { color: var(--red); border-color: var(--red); background: color-mix(in srgb, var(--red) 8%, transparent); }

/* ---- selected task -------------------------------------------------------- */

/* The panel itself is at the bottom of this file, under "the ticket panel".
   What is left here is the vocabulary its body still speaks: .detail-id,
   .detail-body, .detail-source, .detail-log, .detail-input, .hist and
   .hist-note.

   The ticket-panel rewrite orphaned seven rule groups and they sat here for a
   while under a comment saying the cleanup had happened: .detail-left,
   .detail-top, .detail-title, .detail-links (with its .ref fragment down in the
   rounding block), .detail-fail, .detail-side and .detail-acts were referenced
   by nothing in web/app.js or web/index.html. They are gone. The vocabulary
   listed above is what is actually drawn, and .hist/.hist-note are additionally
   pinned by test/web-history.test.mjs. */

.detail-id {
  font-family: var(--mono);
  font-size: var(--fs-base);
  font-weight: 700;
  color: var(--teal);
}

/* The body used to be clipped to 4.4em INSIDE an already-scrollable panel, so a
   ticket carrying a spec, acceptance criteria and a reference implementation
   (3,466 characters at the median on this board) showed about three lines of
   itself with nothing to say more existed. It flows now; `.detail` scrolls. */
.detail-body {
  margin: var(--sp-h) 0 0;
  font-size: var(--fs-sm);
  line-height: 1.5;
  color: var(--ink-dim);
  white-space: pre-wrap;
  /* Long unbroken tokens in a spec (paths, urls) must not widen the panel and
     push the actions off the edge. */
  overflow-wrap: anywhere;
}

/* Reading a whole ticket is a different job from glancing at one. It used to be
   a `read` toggle swapping 34vh for 82vh; the panel is resizable and remembers
   its own rectangle now, so the toggle has nothing left to say. */

.detail-understanding {
  margin-top: var(--sp-1h);
  padding: var(--sp-2) var(--pad-panel);
  border: 1px solid color-mix(in srgb, var(--teal) 45%, var(--rule));
  background: color-mix(in srgb, var(--teal) 5%, var(--rack));
}

/* The sources row is placed in the SAME grid as the summary above it rather
   than indented by a hard-coded 140px. The label column is fluid
   (minmax(130px, 0.24fr)) and resolved to 147px at the panel's default width,
   so the 140 never actually lined up with the sentence it belonged to and was a
   different amount wrong at every panel width. */
.understanding-layer {
  display: grid;
  grid-template-columns: minmax(130px, 0.24fr) minmax(0, 1fr);
  column-gap: var(--sp-2h);
  border-top: 1px solid var(--rule);
}

/* ::details-content, not the div. This element is a <details>, and Chrome wraps
   everything in it except the <summary> in a UA-generated ::details-content box,
   so the div below is not a grid item of this grid at all: the anonymous wrapper
   is, it auto-placed into column ONE, and the chips rendered 130px wide under
   the label rather than 142px to the right under the sentence they cite. A chip
   reading "task-description T-0001" then measured exactly 130px and wrapped its
   own CONTRACT-EXCERPT caption above itself, which made that source row 45px
   tall against 32 for an unwrapped one.

   The wrapper spans both tracks and repeats the template, so the row inside it
   lands on the same two columns the summary above it uses. Measured after:
   sources x2769 against the sentence at x2770, and the row back to 24px. */
.understanding-layer::details-content {
  grid-column: 1 / -1;
  display: grid;
  grid-template-columns: minmax(130px, 0.24fr) minmax(0, 1fr);
  column-gap: var(--sp-2h);
}

.understanding-layer .understanding-sources { grid-column: 2; }

.understanding-layer > summary {
  grid-column: 1 / -1;
  display: grid;
  grid-template-columns: minmax(130px, 0.24fr) minmax(0, 1fr);
  gap: var(--sp-2h);
  padding: var(--sp-1h) var(--sp-h);
  cursor: pointer;
}

.understanding-layer > summary b {
  color: var(--ink-dim);
  font-size: var(--fs-xs);
  letter-spacing: var(--track-label);
  text-transform: uppercase;
}

.understanding-layer > summary span { color: var(--ink); font-size: var(--fs-sm); line-height: 1.4; }

.understanding-layer[data-certainty='unknown'] > summary span { color: var(--ink-faint); font-style: italic; }

.understanding-layer[data-certainty='conflicted'] { border-left: 2px solid var(--amber); padding-left: var(--sp-1h); }

.understanding-sources { display: flex; align-items: center; gap: var(--sp-1); flex-wrap: wrap; padding: 0 0 var(--sp-1h); }

.understanding-sources > span { color: var(--ink-faint); font: var(--fs-micro)/1 var(--mono); text-transform: uppercase; }

/* Fourteen of these sit on an open ticket against five .mini actions, and as
   .btn.btn-tiny they were 26px tall against the actions' 22, wearing the same
   --edge-control boundary and the same ink. A control that scrolls you to a
   source must not out-weigh one that fails a ticket. It is a link here: no
   plate, no box, an underline that says it can be clicked, and the accent under
   the pointer. */
.understanding-sources .btn {
  height: var(--pill-h);
  padding: 0 var(--sp-1);
  border-color: transparent;
  background: transparent;
  color: var(--ink-dim);
  font-family: var(--mono);
  font-weight: 400;
  letter-spacing: 0;
  text-transform: none;
  text-decoration: underline;
  text-decoration-color: var(--edge-control);
  text-underline-offset: 2px;
}

.understanding-sources .btn:hover {
  border-color: transparent;
  background: transparent;
  box-shadow: none;
  transform: none;
  color: var(--teal);
  text-decoration-color: currentColor;
}

/* 820px: the stage restacks into one column, so the label column goes with it. */
@media (max-width: 820px) {
  .understanding-layer,
  .understanding-layer::details-content,
  .understanding-layer > summary { grid-template-columns: minmax(0, 1fr); gap: var(--sp-h); }
  .understanding-layer .understanding-sources { grid-column: 1; }
}

/* ---- the ticket and its log ----------------------------------------------- */

/* Side by side, because stacked they are not both readable: the median body on
   this board is 3,300 characters, so a log underneath one starts below the
   bottom of the strip and stays there. The log takes the narrower half; its
   notes are sentences, the body is a spec. */
/* One column by default and two only when the PANEL is wide enough, which is
   not the same question as whether the window is: the panel is resizable now,
   so a 1600px monitor holding a 420px ticket must still stack. Two columns of
   forty characters each is worse than either of them full width. The measured
   threshold below which the pair stops being readable is 860px of panel. */
.detail-panes {
  display: grid;
  grid-template-columns: minmax(0, 1fr);
  gap: var(--sp-1) var(--sp-5);
  align-items: start;
  margin-top: var(--sp-h);
}

.ticket[data-wide='1'] .detail-panes {
  grid-template-columns: minmax(0, 1fr) minmax(0, 0.8fr);
}

.detail-body.is-empty { color: var(--ink-faint); font-style: italic; }

.detail-log { min-width: 0; }

.detail-source {
  min-width: 0;
}

/* Four, so eight tiles are two clean rows. Three left the block ending on a
   visible notch: a 91 x 40 hole in its own bottom-right corner. */
.overview-counts {
  display: grid;
  grid-template-columns: repeat(4, minmax(0, 1fr));
  /* One height for all eight, whatever their labels do. Four columns is narrow
     enough that "needs you" wraps and "backlog" does not, which put the two
     rows 11px apart and made the block ragged in the other direction. */
  grid-auto-rows: 1fr;
  gap: var(--sp-1);
}

/* A two-row grid, so the four numbers in a row start on one line. grid-auto-rows
   levels the eight tile BOXES, which the block above explains, and nothing then
   levelled what was inside them: "needs you" is the one label of the eight that
   wraps to two lines, so its number sat 5.5px above the three beside it (y1105
   against y1110.5). The number takes row one and the label grows downward into
   the 1fr the tile already has. */
.overview-count {
  display: grid;
  grid-template-rows: auto 1fr;
  align-content: start;
  min-width: 0;
  padding: var(--sp-1) var(--sp-1h);
  border: 1px solid var(--edge-control);
  background: var(--rack-2);
  color: var(--ink-dim);
  text-align: left;
  cursor: pointer;
  transition: border-color 160ms ease, background 160ms ease, transform 160ms ease;
}

.overview-count:hover,
.overview-count:focus-visible {
  border-color: var(--teal);
  color: var(--ink);
  background: var(--accent-wash);
}

.overview-count:focus-visible { outline-offset: -2px; }

.overview-count b { display: block; color: var(--ink); font: 700 var(--fs-lg)/1 var(--mono); }

.overview-count span { display: block; margin-top: var(--sp-h); overflow: hidden; font-size: var(--fs-micro); text-overflow: ellipsis; text-transform: uppercase; }

.overview-count[data-status='blocked'] { border-style: dashed; border-color: var(--ink-faint); }
/* :not(.is-zero) on both, and it is the whole point. These two were the only
   coloured numbers in an eight-tile grid, and on an ordinary morning they were
   also the only two ZEROS in it: "0 needs you" green at 9.27:1 and "0 dead" red
   at 6.85:1, pulling the eye to the two tiles with nothing to report while the
   six counts that had something were all --ink. A tile is coloured by its
   value, not by its name. See the zero block above the deck chips. */
.overview-count[data-status='urgent']:not(.is-zero) b { color: var(--status-urgent); }

.overview-count[data-status='dead']:not(.is-zero) b { color: var(--red); }

.overview-days { margin-top: var(--sp-2); border-top: 1px solid var(--rule); padding-top: var(--sp-1h); }

.overview-days-head { display: flex; justify-content: space-between; color: var(--ink-faint); font-size: var(--fs-micro); letter-spacing: var(--track-label); text-transform: uppercase; }

.overview-day { display: grid; grid-template-columns: 54px minmax(0, 1fr) 18px; gap: var(--sp-1h); align-items: center; margin-top: var(--sp-1); font: var(--fs-xs)/1 var(--mono); color: var(--ink-faint); }

.overview-day-track { height: 3px; background: var(--rack-3); }

.overview-day-track i { display: block; height: 100%; background: var(--teal); }

.overview-day strong { color: var(--ink-dim); text-align: right; }

.detail-activity-pane { min-width: 0; }

.detail-source-meta,
.detail-source-warn {
  margin: var(--sp-1) 0;
  font-size: var(--fs-xs);
  line-height: 1.45;
  color: var(--ink-faint);
}

.detail-source-warn {
  padding: var(--sp-1h) var(--sp-2);
  border-left: 2px solid var(--amber);
  color: var(--amber);
  background: color-mix(in srgb, var(--amber) 8%, transparent);
}

/* The imported Jira description folds away, but its capture stamp and its
   completeness line stay on the summary. An import that is stale or partial is
   exactly the thing that must not be hidden behind a twisty. */
.source-part { margin-top: var(--sp-2h); }

details.source-part > summary {
  display: flex;
  align-items: baseline;
  flex-wrap: wrap;
  gap: var(--sp-2);
  padding: var(--sp-1) 0;
  cursor: pointer;
}

details.source-part > summary b {
  color: var(--teal);
  font-size: var(--fs-xs);
  letter-spacing: var(--track-label);
  text-transform: uppercase;
}

details.source-part > summary .detail-source-meta { margin: 0; }

.source-part h4 {
  margin: 0 0 var(--sp-1);
  color: var(--teal);
  font-size: var(--fs-xs);
  letter-spacing: var(--track-label);
  text-transform: uppercase;
}

.source-entry {
  margin: 0 0 var(--sp-1h);
  padding: var(--sp-1) var(--sp-2);
  border-left: 1px solid var(--edge);
}

.source-entry-head {
  color: var(--ink-faint);
  font-size: var(--fs-xs);
}

.source-entry-body,
.source-change {
  margin: var(--sp-h) 0 0;
  color: var(--ink-dim);
  font-size: var(--fs-sm);
  line-height: 1.45;
  white-space: pre-wrap;
  overflow-wrap: anywhere;
}

.detail-log-head {
  display: flex;
  align-items: baseline;
  gap: var(--sp-2);
  padding-bottom: var(--sp-h);
  border-bottom: 1px solid var(--rule);
}

.detail-log-head h3 {
  margin: 0;
  color: var(--teal);
  font-size: var(--fs-xs);
  font-weight: 600;
  letter-spacing: var(--track-label);
  text-transform: uppercase;
}

/* .detail-log-empty is gone from here and from web/app.js. It was this exact
   treatment, 10px uppercase at 0.1em tracking, wrapped around the 82-character
   sentence "Nothing has been recorded against this task yet. Add a note above
   when work happens." A counter beside a heading can be uppercase; a sentence
   cannot. The activity, runs and links tabs all take .rail-empty now, which is
   the sentence-case 12px state the other eleven call sites in the room use and
   the only one with a loading and an error sibling to go with it. */
.detail-log-head span {
  font-size: var(--fs-xs);
  letter-spacing: var(--track-label);
  text-transform: uppercase;
  color: var(--ink-faint);
}

.hist {
  padding: var(--sp-1) 0 var(--sp-1) var(--sp-2);
  border-left: 1px solid var(--edge);
}

.hist + .hist { border-top: 1px solid var(--rule); }

/* The newest entry is the one somebody opened this panel for. Every note reads
   at full contrast now, so what marks it is the rule down its left edge. */
.hist:first-of-type { border-left-color: var(--teal); }

.hist[data-source='jira-comment'] { border-left-color: color-mix(in srgb, var(--teal) 55%, var(--rule)); }

.hist[data-source='jira-change'] { border-left-color: color-mix(in srgb, var(--amber) 55%, var(--rule)); }

/* The header line above a note. 11px rather than 10px, and the tracking loosened
   a little, because it carries who wrote the thing you are about to read. */
.hist-top {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: var(--sp-1h);
  font-size: var(--fs-sm);
  letter-spacing: var(--track-label);
  text-transform: uppercase;
  color: var(--ink-faint);
}

.hist-event { color: var(--ink-dim); font-weight: 600; }

.hist-source {
  padding: var(--sp-h) var(--sp-1);
  border: 1px solid var(--edge);
  color: var(--teal);
  font-family: var(--mono);
  font-size: var(--fs-xs);
  letter-spacing: 0;
  text-transform: none;
}

.hist[data-source='jira-change'] .hist-source { color: var(--amber); }

.hist[data-event='fail'] .hist-event { color: var(--red); }

.hist[data-event='blocked'] .hist-event { color: var(--amber); }

.hist[data-event='move'] .hist-event,
.hist[data-event='unblocked'] .hist-event { color: var(--green); }

.hist-flow,
.hist-by {
  font-family: var(--mono);
  font-size: var(--fs-xs);
  letter-spacing: 0;
  text-transform: none;
}

.hist-flow { color: var(--ink-faint); }

.hist-by { color: var(--teal); }

/* Pushed to the right edge so the ages form a column you can read down. */
.hist-when { margin-left: auto; white-space: nowrap; }

/* The reading face, the same one .ticket-prose uses.
   The readability rework reached the description and stopped there, and on a
   worked ticket the activity log is where most of the information is: every
   agent note and every imported Jira comment is a .hist-note, so the majority
   of the panel's text was still 11px var(--ink-dim) sitting directly beside
   14px/1.62 var(--ink) prose. The mono face stays on the metadata beside it
   (.hist-source, .hist-by, .hist-flow, .hist-when), which is what it is for. */
.hist-note {
  margin: var(--sp-h) 0 0;
  max-width: 68ch;
  font: var(--fs-lg)/1.6 var(--ui);
  color: var(--ink);
  white-space: pre-wrap;
  /* A commit sha, a path or a URL in a release note must not widen the column
     and push the actions off the edge of the panel. */
  overflow-wrap: anywhere;
}

/* A field-by-field Jira change is a data row rather than prose, so it keeps the
   smaller face: a wall of "status: To Do -> In Progress" at reading size would
   drown the notes this rework exists to make readable. */
.source-change.hist-note {
  font: var(--fs-sm)/1.5 var(--mono);
  color: var(--ink-dim);
}

.activity-note-form {
  margin: 0 0 var(--sp-2h);
  padding: var(--sp-1h);
  border: 1px solid var(--edge);
  background: color-mix(in srgb, var(--rack) 90%, var(--teal));
}

.activity-note-label {
  display: block;
  margin-bottom: var(--sp-1);
  color: var(--teal);
  font-size: var(--fs-xs);
  font-weight: 600;
  letter-spacing: var(--track-label);
  text-transform: uppercase;
}

.activity-note-form textarea {
  box-sizing: border-box;
  width: 100%;
  min-height: 62px;
  resize: vertical;
  padding: var(--sp-1h) var(--sp-1h);
  font-size: var(--fs-sm);
  line-height: 1.45;
}

.activity-note-actions {
  display: flex;
  justify-content: flex-end;
  align-items: center;
  gap: var(--sp-1h);
  margin-top: var(--sp-1);
}

.activity-note-state { color: var(--ink-faint); font-size: var(--fs-xs); }

.detail-input {
  display: flex;
  gap: var(--sp-1);
}

.detail-input input {
  width: 190px;
  height: var(--ctl-h-sm);
  padding: 0 var(--sp-1h);
  font-size: var(--fs-sm);
}

/* One height for the whole acts row: this used to be a 19px select beside six
   16px buttons. */
.ticket select {
  height: var(--ctl-h-sm);
  padding: 0 var(--chev-pad) 0 var(--sp-1h);
  font-size: var(--fs-sm);
  letter-spacing: var(--track-label);
  text-transform: uppercase;
}

/* ---- dialog extras -------------------------------------------------------- */

.field-row {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: var(--sp-3);
}

/* ---- narrow screens ------------------------------------------------------- */

/* The topbar sheds its readouts in order of how well the panes themselves
   already say the same thing. The awaiting-input alert and the slot gauge are
   the last to go, because neither is legible anywhere else. */
@media (max-width: 1180px) {
  .stage { grid-template-columns: var(--coord-w) minmax(0, 1fr); }
  .rail { display: none; }
  /* And the control that claims to open it. It kept aria-pressed="true" and its
     lit on-state plate at every width below this, so the topbar carried a
     switch that was on for a column that could not exist. */
  #btn-rail { display: none; }
  .hint { display: none; }
  body[data-screen='board'] .hint-board { display: none; }
  /* The dock goes under the columns rather than off the side: the map and the
     dead letters are both worth less than a kanban squeezed to two columns. */
  .deck-body { grid-template-columns: minmax(0, 1fr); grid-template-rows: minmax(0, 1fr) minmax(180px, 34%); }
  .dock { flex-direction: row; }
  .deck-body[data-dock] .dock, .deck-body[data-view='dlq'] .dock { grid-row: 1 / -1; }
  .deck-body[data-dock], .deck-body[data-view='dlq'] { grid-template-rows: minmax(0, 1fr); }
}

@media (max-width: 1000px) {
  .gauge-bar { display: none; }
  .attention-text { display: none; }
}

@media (max-width: 820px) {
  .stage { grid-template-columns: minmax(0, 1fr); grid-template-rows: minmax(200px, 40%) minmax(0, 1fr); }
  .commandbar { grid-template-columns: auto minmax(0, 1fr) auto; }
  .commandbar .broadcast, .commandbar .check { display: none; }
  .secrets-head { flex-direction: column; padding-bottom: var(--sp-2h); }
  .secrets-head-actions { width: 100%; }
  .deck-bar { flex-wrap: wrap; }
  .chips { order: 3; flex-basis: 100%; }
  .field-row { grid-template-columns: 1fr; gap: 0; }
  .operator-brief-item > summary { grid-template-columns: 1fr; gap: var(--sp-1); }
  .operator-brief-item-meta { justify-self: start; }
  .operator-brief-actions p { display: none; }
  .mailbox-head { flex-direction: column; padding-bottom: var(--sp-2h); }
  .mailbox-head-actions { width: 100%; flex-wrap: wrap; }
  .mailbox-head-actions select { flex: 1 1 180px; }
  .dlg .mailbox-compose { grid-template-columns: minmax(0, 1fr); }
  .mailbox-thread { max-height: calc(88vh - 275px); }
  .mailbox-message-actions { align-items: stretch; flex-wrap: wrap; }
  .mailbox-reply-field { flex-basis: 100%; }
}

/* ---- short screens --------------------------------------------------------
   A window can be shortened as well as narrowed, and the topbar is allowed to
   wrap onto two or three rows, so on a 380px-tall window it would take a third
   of the height before a single pane was drawn. Below this the bar goes back to
   one compact row and drops the readouts, because on a screen this short the
   panes themselves are the only thing worth the space. */
@media (max-height: 560px) {
  :root { --head-h: 40px; }

  .topbar {
    row-gap: var(--sp-1);
    padding: var(--sp-1) var(--sp-3);
  }

  .usage-strip,
  .tally {
    display: none;
  }

  .shell { padding: var(--sp-1); gap: var(--sp-1); }
}

@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after { animation-duration: 0.001ms !important; animation-iteration-count: 1 !important; }
  .pane[data-state='awaiting-input'] .pane-edge { opacity: 1; }
}

/* ---- the project clock ---------------------------------------------------- */

/* This element is the rail body, so its horizontal inset has to be the rail's
   own --pad-panel: at 18px its content was 10px narrower than every other
   block's on both sides, which is why the timer read as a different panel from
   the four around it.

   And the VERTICAL inset is the rail's too. It was --sp-3 while all five of its
   neighbours were --sp-2, so the one block in the column with a boundary rhythm
   of its own was the one the operator said looked bolted on. If a clock face
   needs air it takes it on the face, not on the panel body.

   Renamed from .video-timer on 2 September 2026, when the recording countdown
   that gave the class its name left the panel. A body named after a face it no
   longer holds is the sort of name that sends the next fix to the wrong rule. */
.project-clock-body {
  position: relative;
  display: flex;
  flex-direction: column;
  justify-content: center;
  padding: var(--sp-2) var(--pad-panel);
}

.project-clock-body::before {
  content: '';
  position: absolute;
  inset: 0;
  pointer-events: none;
  background:
    radial-gradient(circle at 50% 16%, var(--accent-tint), transparent 48%),
    linear-gradient(135deg, color-mix(in srgb, var(--rose) 8%, transparent), transparent 46%);
}

.project-clock-body > * {
  position: relative;
}

/* The daemon's number, and since 2 September 2026 the only face in the block.
   It carried a divider underneath (10px of padding, 10px of margin and a 1px
   rule) whose whole job was to separate it from the recording countdown; with
   nothing below it but its own buttons, that rule was drawing a boundary
   through the middle of one component. */
.project-clock {
  text-align: center;
}

.project-clock-head {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: var(--sp-1h);
  color: var(--ink-faint);
  font-size: var(--fs-micro);
  letter-spacing: var(--track-brand);
  text-transform: uppercase;
}

/* --ink-faint at full strength, which is the state `stopped` wears, because
   `stopped` is the default: projectClockView falls through to it and returns it
   for the no-row case, so it is what every operator sees on first paint and on
   any project whose coordinator is not running. It carried opacity: 0.4, and
   measured against the real tokens on 2 September 2026 that composited to
   1.85:1 in the dark theme and 1.74:1 in the light one, against the 3:1 WCAG
   1.4.11 asks of a meaningful non-text graphic; the same ink at full opacity is
   5.16:1 and 5.26:1. This file already diagnosed exactly this failure on the
   disabled block: never express a state as an alpha on the dimmest ink in the
   file. The four states separate on hue instead, and all four pass. */
.project-clock-dot {
  width: var(--dot);
  height: var(--dot);
  border-radius: 50%;
  background: var(--ink-faint);
}

/* The caption line is the dot and the state word, and nothing else. The
   project's own name used to sit between them in a .project-clock-project
   clamped to 55% of the block, which spent a row of a rail panel repeating the
   word the operator had just chosen in the selector two panels above it. The
   state word stays and so does the meta line under the face: a clock that says
   nothing about WHY it is not moving is the defect the comments in this block
   have warned about twice. */
.project-clock-value {
  display: block;
  /* Measured down this panel, seven consecutive gaps were seven different
     numbers (7, 5, 21, 9, 19, 10, 12.31), four of which were on no grid at all.
     Both of these are on the scale. */
  margin: var(--sp-2) 0 var(--sp-1h);
  color: var(--ink);
  /* 1.14 rather than 1: a display face on line-height 1 clips its own digit
     tails and draws a flat-bottomed 0. And --fs-face-lg, the larger of the two
     reading steps, because this is the face the block is named after and now
     the only one in it. */
  font: 600 var(--fs-face-lg)/1.14 var(--mono);
  letter-spacing: var(--track-face);
  font-variant-numeric: tabular-nums;
}

.project-clock-meta {
  display: block;
  color: var(--ink-faint);
  font-size: var(--fs-xs);
}

/* Two buttons for three verbs, one column each. That is what "a primary and a
   secondary action side by side differ in fill, not in width" means, and the
   1fr/0.5fr this row was first written with still measured 174px against 88, a
   1.98:1 ratio. The app's dialogs already do it: #project-cancel and
   #project-go are both 96px. The fill carries the difference, because the
   toggle is a lit plate and RESET is a ghost. */
.project-clock-actions {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: var(--sp-1h);
  margin-top: var(--sp-2h);
}

/* --ctl-h, not --ctl-h-lg: every other control in this rail is 26px (the alias
   field, the scope select, ADD), and --ctl-h-lg is the dialog and chrome-bar
   scale. This pair was also 207px and 66px wide, a 3.15:1 ratio, which reads as
   one button with an afterthought stuck to it. */
.project-clock-actions .btn {
  height: var(--ctl-h);
}

/* PAUSE is neutral and reversible, so it keeps the primary plate it wears as
   START rather than borrowing an alarming one. The countdown this row replaces
   painted its PAUSE in --rose, which the same panel used to mean "complete", so
   one fill meant two unrelated things.

   And only while there is a daemon to press it at. An id selector scores
   (1,1,0) against the (0,2,1) of the body[data-link='offline'] .btn-go rule
   that withdraws the plate from every primary in the room, so without the guard
   on the front of this selector a running clock would hold a full lit plate all
   the way through an outage: the one control in this panel whose entire effect
   is a POST, and so the one that most needs to stop looking pressable. A plate
   is a promise, and it is drawn only where the promise can be kept. */
body:not([data-link='offline']) #project-clock-toggle[data-running='true'] {
  border-color: var(--teal);
  background: var(--accent-tint);
  color: var(--ink);
}

/* Not while it is armed, and that exclusion is the whole rule. This hover is
   (1,2,0) and the armed rule below it is (1,1,0), so the hover won and the
   second half of the two-step drew rose over the red wash. The operator's
   pointer is necessarily ON the button while they read `discard?`, so the
   alarming plate was the one appearance they never saw. Measured in Chrome on
   2 September 2026 by swapping :hover for an equal-specificity class: armed at
   rest was rgb(255,107,99) and armed under the pointer was rgb(231,164,180). */
#project-clock-reset:not(:disabled):not([data-armed='1']):hover {
  border-color: var(--rose);
  color: var(--rose);
}

/* The same armed plate a worker pane's KILL wears, because it is the same kind
   of question: one more click and a total the daemon has been accruing since
   the project's first coordinator run is gone, with no undo and nothing to
   recover it from. The alarming colour belongs on the alarming button. */
#project-clock-reset[data-armed='1'] {
  border-color: var(--red);
  background: color-mix(in srgb, var(--red) 8%, transparent);
  color: var(--red);
}

/* A running project lights the dot AND the panel edge. The dot is for the
   operator reading the face; the panel glow is the channel that carries across
   a room, and it used to belong to the recording countdown, which was a screen
   recorder's stopwatch. The coordinator's own clock has the better claim to it. */
.project-clock[data-state='running'] .project-clock-dot {
  background: var(--teal);
  opacity: 1;
}

.project-clock[data-state='running'] .project-clock-state { color: var(--teal); }

.project-clock[data-state='running'] .project-clock-value { color: var(--ink); }

/* Paused is the operator's own doing, so it is neither the live teal nor the
   amber that means the socket has gone: a solid neutral dot says a project is
   on the clock and simply not counting. Stopped and paused must not be one
   appearance, because the whole point of the pause verb is that a face which
   has stopped moving no longer means the coordinator has died. */
.project-clock[data-state='paused'] .project-clock-dot { background: var(--ink-dim); opacity: 1; }

.project-clock[data-state='paused'] .project-clock-state { color: var(--ink-dim); }

.project-clock[data-state='paused'] .project-clock-value { color: var(--ink-dim); }

/* The socket is down, so the seconds on screen are this tab's own guess: the
   coordinator may have exited a minute ago and nothing has been able to say so. */
.project-clock[data-state='stale'] .project-clock-dot { background: var(--amber); opacity: 1; }

.project-clock[data-state='stale'] .project-clock-state { color: var(--amber); }

.project-clock[data-state='stale'] .project-clock-value { color: var(--ink-dim); }

.project-clock[data-state='stopped'] .project-clock-value { color: var(--ink-dim); }

#rail-timer[data-state='running'] {
  border-color: var(--accent-line);
  box-shadow: var(--shadow-2);
}

/* The same gesture the running timer uses, for the same reason: something on
   this rail needs to be visible from across the room. A metric at 1.5x or worse
   lights the whole panel, not just its row. */
#rail-perf[data-severity='2'] {
  border-color: color-mix(in srgb, var(--red) 42%, transparent);
  box-shadow: var(--shadow-2);
}

#rail-perf[data-severity='1'] {
  border-color: color-mix(in srgb, var(--amber) 20%, transparent);
}

.btn-warn { border-color: var(--amber); color: var(--amber); }

/* The coordinator's empty state now offers a way out of itself. */
.empty-coordinator .btn { margin-top: var(--sp-2h); }

.empty-note { margin-top: var(--sp-2); font-size: var(--fs-sm); color: var(--ink-faint); min-height: 1em; }

/* ---- Echofold visual system ---------------------------------------------- */

::selection {
  background: var(--accent-line);
  color: var(--on-selection);
}

html {
  background: var(--void);
}

button:focus-visible,
input:focus-visible,
select:focus-visible,
textarea:focus-visible,
[tabindex]:focus-visible {
  outline: 2px solid var(--teal);
  outline-offset: 2px;
} Once the bar has wrapped, the elastic gap that pushed the right-hand group to
   the right has nothing left to do, and leaving it in forces a ragged first row.
   `flex-basis: 100%` on a zero-height spacer makes it a deliberate row break
   instead: brand and status above, readouts and actions below. */
/* Once the bar has wrapped, the elastic gap that pushed the right-hand group to
   the right has nothing left to do, and leaving it in forces a ragged first row.
   `flex-basis: 100%` on a zero-height spacer makes it a deliberate row break
   instead: brand and status above, readouts and actions below.

   This was 1320px, and between there and about 1600 the bar wrapped on its own
   into four and five rows, because an elastic gap wraps wherever the content
   happens to run out rather than where a row belongs. Measured at 1430px it took
   450 of 520 vertical pixels. It breaks at the same width the fleet tally goes
   now, so there is one rung here instead of two and the bar is two rows for the
   whole band down to 1180. */
@media (max-width: 1500px) {
  /* The fleet tally goes first, because every pane on the screen already says
     what it says. */
  .tally { display: none; }

  .topbar {
    padding: var(--sp-1h) var(--pad-bar);
  }

  .topbar-gap {
    flex: 0 0 100%;
    height: 0;
    margin: 0;
  }

  .gauges,
  .topbar-actions {
    flex-wrap: wrap;
    row-gap: var(--sp-1h);
  }
}

/* Narrower still, the bar would grow taller than the panes it sits above. The
   order things are shed in is the order in which the panes themselves already
   say the same thing, and each thing shed keeps a dot, a colour or a tooltip:
   nothing loses its state, only its sentence. Production Safety keeps its words
   at every width, because it is the one control here whose wrong setting is
   expensive and whose colour alone is not an unambiguous answer. */
@media (max-width: 1100px) {
  .link-label {
    display: none;
  }
}

@media (max-width: 700px) {
  .usage-strip {
    display: none;
  }
}

/* The project selector joins this group. It was the only pill in the bar drawing
   its own border and its own surface: --rule at 0.09 and --glass at 0.065,
   against 0.18 and a dark plate on its four neighbours, so its edge read at half
   their strength and its box measured 21px against their 30. --line, the token
   it used, was a duplicate of --rule with exactly one use, this one. */
.production-safety,
.attention,
.operator-brief-button,
.mailbox-button,
.project-focus {
  min-height: var(--ctl-h-lg);
  border-radius: var(--radius-sm);
  background-color: var(--surface-chip);
  backdrop-filter: blur(8px);
}

/* ---- the one readout ------------------------------------------------------

   The bar's second row held five readouts and no two of them agreed. Measured
   on the control screen: FOUR heights (30, 14, 14 and 10px) sitting on three
   different baselines, so nothing in the row shared an edge with anything else;
   FOUR type sizes (9px on the usage meters, 10px on the cost, 11px on the
   session tally, 12px on the SLOTS value) inside a bar whose one micro size is
   10px; and FOUR casings in a single line of text — "Claude Code · Max 5h"
   mixed, "API $ unknown" mixed, "SLOTS" upper and "0 sessions" lower.

   A readout is a LABEL and a VALUE. The label names the thing and the value
   says it; both are the bar's micro type, and the pair takes the same 30px band
   as every control in the row above, so the two rows of the bar share a rhythm
   rather than merely sitting near each other.

   Colour is what still separates the two halves, and it is the ladder the room
   already uses: --ink-dim names, --ink says. A value that carries a STATE keeps
   its state colour instead, which is the whole job of the usage figures and is
   why they are not listed here.

   Case is deliberately NOT set here. Uppercasing the whole row reads tidier for
   about a second and then costs you the reading: the meter names are twenty
   characters, and at --track-label they overflow the column and ellipsise, so
   the 5h pool and the 1w pool both render as "CLAUDE CODE ·" and the one thing
   the three meters exist to let you compare is the thing that gets cut. A LABEL
   is uppercase, from the micro-label rule; a VALUE is a name or a figure and
   keeps the case it came in, exactly as the project selector shows Mushroom.

   No plate on any of them: they are read, never clicked, and a plate is what a
   control wears.                                                             */
.link,
.access-warning,
.usage-pool,
.gauge,
.tally,
.usage-cost {
  min-height: var(--ctl-h-lg);
  align-items: center;
  font-family: var(--mono);
  font-size: var(--fs-xs);
}

/* Same band, same size, and still three baselines: the row measured text at
   46, 53 and 56px down. Two different causes, both invisible until you range
   over the text rather than the box.

   The cost was not a flex box, so align-items never reached it and its line sat
   at the TOP of the 30px band while its neighbours centred in it.

   The meters carry a 2px track under their figures, so what centres in the band
   is the text AND the track, which lifts the text by half the track. The other
   readouts have no track, so they get its height as padding instead and centre
   over the same 26px. Aligning the boxes was never going to be enough here: it
   is the first line of each readout that the eye reads as the row. */
.gauge,
.tally,
.usage-cost {
  padding-bottom: var(--sp-1);
}

/* Five heights sat in one bar (21, 22, 23, 26, 30) and two shape idioms, so
   nothing lined up on either edge. The bar is a 30px band: a status readout is a
   pill, an action is a --radius-sm box, and neither may be a different height
   from the other. A segmented control is a 30px wrapper holding 24px buttons,
   which is the only nesting allowed. */
/* :not(.seg-wrap) on the last of these, because the wrapping variant is a radio
   group in a form column rather than a toggle in a bar: forcing the band height
   onto it pins the wrapper to one row while its second row of profiles carries
   on drawing over the field underneath. */
.topbar-actions .btn,
.commandbar .btn,
.broadcast select,
.seg:not(.seg-wrap) { height: var(--ctl-h-lg); }

.production-safety,
.operator-brief-button,
.mailbox-button {
  transition: border-color 160ms ease, background-color 160ms ease, color 160ms ease, transform 160ms ease;
}

.production-safety:hover,
.operator-brief-button:hover,
.mailbox-button:hover {
  transform: translateY(-1px);
}

.gauge-bar,
.gauge-bar i,
.usage-pool i,
.usage-pool i::after,
.slot-ttl,
.slot-ttl i,
.overview-day-track,
.overview-day-track i {
  border-radius: var(--radius-sm);
}

/* `.btn, .mini, .card-claim, .pane-acts button { border-radius: var(--radius-sm) }`
   was here and every one of its four selectors was wrong to be. The first three
   are <button> elements, which the rule at the top of this file already gives
   --radius-sm; the fourth is listed twelve lines below in the pill group at the
   same specificity (0,1,1) and written later, so it rendered 999px and the 8px
   here never applied once. Two groups, two answers, and the file said the one
   that was not true. A pane-act is a pill: it is 18px tall and sits beside .pill
   in the same header. It is said once, in the group below. */

.btn:hover {
  border-color: var(--accent-line);
  background: var(--accent-wash);
  color: var(--ink);
  box-shadow: var(--shadow-1);
  transform: translateY(-1px);
}

.btn:active {
  transform: translateY(0);
}


.col-rule > h2,
.col-rule span,
.rail-head h2,
.kcol-name {
  font-family: var(--mono);
  font-weight: 600;
  letter-spacing: var(--track-label);
}

.coordinator-settings {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: var(--sp-1) var(--sp-1h);
  min-width: 0;
}

.coordinator-setting {
  display: inline-flex;
  align-items: center;
  gap: var(--sp-1);
  color: var(--ink-faint);
  font-size: var(--fs-micro);
  letter-spacing: var(--track-label);
  text-transform: uppercase;
  white-space: nowrap;
}

.coordinator-setting input {
  width: 38px;
  height: var(--ctl-h-sm);
  padding: 0 var(--sp-1h);
  font-size: var(--fs-sm);
  font-weight: 600;
  text-align: center;
}

.coordinator-setting small {
  color: var(--ink-faint);
  font-size: var(--fs-micro);
  letter-spacing: var(--track-label);
}

.coordinator-settings-submit {
  height: var(--ctl-h-sm);
  padding: 0 var(--sp-1h);
  border: 1px solid var(--accent-line);
  background: var(--accent-wash);
  color: var(--teal);
  font-size: var(--fs-xs);
}

/* A busy control reads as busy, which means a legible label. The opacity that
   was here dropped this button's own --teal to 2.9:1 at the moment it was
   doing the thing it had just been asked to do. The shared :disabled block
   carries the rest of it. */
.coordinator-settings-submit:disabled {
  cursor: wait;
}

.coordinator-settings-state {
  min-width: 0;
  color: var(--ink-faint);
  font-size: var(--fs-micro);
  white-space: nowrap;
}

/* --accent-line, the accented border, not --accent-glow, which is the ring. At
   0.20 against 0.42 a hovered pane's edge was half the strength of every other
   accented hover in the room. */
.pane:hover {
  border-color: var(--accent-line);
}

.pane.is-focused {
  border-color: color-mix(in srgb, currentColor 58%, var(--edge));
  box-shadow: 0 0 0 1px color-mix(in srgb, currentColor 22%, transparent), var(--shadow-2);
}

.pill,
.pane-warn,
.pane-safety,
.pane-acts button {
  display: inline-flex;
  align-items: center;
  height: var(--pill-h);
  padding: 0 var(--sp-2);
  border-radius: var(--radius-sm);
}

.pill-tiny { padding: 0 var(--sp-1h); }

.rail-block,
.dock-panel {
  overflow: hidden;
  background: var(--surface-panel);
}

.commandbar::before {
  content: '';
  position: absolute;
  inset: 0 18% auto;
  height: 1px;
  background: linear-gradient(90deg, transparent, var(--accent-line-hot), color-mix(in srgb, var(--rose) 66%, transparent), transparent);
}

.operator-brief-item,
.mailbox-message,
.overview-count {
  overflow: hidden;
  border-radius: var(--radius);
  background: var(--wash-1);
}

.operator-brief-item,
.mailbox-message { border-color: var(--edge); }

.scope-item.is-on,
.scope-item:hover {
  background: var(--accent-wash);
}

.kcol[data-column='hard-blocked'] {
  border-color: color-mix(in srgb, var(--red) 42%, transparent);
  box-shadow: var(--shadow-2);
}

.tcard-owner,
.tcard-meta .tag,
.dead-kind,
.mailbox-state {
  border-radius: var(--radius-sm);
}

.dm {
  border-radius: var(--radius);
  background: var(--well-bg);
  font-stretch: normal;
}

.overview-count:hover {
  transform: translateY(-1px);
}
@media (max-width: 1180px) {
  .shell,
  body[data-screen='board'] .deck,
  .stage,
  .deck-body {
    gap: var(--sp-2);
  }

  .shell {
    padding: var(--sp-2);
  }
}

@media (max-width: 820px) {
  :root {
    --head-h: 56px;
  }

  .shell {
    gap: var(--sp-1h);
    padding: var(--sp-1h);
    background-size: auto, auto, 28px 28px, 28px 28px, auto;
  }

  .topbar {
    padding: 0 var(--sp-2h);
  }

  .brand-mark {
    width: 28px;
    height: 28px;
    flex-basis: 28px;
  }

  .stage,
  body[data-screen='board'] .deck,
  .deck-body {
    gap: var(--sp-1h);
  }

  /* Was 244px, which is 20px WIDER than the base rule at the width with the
     least room to give: the narrowest viewport was the one shown the fewest
     columns. It reads off the token now and it goes down, not up. */
  .kcol {
    min-width: var(--kcol-min-tight);
  }
}

@media (max-width: 560px) {
  .commandbar {
    grid-template-columns: minmax(0, 1fr) auto;
  }

  .target {
    display: none;
  }

  .cmd-input {
    grid-column: 1;
  }

  #btn-send {
    grid-column: 2;
  }

  .deck-actions {
    width: 100%;
  }

  .deck-stamp {
    margin-right: auto;
  }
}

/* ---- the run record -------------------------------------------------------
   What the machine DID for a ticket, as opposed to what the agents SAID about
   it, which is the note log directly beneath. The two disagreed badly enough on
   23 August to be worth separating: the notes read as one clean pass to green,
   while the runs showed eleven turns on the security lane (`sonar` at the time)
   and three greens thrown away.

   A red gate is deliberately amber and not red. It is the tool working. Paint
   it like a failure and people stop reading the colour at all. */

.detail-runs { margin-bottom: var(--sp-2h); }

.run-row {
  display: grid;
  grid-template-columns: 78px 74px 44px 62px minmax(0, 1fr);
  align-items: baseline;
  gap: var(--sp-2);
  padding: var(--sp-h) 0 var(--sp-h) var(--sp-2);
  border-left: 2px solid var(--rule);
  font-size: var(--fs-sm);
}

.run-row[data-state='ok'] { border-left-color: var(--green); }

.run-row[data-state='red-gate'] { border-left-color: var(--amber); }

.run-row[data-state='error'] { border-left-color: var(--red); }

.run-row[data-state='orphaned'] { border-left-color: var(--red); }

.run-when,
.run-ms { color: var(--ink-faint); font-family: var(--mono); font-size: var(--fs-xs); }

.run-phase { color: var(--ink); font-weight: 600; }

.run-state { font-size: var(--fs-xs); letter-spacing: var(--track-label); text-transform: uppercase; }

.run-row[data-state='ok'] .run-state { color: var(--green); }

.run-row[data-state='red-gate'] .run-state { color: var(--amber); }

.run-row[data-state='error'] .run-state,
.run-row[data-state='orphaned'] .run-state { color: var(--red); }

.run-cmd {
  font-family: var(--mono);
  font-size: var(--fs-xs);
  color: var(--ink-faint);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.run-error {
  grid-column: 2 / -1;
  color: var(--red);
  font-size: var(--fs-xs);
}

.run-foot {
  display: flex;
  gap: var(--sp-3);
  padding: var(--sp-1) 0 0 var(--sp-2);
  font-size: var(--fs-xs);
  letter-spacing: var(--track-label);
  text-transform: uppercase;
  color: var(--ink-faint);
}

.run-foot-red { color: var(--amber); }

.run-foot-waste { color: var(--red); font-weight: 600; }

/* The header project selector.

   The accent comes from a data-project-colour attribute chosen from these eight
   named tokens and never from a hex in a POST body: the control room has to stay
   legible in both themes, and a colour an operator typed is a colour nothing
   validated. Adding a ninth project reuses a token rather than inventing one. */
.project-focus {
  display: inline-flex;
  align-items: center;
  gap: var(--sp-2);
  padding: 0 var(--sp-1) 0 var(--sp-3);
  /* --edge-control, not --edge. This is a control group: it holds a select and
     a button, so WCAG 1.4.11 asks 3:1 of its boundary, and --edge measures
     1.6 to 1.7. It was drawn at HALF the alpha of the + button inside it, so
     the group read weaker than its own child. .scope, which is the identical
     pattern on the bar below, already got this right. */
  border: 1px solid var(--edge-control);
  border-radius: var(--radius-sm);
}

.project-focus-label {
  font-size: var(--fs-xs);
  letter-spacing: var(--track-label);
  text-transform: uppercase;
  color: var(--ink-dim);
}

.project-focus-select {
  --project-accent: var(--green);
  appearance: none;
  border: 0;
  border-left: 3px solid var(--project-accent);
  background-color: transparent;
  color: var(--ink);
  /* `font: inherit` resolved to the body's 13px, which is a size this bar uses
     nowhere else; the group it sits in is 10 and 11px. --fs-md is a deliberate
     step above them, because this names the project everything else on the
     screen is about. And a height, so the selector and the + button beside it
     are one control group inside a 30px pill rather than 21 and 22px. */
  font-family: var(--ui);
  font-size: var(--fs-md);
  font-weight: 600;
  height: var(--ctl-h-sm);
  padding: 0 var(--chev-pad) 0 var(--sp-2);
  cursor: pointer;
}

.project-focus-select[data-project-colour='moss'] { --project-accent: var(--green); }

.project-focus-select[data-project-colour='amber'] { --project-accent: var(--amber); }

.project-focus-select[data-project-colour='slate'] { --project-accent: var(--slate); }

.project-focus-select[data-project-colour='plum'] { --project-accent: var(--plum); }

.project-focus-select[data-project-colour='teal'] { --project-accent: var(--teal); }

.project-focus-select[data-project-colour='rust'] { --project-accent: var(--red); }

.project-focus-select[data-project-colour='indigo'] { --project-accent: var(--blue); }

.project-focus-select[data-project-colour='sand'] { --project-accent: var(--rose); }

/* ---- the ticket panel -----------------------------------------------------
   The operator's words: the ticket "isn't that readable for a human, doesnt
   look great, needs to take off the cognitive load". Three things were wrong
   and each is answered here.

   It was welded to the bottom of the board, so it took a third of the screen
   whether or not you were reading it and there was no way to put it away except
   clicking the same card again. It floats now, remembers where you left it, and
   has an X.

   It was 11px --ink-dim prose at whatever measure the strip happened to be.
   The reading face is 14px/1.62 --ui in --ink at a 68ch measure; monospace is
   kept for the things that are actually machine text: ids, stamps, run rows,
   failures and code.

   And everything arrived at once in one flat column. The body is ordered for a
   reader now (what failed, what this is, then the words, then the log) and
   splits into named sections with live counts, so nothing is hidden without a
   number on screen saying it exists.

   Geometry is four custom properties written by JS, never inline left/top, so
   the no-float media query below can override without !important. */

.ticket {
  position: fixed;
  margin: 0;
  padding: 0;
  color: var(--ink);
  left: var(--tx, 0px);
  top: var(--ty, 0px);
  width: var(--tw, 720px);
  height: var(--th, 620px);
  /* A panel narrower than this cannot hold a fact row; shorter than this cannot
     hold its own header plus one line of the thing you opened it for. */
  min-width: 380px;
  min-height: 220px;
  /* The same insets clampRect() enforces, and no longer a second copy of them:
     web/app.js measures them off the topbar, the deck bar and the command bar
     and publishes the totals here. Two rules disagreeing about this is how the
     panel ended up flush against three window edges with its own corner radius
     sheared off, and 436px of the command bar's input underneath it; the
     fallbacks are what the bars measure at rest, for the frame before the
     first paint. */
  max-width: calc(100vw - var(--t-inset-x, 10px) * 2);
  max-height: calc(100vh - var(--t-inset-y, 212px));
  /* Under .toasts (60) on purpose. A toast is how the panel says it refused a
     write, and a refusal buried behind the panel that caused it is no refusal. */
  z-index: var(--z-panel);
  display: grid;
  grid-template-rows: auto auto auto auto minmax(0, 1fr);
  background: var(--surface-panel);
  backdrop-filter: blur(16px);
  -webkit-backdrop-filter: blur(16px);
  overflow: hidden;
}

.ticket[hidden], .ticket:not([open]) { display: none; }

/* The panel is a child of <body> now rather than of .deck, so nothing hides it
   when the router swaps screens. Eleven terminals with a ticket floating over
   them is not a control room. */
body[data-screen='control'] .ticket { display: none; }

.ticket-head {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: var(--sp-2);
  padding: var(--sp-2) var(--sp-3);
  border-bottom: 1px solid var(--rule);
  background: var(--well-bg);
  cursor: grab;
  /* The whole point of a drag handle: a pointer gesture on it must never be
     read as a text selection. */
  user-select: none;
  touch-action: none;
}

.ticket-head:active { cursor: grabbing; }
/* The move button sits inside the header padding, leaving room for its ring. */
.ticket-handle:focus-visible { outline: 2px solid var(--teal); outline-offset: 2px; }

.ticket-handle {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 24px;
  height: 24px;
  flex: 0 0 24px;
  padding: 0;
  border: 0;
  background: transparent;
  cursor: inherit;
}

.ticket-grab {
  width: 10px;
  height: 14px;
  flex: 0 0 10px;
  background-image: radial-gradient(circle, var(--ink-faint) 1px, transparent 1px);
  background-size: 4px 4px;
  opacity: 0.8;
}

/* Its own row, at a left edge that does not move. It was the fourth item in a
   flex row after an id and two variable-width pills, so measured at one viewport
   with the panel in one place its left edge was x2733.3 on a done ticket and
   x2850.3 on a blocked one: 117px of drift in where the headline of the panel
   starts, decided by how many state pills the ticket happened to have. The id
   and the pills are a meta line and the close box goes with them; the title
   starts a row of its own under them, always at the panel's own inset.

   `break-word`, not `anywhere`. `anywhere` lets the browser break INSIDE a word
   to reach a smaller minimum width, which is what produced "SUBSCRIPTIO / N" on
   the profile tiles; this one only breaks a word that cannot fit on a line by
   itself, which for a title in an 820px panel means never. */
.ticket-title {
  order: 1;
  flex: 1 1 100%;
  min-width: 0;
  margin: 0;
  /* It was 14px: one pixel larger than the 13px board card the operator clicked
     to get here, in an 820px panel, and the smallest headline in the app. */
  font: 600 var(--fs-2xl)/1.3 var(--ui);
  color: var(--ink);
  overflow-wrap: break-word;
}

.ticket-close {
  order: 0;
  flex: 0 0 auto;
  margin-left: auto;
  width: var(--ctl-h);
  height: var(--ctl-h);
  border: 1px solid var(--edge-control);
  border-radius: var(--radius-sm);
  background: transparent;
  color: var(--ink-dim);
  font: 400 var(--fs-2xl)/1 var(--ui);
  cursor: pointer;
}

.ticket-close:hover { color: var(--red); border-color: var(--red); }

/* Facts and actions wrap rather than spilling: this panel is resizable down to
   380px and both rows hold more chips than that width can take on one line. */
.ticket-facts,
.ticket-acts {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: var(--sp-1h);
  padding: var(--sp-2) var(--sp-3);
  border-bottom: 1px solid var(--rule);
}

.ticket-facts { font-size: var(--fs-xs); color: var(--ink-faint); }

.ticket-facts:empty,
.ticket-acts:empty,
.ticket-tabs:empty { display: none; }

/* A fact is not a control. It carried the same 22px pill, the same 1px control
   outline and the same 999px corner as the tabs one row below it, so '#broker'
   and 'runs 0' were the same object and nothing but a hover told them apart.
   The head's vocabulary is: a pill navigates, a rounded box acts, no box is
   read. This is the third of those. A tone still borders it, because a warn or
   a bad fact is a fact you have to see. */
.ticket-fact {
  display: inline-flex;
  align-items: center;
  height: var(--ctl-h-sm);
  padding: 0 var(--sp-h);
  border: 1px solid transparent;
  border-radius: var(--radius-sm);
  color: var(--ink-dim);
  white-space: nowrap;
}

.ticket-fact + .ticket-fact::before {
  content: '·';
  margin-right: var(--sp-1h);
  color: var(--ink-faint);
}

/* A toned fact is a plate with a rule down its left, not an outlined pill. The
   outline gave it the 22px box, the 1px control border and the 999px corner of
   the .ticket-tab buttons 39px below it, so on a blocked ticket the head held a
   red-outlined pill that does nothing directly above pills of identical geometry
   that navigate. This is the language .secret-add-state already uses for the
   same job, and it keeps the head's three-way vocabulary intact: a pill
   navigates, a rounded box acts, no box is read. */
.ticket-fact[data-tone='warn'],
.ticket-fact[data-tone='good'],
.ticket-fact[data-tone='bad'] {
  padding: 0 var(--sp-1h);
  border-radius: var(--radius-sm);
  border-left-width: 2px;
}

.ticket-fact[data-tone='warn'] {
  border-left-color: var(--amber);
  background: color-mix(in srgb, var(--amber) 14%, transparent);
  color: var(--amber);
}

.ticket-fact[data-tone='good'] {
  border-left-color: var(--green);
  background: color-mix(in srgb, var(--green) 14%, transparent);
  color: var(--green);
}

.ticket-fact[data-tone='bad'] {
  border-left-color: var(--red);
  background: color-mix(in srgb, var(--red) 14%, transparent);
  color: var(--red);
}

/* The same gap as the acts row above it. The two rows of 22px pills sat on 6px
   and 4px with 7px and 9px of side padding, so three stacked rows meant to read
   as one block of controls lined up as a grid in neither direction. */
.ticket-tabs {
  display: flex;
  align-items: stretch;
  flex-wrap: wrap;
  gap: var(--sp-1h);
  padding: var(--sp-2) var(--sp-3);
  border-bottom: 1px solid var(--rule);
}

.ticket-tab {
  display: inline-flex;
  align-items: center;
  gap: var(--sp-1);
  height: var(--ctl-h-sm);
  padding: 0 var(--sp-2);
  border: 1px solid var(--edge-control);
  border-radius: var(--radius-sm);
  background: transparent;
  color: var(--ink-faint);
  font: 600 var(--fs-sm)/1.4 var(--ui);
  cursor: pointer;
}

.ticket-tab[aria-selected='true'] { border-color: var(--teal); color: var(--ink); background: var(--accent-wash); }

.ticket-tab-count { font-family: var(--mono); font-size: var(--fs-xs); color: var(--ink-dim); }

.ticket-body {
  min-height: 0;
  padding: var(--sp-3) var(--sp-3) var(--sp-4);
  overflow: auto;
  /* A 380px panel holding an unbroken 200-character path must scroll its own
     code block, never widen the panel and push the close button off the edge. */
  overflow-wrap: anywhere;
}

/* 20px between groups, 8px within one. The old strip used 3px everywhere,
   which is why five unrelated things read as one wall. */
.ticket-section + .ticket-section { margin-top: var(--sp-5); }

.ticket-section > * + * { margin-top: var(--sp-2); }

.ticket-section-head {
  display: flex;
  align-items: baseline;
  flex-wrap: wrap;
  gap: var(--sp-2);
  padding-bottom: var(--sp-1);
  border-bottom: 1px solid var(--rule);
}

.ticket-section-head h3 {
  margin: 0;
  font: 600 var(--fs-xs)/1.4 var(--ui);
  letter-spacing: var(--track-label);
  text-transform: uppercase;
  color: var(--ink-dim);
}

/* Every heading names how much is under it. A section with a 0 beside it is
   still on screen: "nothing has been run yet" is an answer, an absent panel is
   not, and E-15 and E-17 both exist because content was hidden silently. */
.ticket-count { font: var(--fs-xs)/1.4 var(--mono); color: var(--ink-faint); }

/* Prose, in the reading face. 68ch because a measure past about seventy
   characters loses the line return, and this panel can be dragged to 1400px. */
.ticket-prose {
  margin: 0;
  max-width: 68ch;
  font: var(--fs-lg)/1.62 var(--ui);
  color: var(--ink);
  overflow-wrap: anywhere;
  white-space: pre-wrap;
}

.ticket-prose + .ticket-prose { margin-top: var(--sp-2); }

.ticket-h {
  margin: var(--sp-4) 0 0;
  font: 600 var(--fs-xl)/1.35 var(--ui);
  color: var(--ink);
}

.ticket-h[data-level='1'] { font-size: var(--fs-2xl); }

.ticket-h[data-level='3'],
.ticket-h[data-level='4'],
.ticket-h[data-level='5'],
.ticket-h[data-level='6'] { font-size: var(--fs-base); color: var(--ink-dim); }

.ticket-list {
  margin: 0;
  padding-left: var(--sp-5);
  max-width: 68ch;
  font: var(--fs-lg)/1.62 var(--ui);
  color: var(--ink);
}

.ticket-list li { margin-top: var(--sp-h); overflow-wrap: anywhere; }

/* A "Label: value" line is the one part of a spec that is genuinely tabular. */
.ticket-labelled {
  display: grid;
  grid-template-columns: minmax(90px, max-content) minmax(0, 1fr);
  gap: var(--sp-h) var(--sp-3);
  margin: 0;
  max-width: 68ch;
  font: var(--fs-base)/1.55 var(--ui);
}

.ticket-labelled dt { color: var(--ink-faint); font-weight: 600; }

.ticket-labelled dd { margin: 0; color: var(--ink); overflow-wrap: anywhere; }

/* Machine text keeps the machine face. Everything else gave it up. */
.ticket-code {
  margin: 0;
  padding: var(--sp-2) var(--sp-2h);
  border-left: 2px solid var(--edge);
  background: var(--rack);
  font: var(--fs-md)/1.5 var(--mono);
  color: var(--ink-dim);
  white-space: pre;
  overflow-x: auto;
}

.ticket-fail-callout {
  margin: 0 0 var(--sp-1);
  padding: var(--sp-2) var(--sp-2h);
  border-left: 2px solid var(--red);
  border-radius: 0 var(--radius-sm) var(--radius-sm) 0;
  background: color-mix(in srgb, var(--red) 8%, transparent);
  font: var(--fs-md)/1.5 var(--mono);
  color: var(--on-wash-alarm);
  overflow-wrap: anywhere;
}

/* ---- writing into a ticket from the browser ------------------------------ */

.ticket-edit {
  display: grid;
  gap: var(--sp-2);
  padding: var(--sp-2h);
  border: 1px solid var(--rule);
  border-radius: var(--radius);
  background: var(--well-bg);
}

.ticket-edit-row {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: var(--sp-1h);
}

/* :first-child, and that is the whole fix. This selector matched ANY direct
   child span, and web/app.js appends the edit form's status message into the
   save row as a span, so 'priority changed. A reason is required, exactly as
   fractal task edit requires one.' was locked into the 78px label column and
   force-uppercased: a 78 x 135px ribbon of nine shouted lines, on the one
   element in the form whose job is to explain why SAVE is off. */
.ticket-edit-row > span:first-child {
  flex: 0 0 78px;
  color: var(--ink-faint);
  font-size: var(--fs-xs);
  letter-spacing: var(--track-label);
  text-transform: uppercase;
}

.ticket-pri {
  min-width: 28px;
  height: var(--ctl-h);
  padding: 0 var(--sp-2);
  border: 1px solid var(--edge-control);
  border-radius: var(--radius-sm);
  background: transparent;
  color: var(--ink-dim);
  font: 600 var(--fs-sm)/1 var(--mono);
  cursor: pointer;
}

.ticket-pri[aria-pressed='true'] { border-color: var(--teal); color: var(--ink); background: var(--accent-tint); }

.ticket-chip {
  display: inline-flex;
  align-items: center;
  gap: var(--sp-1);
  height: var(--ctl-h-sm);
  padding: 0 var(--sp-1) 0 var(--sp-2);
  border: 1px solid var(--edge-control);
  border-radius: var(--radius-sm);
  color: var(--ink-dim);
  font-size: var(--fs-sm);
}

.ticket-chip button {
  border: 0;
  background: transparent;
  color: var(--ink-faint);
  font: var(--fs-md)/1 var(--ui);
  cursor: pointer;
}

.ticket-chip button:hover { color: var(--red); }

/* The one field in the room that beat the shared rule and drew the wrong edge.
   The group above lists these two selectors and gives them --edge-control; this
   block is 4,700 lines further down at equal specificity and re-declared
   `border: 1px solid var(--edge)`, so the ticket's three edit fields rendered
   1.68:1 where every other field in the app rendered 2.9 to 3.1. It says its
   size and nothing else now. */
.ticket-edit input,
.ticket-edit select {
  min-width: 0;
  height: var(--ctl-h);
  padding: 0 var(--sp-1h);
  font-size: var(--fs-sm);
}

.ticket-edit select { padding-right: var(--chev-pad); }

.ticket-reason { flex: 1 1 240px; }

/* fractal task edit will not change a field without a reason, and neither will
   this. The save control stays disabled until one is typed. */
/* Its own full-width line under the reason field, in sentence case, the way
   .secret-add-state already does it. Written after the label rule above so it
   wins on order at equal specificity. */
.ticket-edit-note {
  flex: 1 1 100%;
  margin: 0;
  color: var(--ink-faint);
  font-size: var(--fs-sm);
  letter-spacing: 0;
  line-height: 1.5;
  text-transform: none;
}

/* This used to be a .ticket-edit .mini-go block restating .btn-go's plate at
   --ctl-h, because the submit of the edit form was built as a .mini: a 22px
   button wearing --rule, the faintest border in the file, in a row of 26px
   fields wearing --edge. Restating a plate is not converging on one, so both
   submits in this panel are .btn.btn-go in the markup now and this block is
   gone. .mini keeps the job it was built for, which is the dense secondary row.
   The two buttons still need their width holding, and that is all that is left
   here. */
.ticket-edit .btn-go,
.activity-note-actions .btn-go { flex: 0 0 auto; }

.ticket-edit-more > summary {
  color: var(--ink-faint);
  font-size: var(--fs-sm);
  cursor: pointer;
}

.ticket-refs { display: flex; align-items: center; flex-wrap: wrap; gap: var(--sp-1h); }

.ticket-ref {
  display: inline-flex;
  align-items: center;
  gap: var(--sp-1);
  height: var(--ctl-h-sm);
  padding: 0 var(--sp-1) 0 var(--sp-2);
  border: 1px solid var(--edge-control);
  border-radius: var(--radius-sm);
  background: var(--rack);
  font: var(--fs-sm)/1 var(--mono);
  color: var(--ink-dim);
}

.ticket-ref > button { border: 0; background: transparent; color: inherit; cursor: pointer; font: inherit; }

.ticket-ref > button.ticket-ref-open { padding: 0; }

.ticket-ref > button.ticket-ref-cut { color: var(--ink-faint); }

.ticket-ref > button.ticket-ref-cut:hover { color: var(--red); }

.ticket-ref[data-done='1'] { color: var(--green); border-color: color-mix(in srgb, var(--green) 42%, transparent); }

.ticket-ref[data-unmet='1'] { border-style: dashed; }

/* ---- resizing ------------------------------------------------------------- */

.ticket-grip {
  position: absolute;
  width: 16px;
  height: 16px;
  touch-action: none;
}

.ticket-grip-se { right: 0; bottom: 0; cursor: nwse-resize; }

.ticket-grip-sw { left: 0; bottom: 0; cursor: nesw-resize; }

.ticket-grip::after {
  content: '';
  position: absolute;
  inset: 5px;
  border-bottom: 1px solid var(--ink-faint);
  opacity: 0.7;
}

.ticket-grip-se::after { border-right: 1px solid var(--ink-faint); }

.ticket-grip-sw::after { border-left: 1px solid var(--ink-faint); }

/* Below this a floating box is worse than a sheet: there is not enough room
   beside it to leave the board usable, which is the only reason it floats.
   Drag and resize refuse here too, and the remembered rectangle is left alone
   so a wide layout survives a narrow session. */
@media (max-width: 1100px), (max-height: 640px) {
  .ticket {
    inset: 0;
    left: 0;
    top: 0;
    width: auto;
    height: auto;
    max-width: none;
    max-height: none;
    border-radius: 0;
  }

  .ticket-head { cursor: default; }
  .ticket-handle { display: none; }
  .ticket-grip { display: none; }
}

/* ---- a card you can reach without a mouse --------------------------------- */

/* Nothing on this board was keyboard reachable before the panel grew an X and a
   drag handle, so the card that opens it had to become a control too. It takes
   the room's one focus ring off [tabindex]:focus-visible: this used to be a 1px
   teal outline, which is byte-identical to what .tcard.is-selected draws, so on
   a keyboard walk there was no telling what had focus from what was chosen. */

/* ---- adding a project ----------------------------------------------------- */

/* The same 22px box the selector beside it takes, so the pair reads as one
   control group.

   And the same corner as the group that holds it. A 999px wrapper with no
   padding needs a pill child or the child's square corner cuts the inside of
   the curve: this was an 8px box pressed against the inside of a 30px pill,
   which is exactly the thing the two comments above claim the pair does not
   do. Concentric corners are the rule for both nested groups in this room. */
.project-add {
  width: var(--ctl-h-sm);
  height: var(--ctl-h-sm);
  margin-left: var(--sp-h);
  border: 1px solid var(--edge-control);
  background: transparent;
  color: var(--ink-dim);
}

.project-add:hover { color: var(--ink); border-color: var(--project-accent, var(--teal)); }

/* ---- the one gradient face ------------------------------------------------
   Three elements painted their type from a gradient clipped to the glyphs,
   which needs `color: transparent` to show, and only one of the three had any
   business doing it. The countdown is a readout and measured a contrast ratio
   of 1 with three different values across a single number; the dialog heading
   is a heading and measured the same 1, in a 13px box one pixel taller than the
   body copy beneath it, so a dialog opened with no readable title at all. Both
   are flat ink now and their sizes carry the hierarchy instead.

   What is left is the wordmark, which is a wordmark: it is the app's name, it
   never changes, and nobody has to read a value off it. It still declares an
   opaque ink first, because under Windows high contrast the UA drops background
   images and a transparent colour then paints nothing.

   Last in the file on purpose: this has to outrank the component rule that sets
   the fallback ink, 3,000 lines above. */
@media not (forced-colors: active) {
  .brand-name {
    background-clip: text;
    -webkit-background-clip: text;
    color: transparent;
    background-image: linear-gradient(100deg, var(--wordmark-ink) 8%, var(--rose) 48%, var(--teal) 94%);
  }
}

/* ---- a control that was refused --------------------------------------------
   web/app.js has set aria-invalid on a rejected control since the dialogs were
   written and this stylesheet had not one selector that matched it: #new-cwd
   carrying aria-invalid="true" was painted byte-for-byte like the eighty valid
   controls around it, and the red banner above it named a problem that nothing
   on screen pointed at. Same for "Not A Valid Alias!" in the secrets rail.
   --red is 5.35:1 on the dialog ground. Last in the file, and qualified by type,
   because it has to outrank `.field input` and every other field rule here. */
input[aria-invalid='true'],
select[aria-invalid='true'],
textarea[aria-invalid='true'],
[aria-invalid='true'] {
  border-color: var(--red);
  box-shadow: 0 0 0 3px color-mix(in srgb, var(--red) 14%, transparent);
}

/* ---- a control that is off, and one that is working -------------------------
   Moved to the end of the file on purpose. "Off" has to beat every component
   that draws a border, and the component rules are spread over 5,000 lines: the
   shared visual system 3,000 lines below the old position re-declared
   `border-color` on every field in the room at equal specificity, so a disabled
   dialog input kept a live control's edge. A state block that wins by being
   last cannot be undone by a component that happens to be written later.
   ============================================================================ */

/* ---- a control that is off -------------------------------------------------
   There were exactly two `:disabled` selectors in 863 of them, and both were a
   bare opacity multiplier on a token that was already the dimmest ink in the
   file: `.mini:disabled {
  opacity: 0.4 }` composited to 1.58:1, which made the
   disabled SAVE button the least readable element in the application, less
   legible than the note beside it explaining why it was disabled. Sixteen
   controls are set `disabled` at runtime by web/app.js and forty-two of the
   forty-three visible ones had no disabled rule at all.

   So: a real colour pair at full opacity, and `pointer-events: none` so a dead
   control cannot still light up under the mouse. --ink-faint at full strength
   is 5.2:1 on this ground, which reads as off beside --ink-dim and --ink
   without reading as absent. Never express a disabled state as an opacity on a
   token that is already the dimmest ink. */
.btn:disabled,
.mini:disabled,
.ticket-pri:disabled,
.coordinator-settings-submit:disabled,
button:disabled,
input:disabled,
select:disabled,
textarea:disabled,
[aria-disabled='true'] {
  opacity: 1;
  border-color: var(--rule);
  background-color: transparent;
  color: var(--ink-faint);
  cursor: default;
  pointer-events: none;
  filter: none;
  transform: none;
  box-shadow: none;
}

/* A primary action that is off is still the primary action. NEW PROJECT opens
   with CREATE disabled until an id is typed, and the shared pair above took its
   border to --rule (0.09) and its ink to --ink-faint while CANCEL beside it kept
   --edge-control (0.40) and --ink-dim: measured, the primary was 3.8x weaker in
   border alpha and one ink step darker than the secondary, so the dialog opened
   with the eye landing on CANCEL and the shape of the pair changed the moment
   the field was filled. It keeps its teal identity here, unfilled and quiet, so
   only the FILL arrives when the form becomes submittable.

   Written after the block above and at the same specificity, which is the only
   way to reach a state block that wins by being last. */
.btn-go:disabled {
  border-color: color-mix(in srgb, var(--teal) 42%, transparent);
  color: var(--ink-faint);
}

/* ---- a control that is working ---------------------------------------------
   A disabled control is off; a busy one is not. It is doing the thing you just
   asked it to do, so it must not read as the dead thing above: the shared
   :disabled block is deliberately overridden here on colour and border. The
   dot after the label is the only motion, and it stops for anybody who has
   asked the OS for less of it. */
/* Both selectors, and the second is the load-bearing one: a busy control is
   also a disabled one, and `.btn:disabled` in the block above is a class plus a
   pseudo-class where `button[aria-busy]` is a type plus an attribute. The
   shorter one loses, so the busy state rendered byte-identical to the dead one
   until this line existed. Measured in Chrome: #project-go mid-submit came back
   #838a88 on transparent, exactly the disabled pair. */
button[aria-busy='true'],
[aria-busy='true']:disabled {
  color: var(--ink-dim);
  border-color: var(--edge);
  background-color: var(--wash-1);
  cursor: progress;
}

button[aria-busy='true']::after,
[aria-busy='true']:disabled::after {
  content: '';
  flex: 0 0 auto;
  width: 6px;
  height: 6px;
  margin-left: var(--sp-1h);
  border-radius: 50%;
  background: var(--teal);
  animation: control-busy 1.1s ease-in-out infinite;
}

@keyframes control-busy {
  0%, 100% { transform: scale(0.6); }
  50% { transform: scale(1); }
}

@media (prefers-reduced-motion: reduce) {
  button[aria-busy='true']::after,
  [aria-busy='true']:disabled::after { animation: none; }
}

/* ---- one dropdown -----------------------------------------------------------
   Seven of the eight selects in this room were `appearance: auto` and drew the
   operating system's own chevron on the operating system's own plate, which is
   why the secrets scope select, the new-project colour select, the new-task
   priority select and the command bar's broadcast select all looked like they
   had been pasted in from another application. `appearance: none` appeared
   exactly once in 4,684 lines, on the topbar project selector, and that one had
   no chevron at all.

   The caret is two clipped gradient corners rather than an inline SVG on
   purpose: a data-URI SVG is a separate document, so `currentColor` does not
   reach it and the mark would have to be a hard-coded grey that stops agreeing
   with its own control the moment the control is hovered, focused or disabled.
   Gradients resolve currentColor, so it tracks the token like any other ink.

   Every select rule above sets background-COLOR rather than the shorthand, so
   this can never be reset from underneath. --chev-pad is the room they leave. */
select {
  appearance: none;
  -webkit-appearance: none;
  background-image:
    linear-gradient(45deg, transparent 50%, currentColor 50%),
    linear-gradient(135deg, currentColor 50%, transparent 50%);
  background-position: right 13px center, right 9px center;
  background-size: 4px 4px, 4px 4px;
  background-repeat: no-repeat;
  cursor: pointer;
}

/* The list itself is the UA's, and on a dark page it inherits a transparent
   background from the control, which paints black text on black. */
select option {
  background-color: var(--rack-2);
  color: var(--ink);
}
/* Fractal account controls fit alongside the project selector. */
.fractal-runtime { display: flex; align-items: center; gap: var(--sp-2); flex-wrap: wrap; }
.fractal-cloud-folders { display: flex; flex-wrap: wrap; gap: var(--sp-2); max-height: 160px; overflow: auto; margin-block: var(--sp-2); }
#dlg-project { max-height: 90vh; overflow-y: auto; }
