/* One stylesheet, no framework. An operator console: dense tables, readable at
   arm's length on a laptop in a packing shed. */
:root {
  /* DECLARED, so the browser stops guessing. Every surface below is a light
     value, and without this line a machine in OS dark mode renders form
     controls and scrollbars dark against them. The console is light-only today;
     this says so rather than leaving it ambiguous. */
  color-scheme: light;
  --ink: #1b1b1b; --muted: #6b6b6b; --line: #d8d8d8; --bg: #fbfbfa;
  --warn: #8a3b00; --refuse: #8a0b0b; --ok: #14532d;
}
* { box-sizing: border-box; }
body { margin: 0; background: var(--bg); color: var(--ink);
  font: 15px/1.45 -apple-system, "Helvetica Neue", Arial, sans-serif; }
.topbar { display: flex; gap: 1.5rem; align-items: baseline;
  padding: .6rem 1rem; border-bottom: 1px solid var(--line); background: #fff; }
.brand { font-weight: 700; text-decoration: none; color: var(--ink); }
nav a { margin-right: 1rem; color: var(--ink); }
main { padding: 1rem 1.25rem 4rem; }
h1 { font-size: 1.35rem; margin: .4rem 0 1rem; }
h2 { font-size: 1.1rem; margin: 1.6rem 0 .5rem; }
h3 { font-size: .98rem; margin: 1.2rem 0 .4rem; }
code { background: #efefec; padding: .15rem .35rem; border-radius: 3px; }
.muted { color: var(--muted); font-weight: 400; }

table.grid { border-collapse: collapse; width: 100%; background: #fff;
  border: 1px solid var(--line); margin-bottom: .5rem; }
table.grid th, table.grid td { border: 1px solid var(--line);
  padding: .3rem .5rem; text-align: left; vertical-align: top; font-size: .9rem; }
table.grid thead th { background: #f1f1ee; position: sticky; top: 0; }

/* A 14x14 confusion matrix is SIXTEEN columns and it renders inside a panel.
   The page must not scroll horizontally at the body level because of it: wide
   content scrolls inside its own box. `display:block` makes the table itself
   that box, which is the version of this fix that needs NO wrapper element --
   and needing no element is what let the matrix markup move into
   `_evaluation_breakdown.html` with the model screen byte-identical afterwards.

   The row header is PINNED. Scrolled to the twelfth predicted column, a count
   with no visible true-class label is a number nobody can attribute -- and the
   off-diagonal cell is the entire reason the matrix is on this screen: per-class
   recall says a green lacewing was missed 21% of the time and only the matrix
   says whether it was called a hover fly (harmless, both beneficial) or a flea
   beetle (`CLAUDE.md`'s worst outcome). */
table.grid[data-table="confusion-matrix"] { display: block; overflow-x: auto;
  width: max-content; max-width: 100%; }
table.grid[data-table="confusion-matrix"] tbody th { position: sticky; left: 0;
  background: #f1f1ee; z-index: 1; }
tr[data-row]:nth-child(even) { background: #fcfcfb; }

/* The data track is a badge and never a plain cell: an experimental-track model
   that looks like a production one is how the wrong thing gets shipped. */
.track { padding: .05rem .4rem; border-radius: 10px; font-size: .8rem;
  border: 1px solid var(--line); }
.track-production { background: #e6f4ea; color: var(--ok); border-color: #a8d5b5; }
.track-experimental { background: #fdf0e3; color: var(--warn); border-color: #e6c199; }
.badge { padding: .05rem .4rem; border: 1px solid var(--line); border-radius: 10px;
  font-size: .8rem; background: #fff; }
.badges { display: flex; gap: .5rem; flex-wrap: wrap; }

.refusal, .verdict { border-left: 4px solid var(--line); padding: .5rem .75rem;
  margin: .75rem 0; background: #fff; }
.refusal { border-left-color: var(--refuse); }
.verdict-refused { border-left-color: var(--refuse); }
.verdict-ok { border-left-color: var(--ok); }
.refusal-code, .verdict-code { font-family: ui-monospace, Menlo, monospace;
  font-size: .82rem; color: var(--refuse); margin: 0 0 .2rem; }
.verdict-ok .verdict-code { color: var(--ok); }
.refusal-message, .verdict-message { margin: 0; }
.refusal-detail, .verdict-detail { margin: .4rem 0 0; font-size: .85rem; }

[data-tripwire] { background: #fdecec; font-weight: 600; }
[data-empty] { color: var(--muted); }
[data-empty] code { display: inline-block; margin-top: .3rem; }

.filters, .actions form { display: flex; gap: .75rem; flex-wrap: wrap;
  align-items: end; margin-bottom: .75rem; }
label { display: flex; flex-direction: column; font-size: .8rem; color: var(--muted); }
input, select, textarea { font: inherit; padding: .2rem .3rem; border: 1px solid var(--line); }
button { font: inherit; padding: .25rem .75rem; border: 1px solid var(--line);
  background: #fff; cursor: pointer; }
.pager { display: flex; gap: 1rem; margin: .5rem 0 1.5rem; }

.log { background: #14161a; color: #e6e6e6; padding: .6rem .8rem; overflow: auto;
  max-height: 26rem; font: 12px/1.4 ui-monospace, Menlo, monospace; white-space: pre-wrap; }
.log-missing { color: #d79b6a; }

/* The control arm is PINNED and MARKED, never just another row: an arm that does
   not beat it is not earning its cost, and a reader scanning a table sorted by
   macro-F1 will not see that unless the comparison is impossible to miss. */
tr.control-arm { background: #eef4fb; box-shadow: inset 3px 0 0 #3b6ea5; }
tr.control-arm td { border-top: 2px solid #3b6ea5; border-bottom: 2px solid #3b6ea5; }
.badge-control { background: #3b6ea5; color: #fff; border-color: #3b6ea5; }
table.arms th a { color: inherit; text-decoration: none; }
table.arms th[data-sorted] a::after { content: " ▾"; }
table.arms th[data-sorted="asc"] a::after { content: " ▴"; }

/* The class-imbalance histogram. A zero-count class is a ROW with a zero-width
   bar and a visible 0 -- the bar the founder most needs to see. */
.histogram { background: #fff; border: 1px solid var(--line); padding: .5rem .75rem; }
.hist-row { display: grid; grid-template-columns: 14rem 6rem 1fr 4rem;
  gap: .5rem; align-items: center; padding: .15rem 0; }
.hist-track { background: #f1f1ee; height: .85rem; border: 1px solid var(--line); }
.hist-bar { display: block; height: 100%; background: #3b6ea5; }
.hist-count { text-align: right; font-variant-numeric: tabular-nums; }
.hist-row[data-zero] { color: var(--warn); }
.hist-row[data-zero] .hist-track { border-color: var(--warn); }
.role-beneficial { background: #e6f4ea; color: var(--ok); }
.role-pest { background: #fdf0e3; color: var(--warn); }

img.thumb { max-width: 160px; height: auto; display: block; background: #f1f1ee; }
tr.retired { opacity: .65; }
tr.retired td:first-child { box-shadow: inset 3px 0 0 var(--warn); }
label.checkbox { flex-direction: row; align-items: center; gap: .3rem; }
.hash { font-family: ui-monospace, Menlo, monospace; font-size: .8rem; }
.hypothesis { font-size: 1.02rem; max-width: 62ch; }

/* Screen 5. The hierarchy is native `<details>`: no JS, and no subtree fetched
   that nobody opened. */
.tree details { border: 1px solid var(--line); background: #fff; padding: .3rem .6rem;
  margin-bottom: .25rem; }
.tree summary { cursor: pointer; display: flex; gap: .5rem; align-items: center; }
.tree .sci { font-style: italic; }
.tree .rank { font-size: .8rem; text-transform: uppercase; color: #6b6b63; }
.taxon-node { margin: .4rem 0 .4rem 1.2rem; }
.notice { border-left: 3px solid #3b6ea5; background: #eef4fb; padding: .5rem .75rem; }
tr[data-change="reordered"] { background: #fdf6e3; }
tr[data-change="removed"] { background: #fdf0e3; }
tr[data-change="added"] { background: #e6f4ea; }

/* Screen 6. The region overlay is CSS ONLY -- absolutely-positioned divs over
   the image, at percentages `box_style` computed. A canvas would be a second
   island and this console has exactly one. */
.frame { position: relative; display: inline-block; max-width: 640px; }
.frame img { display: block; width: 100%; height: auto; }
.region { position: absolute; border: 2px solid #d17b0f; box-sizing: border-box; }
.region-identity { border-style: dashed; border-color: #3b6ea5; }
.region-label { position: absolute; top: -1.1rem; left: 0; font-size: .7rem;
  background: #d17b0f; color: #fff; padding: 0 .2rem; white-space: nowrap; }
tr[data-row="step"] td { vertical-align: top; }
tr.step-moved { background: #eef4fb; }
[data-dirty="true"] { box-shadow: inset 0 0 0 2px #d17b0f; }

/* THE status pill, keyed on the VALUE and never on the screen. Runs and
   experiments carry DIFFERENT vocabularies -- `db/models.py:162` RUN_STATUSES is
   queued/running/succeeded/failed/cancelled and `:168` EXPERIMENT_STATUSES is
   planned/frozen/running/complete/abandoned -- and `running` is the one word
   they share. Because the rule is written against the value, `running` cannot be
   blue on the runs screen and grey on the experiments screen. G80 asserts every
   member of both vocabularies has a rule here, and that `running` has one. */
.pill { display: inline-block; padding: .05rem .5rem; border-radius: 10px;
  font-size: .8rem; border: 1px solid var(--line); background: #fff;
  font-weight: 600; white-space: nowrap; }
.pill[data-status="queued"], .pill[data-status="planned"] {
  background: #f1f1ee; color: var(--muted); border-color: #d8d8d8; }
.pill[data-status="running"] {
  background: #eef4fb; color: #1d4f80; border-color: #9dc0e0; }
.pill[data-status="frozen"] {
  background: #f4f0fb; color: #4a3b7a; border-color: #c3b8e0; }
.pill[data-status="succeeded"], .pill[data-status="complete"] {
  background: #e6f4ea; color: var(--ok); border-color: #a8d5b5; }
.pill[data-status="failed"] {
  background: #fdecec; color: var(--refuse); border-color: #e6a1a1; }
.pill[data-status="cancelled"], .pill[data-status="abandoned"] {
  background: #fdf0e3; color: var(--warn); border-color: #e6c199; }
.pill[data-status="unknown"] { background: #fff; color: var(--muted); }
.tally { display: inline-flex; gap: .3rem; flex-wrap: wrap; }

/* The arm collapse. An arm is the unit of decision; its seed runs are indented
   under it and only when the URL says they are open. */
.expanders { display: flex; gap: 1rem; align-items: baseline; margin: .25rem 0 .75rem; }
tr[data-row="arm"] { background: #f7f7f4; }
tr[data-row="arm"] td { border-top: 2px solid #c9c9c2; }
tr[data-row="arm"].control-arm { background: #eef4fb; }
a.toggle { text-decoration: none; display: inline-block; width: 1rem; color: var(--muted); }
tr.seed-row td:first-child { padding-left: 1.4rem; }
.seed-mark { color: var(--line); }
tr[data-row="unattached-heading"] td { background: #f7f7f4; font-size: .85rem; }

/* The metrics SYNTHESIS. Five numbers and a curve where twelve epochs of four
   series, the seed block and fourteen class counts used to be. */
.synth { display: flex; flex-direction: column; gap: .15rem; }
.synth-pair { font-variant-numeric: tabular-nums; font-weight: 600; }
.synth-line { display: flex; gap: .6rem; flex-wrap: wrap; font-size: .8rem;
  font-variant-numeric: tabular-nums; }
.synth-panel { background: #fff; border: 1px solid var(--line);
  padding: .5rem .75rem; max-width: 32rem; }
/* The EVALUATION panel is the exception, and it is not a style preference: it
   holds a confusion matrix, which for the v1 class set is sixteen columns. The
   32rem cap is right for a metrics pair and wrong for a grid. */
.synth-panel[data-row="evaluation"] { max-width: none; }
.spark { display: inline-flex; align-items: flex-end; gap: 1px; height: 1.6rem;
  border-bottom: 1px solid var(--line); }
.spark-bar { display: inline-block; width: 4px; background: #3b6ea5; min-height: 1px; }
details.disclosure { margin: .5rem 0 0; }
details.disclosure > summary { cursor: pointer; font-size: .85rem; color: var(--muted); }

/* The progress pane (ADR-011 §6). One polled fragment, not a bar per row: the
   width comes from `progress_percent`, which the SERVICE computed, so a
   terminal run has no bar at all rather than a bar frozen at 58%. */
.progress-pane { background: #fff; border: 1px solid var(--line); padding: .5rem .75rem; }
.progress-track { background: #f1f1ee; height: 1.1rem; border: 1px solid var(--line);
  margin: .35rem 0 .2rem; }
.progress-bar { display: block; height: 100%; background: #3b6ea5; }
.progress-figure { font-variant-numeric: tabular-nums; margin: 0 0 .5rem; }
/* The list cell (screen 3). The same bar as the detail pane, at row height:
   the founder scans six of these at once, and a number with no bar beside it is
   six numbers to compare by eye. */
.progress-cell { font-size: .85rem; line-height: 1.35; }
.progress-cell .progress-figure { display: inline; margin: 0; }
.progress-inline { display: inline-block; width: 4.5rem; height: .55rem;
                   vertical-align: middle; margin: 0 .25rem; }
.progress-cell [data-progress-run] { display: block; }
tr[data-row=refresh] td { border-top: 1px dashed var(--line); font-size: .85rem; }
/* Slow and dead look identical in a process table. They do not look identical
   here: this is the reaper's signal, before the reaper gets to it. */
.stale-warning { border-left: 3px solid var(--warn); padding-left: .6rem; color: var(--warn); }
td.curve-cell { display: grid; grid-template-columns: 4.5rem 1fr; gap: .4rem;
  align-items: center; }
tr[data-compilation-epoch] td:first-child { color: var(--muted); }

/* The runs list is EXPERIMENT -> ARM -> SEED. The band is a heading and reads as
   one, so the control arm underneath it is visibly the control FOR those arms
   and not for whatever happens to be above it on the page. */
tr.experiment-band td { background: #f4f1ea; border-top: 2px solid #8a7a5c;
  border-bottom: 1px solid #d8d0bd; }
.split-reason { font-size: .78rem; margin-top: .15rem; }

/* WHICH RULER. Not a colour that means "good" or "bad": a holdout number is not
   a worse number, it is a number measured on a different population, and the
   badge says which. */
.badge-split { background: #eef2f7; border-color: #b9c6d6; color: #33506f;
  font-variant-numeric: tabular-nums; }
.badge-split-none { background: #fdf0e3; border-color: #e6c199; color: var(--warn); }
table.arms > caption { caption-side: top; text-align: left; padding: .35rem 0;
  font-size: .85rem; }

/* The train/val gap. THE SIGN IS THE READING (ADR-012 §5): positive is
   overfitting, negative is the underfit-under-augmentation signature that
   overturned wave 1's headline. Two colours, one per sign, and a tabular figure
   so a column of them lines up on the decimal point. */
td.gap { font-variant-numeric: tabular-nums; white-space: nowrap; }
td.gap .gap-loss { font-size: .78rem; display: block; }
/* THE CELL SAYS WHAT IT COMPARED. "2 over / 1 under" sent the founder to the
   column header to find out over WHAT, and the header is in `runs/list.html`
   while these rows are also swapped in as a headerless fragment. */
td.gap .gap-basis { font-size: .72rem; display: block; font-style: italic; }
.gap-over { color: #a2543c; font-weight: 600; }
.gap-under { color: #33506f; font-weight: 600; }
td.gap-over { background: #fbf1ec; }
td.gap-under { background: #eef2f7; }
[data-gap][data-scope="arm"] .gap-over, [data-gap][data-scope="arm"] .gap-under {
  display: block; font-size: .8rem; }

/* WHICH OPS. Zero of them is a sentence in the panel and not an empty list. */
.op-list { margin: .25rem 0; padding-left: 1.1rem; font-size: .82rem; }
.op-list code { background: #f4f4f1; padding: 0 .2rem; }

/* The loss curve is a DIFFERENT chart from the accuracy curve and looks like
   one: down is good here and up is good there, so they never share a box. */
.spark-bar-loss { background: #a2543c; }
[data-spark-caption] { display: block; font-size: .75rem; max-width: 26rem; }

/* ---------------------------------------------------------------------------
   THE CONFUSION MATRIX PALETTE. HUE IS WHAT THE ERROR COSTS; SHADE IS ITS RATE.

   Measured on the real registry, and this is the whole design. On evaluation 14
   the largest cell BY COUNT is Spiders -> Background, n=23, rate 0.128; the
   largest BY RATE is Mexican bean beetle -> Lady beetle, n=3, rate 0.167 --
   `01-taxonomy-v1.md` §5's pair A. A three-image cell outranks a
   twenty-three-image one, so a heatmap scaled by `count` renders the cell that
   matters most as the faintest thing on the page. The two directions of that one
   pair are different prices -- `44 -> 24` is a pest called a beneficial, `24 ->
   44` is the reverse and is the worst outcome -- which is why hue is taken from
   the role pair and shade from the rate, and never one from the other.

   THE ORDER OF ALARM IS `CLAUDE.md`'s ASYMMETRY TABLE, and G169 asserts it from
   these numbers rather than from the comment: red for the grower spraying their
   own biocontrol, amber for a pest they will now ignore, blue for a miss that
   changes no spray decision, and GREY -- the least saturated triple here -- for
   an open-set error, which costs them nothing. Swap two of these rows and G169
   is red; that is what stops the colour from being decoration.

   `correct` carries a hue and is only ever drawn at step 0, so the diagonal is
   unfilled. It is not an event to price, and a diagonal shaded by its own recall
   is a matrix that shouts where the model was RIGHT.

   THEME-AWARE BY CONSTRUCTION, not by a second palette. Every fill is an alpha
   over whatever surface it lands on and no rule below sets `color`, so the text
   stays `--ink` and a future dark theme is an override of `:root` in one place
   rather than a re-tune of twenty cells. G171 asserts both properties. */
[data-severity="beneficial_called_pest"] { --sev-rgb: 176 26 26; }
[data-severity="pest_called_beneficial"] { --sev-rgb: 197 103 12; }
[data-severity="same_role"]              { --sev-rgb: 59 110 165; }
[data-severity="open_set"]               { --sev-rgb: 120 122 126; }
[data-severity="correct"]                { --sev-rgb: 20 83 45; }
/* Step 4 is the WORST RATE IN THIS MATRIX and not a top quartile
   (`screens/support.py::_intensity`), so the ring below marks the outlier the
   founder asked to see rather than everything near it. */
[data-intensity="0"] { --sev-alpha: 0; }
[data-intensity="1"] { --sev-alpha: .14; }
[data-intensity="2"] { --sev-alpha: .30; }
[data-intensity="3"] { --sev-alpha: .48; }
[data-intensity="4"] { --sev-alpha: .70; }
td[data-severity], .severity-legend .swatch {
  background-color: rgb(var(--sev-rgb) / var(--sev-alpha, 0));
}
/* THE NON-COLOUR CHANNEL. A ring is weight, not hue: it survives a greyscale
   printout, a colour-blind reader and a stylesheet that never loaded a palette.
   `currentColor` keeps it legible whatever `--ink` becomes. */
td[data-severity][data-intensity="4"] { box-shadow: inset 0 0 0 2px currentColor; }
/* AND THE ROLE-CROSSING SEVERITIES DO NOT FADE. Shade is the RATE, and measured
   on evaluation 14 every one of the twenty beneficial-called-pest cells lands at
   step 1 -- 0.028 at the worst -- so the cell where a grower sprays their own
   biocontrol would be the palest thing on the page precisely BECAUSE it is rare.
   Rarity is not cheapness (`CLAUDE.md`'s asymmetry table prices it the other way
   round). These two get a full-opacity edge no rate can dim, and the WIDTH is
   the ordering, which is a second non-colour channel: 4px for the worst outcome
   the product has, 2px for the one below it, none for the two cheap ones. */
td[data-severity="beneficial_called_pest"] { border-left: 4px solid rgb(var(--sev-rgb)); }
td[data-severity="pest_called_beneficial"] { border-left: 2px solid rgb(var(--sev-rgb)); }
td[data-cell] { text-align: right; font-variant-numeric: tabular-nums;
  white-space: nowrap; }
.cell-count { display: block; }
/* The rate is TEXT, so the magnitude ordering is readable with no colour at all
   -- and it is the number the ranking is actually made on. */
.cell-rate { font-size: .72rem; opacity: .8; }
.cell-mark { font-weight: 700; font-size: .74rem; margin-left: .25rem;
  font-family: ui-monospace, Menlo, monospace; }

/* The key to the notation. All five, always: a legend describes the marks, and
   one that appeared and vanished per matrix would teach nothing portable. */
.severity-legend { display: flex; flex-wrap: wrap; gap: .35rem 1.25rem;
  margin: .35rem 0 1rem; font-size: .8rem; }
.severity-legend .legend-item { display: flex; gap: .4rem; align-items: baseline;
  max-width: 30rem; }
.severity-legend dt { display: flex; gap: .3rem; align-items: baseline; }
.severity-legend dd { margin: 0; color: var(--muted); }
.severity-legend .swatch { display: inline-block; min-width: 1.4rem;
  text-align: center; border: 1px solid var(--line); font-weight: 700;
  font-family: ui-monospace, Menlo, monospace; }
