/* CRE-Hub — the house design language, light by default.
 *
 * WHY THIS FILE IS A LIGHT THEME AND THE INTERNAL HUB IS NOT. Guillaume built
 * the floor-report app light for exactly this reason: dark does not read as
 * enterprise software in front of a colleague. The plan (§2, 2026-08-18 second
 * round) rules the same for this site — same design language as the internal
 * hub, light palette, login portal in front.
 *
 * HOW THE TWO THEMES ARE EXPRESSED. The palette lives entirely in tokens on
 * :root, and :root[data-theme="dark"] redefines the same names — so no rule
 * below this block ever knows which theme it is in. The inversion against
 * floor-report is deliberate: there dark was the default and light the opt-in;
 * here light is the default and dark is the opt-in, because the audience is
 * the colleague, not him.
 *
 * TOKEN NAMES ARE THE INTERNAL HUB'S, unchanged (--bg --panel --panel2 --line
 * --fg --dim --dimmer --acc --ok --fail --warn --int, plus the --t-* tone
 * list). That is what "the same furniture" means in practice: a component
 * lifted from tracker2 lands here and paints itself correctly.
 *
 * ⚠ THE ONE THING THAT COULD NOT BE COPIED. tracker2's notices hardcode rgba
 * literals of their hue instead of reading the tokens, which is invisible in a
 * one-theme app and wrong the moment there are two. They are rewritten onto
 * tokens here; the same defect is worth fixing upstream when tracker2 next
 * moves.
 */

:root{
  /* ── surfaces ─────────────────────────────────────────────────────────── */
  --bg:#f4f5f7;
  --panel:#ffffff;
  --panel2:#eef0f4;
  --sunken:#f7f8fa;          /* inputs, code, anything recessed */
  --line:#dce0e7;
  --hover:#00000006;
  --shadow:0 6px 22px rgb(16 22 34 / .13);

  /* ── ink ──────────────────────────────────────────────────────────────── */
  --fg:#1c2129;
  --dim:#666f7d;
  --dimmer:#8b94a1;
  --strong:#0f141b;          /* headings — the near-black counterpart of the
                                internal hub's off-token near-whites */

  /* ── meaning ──────────────────────────────────────────────────────────── */
  --acc:#0b5fa5;             /* links + app identity. The hub's pale #a5d6ff is
                                unreadable on white; this is the same blue, dark
                                enough to be link text. */
  --on-acc:#ffffff;
  --ok:#1a7f37;
  --fail:#cf222e;
  --warn:#b4600f;
  --int:#6f42c1;             /* admin-only / not public — a TIER, not a status */
  --appbar-accent:var(--acc);

  /* ── ink ON the app bar ───────────────────────────────────────────────────
     The bar is painted in the accent, so everything sitting on it needs an ink
     that contrasts with the ACCENT rather than with the page. These four are
     that ink at four strengths, and they are the reason the bar survives the
     theme flip untouched: in light the band is dark blue and its ink is white,
     in dark the band is pale blue and its ink is near-black. Nothing in the bar
     names a literal colour. */
  --acc-ink:#ffffff;
  --acc-ink-70:rgb(255 255 255 / .78);   /* a tab that is not the open one */
  --acc-ink-35:rgb(255 255 255 / .34);
  --acc-ink-15:rgb(255 255 255 / .15);   /* hover wash, and the count chip */

  /* ── the one tone list ────────────────────────────────────────────────────
     Every coloured mark on every page is painted from THIS list and nothing
     else. Each tone is an `R G B` triple plus the one foreground readable on a
     wash of it; the triple is spaced, not comma'd, so `rgb(var(--t-x) / .16)`
     works for both the fill and the border.

     The triples are the internal hub's, unchanged — the hue is the identity.
     What flips for a light ground is every `-fg`: on dark it is a light TINT of
     the hue, here it is a dark SHADE of the same hue. The wash alpha is a notch
     higher (.16 against .14) because a tint reads weaker on white. */
  --t-red:248 81 73;        --t-red-fg:#a4231c;
  --t-rose:196 92 88;       --t-rose-fg:#9c3733;
  --t-orange:240 136 62;    --t-orange-fg:#9c5210;
  --t-amber:227 160 8;      --t-amber-fg:#7d5806;
  --t-yellow:224 190 68;    --t-yellow-fg:#6f5c0a;
  --t-green:63 185 80;      --t-green-fg:#1a7f37;
  --t-teal:25 158 112;      --t-teal-fg:#0f6b4c;
  --t-cyan:57 181 229;      --t-cyan-fg:#0b6a8c;
  --t-blue:86 138 220;      --t-blue-fg:#1b4f9e;
  --t-slate:104 122 148;    --t-slate-fg:#46536a;
  --t-violet:144 133 233;   --t-violet-fg:#4f45a8;
  --t-pink:192 90 158;      --t-pink-fg:#8e3670;

  --font:-apple-system,Segoe UI,Roboto,sans-serif;
  --mono:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;
  --appbar-h:46px;
  color-scheme:light;
}

:root[data-theme="dark"]{
  --bg:#0f1013; --panel:#16181c; --panel2:#1b1e23; --sunken:#0b0c0e;
  --line:#2a2d33; --hover:#ffffff08; --shadow:0 6px 22px rgb(0 0 0 / .45);
  --fg:#dfe2e6; --dim:#8b9199; --dimmer:#5c6169; --strong:#eceff2;
  --acc:#a5d6ff; --on-acc:#0f1013;
  --ok:#3fb950; --fail:#f85149; --warn:#e3a008; --int:#b083f0;
  --t-red-fg:#f3a49e; --t-rose-fg:#df9f9f; --t-orange-fg:#eda870;
  --t-amber-fg:#e3bb59; --t-yellow-fg:#dcc46b; --t-green-fg:#6ecb7e;
  --t-teal-fg:#5fd3a5; --t-cyan-fg:#7fd4f5; --t-blue-fg:#9cbcf2;
  --t-slate-fg:#9dabbe; --t-violet-fg:#c4bdf5; --t-pink-fg:#e19fd0;
  --acc-ink:#0f1013;
  --acc-ink-70:rgb(15 16 19 / .74);
  --acc-ink-35:rgb(15 16 19 / .30);
  --acc-ink-15:rgb(15 16 19 / .11);
  color-scheme:dark;
}

/* ── frame ────────────────────────────────────────────────────────────────── */

*{box-sizing:border-box}
/* The scrollbar's width is reserved on every page, short or long. Without it a
   page that scrolls is ~15px narrower than one that does not, so the app bar's
   right-hand block — and the centred column under it — SHIFT as you move
   between pages. Reported 2026-08-18: "the top menu moves slightly when
   clicking on a different page". It is the viewport changing width, not the
   bar. */
html{scrollbar-gutter:stable}
body{margin:0;background:var(--bg);color:var(--fg);
  font:13.5px/1.5 var(--font);-webkit-font-smoothing:antialiased}
a{color:var(--acc);text-decoration:none}
a:hover{text-decoration:underline}

/* The icon sprite in base.html. Its size and its removal from the flow live
   HERE and not on the element, because `style-src 'self'` refuses a `style=`
   attribute — an inline rule would be blocked, the sprite would sit in the
   page's flow, and every page would log a violation. */
svg.sprite{position:absolute;width:0;height:0;overflow:hidden}

/* ── the app bar ────────────────────────────────────────────────────────────
 * ONE ROW, not two. The brand, every page this account may open, and who you
 * are signed in as, all on a single 46px band painted in the accent. Chosen
 * 2026-08-18 from a set of fifteen drawn variants; the two rows it replaces
 * were the same white as the page under them, so nothing on the bar read as
 * clickable and the open page was marked only by a hairline.
 *
 * Three things carry the work:
 *   - the band is `--appbar-accent`, so the chrome is a coloured object and the
 *     page below it is plainly a different thing;
 *   - the open page is a filled lozenge in `--acc-ink`, the one solid shape on
 *     the bar;
 *   - every page carries a mark, and a page whose state is a NUMBER carries
 *     that number (see `nav_counts` in apps.py — empty today, by design).
 *
 * ⚠ IT IS A SINGLE ROW AND IT DOES NOT WRAP, and it will not TELL you when it
 * stops fitting. A flex row that runs out of space does not overflow visibly,
 * it SQUEEZES — the tabs give up their padding and the account name slides off
 * the right edge — so a bar that no longer fits looks merely cramped rather
 * than broken, and `scrollWidth` reports that it fits at every width. Measure
 * it off a `max-content` clone, which cannot shrink, or the check is one that
 * can never fail.
 *
 * ✅ Re-measured that way 2026-08-19 at Guillaume's real viewport (1246, of
 * which 1231 is content once the scrollbar is out), admin — the widest account,
 * five app tabs plus the audit link: **needs 960px, so 286px spare.** It was
 * 919 the day before, with seven tabs. Losing the Home tab gave back less than
 * moving Audit into `.meta` cost: in `<nav>` it sat in a 3px gap, and in `.meta`
 * it pays the group's 16px padding and 11px gaps as well as its own. Keep
 * `App.nav` to one short word (apps.py) and re-measure before adding a page — a
 * SIXTH app costs roughly 110px of the 286.
 *
 * ✅ Below 1000px the rule at the foot of this file drops every label but the
 * open tab's — the audit link included, which is the whole reason that rule
 * names `.meta .auditlink` as well as `nav a` — and the row needs 626. Measured
 * 2026-08-19 at 1000 / 980 / 900 / 760: the labels are gone at all four and the
 * document does not scroll sideways at any of them.
 *
 * ⚠ AND ON A PHONE IT HAS RUN OUT OF SQUEEZE. ✅ Measured 2026-08-19 in a
 * headless browser at 420px, admin: `document.scrollWidth` is 451 on the page
 * `/` then served, 462 on the tracker and 486 on the scratchpad — the bar has hit
 * its minimum content width and the whole PAGE scrolls sideways with it, on
 * every page of the site. That is the boundary of the ⚠ above: between about
 * 460px and 1246px the bar shrinks silently, and below that it stops lying and
 * simply overflows. Nobody has asked for a phone yet, so this is recorded and
 * not fixed — but it means a page-level "does not scroll sideways" check must
 * measure the page's OWN columns at phone width, never the document, or it
 * fails for the bar's overflow rather than for anything that page did.
 */
.topbar{position:sticky;top:0;z-index:900;display:flex;align-items:center;
  height:var(--appbar-h);padding:0 14px;gap:3px;
  background:var(--appbar-accent);color:var(--acc-ink)}
.topbar .brand{font-size:15px;font-weight:700;color:var(--acc-ink);
  margin-right:13px;white-space:nowrap;letter-spacing:.01em}
.topbar .brand:hover{text-decoration:none}

.topbar nav{display:flex;align-items:center;gap:3px;min-width:0}
.topbar nav a{display:flex;align-items:center;gap:7px;color:var(--acc-ink-70);
  font-size:13px;font-weight:600;padding:6px 10px;border-radius:7px;
  white-space:nowrap;
  /* Only colour changes on hover — never size, never spacing. A nav that
     reflows under the pointer moves the thing you are aiming at. */
  transition:color .08s,background .08s}
.topbar nav a:hover{color:var(--acc-ink);background:var(--acc-ink-15);
  text-decoration:none}
/* ⚠ THE OPEN TAB CHANGES COLOUR AND FILL, NEVER WEIGHT — every tab is 600, on
   this bar and in every tab strip on the site. A heavier face is a WIDER one:
   at 650 the open tab grew 3–5px and pushed every tab to its right along, so
   the row landed in a different place on each page. Reported 2026-08-18 as "the
   top menu moves slightly when clicking on a different page" and measured that
   day — Scratchpad's left edge sat at 509.3 / 510.8 / 513.2 / 514.4 px
   depending on which page was open. The white lozenge is what says which tab
   is the open one, and it costs no width. */
.topbar nav a.cur{background:var(--acc-ink);color:var(--acc)}
.topbar nav a svg{width:15px;height:15px;flex:none}

/* The count on a tab. `--acc-ink-15` on the band, and a solid accent fill once
   the tab is the open one, because the lozenge underneath it is `--acc-ink`
   and a wash of that ink would vanish into it. */
.topbar .badge{font-size:10.5px;font-weight:700;line-height:1.55;padding:0 6px;
  border-radius:9px;background:var(--acc-ink-15);color:var(--acc-ink)}
.topbar nav a.cur .badge{background:var(--acc);color:var(--acc-ink)}

.topbar .meta{margin-left:auto;display:flex;align-items:center;gap:11px;
  padding-left:16px;font-size:11.5px;color:var(--acc-ink-70);white-space:nowrap}

/* AUDIT, admin only, and it lives HERE rather than in <nav> — 🗣️ 2026-08-19,
   "on the right of the top menu, next to the login info". It is a page about
   the site rather than one of the site's apps, and the run of tabs on the left
   is the set colleagues share.

   ⚠ It wears the tab metrics deliberately: same 13px/600, same 7px lozenge,
   same open-state fill. It is still a page you open and it must not read as a
   second kind of thing — and matching the tabs is also what keeps the open
   state from changing the row's width (see the ⚠ on `.topbar nav a.cur`). */
.topbar .meta .auditlink{display:flex;align-items:center;gap:7px;
  color:var(--acc-ink-70);font-size:13px;font-weight:600;padding:6px 10px;
  border-radius:7px;white-space:nowrap;
  transition:color .08s,background .08s}
.topbar .meta .auditlink:hover{color:var(--acc-ink);background:var(--acc-ink-15);
  text-decoration:none}
.topbar .meta .auditlink.cur{background:var(--acc-ink);color:var(--acc)}
.topbar .meta .auditlink svg{width:15px;height:15px;flex:none}

.topbar .who{white-space:nowrap}
.topbar .role{font-size:10px;text-transform:uppercase;letter-spacing:.05em;
  opacity:.8}
.topbar .signout{color:var(--acc-ink-70)}
.topbar .signout:hover{color:var(--acc-ink)}

main{max-width:1100px;margin:0 auto;padding:14px 16px 60px}
main.wide{max-width:1500px}
footer{max-width:1100px;margin:0 auto;padding:0 16px 40px;
  font-size:11.5px;color:var(--dimmer)}

h1{font-size:19px;font-weight:700;color:var(--strong);margin:10px 0 2px}
h1 .count{font-size:12px;font-weight:400;color:var(--dim);margin-left:8px}
h2{font-size:15px;font-weight:650;color:var(--strong);margin:22px 0 6px}
.sub{margin:2px 0 16px;font-size:12.5px;color:var(--dim);max-width:70ch}
.sub a{text-decoration:underline}

/* ── cards ────────────────────────────────────────────────────────────────── */

.card{background:var(--panel);border:1px solid var(--line);border-radius:10px;
  padding:12px 16px;margin:6px 0 14px}
/* A card set apart from the one above it. A class, not a `style=` attribute on
   the element: `style-src 'self'` refuses inline style attributes, so the rule
   would be dropped and the page would log a violation instead of being spaced. */
.card.spaced{margin-top:18px}
/* Label above value, pairs flowing across and wrapping — a `dl` whose dt and
   dd land in adjacent grid CELLS reads as a spread-out table instead, which is
   what it did before each pair got its own block. */
.facts{display:flex;flex-wrap:wrap;gap:10px 34px;margin:0}
.facts .fact{min-width:110px}
.facts dt{color:var(--dimmer);font-size:10px;text-transform:uppercase;
  letter-spacing:.05em}
.facts dd{margin:2px 0 0;font-size:12.5px;color:var(--fg)}

/* ── chips ────────────────────────────────────────────────────────────────── */

.chip{display:inline-block;font-size:11px;line-height:1.6;padding:0 8px;
  border-radius:12px;background:var(--panel2);color:var(--dim);
  border:1px solid var(--line);white-space:nowrap}
.chip.mono{font-family:var(--mono);border-radius:5px;background:transparent}
/* A mock page is not a status — it is a statement about the page itself, so it
   gets the dashed border every terminal / not-real state carries. */
.chip.mock{color:var(--t-amber-fg);border-color:rgb(var(--t-amber) / .5);
  background:rgb(var(--t-amber) / .16);border-style:dashed}
.chip.phase{color:var(--t-blue-fg);border-color:rgb(var(--t-blue) / .45);
  background:rgb(var(--t-blue) / .16)}
.chip.internal{color:var(--int);border-color:var(--int);border-style:dashed;
  background:transparent}

/* ── notices ──────────────────────────────────────────────────────────────── */

.banner{background:rgb(var(--t-red) / .12);border:1px solid rgb(var(--t-red) / .38);
  color:var(--t-red-fg);border-radius:8px;padding:7px 12px;margin:8px 0;
  font-size:12.5px}
.note{background:rgb(var(--t-blue) / .10);border:1px solid rgb(var(--t-blue) / .3);
  color:var(--t-blue-fg);border-radius:8px;padding:7px 12px;margin:8px 0;
  font-size:12.5px}
.banner.ok{background:rgb(var(--t-green) / .12);
  border-color:rgb(var(--t-green) / .38);color:var(--t-green-fg)}
.stub{background:var(--panel);border:1px solid var(--line);
  border-style:dashed;border-radius:10px;padding:26px 22px;text-align:center;
  color:var(--dim);margin-top:16px}
.stub h2{margin:0 0 6px;color:var(--strong)}
.stub p{margin:6px auto;max-width:56ch;font-size:12.5px}

/* ── forms ────────────────────────────────────────────────────────────────── */

.input,input[type=text],input[type=password]{background:var(--sunken);
  border:1px solid var(--line);color:var(--fg);border-radius:7px;
  padding:7px 10px;font-size:13px;font-family:inherit;width:100%}
.input:focus,input:focus{outline:none;border-color:var(--acc);
  box-shadow:0 0 0 3px rgb(var(--t-blue) / .18)}
.btn{cursor:pointer;background:var(--acc);color:var(--on-acc);
  border:1px solid var(--acc);border-radius:7px;padding:7px 14px;
  font-size:13px;font-family:inherit;font-weight:600}
.btn:hover{filter:brightness(1.08)}
.btn.quiet{background:var(--panel2);color:var(--fg);border-color:var(--line);
  font-weight:400}
/* One column, capped at a field's own useful width — a password box stretched
   across a 1500px page reads as a search bar. */
.pwform{max-width:320px}
.pwform label{display:block;font-size:10px;text-transform:uppercase;
  letter-spacing:.05em;color:var(--dimmer);margin:12px 0 4px}
.pwform .btn{margin-top:16px}

/* ── the portal ───────────────────────────────────────────────────────────── */

body.portal{display:flex;align-items:center;justify-content:center;
  min-height:100vh;padding:20px}
.portal .card{width:100%;max-width:340px;margin:0;padding:24px 22px;
  box-shadow:var(--shadow)}
.portal h1{margin:0 0 2px;font-size:20px}
.portal .sub{margin:0 0 18px}
.portal label{display:block;font-size:10px;text-transform:uppercase;
  letter-spacing:.05em;color:var(--dimmer);margin:10px 0 4px}
.portal .btn{width:100%;margin-top:16px}
.portal .err{margin:12px 0 0;font-size:12.5px;color:var(--t-red-fg)}

/* ── tables ───────────────────────────────────────────────────────────────── */

.tablewrap{overflow-x:auto;border:1px solid var(--line);border-radius:9px;
  background:var(--panel)}
table.list{width:100%;border-collapse:collapse;font-size:13px}
table.list th{text-align:left;color:var(--dim);font-weight:600;font-size:10.5px;
  text-transform:uppercase;letter-spacing:.04em;padding:6px 12px;
  border-bottom:1px solid var(--line);background:var(--panel2)}
table.list td{padding:6px 12px;border-bottom:1px solid var(--line);
  vertical-align:middle}
table.list tbody tr:last-child td{border-bottom:none}
table.list tbody tr:hover{background:var(--panel2)}
table.list .c-date,table.list .c-id{font-family:var(--mono);font-size:12px;
  color:var(--dimmer);white-space:nowrap}
.empty{padding:22px;text-align:center;color:var(--dim);font-size:12.5px}

/* Below this the row genuinely cannot hold seven labels, so the labels stand
   down and the marks carry the nav on their own; the open page keeps its word,
   because an unlabelled bar with nothing named on it is a puzzle. The desktop
   layout above is untouched — this is the same bar surviving a small window,
   not a second design. */
@media (max-width:1000px){
  .topbar nav a:not(.cur) .lbl{display:none}
  .topbar nav a{padding:6px 8px}
  /* The audit link is not in <nav> any more (2026-08-19) and would otherwise
     keep its label while every tab dropped theirs — the one thing that must
     not happen on the bar that runs out of room first. */
  .topbar .meta .auditlink:not(.cur) .lbl{display:none}
  .topbar .meta .auditlink{padding:6px 8px}
}
@media (max-width:640px){
  .topbar{padding:0 10px}
  .topbar .brand{margin-right:8px}
  .topbar .meta .who{display:none}
  main{padding:12px 12px 40px}
}

/* ── P2: records ──────────────────────────────────────────────────────────── */

/* The counterpart of `.chip.mock`. Solid border, not dashed: a built page is a
   settled state, and the dashed edge is this site's mark for "not real yet". */
.chip.live{color:var(--t-green-fg);border-color:rgb(var(--t-green) / .45);
  background:rgb(var(--t-green) / .16)}

/* ── the state marks ──────────────────────────────────────────────────────
   A pill, not a bare word. The records page carries two vocabularies that used
   to share one column, and colour is what stops `major` and `high` reading as
   the same kind of thing. The tone per value is chosen in `tickets.py`'s TONE;
   here is only what a pill LOOKS like.

   ⚠ One rule per tone, and the twelve below are the twelve `--t-*` triples
   declared at the top of this file — no more. A pill whose tone has no rule
   renders as unstyled text, which is why TONE falls back to `slate` rather
   than passing a name through. */
.pill{display:inline-block;font-size:10.5px;line-height:1.7;padding:0 7px;
  border-radius:11px;white-space:nowrap;font-weight:600}
.pill+.pill{margin-left:4px}
/* The dim em dash: nothing recorded. Never used for `published` — see the ⚠ in
   tickets_list.html. */
.c-none{color:var(--dimmer)}
.p-red{color:var(--t-red-fg);background:rgb(var(--t-red) / .16);border:1px solid rgb(var(--t-red) / .4)}
.p-rose{color:var(--t-rose-fg);background:rgb(var(--t-rose) / .16);border:1px solid rgb(var(--t-rose) / .4)}
.p-orange{color:var(--t-orange-fg);background:rgb(var(--t-orange) / .16);border:1px solid rgb(var(--t-orange) / .4)}
.p-amber{color:var(--t-amber-fg);background:rgb(var(--t-amber) / .16);border:1px solid rgb(var(--t-amber) / .4)}
.p-yellow{color:var(--t-yellow-fg);background:rgb(var(--t-yellow) / .16);border:1px solid rgb(var(--t-yellow) / .4)}
.p-green{color:var(--t-green-fg);background:rgb(var(--t-green) / .16);border:1px solid rgb(var(--t-green) / .4)}
.p-teal{color:var(--t-teal-fg);background:rgb(var(--t-teal) / .16);border:1px solid rgb(var(--t-teal) / .4)}
.p-cyan{color:var(--t-cyan-fg);background:rgb(var(--t-cyan) / .16);border:1px solid rgb(var(--t-cyan) / .4)}
.p-blue{color:var(--t-blue-fg);background:rgb(var(--t-blue) / .16);border:1px solid rgb(var(--t-blue) / .4)}
.p-slate{color:var(--t-slate-fg);background:rgb(var(--t-slate) / .16);border:1px solid rgb(var(--t-slate) / .4)}
.p-violet{color:var(--t-violet-fg);background:rgb(var(--t-violet) / .16);border:1px solid rgb(var(--t-violet) / .4)}
.p-pink{color:var(--t-pink-fg);background:rgb(var(--t-pink) / .16);border:1px solid rgb(var(--t-pink) / .4)}

/* ── the records switch and its filter rail ───────────────────────────────
   Both are ANCHORS wearing the shape of a control, because the selection lives
   in the URL (tickets.py's ⚠). So both need the underline suppressed and both
   need `cur` to be the thing that reads as pressed — a hover that moved
   anything would make the rail flicker as the cursor crosses eight chips. */
.seg{display:inline-flex;background:var(--panel2);border:1px solid var(--line);
  border-radius:9px;padding:3px;gap:3px;margin:0 0 12px}
.seg a{border-radius:6px;padding:5px 14px;font:600 12.5px var(--font);
  color:var(--dim)}
.seg a:hover{text-decoration:none;color:var(--fg)}
.seg a b{font-weight:400;color:var(--dimmer);margin-left:5px}
.seg a.cur{background:var(--panel);color:var(--strong);
  box-shadow:0 1px 3px rgb(16 22 34 / .12)}

.rail{display:flex;flex-wrap:wrap;gap:6px;margin:0 0 11px}
.fchip{display:inline-flex;align-items:center;gap:6px;background:var(--panel);
  border:1px solid var(--line);border-radius:16px;padding:3px 11px;
  font:12px var(--font);color:var(--dim)}
.fchip:hover{text-decoration:none;border-color:var(--dim)}
.fchip.cur{background:var(--panel2);color:var(--strong);font-weight:600}
.fchip .fn{font-size:11px;color:var(--dimmer);font-variant-numeric:tabular-nums}

/* The title column carries the sentence, so it takes whatever width is left and
   is inked as text rather than as a link — the ref beside it is the blue one,
   and two blue cells on one row would leave nothing looking primary. */
table.list td.c-title a{color:var(--fg)}
table.list td.c-title a:hover{color:var(--acc)}

/* ── a row this account may not read ──────────────────────────────────────
   Dimmed, not hidden: the ruling is that a manager can see the record exists
   and ask about it. Nothing here is what STOPS them reading it — the title is
   already gone and the links are already unrendered by the time this paints
   (tickets.py's ⚠). This is the row saying so, not the row enforcing it.

   ⚠ No `pointer-events:none` and no faded-out opacity on the whole row. The
   cells that are still true — the id, the state, the dates — have to stay
   readable, because they are the whole reason the row is drawn at all. */
table.list tr.redacted td{color:var(--dimmer)}
table.list tr.redacted .c-title{font-style:italic}
/* The marks lose their fill and keep their outline: a state is still a fact
   about a record nobody may open, but it must not compete with the rows that
   can actually be clicked. */
table.list tr.redacted .pill{background:none;color:var(--dimmer);
  border-color:var(--line)}

/* The show/hide chip sits at the end of the rail and is not one of the state
   filters, so it is pushed away from them rather than lined up with them. */
.fchip.toggle{margin-left:auto}
@media (max-width:760px){.fchip.toggle{margin-left:0}}

.dimtext{color:var(--dim);font-size:12px}
.tagrow{margin:10px 0 0;display:flex;flex-wrap:wrap;gap:5px}
.files{margin:6px 0 0;padding-left:18px;font-size:12.5px}
.files li{margin:3px 0}

/* The rendered band. `.doc` is the only place on the site where markdown-shaped
   HTML lands, so every element it can produce is styled HERE — a heading or a
   table arriving from a record must not fall back to the browser default and
   look like a different application. */
.doc{background:var(--panel);border:1px solid var(--line);border-radius:10px;
  padding:14px 18px;margin:6px 0 14px;font-size:13px;line-height:1.62;
  color:var(--fg);overflow-wrap:break-word}
.doc h1,.doc h2,.doc h3,.doc h4{color:var(--strong);margin:18px 0 6px;
  line-height:1.3}
.doc h1{font-size:17px}
.doc h2{font-size:15px}
.doc h3{font-size:13.5px}
.doc h4{font-size:12.5px;color:var(--dim);text-transform:uppercase;
  letter-spacing:.04em}
.doc > :first-child{margin-top:0}
.doc p,.doc ul,.doc ol{margin:8px 0}
.doc li{margin:3px 0}
.doc code{font-family:var(--mono);font-size:12px;background:var(--sunken);
  border:1px solid var(--line);border-radius:4px;padding:0 4px}
.doc pre{background:var(--sunken);border:1px solid var(--line);
  border-radius:8px;padding:10px 12px;overflow-x:auto;margin:10px 0}
.doc pre code{border:none;background:none;padding:0}
.doc blockquote{margin:10px 0;padding:2px 0 2px 12px;
  border-left:3px solid var(--line);color:var(--dim)}
.doc hr{border:none;border-top:1px solid var(--line);margin:16px 0}
/* A wide table inside a record scrolls in its own box; the page never does. */
.doc table{display:block;width:max-content;max-width:100%;overflow-x:auto;
  border-collapse:collapse;font-size:12.5px;margin:10px 0}
.doc th,.doc td{border:1px solid var(--line);padding:5px 9px;
  vertical-align:top;text-align:left}
.doc th{background:var(--panel2);color:var(--dim);font-weight:650}

/* The internal band. The purple is `--int`, the site's mark for a TIER rather
   than a status, and the rule is dashed for the same reason `.chip.internal`
   is: it says "not for everyone", never "something is wrong". */
.internal{margin-top:26px;padding-top:4px;border-top:2px dashed var(--int)}
.internal h2{color:var(--int)}
.internal h3{font-size:13px;color:var(--dim);margin:18px 0 4px}

/* Table column alignment, arriving from a record's markdown. It is a CLASS and
   not the `style="text-align: …"` python-markdown writes, because this app
   ships `style-src 'self'` and the browser refuses that attribute — see the ⚠
   in mdrender.py. Two cells of the same column disagreeing is what it looks
   like when this rule goes missing. */
.doc th.ta-r,.doc td.ta-r{text-align:right}
.doc th.ta-c,.doc td.ta-c{text-align:center}
.doc th.ta-l,.doc td.ta-l{text-align:left}

/* ── the Knowledge sheet ────────────────────────────────────────────────────
 * One page, one control: a box you type a code into. Everything below it is
 * either the answer or the way back to the whole list, so the search box is the
 * only thing on the page that gets the accent ring — nothing here is editable
 * and a ring on read-only content would say it was (the contract in
 * knowledge/__init__.py names every id and class this section styles, and
 * knowledge/static/find.js is the script that binds to them).
 */
.k-search{display:flex;align-items:center;gap:10px;margin:0 0 14px;max-width:820px}
#q{flex:1;min-width:0;font:15px/1.3 var(--font);padding:10px 13px;border-radius:8px;
  color:var(--fg);background:var(--panel);border:1px solid var(--line)}
#q:focus{outline:none;border-color:var(--acc);box-shadow:0 0 0 3px rgb(11 95 165 / .15)}
#q::placeholder{color:var(--dimmer)}
.k-search .hits{color:var(--dim);font-size:12.5px;white-space:nowrap}
#clear{font:12px var(--font);color:var(--dim);background:var(--panel);cursor:pointer;
  border:1px solid var(--line);border-radius:7px;padding:6px 11px}
#clear:hover{color:var(--fg)}

/* The letter strip is the legend AND the filter: before anything is typed it is
   the only place the twelve groups are stated in full. */
.k-letters{display:flex;flex-wrap:wrap;gap:6px;margin:0 0 20px}
.k-letters .L{display:flex;align-items:baseline;gap:6px;padding:5px 10px;border-radius:8px;
  background:var(--panel);border:1px solid var(--line);color:var(--fg);cursor:pointer;
  font:12px var(--font);text-align:left}
.k-letters .L:hover{background:var(--panel2)}
.k-letters .L.on{border-color:var(--acc);background:rgb(11 95 165 / .09)}
.k-letters .L b{font-family:var(--mono);font-size:13px;color:var(--acc)}
.k-letters .L .zh{color:var(--dim)}

section.grp{margin:0 0 24px;max-width:820px}
section.grp h2{display:flex;align-items:baseline;gap:8px;flex-wrap:wrap;font-size:14.5px;
  margin:0 0 7px}
section.grp h2 .lt{font-family:var(--mono);font-size:15px;color:var(--acc);line-height:1.5;
  border:1px solid rgb(11 95 165 / .35);border-radius:6px;padding:0 7px}
section.grp h2 .zh{font-weight:400;color:var(--dim)}
section.grp h2 .gloss{font-weight:400;font-size:12.5px;color:var(--dimmer)}
section.grp h2 .h2sub{font-weight:400;font-size:11.5px;color:var(--dimmer)}

table.k-grid .c-code{width:88px}
table.k-grid .c-zh{width:150px;color:var(--dim)}
table.k-grid .c-uom{width:92px;color:var(--dim)}
table.k-grid .c-n{width:92px;text-align:right;font-variant-numeric:tabular-nums}
code.kcode{font-family:var(--mono);font-size:12.5px;font-weight:600;color:var(--acc)}

.k-asof{font-size:11.5px;color:var(--dimmer);margin:0}
/* The row the search was actually asking for. A left bar rather than a second
   background wash — hover already washes, and two washes read as neither. */
table.k-grid tr.exact td{background:rgb(11 95 165 / .07)}
table.k-grid tr.exact td:first-child{box-shadow:inset 3px 0 0 var(--acc)}
/* ⚠ THIS PAGE USED TO PUT ITS CAVEATS UNDER THE ROW, and the rule here said a
   note that only exists on hover is one nobody reads. 🗣️ Guillaume ruled the
   other way on 2026-08-18 — "for some important info you can use a small ?
   tooltip" — because eleven note lines under eleven rows is exactly the
   wordiness rule 6 forbids. The `?` is the visible affordance that makes the
   trade survivable: a bare `title=` with nothing to point at reads as cryptic.
   `.tip` is site-wide and lives in its own section below. */
.k-nohits{font-size:13.5px;color:var(--dim);padding:12px 0}

/* ── the `?` tooltip ────────────────────────────────────────────────────────
 * A visible affordance carrying one short line, so a page can hold a caveat
 * without holding a paragraph. CSS only — the site ships `script-src 'self'`
 * and `style-src 'self'`, so this cannot be a javascript popper and it cannot
 * carry an inline style. `tabindex` is what makes it reachable without a mouse:
 * `:focus` opens it exactly as `:hover` does.
 *
 * ⚠ It escapes its cell by being `position:absolute` inside a `relative` span,
 * so any ancestor with `overflow:hidden` (`.tablewrap`) will clip it. The
 * Knowledge sheet draws its tables unwrapped for that reason.
 */
.tip{position:relative;display:inline-flex;align-items:center;justify-content:center;
  width:15px;height:15px;flex:none;border-radius:50%;border:1px solid var(--line);
  background:var(--sunken);color:var(--dimmer);font:600 10px/1 var(--font);
  cursor:help;vertical-align:middle;user-select:none}
.tip:hover,.tip:focus{border-color:var(--acc);color:var(--acc);outline:none}
.tip::after{content:attr(data-tip);position:absolute;left:0;bottom:calc(100% + 8px);
  width:max-content;max-width:330px;padding:9px 11px;border-radius:8px;
  background:var(--panel);color:var(--fg);border:1px solid var(--line);
  box-shadow:var(--shadow);font:400 11.5px/1.55 var(--font);text-align:left;
  white-space:normal;opacity:0;visibility:hidden;pointer-events:none;z-index:60}
.tip:hover::after,.tip:focus::after{opacity:1;visibility:visible}
/* ⚠ A `?` sitting in a page HEADING has nothing above it to open into — the
   default upward tooltip renders behind the app bar and a reader sees its last
   line and nothing else (seen on the Scratchpad heading, 2026-08-19). `below`
   drops it under instead. It is a modifier rather than a change to the default
   because every `?` in a table row is where it should be already. */
.tip.below::after{top:calc(100% + 8px);bottom:auto}
/* Near the right edge the balloon would run off the page, so a row's last cells
   hang it from the right instead. */
td:last-child .tip::after,td:nth-last-child(2) .tip::after{left:auto;right:0}

/* ── the details link ───────────────────────────────────────────────────────
   Beside a page heading, admin only (templates/crehub/_details_link.html).
   Deliberately quiet: it is a door for one person on a page five other people
   read, so it must not compete with the heading it sits next to. It reads as a
   link rather than a button because it goes somewhere and does nothing. */
h1 .details-link {
  font-size: .5em;
  font-weight: 500;
  letter-spacing: .04em;
  text-transform: uppercase;
  vertical-align: middle;
  margin-left: .5rem;
  padding: .15em .5em;
  border: 1px solid var(--line);
  border-radius: 999px;
  color: var(--dim);
  text-decoration: none;
  white-space: nowrap;
}
h1 .details-link:hover { color: var(--fg); border-color: var(--fg); }

/* ── the demo ribbon ─────────────────────────────────────────────────────────
   Beside a page heading, on every page of an app whose `demo` flag is set
   (apps.py, templates/crehub/_demo_ribbon.html). 🗣️ 2026-08-19: Production and
   Floor "are demo, meant for analysing and brainstorming… a sort of mini demo
   ribbon".

   It is a RIBBON and not another chip on purpose: the site already spends pills
   on states a row is in, and this is a statement about the page itself. The
   notch on its right edge is what makes it read as a tag pinned to the title
   rather than as one more status. Amber, which is the colour this stylesheet
   already gives every "not the real thing" state (`.chip.mock`).

   ⚠ The notch is cut with `clip-path`, so the padding on the right has to pay
   for what the cut takes away — drop the extra padding and the last letter goes
   with the corner. */
h1 .demo-ribbon {
  display: inline-block;
  font-size: .5em;
  font-weight: 700;
  letter-spacing: .1em;
  text-transform: uppercase;
  vertical-align: middle;
  margin-left: .6rem;
  padding: .2em 1.15em .2em .6em;
  background: rgb(var(--t-amber) / .9);
  color: #2a1c00;
  border-radius: 3px;
  cursor: help;
  clip-path: polygon(0 0, 100% 0, calc(100% - .55em) 50%, 100% 100%, 0 100%);
}

/* ── the four apps of P6–P9 ─────────────────────────────────────────────────
 * Everything below is shared by more than one of them, which is the test for
 * living in this file rather than in an app's own stylesheet. Nothing here
 * invents a colour: every rule reads the tokens at the top.
 */

/* A number column. Tabular figures so a column of quantities lines up on the
   digit rather than on the glyph width — without it 1,000 and 999 look like
   different lengths of the same number. */
.c-num{text-align:right;font-variant-numeric:tabular-nums;white-space:nowrap}
/* ⚠ The heading needs the table in its selector to beat `table.list th`, which
   sets every heading left. Without it the word sits at the left of a column
   whose numbers are at the right, and the column reads as two columns. */
table.list th.c-num{text-align:right}
.c-act{width:1%;white-space:nowrap}

/* A button that is a link. The site's `.btn` is the accent lozenge and it is
   for the ONE primary action on a page; a row's `void` or `done` is neither
   primary nor worth a lozenge, but it posts a form and so cannot be an `<a>`.
   The forms are inline-block so several sit on one line. */
.c-act form,.nacts form{display:inline-block;margin:0}
.linkbtn{background:none;border:none;padding:0 4px;cursor:pointer;
  font:inherit;font-size:11.5px;color:var(--dim)}
.linkbtn:hover{color:var(--acc);text-decoration:underline}
.linkbtn.danger:hover{color:var(--fail)}

/* A row that no longer counts — the floor's voided report. Struck rather than
   removed: the totals above it changed, and a row that vanished would leave
   nothing saying why. */
table.list tr.voided td{color:var(--dimmer);text-decoration:line-through}
table.list tr.voided .dimtext{text-decoration:none}

/* The rail, when it is not the first thing under the heading. */
.rail.spaced{margin-top:14px}
.backlink{margin:16px 0 0;font-size:12.5px}

/* ── the floor's report form ───────────────────────────────────────────────
   Four boxes and a button on one row. The widths are fixed to the shape of what
   goes in them — an order is eight characters, a quantity is four — so the note
   takes the rest and nothing wraps until the row genuinely cannot hold it. */
.report{display:flex;flex-wrap:wrap;gap:8px;margin:0 0 14px;align-items:center}
.report .input{font-size:13px;padding:7px 10px}
.report .o{width:110px}
.report .s{width:130px}
.report .q{width:90px;text-align:right}
.report .n{flex:1;min-width:160px}
@media (max-width:640px){.report .n{flex-basis:100%}}

/* ── the scratchpad ────────────────────────────────────────────────────────
   A board, not a table: a note is a paragraph somebody typed and a table cell
   would either wrap it into a column or cut it.

   THE PAGE IS TWO COLUMNS since 2026-08-19 (his ask): a small menu on the left
   and the notes beside it. The wrapper is inside the page's own content, never
   in the shared chrome — the app bar across the top is the site's menu and this
   is the page's, and the two are on two axes on purpose. */
.padwrap{display:flex;gap:16px;align-items:flex-start}
.padmain{flex:1;min-width:0}

/* Three rows, one per list. It sticks, because the notes column is the long one
   and a menu that scrolls away is a menu you go back to the top for. */
.padnav{flex:0 0 168px;display:flex;flex-direction:column;gap:1px;
  align-self:flex-start;position:sticky;top:12px;
  background:var(--panel);border:1px solid var(--line);border-radius:10px;
  padding:6px}

/* Every row has one shape: a rail down its left carrying the state, the name,
   and the count pinned right. The rail is drawn on every row and only ever
   changes COLOUR — nothing here moves when the mouse crosses it, so the eye can
   read the three names without the column rearranging itself. */
.padnav a{position:relative;display:flex;align-items:center;gap:8px;
  padding:6px 9px 6px 13px;border-radius:7px;font-size:12.5px;color:var(--dim);
  text-decoration:none}
.padnav a::before{content:"";position:absolute;left:4px;top:6px;bottom:6px;
  width:2px;border-radius:2px;background:transparent}
.padnav a:hover{color:var(--fg);background:var(--panel2);text-decoration:none}
.padnav a:hover::before{background:var(--line)}
.padnav a.cur{color:var(--fg);background:var(--panel2);font-weight:600}
.padnav a.cur::before{background:var(--acc)}
.padnav .ic{flex:0 0 auto;width:14px;text-align:center;font-size:11.5px;
  color:var(--dimmer)}
.padnav a:hover .ic,.padnav a.cur .ic{color:var(--acc)}
.padnav .t{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;min-width:0}
/* The count. Only the pad's own goes accent, and only when it is not zero: what
   is still on the board is the one live signal here — how many were pinned or
   dealt with is a fact, not something waiting on anybody. */
.padnav .n{margin-left:auto;flex:0 0 auto;font-family:var(--mono);
  font-size:10.5px;font-weight:400;line-height:16px;color:var(--dimmer);
  border:1px solid var(--line);border-radius:20px;padding:0 6px}
.padnav .n.live{color:var(--acc);border-color:rgb(var(--t-blue) / .45);
  background:rgb(var(--t-blue) / .10)}

@media (max-width:700px){
  /* The menu goes above the notes rather than beside them — 168px of column on
     a phone leaves nothing to read in. It becomes a row of three, since three
     short names fit across a phone where a list of note titles would not. */
  .padwrap{flex-direction:column}
  .padnav{flex:1 1 auto;width:100%;flex-direction:row;position:static}
  .padnav a{flex:1 1 0;min-width:0}
}

.pad{display:flex;gap:9px;align-items:flex-start;margin:0 0 16px}
/* The composer is a COLUMN inside that row since the pad takes files: the text
   box on top, the clip and whatever is queued under it, and the Post button
   still beside the whole thing rather than under the queue. */
.padbox{flex:1;min-width:0;display:flex;flex-direction:column;gap:6px}
.padbox textarea,.nedit textarea{width:100%;font:13.5px/1.55 var(--font);
  padding:9px 11px;border-radius:8px;resize:vertical}
.padfoot{display:flex;align-items:center;gap:8px;flex-wrap:wrap}
/* The clip is a LABEL wrapping a hidden file input, which is the only way to
   restyle a file picker at all — the input is still what the form submits, and
   it is still what a pasted screenshot is put into. */
.clip{cursor:pointer;font-size:11.5px;color:var(--dim);background:var(--panel2);
  border:1px solid var(--line);border-radius:7px;padding:3px 8px;
  white-space:nowrap}
.clip:hover{border-color:var(--acc);color:var(--acc)}
.picked{display:flex;gap:5px;flex-wrap:wrap}
/* The keyboard, said out loud. Enter posting the note is the one behaviour on
   this page a person cannot discover by looking at it, so the box says so
   instead of the shortcut waiting to be stumbled on. Dim and small: a hint that
   competes with the note being written is worse than no hint. */
.padhint{font-size:11px;color:var(--dimmer);white-space:nowrap}
@media (max-width:520px){.padhint{display:none}}
/* One file that is queued but not posted yet. It is a button because clicking
   it takes the file back off — a mis-paste you cannot undo without reloading
   the page is the whole reason the queue is drawn at all. */
.chip{cursor:pointer;font:inherit;font-size:11px;color:var(--dim);
  background:var(--sunken);border:1px solid var(--line);border-radius:6px;
  padding:2px 7px;max-width:220px;overflow:hidden;text-overflow:ellipsis;
  white-space:nowrap}
.chip:hover{border-color:var(--fail);color:var(--fail)}
.notes{list-style:none;margin:0;padding:0;display:grid;gap:9px}
.note{background:var(--panel);border:1px solid var(--line);border-radius:9px;
  padding:11px 13px}
/* A pinned note is marked on its edge rather than by a second background wash:
   hover already washes, and two washes read as neither (the same call the
   Knowledge sheet's exact-match row makes). */
.note.pinned{box-shadow:inset 3px 0 0 var(--acc)}
.note.is-done{opacity:.72}
.note.is-done .nbody{text-decoration:line-through;color:var(--dim)}
/* The note's own text, kept as typed. `pre-wrap` is the whole reason a shared
   pad is usable: somebody pastes three lines and gets three lines. */
.nbody{white-space:pre-wrap;overflow-wrap:break-word;font-size:13.5px;
  line-height:1.55;color:var(--fg)}
.nfoot{display:flex;align-items:center;gap:10px;margin-top:8px;
  font-size:11.5px;color:var(--dimmer)}
.nwho{font-weight:600;color:var(--dim)}
.ndone{color:var(--ok)}
/* A note that was rewritten after somebody may already have read it. Warn, not
   dim: it is a fact about the text above it, not bookkeeping. */
.nedited{color:var(--warn)}
.nacts{margin-left:auto}

/* The note being edited in place. `?edit=<id>` is a page state, so this is a
   note that renders as a form rather than a panel a script opens. */
.nedit{display:flex;flex-direction:column;gap:7px}
.nedacts{margin-left:auto;display:flex;align-items:center;gap:9px}
.nedacts .btn{padding:5px 13px;font-size:12px}

/* What is attached to a note: pictures as thumbnails, everything else as a clip
   carrying its own name. The two are told apart by the media type read off the
   BYTES when the file landed, never by what the browser called it. */
.nfiles{list-style:none;margin:9px 0 0;padding:0;display:flex;flex-wrap:wrap;
  gap:9px;align-items:center}
.nfiles li{display:flex;align-items:center;gap:6px}
.nfiles form{display:inline-block;margin:0}
.nthumb{display:block;height:64px;width:auto;max-width:190px;object-fit:cover;
  border:1px solid var(--line);border-radius:7px;background:var(--sunken)}
.nthumb:hover{border-color:var(--acc)}
.nclip{font-size:11.5px;color:var(--dim);background:var(--sunken);
  border:1px solid var(--line);border-radius:7px;padding:4px 9px;
  text-decoration:none;max-width:270px;overflow:hidden;text-overflow:ellipsis;
  white-space:nowrap}
.nclip:hover{border-color:var(--acc);color:var(--acc)}
.nsize{color:var(--dimmer)}

/* ── the changelog feed ────────────────────────────────────────────────────
   ONE GRID, five fixed tracks, every row: caret · date · kind · headline · who.
   It replaced a stack of bordered cards on 2026-08-19 — twenty-one changes drew
   twenty-one boxes, nothing lined up down the page, and each entry is one
   sentence. The shape is the internal tracker's own changelog, which Guillaume
   picked off five drawn drafts; this is that design in the house's light
   palette, which is all it needed to move.

   ⚠ NO READING MEASURE ON THE FEED, and that is deliberate. The tab is
   `main.wide` like the other two (a page whose width changes between tabs moves
   the strip you are clicking), so the rows run the full width — they are
   single lines, and a wide line is what stops a headline being cut. The prose
   that DOES need a measure is the detail, and it carries its own below.

   The last track is a FIXED width sized to its longest value — the two-name
   form, "by MJB/Benoit" — so no row ever wraps to a second line and the feed's
   row heights stay even. */
.feed{border:1px solid var(--line);border-radius:9px;background:var(--panel);
  overflow:hidden}
.feed details{border-bottom:1px solid var(--line)}
.feed details:last-child{border-bottom:none}
.feed summary{list-style:none;cursor:pointer;padding:7px 12px;display:grid;
  grid-template-columns:12px 82px 108px minmax(0,1fr) 132px;
  gap:10px;align-items:baseline}
.feed summary::-webkit-details-marker{display:none}
.feed summary:hover{background:var(--panel2)}
.feed details[open] summary{background:var(--panel2)}
.feed .date{font-family:var(--mono);font-size:11.5px;color:var(--dim)}
/* Clamped shut, whole when open — the row never costs a second line, and the
   full headline is one click away with the detail it belongs to. */
.feed .line{color:var(--fg);min-width:0;overflow:hidden;
  text-overflow:ellipsis;white-space:nowrap}
.feed details[open] .line{white-space:normal;overflow:visible;
  font-weight:600;color:var(--strong)}
.feed .stat{color:var(--dimmer);font-size:11px;text-align:right;
  white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.feed .stat b{color:var(--dim);font-weight:600}
.feed .caret{color:var(--dimmer);font-size:10px;transition:transform .12s}
.feed details[open] .caret{color:var(--dim);transform:rotate(90deg)}
/* The detail hangs under the headline, in the headline's own column. */
.feed .detail{padding:2px 16px 14px 118px;font-size:12.5px;color:var(--dim)}
/* ⚠ THE MEASURE IS ON THE PARAGRAPH, NOT ON THE BOX. A `max-width` on
   `.detail` is measured INCLUDING its 118px gutter, which cuts the reading
   column to two thirds of what the number says (seen on the drafts, 2026-08-19). */
.feed .detail>*{max-width:88ch}
.feed .detail p{margin:0 0 8px;line-height:1.65}
.feed .detail p:last-child{margin-bottom:0}
.feed .detail code{font-family:var(--mono);font-size:11.5px;
  background:var(--sunken);border:1px solid var(--line);border-radius:4px;
  padding:0 4px}
/* The deploy state, said only when it is not the ordinary one. */
.feed .detail .state{font-size:11px;color:var(--dimmer);margin:0 0 8px;
  text-transform:uppercase;letter-spacing:.05em}
.feed .nodetail{color:var(--dimmer);font-style:italic}
.feedempty{padding:14px;color:var(--dimmer);font-style:italic}
/* Narrow: the five tracks collapse to three and everything that was to the
   right of the date stacks under it, still in one column. */
@media (max-width:900px){
  .feed summary{grid-template-columns:12px 78px minmax(0,1fr);row-gap:3px}
  .feed .kind,.feed .line,.feed .stat{grid-column:2/4;text-align:left}
  .feed .detail{padding-left:16px}}

/* ── production watch ──────────────────────────────────────────────────────
   The sentence under a row of numbers. It is a second `<tr>` rather than a
   wide cell on the first, because the numbers have to stay in their columns —
   the sentence is what a colleague reads and the numbers are what they check
   it against. */
table.list tr.detailrow td{color:var(--dim);font-size:12px;line-height:1.5;
  padding:0 12px 8px;border-bottom:1px solid var(--line)}
table.list tr.detailrow:hover{background:none}

/* The one sentence the production order page is allowed: what the table is not
   showing. It is a number, not an explanation of the page — the rule against
   prose (CLAUDE.md 6) is about the second, and rule 11 is about never letting a
   cap be silent. */
.capped{margin:0 0 9px;font-size:11.5px;color:var(--dim)}

/* ── the floor demo ────────────────────────────────────────────────────────
 * The Production Tracker's three pages: the progress table, the phone the
 * messages arrive through, and the triage queue. Everything below is used by
 * this app alone — the test for a rule living up in the shared part of this
 * file is that MORE THAN ONE app needs it — and it is here rather than in
 * `floor/static/` because that is the call the Knowledge sheets already made:
 * one stylesheet request per page, and the app bar's own tokens in scope.
 *
 * Nothing here invents a colour. Every rule reads the tokens at the top.
 */

/* ── the app's own tab strip ───────────────────────────────────────────────
 * The site's app bar carries one tab per APP, so this app's three pages were
 * reachable only through buttons on the progress page. This is the row that
 * fixes it: the app's name, its three pages, and what the orders are as of.
 * Metrics and grammar are the local prototype's, which is the design Guillaume
 * asked for on 2026-08-18; every colour in it is read off this file's tokens.
 *
 * It sits UNDER the app bar rather than in it: the bar is shared by six apps
 * and none of the others has pages of its own to offer. */
.ftabs{display:flex;align-items:center;gap:8px 16px;flex-wrap:wrap;
  margin:6px 0 14px;padding-bottom:9px;border-bottom:1px solid var(--line)}
.ftabs h1{margin:0;white-space:nowrap}
.ftabs nav{display:flex;gap:2px;flex-wrap:wrap}
.ftabs nav a{color:var(--dim);font-size:13px;font-weight:600;line-height:1.4;
  padding:5px 11px;border-radius:7px;white-space:nowrap}
.ftabs nav a:hover{color:var(--fg);background:var(--panel2);
  text-decoration:none}
/* The open page is stated by colour and a wash of the accent, not by a border
   and not by weight: a box that appears and disappears under the pointer moves
   what you aimed at, and a heavier face is a wider one — see the ⚠ on the app
   bar's `.cur`, which is the same defect one row up the page. */
.ftabs nav a.cur{color:var(--acc);background:rgb(var(--t-blue) / .14)}
/* Amber, not the accent and not red: the count is work waiting, which is the
   same thing the button above the table says in words and the same colour. */
.ftabs .tbadge{display:inline-block;min-width:16px;margin-left:3px;padding:0 5px;
  border-radius:9px;font-size:10.5px;font-weight:700;text-align:center;
  background:rgb(var(--t-amber) / .2);color:var(--t-amber-fg);
  border:1px solid rgb(var(--t-amber) / .45)}
.ftabs .fmeta{margin-left:auto;font-size:11.5px;color:var(--dimmer);
  white-space:nowrap}

/* The row of things a reader can act on, above the table. */
.fbarrow{display:flex;flex-wrap:wrap;gap:8px;margin:0 0 14px;align-items:center}
/* A queue with something in it is not "the way on" — it is work waiting — so it
   wears the amber of the strip's count rather than the accent lozenge. */
.fbarrow .btn.flag{background:rgb(var(--t-amber) / .16);color:var(--t-amber-fg);
  border-color:rgb(var(--t-amber) / .5)}
.fbarrow .btn.flag:hover{filter:none;background:rgb(var(--t-amber) / .26)}

/* ── the progress table ──────────────────────────────────────────────────── */

/* The widths are DECLARED, not measured. Left to the browser the table sizes
   its columns off whatever rows are in it, so the toggle above — three orders
   one way, every order the other — would move every column sideways.
   ⚠ THEY HAVE TO FIT THE RIGHT-HAND COLUMN, not the page: the phone takes 354px
   and the gap 18 out of `main.wide`'s 1468, which leaves about 1096. They sum
   to 1058 today. Widen one past that and `.tablewrap` starts scrolling
   sideways — which it will do quietly, so add it up before changing one. */
table.ftable{table-layout:fixed}
.ftable .w-order{width:130px}
.ftable .w-cust{width:78px}
.ftable .w-date{width:104px}
.ftable .w-prod{width:220px}
.ftable .w-num{width:92px}
.ftable .w-bar{width:160px}
.ftable .w-when{width:112px}
.ftable .w-msg{width:70px}
/* ⚠ FIXED ONLY WHERE THE FIXED WIDTHS FIT. The sum above needs about 1074px of
   right-hand column, which `main.wide` only has at a window of roughly 1490 and
   up. Narrower than that, honouring them would send `.tablewrap` scrolling
   sideways — quietly, which is the worst way for a table to fail — so the
   columns are measured instead and the table simply gets narrower. */
@media (max-width:1490px){table.ftable{table-layout:auto}}

/* ── READABILITY: this table does not wear the site's list metrics ─────────
 * Everywhere else on the site a `table.list` is something you scan for one
 * row, and its 10.5px uppercase head is right for that. This one is read
 * ACROSS — nine columns, three grains of row, a figure to compare against a
 * quantity — and Guillaume rejected the site metrics on it on 2026-08-18
 * against the local prototype: "the old one is much more readable".
 *
 * What changed, and each is a legibility fault the prototype did not have:
 * the head is sentence case at reading size instead of a letterspaced strip;
 * the dates and identifiers are read-strength ink instead of `--dimmer`, which
 * is a hint colour and was carrying half the columns; the rows are vertically
 * centred, so the bar sits on the figure's line; and the three grains are told
 * apart by an accent edge and a tint rather than by indentation alone. */
/* ⚠ 7px of side padding, not the site's 12. Nine columns pay for it eighteen
   times over, and since the merge the table only has what the phone leaves it —
   at Guillaume's own window that is the difference between the table fitting
   and `.tablewrap` scrolling sideways. */
table.ftable th{text-transform:none;letter-spacing:0;font-size:12px;
  font-weight:600;color:var(--dim);padding:7px 7px}
table.ftable td{padding:6px 7px;vertical-align:middle;font-size:13px}
table.ftable .c-date{color:var(--dim);font-size:12px}
table.ftable .c-id{color:var(--fg);font-size:12.5px}

/* LEVEL 1 — the order. Its identifier is the thing being scanned for, so it is
   the heaviest row, and it carries the accent edge that says "a new order
   starts here" without a colour legend. */
.ftable tr.orow > td{border-top:1px solid var(--line);background:var(--panel)}
.ftable tr.orow > td:first-child{box-shadow:inset 2px 0 0 0 var(--acc)}
.ftable tr.orow{cursor:pointer}
.ftable tr.orow .c-id b{font-size:13.5px;color:var(--strong)}
.ftable tr.orow[aria-expanded="true"] > td{background:var(--panel2)}
.ftable tr.orow:hover > td{background:var(--panel2)}

/* LEVEL 2 — the product. It leaves the order's own cells EMPTY and indents
   inside the Product column: the row above already says the order, the customer
   and the ship date, and repeating them down every product is the noise this
   level was added to remove. The tint is what makes that emptiness read as
   "belongs to the row above" rather than as a gap. */
.ftable tr.prow > td{background:var(--sunken)}
.ftable tr.prow.openable{cursor:pointer}
.ftable tr.prow:hover > td{background:var(--panel2)}
.ftable tr.prow[aria-expanded="true"] > td{background:var(--panel2)}
.ftable td.c-prod{padding-left:18px}

.ftable .c-sub{font-size:11px;color:var(--dim);font-family:var(--mono)}
/* The last-update stamp, date over time. One line it is the widest cell in the
   table; two lines it is narrower than the header above it, and the time reads
   as the detail of the date rather than as a second fact of equal weight. */
.ftable .c-when .d,.ftable .c-when .t{display:block;line-height:1.35}
.ftable .c-when .t{color:var(--dimmer);font-size:11px}
.ftable .c-dim{color:var(--dim);font-size:12.5px}
/* The reported figure is the one number on the row that is not reference — it
   is what the floor said — so it is the only one carrying weight and full ink. */
.ftable .c-num b{font-size:13.5px;color:var(--strong)}
.caret{display:inline-block;width:9px;color:var(--dim);font-size:9px;
  line-height:1;vertical-align:middle}

/* The advancement bar. Twenty steps as classes rather than one inline width,
   because this site ships `style-src 'self'` and a `style=` attribute is
   REFUSED — not ignored — so an inline width draws no bar at all and logs a
   violation on every page. */
/* It fills its column instead of sitting at a fixed 74px, and it is green
   rather than the accent: this is the one mark on the page that is not
   navigation, and the accent is what "the way on" looks like everywhere else
   here. `--ok` is a dark green on white and a bright one on black, so the bar
   survives the theme flip without naming a colour. */
.c-bar{white-space:nowrap}
.fbar{display:inline-block;width:calc(100% - 38px);min-width:40px;height:9px;
  border-radius:5px;background:var(--sunken);border:1px solid var(--line);
  overflow:hidden;vertical-align:middle}
.fbar .fill{display:block;height:100%;background:var(--ok)}
.fpct{display:inline-block;width:32px;margin-left:6px;text-align:right;
  font-size:11.5px;color:var(--dim);font-variant-numeric:tabular-nums}
.fill.w0{width:0}      .fill.w1{width:5%}   .fill.w2{width:10%}  .fill.w3{width:15%}
.fill.w4{width:20%}    .fill.w5{width:25%}  .fill.w6{width:30%}  .fill.w7{width:35%}
.fill.w8{width:40%}    .fill.w9{width:45%}  .fill.w10{width:50%} .fill.w11{width:55%}
.fill.w12{width:60%}   .fill.w13{width:65%} .fill.w14{width:70%} .fill.w15{width:75%}
.fill.w16{width:80%}   .fill.w17{width:85%} .fill.w18{width:90%} .fill.w19{width:95%}
.fill.w20{width:100%}

/* ── one message, drawn the same way wherever it is met ──────────────────── */
/* The progress page's opened rows and the triage queue share this markup, so
   the same message cannot look like two different objects depending on which
   page you are on. The right-hand slot is the only thing that differs: there it
   carries the figure this message contributed, here the reason it contributed
   none. */
.ftable tr.detail > td{background:var(--sunken);padding:10px 12px 12px}
.msglist{display:grid;gap:7px}
.msg{display:flex;align-items:flex-start;gap:10px;font-size:12.5px}
.msg .when{flex:0 0 108px;color:var(--dimmer);font-size:11px;
  font-variant-numeric:tabular-nums;padding-top:2px}
.msg .when .t{color:var(--dim)}
.msg .who{flex:0 0 96px}
.msg .who,.msg .what{display:inline-block;border-radius:999px;padding:1px 10px;
  line-height:1.65}
.msg .who{background:var(--panel2);border:1px solid var(--line);
  color:var(--dim);font-weight:600;font-size:11.5px;
  overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
/* The message itself, in the outgoing green of a chat app — the same size and
   shape as the name beside it, and NOT bold: the weight is what separates the
   speaker from what they said. */
.msg .what{flex:1 1 auto;min-width:0;background:rgb(var(--t-green) / .14);
  border:1px solid rgb(var(--t-green) / .32);color:var(--fg);
  overflow-wrap:break-word}
.msg .verdict{flex:0 0 300px;text-align:right;color:var(--dim);font-size:11.5px;
  padding-top:2px}
.msg .verdict b{margin-left:8px;color:var(--fg);
  font-variant-numeric:tabular-nums}
.msg .thumb{height:26px;border-radius:4px;vertical-align:middle;margin-left:6px}
/* The header row of the queue wears the same metrics and none of the pills, so
   the columns line up with the messages under it. */
/* Sentence case at reading size, the same call the progress table's head made:
   a letterspaced 10.5px strip is right above a list you scan for one row and
   wrong above four columns a person reads across. */
.msg.mhead{color:var(--dim);font-size:12px;font-weight:600;
  padding-bottom:3px;border-bottom:1px solid var(--line)}
.msg.mhead .who,.msg.mhead .what{background:none;border:none;padding:1px 0;
  color:var(--dim);font-size:12px;font-weight:600}

/* ── the triage queue ────────────────────────────────────────────────────── */
.msglist.queue{gap:0}
/* Left, not right: this slot holds a SENTENCE here where the progress page
   holds a figure, and a three-line sentence ragged down its left edge is
   one nobody finishes reading. */
.msglist.queue .verdict{text-align:left}
.msglist.queue .msg.openable{cursor:pointer;padding:7px 0;
  border-bottom:1px solid var(--line)}
.msglist.queue .msg.openable:hover{background:var(--panel2)}
.qdetail{background:var(--sunken);border:1px solid var(--line);
  border-radius:0 0 9px 9px;padding:12px 14px 14px;margin-bottom:8px}
.qdetail .shot{max-width:280px;border-radius:8px;border:1px solid var(--line)}
.whylist{margin:0 0 10px;padding-left:18px;color:var(--dim);font-size:12px;
  line-height:1.6}
.qform{display:flex;flex-wrap:wrap;gap:9px;align-items:flex-end;
  margin:0 0 9px}
.qform .qf label{display:block;font-size:10px;text-transform:uppercase;
  letter-spacing:.05em;color:var(--dimmer);margin-bottom:3px}
.qform .f2{flex:2 1 320px;min-width:0}
.qform .f1{flex:1 1 160px;min-width:0}
.qform .f0{flex:0 1 100px}
.qform .qf input{width:100%;box-sizing:border-box}
.qform select{width:100%;box-sizing:border-box;background:var(--sunken);
  color:var(--fg);border:1px solid var(--line);border-radius:7px;
  padding:7px 9px;font:13px var(--font)}
.qform select:focus{outline:none;border-color:var(--acc);
  box-shadow:0 0 0 3px rgb(11 95 165 / .15)}

/* ── the report screen: the phone and the table it feeds ─────────────────── */
/* ONE SCREEN, TWO COLUMNS since 2026-08-18 — Guillaume: "the messenger on the
   left, the report on the right". They were two tabs, and the demo's whole
   argument is that a figure typed into the handset moves a bar beside it
   without the page changing under you; two tabs made that two screens and a
   click, which is exactly the thing being demonstrated, hidden.

   The left column is FIXED at the handset's width and the right takes whatever
   is left (`flex:1 1 0` + `min-width:0`, so the fixed-layout table inside it
   scrolls in its own wrap rather than pushing the phone out of the row).
   ⚠ the verdict NEVER appears inside a bubble — the annotation lines and the
   card under the handset are where it goes. */
.fstage{display:flex;gap:18px;align-items:flex-start}
.fside{flex:0 0 354px;min-width:0}
.fmain{flex:1 1 0;min-width:0}
/* Stacked only when the right-hand column has stopped being a table's worth of
   room. Above this the table compresses (see `table-layout` below); below it,
   the phone sits at its own width and the table takes the page's. */
@media (max-width:1220px){
  .fstage{flex-direction:column}
  .fside{flex:none;width:354px;max-width:100%}
  .fmain{width:100%;align-self:stretch}
}
/* ⚠ THE HANDSET IS 354px WIDE AND ITS CHROME IS TRIMMED TO MATCH. It was 372
   as a page of its own; the merge gave those 18px to the table, and the
   composer's paddings below gave the same 18 back to the text box — so the
   taught example still sits on ONE line in it, which is the whole reason the
   placeholder is a real message rather than "type here".
   ⚠ THE BORDER IS THICK ON PURPOSE and it is the only element on the site that
   is. A 1px rule is the same edge every card here draws, so the phone read as
   another panel; this is a handset, and what says so is a heavy bezel plus the
   deeper corner radius inside it. Ruled 2026-08-18. */
.phone{width:100%;max-width:100%;background:var(--panel);
  border:5px solid var(--strong);border-radius:26px;overflow:hidden;
  box-shadow:0 8px 28px rgb(0 0 0 / .16)}
.chead{display:flex;align-items:center;gap:9px;padding:10px 14px;
  background:var(--panel2);border-bottom:1px solid var(--line)}
.chead .avatar{width:30px;height:30px;border-radius:9px;flex:none;
  display:flex;align-items:center;justify-content:center;
  background:var(--acc);color:var(--on-acc);font-size:14px;font-weight:700}
.chead .cwho{display:flex;flex-direction:column;font-size:13px;font-weight:650;
  color:var(--strong);line-height:1.35}
.chead .cwho small{font-size:10.5px;font-weight:400;color:var(--dimmer)}
.chead .crest{margin-left:auto;font-size:11px;color:var(--dimmer);
  font-variant-numeric:tabular-nums}

.thread{height:520px;overflow-y:auto;padding:12px 14px;background:var(--bg)}
.thread .sep{text-align:center;font-size:10.5px;color:var(--dimmer);
  margin:10px 0 8px}
/* A turn is the mark and the words beside it, so the row direction is what puts
   a speaker on their own side of the thread — `me` is whoever is holding the
   phone (a floor manager), `them` is the company account answering. */
.turn{display:flex;align-items:flex-end;gap:8px;margin:0 0 4px}
.turn.me{flex-direction:row-reverse}
.turn .side{display:flex;flex-direction:column;min-width:0;
  max-width:calc(100% - 42px)}
.turn.me .side{align-items:flex-end}
.turn .hdr{display:flex;gap:7px;align-items:baseline;font-size:10.5px;
  color:var(--dimmer);margin:6px 2px 3px}
.turn .hdr b{color:var(--dim);font-size:11px}

/* ⚠ ONE MARK FOR EVERY SENDER, and a different one for the account answering.
   A chat message arrives with a typed name and nothing else, so a per-sender
   avatar would draw an identity the demo does not have; what the two marks
   separate is the two SIDES of the conversation, which is the only distinction
   there is. `生` is the company account, the same character its header wears. */
.uav{flex:none;width:26px;height:26px;border-radius:50%;margin-bottom:2px;
  display:flex;align-items:center;justify-content:center;
  border:1px solid var(--line);box-sizing:border-box}
.uav svg{width:16px;height:16px;display:block}
.uav.gen{background:var(--panel2);color:var(--dim)}
.uav.app{background:var(--acc);color:var(--on-acc);border-color:var(--acc);
  font-size:13px;font-weight:700;line-height:1}
/* ⚠ `pre-wrap`, which is why the template strips every whitespace boundary
   inside this element — an indented tag renders as a blank line in the bubble. */
.bub{max-width:100%;padding:7px 11px;border-radius:13px;font-size:13px;
  line-height:1.5;white-space:pre-wrap;overflow-wrap:break-word;
  box-sizing:border-box}
.turn.me .bub{background:rgb(var(--t-green) / .2);
  border:1px solid rgb(var(--t-green) / .38);color:var(--fg);
  border-bottom-right-radius:4px}
.turn.them .bub{background:var(--panel);border:1px solid var(--line);
  color:var(--fg);border-bottom-left-radius:4px}
.bub img{display:block;max-width:100%;border-radius:8px;margin-top:6px}
/* Not part of the conversation, and styled so it cannot be mistaken for part of
   it: the sender is told 收到 and nothing else, forever.
   ⚠ IT IS RIGHT-ALIGNED AND NO WIDER THAN THE BUBBLE IT JUDGES. It ran the full
   width of the phone until 2026-08-18, which left a refusal pinned to the left
   edge while the message it was about sat on the right — the reader had to pair
   them up. The dashed rule now spans the message, not the handset. */
/* 34px is the mark plus its gap: the same inset the bubble above already has,
   so the dashed rule ENDS where the message ends rather than running on under
   the avatar. Change one and change the other. */
.annot{display:flex;justify-content:flex-end;gap:7px;align-items:baseline;
  flex-wrap:wrap;text-align:right;
  width:76%;margin:2px 34px 9px auto;padding:3px 0 0;
  font-size:11px;color:var(--dim);border-top:1px dashed var(--line)}

.composer{border-top:1px solid var(--line);background:var(--panel2);
  padding:9px 8px 11px}
.composer .asline{display:flex;align-items:center;gap:8px;margin-bottom:7px}
.composer .asline label{font-size:10px;text-transform:uppercase;
  letter-spacing:.05em;color:var(--dimmer);white-space:nowrap}
.composer .asline .input{flex:1;min-width:0;font-size:12.5px;padding:5px 9px}
.composer .picked{font-size:10.5px;color:var(--dim);white-space:nowrap}
.composer .cline{display:flex;align-items:flex-end;gap:6px}
.composer textarea{flex:1;min-width:0;resize:none;max-height:120px;
  font:13px/1.5 var(--font);padding:8px 11px;border-radius:16px;
  background:var(--panel);color:var(--fg);border:1px solid var(--line)}
.composer textarea:focus{outline:none;border-color:var(--acc);
  box-shadow:0 0 0 3px rgb(11 95 165 / .15)}
.composer .iconbtn{flex:none;cursor:pointer;color:var(--dim);padding:6px 3px}
.composer .iconbtn:hover{color:var(--acc)}
.composer .iconbtn svg{width:20px;height:20px;display:block}
.composer .send{flex:none;border-radius:16px;padding:7px 12px}

/* ── what sits UNDER the phone ─────────────────────────────────────────────
   It sat BESIDE the phone until 2026-08-18, in a 560px reading column that took
   the half of the screen the report now has. Guillaume cut it: "remove the text
   that sits next to the messenger phone… reduce the texte and put it under the
   cellphone for a quick explanation". So it is the handset's own width, and
   what is left in it is two paragraphs plus the verdict of the last send. */
.beside{margin-top:14px;min-width:0}
.beside .card{margin:0 0 14px}
.beside .card h2{margin:0 0 9px;font-size:14px}
.fverdict{border-radius:8px;padding:9px 12px;font-size:13px;
  border:1px solid var(--line);background:var(--sunken)}
.fverdict.record{border-color:rgb(var(--t-green) / .45);
  background:rgb(var(--t-green) / .10)}
.fverdict.review{border-color:rgb(var(--t-amber) / .45);
  background:rgb(var(--t-amber) / .10)}
.fverdict ul{margin:7px 0 0;padding-left:18px;font-size:12px;color:var(--dim);
  line-height:1.6}
.fverdict ul.opts{font-family:var(--mono);font-size:11.5px}
table.list.kv{margin-top:11px}
/* Wide enough for the longest label on ONE line — "Products named" wraps at
   anything under about 112, and a two-line label beside a one-line value reads
   as two rows. */
table.list.kv th{width:116px;padding:5px 8px;text-transform:none;
  font-size:11.5px;vertical-align:top}
/* Two short paragraphs at the handset's width, so it reads as a caption on the
   phone rather than a second column of prose competing with the report. */
.fnote{background:rgb(var(--t-blue) / .09);border:1px solid rgb(var(--t-blue) / .28);
  border-radius:9px;padding:9px 12px;font-size:12px;line-height:1.55;
  color:var(--dim)}
.fnote p{margin:7px 0}
.fnote p:first-child{margin-top:0}
.fnote p:last-child{margin-bottom:0}
.fnote code{font-family:var(--mono);font-size:12px;color:var(--fg)}

@media (max-width:760px){
  .fside{width:100%}
  .msg{flex-wrap:wrap}
  .msg .verdict{flex:1 1 100%;text-align:left}
}


/* ══ Production watch — the orders list ══════════════════════════════════════
   The ruled layout (2026-08-18, Guillaume's option 2 of five drawn against the
   real corpus): ELEVEN COLUMNS, and an order's finished products unfold as rows
   of the SAME table so a product's bar sits directly under its order's bar.

   ⚠ Everything here is prefixed `p…` or scoped to `table.plist` on purpose.
   `.seg` on this site is already the segmented control at the top of the records
   page, and `.rail`/`.fchip` are already the filter chips — which this page
   reuses unchanged, because they mean the same thing here. Only the things that
   would have COLLIDED are renamed. */

table.plist{table-layout:fixed}
table.plist td,table.plist th{padding-left:9px;padding-right:9px}
table.plist .small{font-size:11.5px}
/* ⚠ ELEVEN COLUMNS AT GUILLAUME'S REAL 1246px, and they are declared because a
   fixed-layout table with no colgroup shares its width equally: the first render
   put the advancement bar's percentage through the 布 chip beside it and pushed
   Captured off the right edge. Eleven was already the drawn layout's own budget,
   and the eleventh was what had to be squeezed to fit. */
.plist .p-exp{width:24px}
.plist .p-order{width:90px}
/* ⚠ Wider than a three-letter client code needs, and that is the borrowed
   column paying for itself: on an unfolded row this cell carries the finished
   product's NAME, and at the width the code alone wanted it read
   "SHOULDER B…". The full name still rides the cell as a title, because even at
   120px a long one clips. */
.plist .p-client{width:120px}
.plist .p-items{width:58px}
.plist .p-ship{width:90px}
.plist .p-left{width:66px}
.plist .p-adv{width:146px}
.plist .p-strip{width:132px}
.plist .p-spend{width:94px}
.plist .p-dis{width:112px}
.plist .p-cap{width:88px}
/* The two headings long enough to wrap sit over narrow numeric columns, and a
   two-line heading band drags every other heading's baseline with it. */
table.plist th{white-space:nowrap}

/* An expandable row is a button in everything but name, so the whole row takes
   the pointer and the chevron is the affordance that says so — a row that only
   reveals itself on hover is a row nobody finds. */
table.plist th.c-exp,table.plist td.c-exp{width:22px;padding-left:10px;padding-right:0}
table.plist tr.orow{cursor:pointer}
.pchev{display:inline-block;color:var(--dimmer);font-size:15px;line-height:1;
  transition:transform .12s ease}
table.plist tr.orow[aria-expanded="true"] .pchev{transform:rotate(90deg);color:var(--acc)}
table.plist tr.orow[aria-expanded="true"]>td{background:var(--panel2);font-weight:600}

/* The product rows. Sunken, with the accent rule down their left edge, so a scan
   down the table can see at a glance which rows are parts and which are wholes. */
table.plist tr.kidrow>td{background:var(--sunken);color:var(--dim)}
table.plist tr.kidrow:hover>td{background:var(--sunken)}
table.plist tr.kidrow>td:first-child{box-shadow:inset 3px 0 0 var(--acc)}
table.plist .kidid{padding-left:20px !important}
/* The two BORROWED columns — a product's name where Client sits, its sheet
   number where Days left sits. Quiet on purpose: the column heading is about
   orders, and a child row saying something else must not read as the same fact
   one line down. */
table.plist .c-prod{color:var(--dim);font-weight:400;overflow:hidden;
  text-overflow:ellipsis;white-space:nowrap}
table.plist .sheetcell{color:var(--dimmer)}
/* A parsed code is a weaker claim than a stored one, and the mark says so
   without spending a column on it. */
.pmark{color:var(--dimmer);font-size:11px;margin-left:3px;cursor:help}

/* ── the advancement bar ──────────────────────────────────────────────────── */
.c-adv{white-space:nowrap}
/* ⚠ `max-width:100%` and `min-width:0` together are what keep the bar INSIDE its
   column. A flex item's default `min-width:auto` refuses to shrink below its
   content, so the bar held its 96px floor, overflowed the cell and painted the
   percentage on top of the material chips in the next column. */
.pcell{display:inline-flex;align-items:center;gap:6px;vertical-align:middle;
  max-width:100%;width:100%}
.pbar{display:flex;height:11px;border-radius:6px;overflow:hidden;
  background:var(--sunken);border:1px solid var(--line);
  flex:1 1 auto;min-width:0}
.pbar.mini{height:8px;border-radius:4px}
/* ⚠ The fallback sits BEFORE the named rungs and must stay there: it has the
   same specificity as `.pseg.st-received`, so placed after it would repaint
   every segment slate. It exists because a rung this stylesheet has never heard
   of still has to PAINT — an invisible segment makes the bar draw a smaller
   population than the number printed beside it. */
.pbar .pseg{height:100%;background:rgb(var(--t-slate) / .5)}
.ppct{font-variant-numeric:tabular-nums;color:var(--dim);font-size:11px;
  flex:0 0 auto;width:29px;text-align:right}

/* One rung of the readiness ladder, one colour. The class arrives ON THE DATA
   from the producer (`st-received` …), never derived here — a rung added
   upstream reaches the page already named, and only its colour is missing. */
.pseg.st-received{background:rgb(var(--t-green))}
.pseg.st-ordered{background:rgb(var(--t-amber))}
.pseg.st-stock{background:rgb(var(--t-blue))}
.pseg.st-covered{background:rgb(var(--t-cyan))}
.pseg.st-filed{background:rgb(var(--t-teal))}
.pseg.st-offsheet{background:rgb(var(--t-violet))}
.pseg.st-supply{background:rgb(var(--t-slate) / .55)}
.pseg.st-partial{background:rgb(var(--t-orange))}
.pseg.st-filedpart{background:rgb(var(--t-orange) / .55)}
.pseg.st-drafted{background:rgb(var(--t-rose) / .6)}
.pseg.st-open{background:rgb(var(--t-red))}
.pseg.st-zeroed{background:rgb(var(--t-slate) / .4)}
.pseg.st-unexpl{background:rgb(var(--t-slate))}
.pseg.st-nil{background:rgb(var(--t-slate) / .25)}
/* One class per whole percent. ⚠ The site ships `style-src 'self'`, so a width
   cannot ride in a `style=` attribute — it is refused outright, and the bar
   renders as a hairline while the console fills with violations. The same answer
   the floor page's bar already gives, at a finer step. */
.pw0{width:0%}.pw1{width:1%}.pw2{width:2%}.pw3{width:3%}.pw4{width:4%}.pw5{width:5%}.pw6{width:6%}.pw7{width:7%}
.pw8{width:8%}.pw9{width:9%}.pw10{width:10%}.pw11{width:11%}.pw12{width:12%}.pw13{width:13%}.pw14{width:14%}.pw15{width:15%}
.pw16{width:16%}.pw17{width:17%}.pw18{width:18%}.pw19{width:19%}.pw20{width:20%}.pw21{width:21%}.pw22{width:22%}.pw23{width:23%}
.pw24{width:24%}.pw25{width:25%}.pw26{width:26%}.pw27{width:27%}.pw28{width:28%}.pw29{width:29%}.pw30{width:30%}.pw31{width:31%}
.pw32{width:32%}.pw33{width:33%}.pw34{width:34%}.pw35{width:35%}.pw36{width:36%}.pw37{width:37%}.pw38{width:38%}.pw39{width:39%}
.pw40{width:40%}.pw41{width:41%}.pw42{width:42%}.pw43{width:43%}.pw44{width:44%}.pw45{width:45%}.pw46{width:46%}.pw47{width:47%}
.pw48{width:48%}.pw49{width:49%}.pw50{width:50%}.pw51{width:51%}.pw52{width:52%}.pw53{width:53%}.pw54{width:54%}.pw55{width:55%}
.pw56{width:56%}.pw57{width:57%}.pw58{width:58%}.pw59{width:59%}.pw60{width:60%}.pw61{width:61%}.pw62{width:62%}.pw63{width:63%}
.pw64{width:64%}.pw65{width:65%}.pw66{width:66%}.pw67{width:67%}.pw68{width:68%}.pw69{width:69%}.pw70{width:70%}.pw71{width:71%}
.pw72{width:72%}.pw73{width:73%}.pw74{width:74%}.pw75{width:75%}.pw76{width:76%}.pw77{width:77%}.pw78{width:78%}.pw79{width:79%}
.pw80{width:80%}.pw81{width:81%}.pw82{width:82%}.pw83{width:83%}.pw84{width:84%}.pw85{width:85%}.pw86{width:86%}.pw87{width:87%}
.pw88{width:88%}.pw89{width:89%}.pw90{width:90%}.pw91{width:91%}.pw92{width:92%}.pw93{width:93%}.pw94{width:94%}.pw95{width:95%}
.pw96{width:96%}.pw97{width:97%}.pw98{width:98%}.pw99{width:99%}.pw100{width:100%}

/* ── the six material-type chips ──────────────────────────────────────────── */
.pstrip{display:inline-flex;gap:3px;white-space:nowrap}
.pstrip .pcat{display:inline-flex;align-items:center;justify-content:center;
  width:17px;height:17px;border-radius:3px;font-size:10px;line-height:1;
  border:1px solid transparent;cursor:help}
.pcat.cat-recv{background:rgb(var(--t-green) / .18);color:var(--t-green-fg);
  border-color:rgb(var(--t-green) / .45)}
.pcat.cat-ord{background:rgb(var(--t-amber) / .18);color:var(--t-amber-fg);
  border-color:rgb(var(--t-amber) / .45)}
.pcat.cat-filedrecv{background:rgb(var(--t-teal) / .18);color:var(--t-teal-fg);
  border-color:rgb(var(--t-teal) / .45)}
.pcat.cat-open{background:rgb(var(--t-red) / .18);color:var(--t-red-fg);
  border-color:rgb(var(--t-red) / .45)}
.pcat.cat-unknown{background:rgb(var(--t-slate) / .16);color:var(--t-slate-fg);
  border-color:rgb(var(--t-slate) / .35)}
.pcat.cat-nil{background:rgb(var(--t-slate) / .1);color:var(--dimmer)}
/* ⚠ A type this order has NO line of keeps its slot as a dashed outline. "No
   packaging line" and "packaging is fine" are different facts and must not look
   alike — which is the whole reason the strip is fixed at six. */
.pcat.cat-none{background:transparent;color:var(--dimmer);border-style:dashed;
  border-color:var(--line)}

/* ── days left ────────────────────────────────────────────────────────────────
   Toned by proximity, and an overdue row says "over" rather than showing a minus
   sign: a negative number in a column of positive ones reads as an error in the
   data rather than as a fact about the order. */
.pdl{font-variant-numeric:tabular-nums;font-size:11.5px;white-space:nowrap;
  border-radius:10px;padding:1px 7px;font-weight:600}
.pdl.over{color:var(--t-red-fg);background:rgb(var(--t-red) / .16);
  border:1px solid rgb(var(--t-red) / .4)}
.pdl.d0{color:var(--t-red-fg);background:rgb(var(--t-red) / .2);
  border:1px solid rgb(var(--t-red) / .5)}
.pdl.d7{color:var(--t-orange-fg);background:rgb(var(--t-orange) / .16);
  border:1px solid rgb(var(--t-orange) / .4)}
.pdl.d21{color:var(--t-amber-fg);background:rgb(var(--t-amber) / .14);
  border:1px solid rgb(var(--t-amber) / .35)}
.pdl.dfar{color:var(--dim);background:none;border:1px solid transparent;font-weight:400}

/* ── what is held back until asked for ────────────────────────────────────────
   The product rows carry `hidden` already, so only the order row itself needs a
   rule. ⚠ `display:none` and not `visibility`: a hidden table row that still
   occupies its space leaves a band of blank grid where the reader expects the
   next order. */
tr.orow.xrow{display:none}
.showall tr.orow.xrow{display:table-row}
/* Brought back, but still marked. A quiet rule down the left edge and a dimmed
   order number say "this one was off the page" without a badge on 45 rows. */
.showall tr.orow.xrow>td:first-child{box-shadow:inset 3px 0 0 var(--line)}
/* ⚠ The CELL, not an anchor inside it. The order number stopped being a link on
   2026-08-18 (Guillaume: the page it opened was useless), and this rule went on
   selecting `.c-id a` for an hour — matching nothing, so a row brought back by
   `Show all` looked exactly like one that had never left. */
.showall tr.orow.xrow .c-id{color:var(--dim)}
.uchip.xrow{display:none}
.showall .uchip.xrow{display:inline-block}
/* The chips are <button>s here rather than the links they are elsewhere: this
   filter changes nothing about what the page IS, so it must not be a URL that
   somebody bookmarks and comes back to as a different-looking page. */
button.fchip{cursor:pointer;font:12px var(--font);color:var(--dim)}
button.fchip:hover{border-color:var(--dim);color:var(--fg)}
.railnote{font-size:11.5px;color:var(--dimmer);max-width:66ch;line-height:1.5}

/* ── the block that cannot be sorted ──────────────────────────────────────── */
.undated{margin:12px 0 0;border:1px dashed var(--line);border-radius:9px;
  padding:9px 12px;background:var(--sunken)}
.uhead{font-size:11.5px;color:var(--dim);margin:0 0 7px;max-width:88ch}
.urow{display:flex;flex-wrap:wrap;gap:6px}
.uchip{display:inline-block;font-size:11.5px;background:var(--panel);
  border:1px solid var(--line);border-radius:6px;padding:2px 8px;color:var(--dim)}
/* ⚠ NO HOVER STATE. These were links to the order page and are plain spans since
   it was removed; a chip that lights under the cursor is a chip promising a click
   that does nothing. A dateless order's products are genuinely unreachable from
   this page — that hole is named on the details page rather than hidden here. */
.uchip b{font-family:var(--mono);color:var(--acc);font-weight:700}

@media (max-width:1100px){table.plist{table-layout:auto}}


/* ══ Production watch — one finished product, material by material ═══════════
   Built 2026-08-18 on Guillaume's ask, replacing the order page he called
   useless. Every material of one purchasing sheet, grouped into the sheet's own
   sections, with the disagreements as sentences under the row they are about.

   ⚠ Prefixed `m…` or scoped to `table.mlist`, for the reason the orders list
   gives above: `.pill`, `.rail` and the `st-*` rung names are already site-wide
   and mean the same thing here, so only what would have COLLIDED is renamed. */

table.mlist{table-layout:fixed}
table.mlist td,table.mlist th{padding-left:9px;padding-right:9px}
table.mlist th{white-space:nowrap}
/* ⚠ ELEVEN COLUMNS, DECLARED. A fixed-layout table with no colgroup shares its
   width equally, and eleven equal shares put the four quantity columns under a
   material name that needs three of them. They add to less than the table, so
   the browser hands the slack out in proportion. */
.mlist .m-name{width:24%}
.mlist .m-code{width:78px}
.mlist .m-stat{width:118px}
.mlist .m-q{width:74px}
.mlist .m-uom{width:44px}
.mlist .m-p{width:66px}
.mlist .m-vendor{width:15%}
/* The name and the supplier are the only free-text cells and both clip rather
   than wrap: a row that grows to three lines when one supplier has a long name
   breaks the scan down the quantity columns, which is what the page is for. The
   whole string rides each cell as a title. */
.mlist .mname,.mlist .mvendor{overflow:hidden;text-overflow:ellipsis;
  white-space:nowrap}
.mlist .mvendor{color:var(--dim);font-size:11.5px}
.mlist .c-uom{color:var(--dim);font-size:11.5px}
/* A row something is wrong with. A rule down its left edge and nothing else —
   a tinted row would fight the rung pill, which is the thing actually saying how
   bad it is, and on a sheet where half the rows drift a tint is the background. */
table.mlist tr.bad>td:first-child{box-shadow:inset 3px 0 0 rgb(var(--t-rose) / .55)}

/* ── the section headings ─────────────────────────────────────────────────────
   A row of the same table, not a second table: the eleven columns have to stay
   in one grid down the whole page or the quantities stop lining up across
   sections, which is the only reason to group them in the first place. */
table.mlist tr.secrow>td{background:var(--panel2);border-top:1px solid var(--line);
  padding-top:7px;padding-bottom:7px}
table.mlist tr.secrow:hover>td{background:var(--panel2)}
.secchar{display:inline-flex;align-items:center;justify-content:center;
  width:19px;height:19px;border-radius:4px;font-size:11px;
  background:var(--sunken);border:1px solid var(--line);color:var(--dim);
  vertical-align:middle;cursor:help}
.secname{font-weight:650;color:var(--strong);font-size:12.5px;margin-left:7px}
.secn{color:var(--dimmer);font-size:11.5px;margin-left:9px}
.secrow .pill{margin-left:8px}

/* ── one rung of the readiness ladder, as a pill ──────────────────────────────
   ⚠ The class arrives ON THE DATA from the producer (`st-received` …), the same
   names the advancement bar's segments wear, so the pill on a row and the colour
   in the bar above it are the same rung by construction rather than by two lists
   agreeing. A fifteenth rung reaches the page named, and falls back to the slate
   below rather than rendering as unstyled text. */
.mstat{display:inline-block;font-size:10.5px;line-height:1.7;padding:0 7px;
  border-radius:11px;white-space:nowrap;font-weight:600;cursor:help;
  color:var(--t-slate-fg);background:rgb(var(--t-slate) / .16);
  border:1px solid rgb(var(--t-slate) / .4)}
.mstat.st-received{color:var(--t-green-fg);background:rgb(var(--t-green) / .16);border-color:rgb(var(--t-green) / .4)}
.mstat.st-ordered{color:var(--t-amber-fg);background:rgb(var(--t-amber) / .16);border-color:rgb(var(--t-amber) / .4)}
.mstat.st-stock{color:var(--t-blue-fg);background:rgb(var(--t-blue) / .16);border-color:rgb(var(--t-blue) / .4)}
.mstat.st-covered{color:var(--t-cyan-fg);background:rgb(var(--t-cyan) / .16);border-color:rgb(var(--t-cyan) / .4)}
.mstat.st-filed{color:var(--t-teal-fg);background:rgb(var(--t-teal) / .16);border-color:rgb(var(--t-teal) / .4)}
.mstat.st-offsheet{color:var(--t-violet-fg);background:rgb(var(--t-violet) / .16);border-color:rgb(var(--t-violet) / .4)}
.mstat.st-partial{color:var(--t-orange-fg);background:rgb(var(--t-orange) / .16);border-color:rgb(var(--t-orange) / .4)}
.mstat.st-filedpart{color:var(--t-orange-fg);background:rgb(var(--t-orange) / .1);border-color:rgb(var(--t-orange) / .3)}
.mstat.st-drafted{color:var(--t-rose-fg);background:rgb(var(--t-rose) / .1);border-color:rgb(var(--t-rose) / .3)}
.mstat.st-open{color:var(--t-red-fg);background:rgb(var(--t-red) / .16);border-color:rgb(var(--t-red) / .4)}
/* The three that mean "there is nothing to read here" lose their fill: a supply
   line, a labour line and a job are the normal state of their sections, and
   painting them as loudly as a missing purchase order is how 2,976 rows came to
   say something was wrong when nothing was. */
.mstat.st-supply,.mstat.st-nil,.mstat.st-work{color:var(--dimmer);
  background:none;border-color:var(--line);font-weight:400}

/* An archived product — retired from the catalogue, cannot be bought again
   without being restored. 🗣️ 2026-08-14: *"I just would like my production
   watcher to show which raw products are archived"*. */
.pmark.arch{color:var(--t-red-fg);font-weight:700}

/* The advancement bar and the six chips share the card's last fact, which needs
   the whole row rather than the 110px a fact gets. */
.facts .fact.wide{flex:1 1 100%;min-width:0}
.facts .fact.wide dd{display:flex;align-items:center;gap:14px;max-width:420px}

@media (max-width:1100px){table.mlist{table-layout:auto}}
/* A dateless order's finished products, beside its chip. They have no row in the
   table to unfold — the deadline axis has no place for them — so this is the only
   door to their pages. ✅ Seven such orders, one product between them. */
.ukid{font-family:var(--mono);font-size:11px;margin-left:5px;
  border-bottom:1px solid rgb(var(--t-blue) / .35)}
.ukid:hover{text-decoration:none;border-bottom-color:var(--acc)}
