/* ============================================================
   BRAND TOKENS
   Biostream signal colours carried onto a light board. The dark
   field interface belongs on the device and in the operational
   product. A trusted research environment is used for hours at a
   time, on university and institutional desktops, often beside
   printed material, so the environment reads as paper rather than
   as a cockpit. The five brand colours below are unchanged in
   role and darkened only enough to hold contrast on white.
   Replace them with the exact brand guide values and every
   screen in this deck updates.
   ============================================================ */
:root{
  --brand-teal:#16686D;      /* primary action (POC design) */
  --brand-teal-lift:#0F5257; /* hover, focus, small text (POC design) */
  --control:#A87A22;         /* reserved: perimeter, suppression, airlock */
  --blocked:#AF432B;         /* reserved: a check that did not pass */
  --cleared:#2E7A59;         /* reserved: a check that passed */
  --paper:#10222A;           /* high emphasis text, the ink */

  /* tints, used behind the signal colours */
  --teal-wash:#E2EFEF;
  --control-wash:#F8F0DE;
  --blocked-wash:#F9E8E3;
  --cleared-wash:#E5F0EA;

  /* environment chrome */
  --base:#EFF4F6;
  --surface:#FFFFFF;
  --surface-2:#F7FAFB;
  --surface-3:#EDF3F5;
  --hairline:#D4E0E4;
  --hairline-soft:#E6EDEF;
  --text:#33474F;
  --muted:#647E88;
  --dim:#83969F;

  --ui:"Inter",-apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif;
  --label:"Barlow Condensed","Inter",sans-serif;
  --mono:"JetBrains Mono",ui-monospace,Menlo,monospace;
}

*{box-sizing:border-box;}
/* Not smooth. Smooth scrolling on the root swallows the scroll a browser does
   when it lands on a fragment: `/instructions/?open=ask#ask` opened the right
   section and stayed at the top of the page, so every deep link on the platform
   arrived looking like it had done nothing. Measured rather than guessed at:
   with smooth, a fresh load left scrollY at 0 with the target 485px down; with
   auto it landed on it. The animation was worth less than working deep links. */
html{scroll-behavior:auto;}
body{
  margin:0;background:var(--base);color:var(--text);
  font-family:var(--ui);font-size:15px;font-weight:400;line-height:1.62;
  -webkit-font-smoothing:antialiased;
}
h1,h2,h3,h4{margin:0;font-weight:500;color:var(--paper);letter-spacing:-.01em;}
p{margin:0 0 12px;}
button{font-family:inherit;cursor:pointer;}
:focus-visible{outline:2px solid var(--brand-teal-lift);outline-offset:2px;}



.stage{max-width:1360px;margin:0 auto;padding:34px 26px 0;}
/* Sign in card fades and lifts away as the code is accepted... */
@keyframes appleave{from{opacity:1;transform:none;}to{opacity:0;transform:translateY(-14px);}}
.app.leaving{animation:appleave .5s ease both;pointer-events:none;}
/* ...then the environment fades and rises in on the first page after sign in. */
@keyframes appenter{from{opacity:0;transform:translateY(20px);}to{opacity:1;transform:none;}}
.stage.entering{animation:appenter .5s cubic-bezier(.33,.68,.28,1) both;}
@media (prefers-reduced-motion:reduce){.app.leaving,.stage.entering{animation:none;}}
.caption{
  display:flex;gap:16px;align-items:baseline;flex-wrap:wrap;
  margin:0 0 18px;padding-bottom:14px;border-bottom:1px solid var(--hairline-soft);
}
.caption h2{font-size:22px;}
.caption span{font-size:13px;color:var(--dim);max-width:62ch;}

/* ---------- app shell ---------- */
.app{
  border:1px solid var(--hairline);background:var(--surface);
  border-radius:12px;overflow:hidden;
  box-shadow:0 18px 44px rgba(16,34,42,.10);
}
.topbar{
  display:flex;align-items:center;gap:14px 18px;flex-wrap:wrap;
  padding:14px 20px;background:var(--surface-2);border-bottom:1px solid var(--hairline);
}
.wordmark{
  display:inline-block;width:101.5px;height:15.4px;
  background:url("../images/text-logo-dark.3dbc3100a7cf.png") left center/contain no-repeat;
  font-size:0;text-indent:-9999px;overflow:hidden;
}
.env{
  font-family:var(--label);text-transform:uppercase;letter-spacing:.16em;
  font-size:12px;color:var(--muted);padding-left:18px;border-left:1px solid var(--hairline);
}
.who{margin-left:auto;display:flex;align-items:center;gap:12px;font-size:13px;color:var(--text);font-weight:500;}
.who form{margin:0;display:inline-flex;}
.whoemail{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;max-width:220px;}
.pill{
  font-family:var(--label);text-transform:uppercase;letter-spacing:.14em;font-size:12px;
  border:1px solid var(--hairline);padding:3px 9px;border-radius:6px;color:var(--muted);
}
.pill.admin{border-color:var(--control);color:var(--control);}

/* SIGNATURE ELEMENT: the perimeter bar.
   Present on every screen. Amber means a control boundary and
   nothing else in this interface uses amber. */
.perimeter{
  display:flex;align-items:center;gap:14px;
  padding:9px 20px 9px 17px;
  background:linear-gradient(90deg,rgba(168,122,34,.11),rgba(168,122,34,0) 62%);
  border-bottom:1px solid var(--hairline);
  border-left:3px solid var(--control);
}
/* Dismissing the perimeter banner.
   
   The class is on the document element and set from the session, so the state
   is right on the first paint rather than the banner appearing and folding
   away. The wording is untouched: every screen still writes its own sentence,
   and the control is attached to it by the shell.
   
   The operating picture has no perimeter banner at all and is unaffected. */
.perimeter-hid .perimeter{display:none;}
.perimeter-x{
  flex:none;width:60px;margin-left:auto;background:none;border:0;cursor:pointer;
  /* Darker than --control, which is 3.59:1 on this wash and fails at the
     sizes these two controls use. Measured: 6.26:1 for the cross, 7.22:1 for
     the marker's 9.5px label. The banner's own tag has the same problem at
     12px and is left alone here, because changing it changes thirty screens
     (ISSUES.md BIO-51). */
  color:#785612;font-size:20px;line-height:1;padding:4px 0;
  border-radius:5px;
}
.perimeter-x:hover{background:rgba(168,122,34,.14);color:var(--text);}
.perimeter-x:focus-visible{outline:2px solid var(--control);outline-offset:-2px;}
/* The way back, sitting where the banner was so the statement is never out of
   reach inside a session. */
.perimeter-mark{
  display:block;width:100%;text-align:left;
  padding:4px 20px 4px 17px;background:rgba(168,122,34,.06);
  border:0;border-bottom:1px solid var(--hairline);
  border-left:3px solid var(--control);cursor:pointer;
  font-family:var(--label);text-transform:uppercase;letter-spacing:.2em;
  font-size:9.5px;font-weight:600;color:#6B4E0F;
  /* Vertically centred, because the height is set to whatever the banner it
     replaced was and that is taller than this one line. */
  display:flex;align-items:center;
}
.perimeter-mark:hover{background:rgba(168,122,34,.14);color:var(--text);}
.perimeter-mark:focus-visible{outline:2px solid var(--control);outline-offset:-2px;}
.perimeter-mark[hidden]{display:none;}
@media print{.perimeter-mark,.perimeter-x{display:none !important;}}
/* Sixty pixels is the desktop figure. On a phone the banner is already three
   words wide and sixty of them is a fifth of the screen, so the control keeps
   its touch target and gives back the rest. */
@media (max-width:620px){
  .perimeter-x{width:40px;align-self:flex-start;}
}

.perimeter .tag{
  font-family:var(--label);text-transform:uppercase;letter-spacing:.2em;
  font-size:12px;color:var(--control);font-weight:600;white-space:nowrap;
}
.perimeter .say{font-size:13px;color:var(--muted);}
.perimeter.gate{border-left-color:var(--blocked);background:linear-gradient(90deg,rgba(175,67,43,.11),rgba(175,67,43,0) 62%);}
.perimeter.gate .tag{color:var(--blocked);}
.perimeter.pass{border-left-color:var(--cleared);background:linear-gradient(90deg,rgba(46,122,89,.11),rgba(46,122,89,0) 62%);}
.perimeter.pass .tag{color:var(--cleared);}

.body{display:grid;grid-template-columns:214px 1fr;min-height:520px;}
/* A page with no navigation rail: the error pages, which cannot render the rail
   because building it reads the session and therefore the database.
   
   Without this the single .main lands in the 214px column the grid reserves for
   the rail, so the content sits in a fifth of the page with the rest empty. It
   looked correct below 1040px, where the grid already collapses to one column,
   and broken on every desktop (ISSUES.md BIO-36). */
.body-solo{grid-template-columns:1fr;}
.rail{background:var(--surface-2);border-right:1px solid var(--hairline);padding:16px 0;}
.rail-h{
  font-family:var(--label);text-transform:uppercase;letter-spacing:.18em;
  font-size:11px;color:var(--dim);padding:0 18px;margin-bottom:10px;
}
.rail a{
  display:block;padding:10px 18px;color:var(--text);text-decoration:none;
  font-size:14px;font-weight:500;border-left:3px solid transparent;
}
.rail a:hover{color:var(--paper);background:rgba(16,34,42,.04);}
.rail a.on{color:var(--brand-teal-lift);border-left-color:var(--brand-teal);background:var(--teal-wash);font-weight:600;}
.rail hr{border:none;border-top:1px solid var(--hairline-soft);margin:16px 0;}

/* min-width:0 lets the main column be narrower than its widest content, so a
   wide table scrolls inside its own box rather than widening the page. As a
   grid item it otherwise grew to fit, which at phone width made the browser
   lay the whole page out wider than the screen (ISSUES.md BIO-75). */
.main{padding:26px 28px 34px;min-width:0;}
.eyebrow{
  font-family:var(--label);text-transform:uppercase;letter-spacing:.1em;
  font-size:13px;font-weight:600;color:var(--brand-teal-lift);margin:0 0 8px;
}
.main h3{font-size:20px;margin-bottom:6px;}
.lede{font-size:14px;color:var(--muted);max-width:70ch;margin-bottom:22px;}

/* ---------- shared parts ---------- */
.card{background:var(--surface-2);border:1px solid var(--hairline);border-radius:10px;padding:20px;}
/* Wraps a data table: no padding and clipped corners so the header band
   fills to the edges instead of leaving a strip of card colour above it. */
.tablecard{background:var(--surface);border:1px solid var(--hairline);border-radius:10px;overflow:hidden;}
/* Field dictionary revealed inside a dataset card. */
.dict{margin-top:18px;background:var(--surface);border:1px solid var(--hairline);border-radius:10px;overflow-x:auto;}
.dict[hidden]{display:none;}
.dict table{min-width:460px;}
/* Fields banded, generalised or pseudonymised at ingest. The treatment cell is
   tinted so the recoded rows stand out from the ones stored as recorded. */
.dict tr.recoded td:last-child{color:var(--control);font-weight:500;}
.grid2{display:grid;grid-template-columns:1fr 1fr;gap:18px;}
.grid3{display:grid;grid-template-columns:repeat(3,1fr);gap:14px;}
.grid4{display:grid;grid-template-columns:repeat(4,1fr);gap:14px;}
.statcard{background:var(--surface);border:1px solid var(--hairline-soft);border-radius:9px;padding:13px 15px;}
.statcard p{margin:0 0 3px;font-size:12.5px;color:var(--muted);font-weight:500;}
.statcard .v{font-family:var(--mono);font-weight:600;font-size:19px;color:var(--paper);}
.statcard .sub{display:block;margin-top:4px;font-size:11.5px;line-height:1.35;color:var(--muted);}

.field{margin-bottom:16px;}
.field label{
  display:block;font-family:var(--label);text-transform:uppercase;letter-spacing:.14em;
  font-size:12px;color:var(--muted);margin-bottom:6px;
}
.input,.ta,.sel{
  width:100%;background:var(--surface);border:1px solid var(--hairline);border-radius:8px;
  color:var(--paper);font-family:var(--ui);font-size:14px;padding:10px 12px;
}
.ta{min-height:96px;resize:vertical;line-height:1.55;}
.input::placeholder,.ta::placeholder{color:#9BAEB6;}

.btn{
  display:inline-block;text-decoration:none;
  background:var(--brand-teal);border:1px solid var(--brand-teal);color:#FFFFFF;
  font-weight:600;font-size:14px;padding:10px 18px;border-radius:8px;transition:.16s;
}
.btn:hover{background:var(--brand-teal-lift);border-color:var(--brand-teal-lift);}
.btn.ghost{background:transparent;color:var(--text);border-color:var(--hairline);font-weight:400;}
.btn.ghost:hover{border-color:var(--muted);color:var(--paper);}
.btn.danger{background:transparent;border-color:var(--blocked);color:var(--blocked);font-weight:500;}
.btn.danger:hover{background:var(--blocked-wash);}
.row{display:flex;gap:10px;align-items:center;flex-wrap:wrap;}

.hint{font-size:12.5px;color:var(--dim);}
.meta{font-family:var(--mono);font-size:12px;color:var(--dim);}

table{width:100%;border-collapse:collapse;font-size:13.5px;}
th{
  text-align:left;font-family:var(--label);text-transform:uppercase;letter-spacing:.12em;
  font-size:14px;font-weight:600;color:var(--text);background:var(--surface-3);
  padding:9px 12px;border-bottom:1.5px solid var(--hairline);
}
td{padding:10px 12px;border-bottom:1px solid var(--hairline-soft);color:var(--text);vertical-align:top;}
tr:last-child td{border-bottom:none;}
td.n{font-family:var(--mono);font-size:12.5px;}

/* SIGNATURE: withheld values read as a deliberate control */
.withheld{
  display:inline-flex;align-items:center;gap:7px;
  font-family:var(--label);text-transform:uppercase;letter-spacing:.14em;font-size:12px;
  color:var(--control);border:1px dashed var(--control);border-radius:6px;padding:2px 8px;
  background:var(--control-wash);
}
.withheld::before{content:"";width:5px;height:5px;background:var(--control);transform:rotate(45deg);}

.state{
  display:inline-flex;align-items:center;gap:7px;font-family:var(--label);
  text-transform:uppercase;letter-spacing:.14em;font-size:12px;font-weight:500;
}
.state::before{content:"";width:7px;height:7px;border-radius:50%;}
.state.ok{
  color:var(--cleared);background:var(--cleared-wash);
  border:1px solid rgba(46,122,89,.3);border-radius:6px;padding:4px 10px;
}
.state.ok::before{background:var(--cleared);}
.state.bad{color:var(--blocked);}
.state.bad::before{background:var(--blocked);}
.state.wait{color:var(--control);}
.state.wait::before{background:var(--control);}

.note{
  border-left:3px solid var(--control);background:var(--control-wash);
  border-radius:0 8px 8px 0;padding:11px 14px;
  font-size:13px;color:var(--text);margin:16px 0;
}
.note strong{color:var(--paper);font-weight:600;}
/* A positive note, for an outcome that is good news but still not a clearance,
   so passing the checks reads apart from a warning without reading as approval. */
.note.ok{border-left-color:var(--cleared);}

/* ---------- staged processing ----------
   The four steps shown while a run is built. States reuse the signal colours:
   a running step is teal and pulses, a done step is the cleared green, a failed
   step is the blocked red. Queued steps are dim so the eye lands on the one that
   is live. */
.proc-steps{list-style:none;margin:0;padding:0;}
.proc-step{
  display:flex;align-items:center;gap:12px;padding:12px 2px;
  border-bottom:1px solid var(--hairline-soft);
}
.proc-step:last-child{border-bottom:none;}
.proc-dot{width:9px;height:9px;border-radius:50%;background:var(--dim);flex:none;}
.proc-label{flex:1;font-family:var(--ui);font-size:14.5px;font-weight:500;color:var(--muted);}
.proc-tag{
  font-family:var(--label);text-transform:uppercase;letter-spacing:.14em;
  font-size:12.5px;font-weight:600;color:var(--dim);
}
.proc-step.is-running .proc-dot{background:var(--brand-teal);animation:procpulse 1s ease-in-out infinite;}
.proc-step.is-running .proc-label{color:var(--paper);}
.proc-step.is-running .proc-tag{color:var(--brand-teal-lift);}
.proc-step.is-done .proc-dot{background:var(--cleared);}
.proc-step.is-done .proc-label{color:var(--paper);}
.proc-step.is-done .proc-tag{color:var(--cleared);}
.proc-step.is-failed .proc-dot{background:var(--blocked);}
.proc-step.is-failed .proc-label{color:var(--paper);}
.proc-step.is-failed .proc-tag{color:var(--blocked);}
.proc-fail{margin-top:16px;}
.proc-note{border-left-color:var(--blocked);background:var(--blocked-wash);}
@keyframes procpulse{0%,100%{opacity:1;}50%{opacity:.35;}}
/* Reduced motion still shows every state change, it just does not pulse. */
@media (prefers-reduced-motion:reduce){
  .proc-step.is-running .proc-dot{animation:none;}
}

/* ---------- sign in ---------- */
.brand{
  background:var(--surface-2);border-bottom:1px solid var(--hairline-soft);
  padding:24px 30px 20px;
}
.brand .wordmark{display:block;width:184px;height:28px;margin-bottom:8px;}
.brand span{
  display:block;font-family:var(--label);text-transform:uppercase;letter-spacing:.2em;
  font-size:12.5px;color:var(--muted);
}
.signinbody{padding:30px 34px 36px;}
.codeboxes{display:flex;gap:9px;margin-bottom:8px;}
.codeboxes span{
  width:46px;height:56px;border:1px solid var(--hairline);border-radius:2px;background:var(--surface);
  display:flex;align-items:center;justify-content:center;
  font-family:var(--mono);font-size:22px;color:var(--paper);
}
.codeboxes span.empty{color:#C7D5DA;}
.codeboxes span.cursor{border-color:var(--brand-teal-lift);}

/* ---------- filter builder ---------- */
.frow{
  display:grid;grid-template-columns:1.2fr .8fr 1.2fr auto;gap:10px;align-items:center;
  padding:10px 0;border-bottom:1px solid var(--hairline-soft);
}
.frow .join{
  font-family:var(--label);text-transform:uppercase;letter-spacing:.16em;font-size:11px;color:var(--dim);
  grid-column:1 / -1;padding-top:4px;
}
.x{
  background:var(--surface);border:1.5px solid var(--hairline);border-radius:8px;
  color:var(--muted);font-size:16px;line-height:1;padding:9px 12px;cursor:pointer;
}
.x:hover{border-color:var(--blocked);color:var(--blocked);}

/* Editing an existing analysis: banner above the cohort builder. */
.editnote{
  display:flex;align-items:center;justify-content:space-between;gap:12px;flex-wrap:wrap;
  margin:0 0 20px;padding:11px 14px;border-radius:8px;
  background:var(--teal-wash);border:1px solid var(--brand-teal);
  font-size:13px;color:var(--text);
}
.editnote strong{color:var(--paper);font-weight:600;}

/* Query mode tabs. Only Manual is built for the POC; the others are inert. */
.qtabs{display:flex;gap:6px;margin-bottom:-1px;position:relative;z-index:1;}
.qtab{
  display:inline-flex;align-items:center;font-family:var(--label);font-weight:600;
  text-transform:uppercase;letter-spacing:.14em;font-size:12.5px;padding:9px 16px;
  border:1px solid transparent;border-radius:8px 8px 0 0;
}
/* The four tabs are equal citizens: Manual runs the query, the other three show
   it as code. Nothing here is greyed out any more. */
.qtab{background:var(--surface);color:var(--muted);cursor:pointer;font-family:var(--label);
  text-decoration:none;border-color:var(--hairline-soft);}
.qtab:hover{color:var(--paper);}
.qtab.on{background:var(--surface-2);border-color:var(--hairline);border-bottom:none;color:var(--paper);}

/* The help control, shown only while a code tab is open, since it is the code
   that needs explaining. */
.qhelp{
  display:inline-flex;align-items:center;justify-content:center;width:24px;height:24px;
  align-self:center;margin-left:6px;border:1px solid var(--hairline);border-radius:50%;
  background:var(--surface);color:var(--brand-teal-lift);font-size:12px;font-weight:600;
  text-decoration:none;
}
.qhelp:hover{border-color:var(--brand-teal);background:var(--teal-wash);}
.card.qcard{border-radius:0 10px 10px 10px;}

/* A rendered code panel. Read only text, never an editor: there is nothing to
   type into and nothing here executes. */
.codehost{margin-bottom:20px;}
.codepanel-top{display:flex;justify-content:space-between;align-items:center;gap:12px;margin-bottom:10px;}
.codeblock{
  margin:0;padding:14px 16px;background:var(--surface-3);border:1px solid var(--hairline);
  border-radius:8px;overflow-x:auto;font-family:var(--mono);font-size:12.5px;line-height:1.65;
}
.codeblock code{display:block;white-space:pre;}

/* Code panel themes.
   Two palettes for the same markup. The light one belongs to the rest of the
   environment; the dark one is genuinely dark rather than a tinted light theme,
   because a near white panel in a dark room is the thing people turn a dark mode
   on to escape. Both were checked for contrast against their own background.
   Only the panel changes, never the page around it. */
.codeblock.theme-light{
  background:var(--surface);border:1px solid var(--hairline);color:#243b44;
}
/* Plain text is coloured explicitly in both themes rather than inherited. It
   used to inherit, and .codeall sets the ink for an unthemed panel, so in the
   dark theme everything that was not a keyword, a string or a comment came out
   near black on near black. Setting it per theme means a rule elsewhere cannot
   quietly take it back. */
.codeblock.theme-light .tok-plain,
.codeblock.theme-light code{color:#243b44;}
.codeblock.theme-dark .tok-plain,
.codeblock.theme-dark code{color:#d6e3e8;}
.codeblock.theme-light .tok-keyword{color:#8250a8;font-weight:600;}
.codeblock.theme-light .tok-string{color:#1d6b3f;}
.codeblock.theme-light .tok-number{color:#9a5b13;}
.codeblock.theme-light .tok-comment{color:#6b8089;font-style:italic;}
.codeblock.theme-light .tok-field{color:#0f5257;font-weight:600;}
.codeblock.theme-light .tok-function{color:#1f4f9c;}

.codeblock.theme-dark{
  background:#0e1b21;border:1px solid #1e343e;color:#d6e3e8;
}
.codeblock.theme-dark .tok-keyword{color:#c8a2f0;font-weight:600;}
.codeblock.theme-dark .tok-string{color:#8fd6a4;}
.codeblock.theme-dark .tok-number{color:#f0be7a;}
.codeblock.theme-dark .tok-comment{color:#7c95a0;font-style:italic;}
.codeblock.theme-dark .tok-field{color:#5fd4d9;font-weight:600;}
.codeblock.theme-dark .tok-function{color:#8ab6f0;}

/* The theme control sits with the copy control rather than above the code, so
   the panel's top row reads as one set of things you can do to the panel. */
.paneltheme{display:inline-flex;margin:0;}

/* A label that belongs to a screen reader rather than to the layout. The code
   editor's textarea has one: the panel heading names the language above it, but
   a control with no label of its own is a control nobody navigating by keyboard
   can identify. */
.visually-hidden{
  position:absolute;width:1px;height:1px;margin:-1px;padding:0;overflow:hidden;
  clip:rect(0 0 0 0);white-space:nowrap;border:0;
}

/* The code editor. Same box as the read only panel so switching between them
   does not move the page, and monospaced so the code a researcher edits lines up
   the way the code they were shown did. Resizable vertically only: sideways
   would break out of the panel. */
/* The editor: a gutter of line numbers and the textarea, sharing one box that
   carries the theme. The gutter is separate elements rather than text inside the
   field, so the numbers are never part of the value, never posted and never
   copied with the code.

   The textarea does not wrap. One logical line is one visual row, so a number
   cannot drift from the line it belongs to, which is the way a gutter usually
   goes wrong. */
.editorwrap{display:flex;align-items:stretch;padding:0;overflow:hidden;}
.editorgutter{
  flex:none;padding:14px 10px 14px 14px;text-align:right;
  /* Not selectable, so a drag across the code does not pick the numbers up,
     and not copied with it. */
  user-select:none;-webkit-user-select:none;
  /* No overflow and no scrollbar of its own. It is as tall as its numbers and
     the page scrolls past it, which is the whole reason it cannot drift. */
  overflow:visible;
  /* Every metric that decides where a line sits is shared with the textarea.
     If these two ever disagree the numbers slide by a fraction of a line per
     row, which looks fine at the top of a short query and is a line out by the
     bottom of a long one. */
  font-family:var(--mono);font-size:12.5px;line-height:1.65;
  border-right:1px solid var(--hairline);
}
.codeblock.theme-dark .editorgutter{border-right-color:#1e343e;color:#5b7480;}
.codeblock.theme-light .editorgutter{color:#8fa4ac;}
.editorgutter .editorline{display:block;}
/* The line a refusal points at, so the message and the code line up without
   anybody counting. */
.editorline-bad{
  font-weight:700;color:var(--blocked);position:relative;
}
.editorline-bad::after{
  content:"";position:absolute;right:-11px;top:0;bottom:0;width:2px;
  background:var(--blocked);
}
.editorarea{
  flex:1;border:0;background:transparent;color:inherit;padding:14px 16px;
  outline-offset:-2px;
  font-family:var(--mono);font-size:12.5px;line-height:1.65;white-space:pre;
  /* Sideways only. The rows attribute holds every line, so there is nothing to
     scroll vertically and no second scroll position for the gutter to fall out
     of step with. Not resizable for the same reason: dragging the handle would
     create the inner scroller this design exists to avoid. */
  overflow-x:auto;overflow-y:hidden;resize:none;
}

textarea.codeblock{
  display:block;width:100%;box-sizing:border-box;resize:vertical;min-height:220px;
  font-family:var(--mono);font-size:12.5px;line-height:1.65;
}
/* The comment block reads as an aside, and the copy control leaves it behind. */
/* One block: the comment header travels with the code when it is copied,
   because reaching a researcher who has taken the query elsewhere is the whole
   point of that comment. */
/* The base ink for a panel with no theme on it. A themed panel sets its own,
   below, on the tokens rather than on the element, so nothing inherits a colour
   from outside the panel. */
.codeall{color:var(--paper);}
.codepanel-top{align-items:flex-start;}

/* Controls for features not built yet. Rendered as a span or an href-less link,
   so neither takes keyboard focus nor does anything when clicked; hovering shows
   why. Same tooltip as the query tabs above. */
.soon{cursor:not-allowed;position:relative;opacity:.55;}
.soon::after{
  content:attr(data-soon);position:absolute;bottom:calc(100% + 8px);left:50%;transform:translateX(-50%);
  white-space:nowrap;background:var(--paper);color:#FFFFFF;font-family:var(--ui);
  text-transform:none;letter-spacing:0;font-weight:500;font-size:12px;padding:6px 10px;border-radius:6px;
  box-shadow:0 8px 18px rgba(16,34,42,.25);opacity:0;pointer-events:none;transition:opacity .12s;
}
.soon:hover::after{opacity:1;}
.rail a.soon{opacity:1;color:var(--dim);}
.rail a.soon:hover{background:transparent;color:var(--dim);}

.count{
  display:flex;align-items:baseline;gap:14px;padding:18px 20px;
  background:var(--surface-3);border:1px solid var(--hairline);border-radius:10px;
}
.count b{font-family:var(--mono);font-size:34px;font-weight:500;color:var(--paper);letter-spacing:-.02em;}
.count span{font-family:var(--label);text-transform:uppercase;letter-spacing:.16em;font-size:12px;color:var(--muted);}

/* ---------- checks ---------- */
.check{
  display:grid;grid-template-columns:26px 1fr auto;gap:14px;align-items:start;
  padding:14px 0;border-bottom:1px solid var(--hairline-soft);
}
.check:last-child{border-bottom:none;}
.check .idx{font-family:var(--mono);font-size:12px;color:var(--dim);padding-top:2px;}
.check h4{font-size:14.5px;margin-bottom:3px;}
.check p{font-size:13px;color:var(--muted);margin:0;}
.finding{
  font-family:var(--mono);font-size:12px;color:var(--blocked);
  background:var(--blocked-wash);border:1px solid rgba(175,67,43,.35);
  border-radius:2px;padding:6px 10px;margin-top:8px;display:inline-block;
}

.banner{
  display:flex;gap:16px;align-items:flex-start;padding:16px 18px;border-radius:3px;
  border:1px solid rgba(175,67,43,.45);background:var(--blocked-wash);margin-bottom:22px;
}
.banner .bar{width:3px;align-self:stretch;background:var(--blocked);}
.banner h4{color:var(--blocked);font-size:15px;margin-bottom:4px;}
.banner p{font-size:13.5px;color:var(--text);margin:0;}

/* ---------- queue ---------- */
.queue{display:grid;grid-template-columns:1.15fr 1fr;gap:18px;}
.qitem{
  padding:13px 14px;border:1px solid var(--hairline);border-radius:3px;
  background:var(--surface-2);margin-bottom:10px;
}
.qitem.sel{border-color:var(--brand-teal);background:var(--surface-3);}
.qitem .top{display:flex;justify-content:space-between;align-items:baseline;gap:10px;margin-bottom:5px;}
.qitem h4{font-size:14.5px;}
.qitem .sub{font-size:12.5px;color:var(--muted);}

.quote{
  border-left:2px solid var(--hairline);padding:2px 0 2px 14px;
  font-size:13.5px;color:var(--text);font-style:italic;
}

/* ---------- SIGNATURE: audit thread ---------- */
.thread{position:relative;padding-left:132px;}
.thread::before{
  content:"";position:absolute;left:118px;top:6px;bottom:6px;width:1px;background:var(--hairline);
}
.ev{position:relative;padding:0 0 20px;}
.ev .t{
  position:absolute;left:-132px;width:104px;text-align:right;
  font-family:var(--mono);font-size:11.5px;color:var(--dim);line-height:1.5;padding-top:1px;
}
.ev::before{
  content:"";position:absolute;left:-18px;top:6px;width:7px;height:7px;
  background:var(--surface);border:1px solid var(--muted);border-radius:50%;
}
.ev.amber::before{border-color:var(--control);background:var(--control);}
.ev.red::before{border-color:var(--blocked);background:var(--blocked);}
.ev.green::before{border-color:var(--cleared);background:var(--cleared);}
.ev h4{font-size:14px;margin-bottom:2px;}
.ev p{font-size:13px;color:var(--muted);margin:0;}
.ev .who2{font-family:var(--mono);font-size:11.5px;color:var(--dim);margin-top:4px;}

/* ---------- screens ---------- */


@keyframes in{from{opacity:0;transform:translateY(8px);}to{opacity:1;transform:none;}}
@media (prefers-reduced-motion:reduce){
  html{scroll-behavior:auto;}
}

footer{
  max-width:1360px;margin:0 auto;padding:20px 26px 44px;
  font-size:12.5px;color:var(--dim);
}

@media (max-width:1040px){
  /* The rail stacks above the content but keeps its vertical, full-width menu
     with the Project and Admin groups intact, matching the mobile design
     rather than collapsing into a horizontal strip. */
  .body{grid-template-columns:1fr;}
  .rail{border-right:none;border-bottom:1px solid var(--hairline);}
  .signin,.grid2,.queue,.charts{grid-template-columns:1fr;}
  .grid3{grid-template-columns:1fr;}
  .grid4{grid-template-columns:repeat(2,1fr);}
}
@media (max-width:620px){
  .stage{padding:22px 14px 60px;}
  .main{padding:20px 16px 26px;}
  .frow{grid-template-columns:1fr;}
  .thread{padding-left:0;}
  .thread::before{display:none;}
  .ev .t{position:static;width:auto;text-align:left;display:block;margin-bottom:2px;}
  .ev::before{display:none;}
  .ev{border-left:2px solid var(--hairline);padding-left:14px;}
}

/* ------------------------------------------------------------
   Added for the Django build. Not part of the original deck.
   ------------------------------------------------------------ */
.staticmark{
  max-width:1360px;margin:0 auto;padding:10px 26px;
  font-family:var(--label);text-transform:uppercase;letter-spacing:.16em;
  font-size:12px;color:var(--control);background:var(--control-wash);
  border-bottom:1px solid rgba(168,122,34,.35);
}
.errors{
  border:1px solid rgba(175,67,43,.45);background:var(--blocked-wash);
  border-radius:2px;padding:10px 14px;margin-bottom:16px;
  font-size:13.5px;color:var(--blocked);
}
.errors ul{margin:0;padding-left:18px;}
.codeinput{
  width:100%;font-family:var(--mono);font-size:26px;letter-spacing:.42em;
  text-align:center;padding:14px 12px;background:var(--surface);
  border:1px solid var(--hairline);border-radius:2px;color:var(--paper);
}
.codeinput:focus{border-color:var(--brand-teal-lift);}
.linkbtn{
  background:none;border:none;padding:0;color:var(--brand-teal-lift);
  font-size:13px;text-decoration:underline;cursor:pointer;
}
.linkbtn:disabled{color:var(--dim);text-decoration:none;cursor:not-allowed;}
/* Deliberately understated so it sits quietly under the sign-in form. */
.forgotlink{
  appearance:none;-webkit-appearance:none;
  background:none;border:none;padding:0;margin-top:16px;
  color:var(--dim);font-size:13px;text-decoration:underline;cursor:pointer;
}
.forgotlink:hover{color:var(--muted);}
.signout{
  background:none;border:1px solid var(--hairline);border-radius:6px;
  padding:5px 11px;color:var(--text);font-size:13px;font-weight:500;
  text-decoration:none;cursor:pointer;
}
.signout:hover{border-color:var(--muted);color:var(--paper);}
/* POC scope note in the project rail. */
.rail .scope{margin:18px 18px 0;padding-top:16px;border-top:1px solid var(--hairline-soft);}
.rail .scope .rail-h{padding:0;margin:0 0 6px;}
.rail .scope-note{margin:0;font-size:12.5px;color:var(--muted);line-height:1.5;}
/* Build version, sat faint at the foot of the rail. */
.rail-version{margin:18px 0 0;padding:0 18px;font-family:var(--mono);font-size:11px;color:var(--dim);}

/* ---------- results: saved list, back link and charts ---------- */
.backlink{
  display:inline-flex;align-items:center;gap:6px;background:none;border:none;
  color:var(--muted);font-size:13.5px;text-decoration:none;margin-bottom:14px;
}
.backlink:hover{color:var(--paper);}
.runitem{
  display:block;background:var(--surface);border:1px solid var(--hairline);border-radius:10px;
  padding:16px 18px;margin-bottom:12px;text-decoration:none;transition:border-color .14s;
}
.runitem:hover{border-color:var(--brand-teal);}
.runitem-top{display:flex;justify-content:space-between;align-items:baseline;gap:12px;margin-bottom:5px;}
.runitem-top h4{font-size:16px;color:var(--paper);}
.runitem-sub{margin:0 0 6px;font-size:13px;color:var(--muted);}
.runitem-n{font-family:var(--mono);font-size:12.5px;color:var(--brand-teal-lift);}

/* Narrowing the saved results. The filter sits on the subtitle's row, aligned
   right, so it reads as part of the content rather than as a toolbar laid across
   the top of the screen. It wraps under the subtitle when there is no room. */
.savedbar{
  display:flex;justify-content:space-between;align-items:flex-end;gap:18px;
  flex-wrap:wrap;margin-bottom:18px;
}
.savedbar .lede{max-width:40ch;}
/* Pushed right whether it shares the subtitle's row or wraps under it, so it
   stays aligned to the content edge instead of drifting left when it wraps. */
.savedfilter{display:flex;align-items:flex-end;gap:8px;flex-wrap:wrap;margin-left:auto;}
.savedfilter .field{margin:0;}
.savedfilter label{
  display:block;font-family:var(--label);text-transform:uppercase;letter-spacing:.14em;
  font-size:11.5px;color:var(--muted);margin-bottom:4px;
}
.savedfilter .input{height:34px;padding:0 9px;font-size:13px;}
.savedfilter input[type="search"]{width:170px;}
.savedfilter input[type="date"]{width:132px;}
.savedfilter .btn{height:34px;padding:0 13px;}
.savedcount{margin:-6px 0 14px;}

.charts{display:grid;grid-template-columns:1fr 1fr;gap:18px;margin-bottom:18px;}
.chartcard{position:relative;background:var(--surface);border:1px solid var(--hairline);border-radius:12px;padding:20px 22px;min-width:0;}
/* The results table (Sprint Enchilada E09): the first card in the grid.
   Wider spans both columns of the grid and moves the cards after it; at 1040px
   and below the grid is one column already, so there is nothing to widen and
   the control is not offered. Every background here is light enough to keep
   the research text colour at 7.0 or better (tests_result_table measures it).
   The shading is a second signal only: every cell prints its number. */
.rtable-wide,.rtable-open{grid-column:1 / -1;}
@media (max-width:1040px){ .rtable [data-rtable-form="size"]{display:none;} }
.rtable-sizes{display:flex;gap:6px;flex:none;}
.rtable-controls{display:flex;flex-direction:column;gap:8px;margin:8px 0 10px;}
.rtable-fields,.rtable-tools{display:flex;flex-wrap:wrap;gap:8px 12px;align-items:center;}
.rtable-field{display:inline-flex;align-items:center;gap:4px;border:1px solid var(--hairline);border-radius:999px;
  padding:2px 4px 2px 10px;font-size:12.5px;color:var(--text);background:var(--surface);margin:0;}
.rtable-x{border:0;background:none;color:var(--text);font-size:15px;line-height:1;cursor:pointer;padding:0 5px;}
.rtable-add,.rtable-inline{display:flex;flex-wrap:wrap;gap:6px;align-items:center;font-size:12.5px;color:var(--text);margin:0;}
.rtable-warn,.rtable-cap{margin:0;}
/* The Apply buttons are for JavaScript off; `.btn` sets display, so hidden needs saying. */
.rtable [data-rtable-apply][hidden],.rtable-dialog [data-rtable-apply][hidden],
.chart-rtable [data-rtable-apply][hidden]{display:none;}
/* A chart drawn as a table: the results table's controls and grid, under the
   chart's own header. */
.chart-rtable .rtable-tools{margin:4px 0 10px;}
.rtable-scroll{overflow:auto;max-height:420px;border:1px solid var(--hairline);border-radius:8px;background:var(--surface);}
.rtable-wide .rtable-scroll,.rtable-open .rtable-scroll{max-height:560px;}
.rtable-grid{border-collapse:separate;border-spacing:0;min-width:100%;font-size:12.5px;color:var(--text);}
.rtable-grid th,.rtable-grid td{border-right:1px solid var(--hairline);border-bottom:1px solid var(--hairline);padding:5px 9px;white-space:nowrap;}
.rtable-grid thead{position:sticky;top:0;z-index:3;}
.rtable-grid thead th,.rtable-grid thead td{background:#EEF4F5;font-weight:600;text-align:left;}
.rtable-grid tr > :first-child{position:sticky;left:0;z-index:1;background:#F7FAFB;}
.rtable-grid thead tr > :first-child{z-index:4;background:#EEF4F5;}
.rtable-grid tbody th,.rtable-grid tfoot th{font-weight:500;text-align:left;}
.rt-across{text-align:center;}
.rt-num{text-align:right;font-variant-numeric:tabular-nums;}
.rtable-grid thead .rt-num{text-align:right;}
.rt-share{display:block;font-size:11px;}
.rt-partial{font-size:10.5px;font-style:italic;}
.rt-total{font-weight:600;background:#F4F8F9;}
.rt-b1{background:#F5FAFA;}
.rt-b2{background:#EBF5F6;}
.rt-b3{background:#E0EFF1;}
.rt-b4{background:#D3E8EB;}
.rt-b5{background:#C6E2E5;}
.rt-bw{font-style:italic;background:repeating-linear-gradient(135deg,#FFFFFF 0 5px,#EAF0F2 5px 10px);}
.rt-sortbtn{font:inherit;color:inherit;background:none;border:0;padding:0;cursor:pointer;text-align:inherit;}
.rt-sortbtn:focus-visible{outline:2px solid var(--brand-teal);outline-offset:2px;}
th[aria-sort="ascending"] .rt-sortbtn::after{content:" \25B2";font-size:9px;}
th[aria-sort="descending"] .rt-sortbtn::after{content:" \25BC";font-size:9px;}
.rtable-status{min-height:1em;margin:4px 0 0;}
.rtable-foot{display:flex;flex-wrap:wrap;gap:8px;align-items:center;margin-top:8px;}
.rtable-reset{margin:0 0 0 auto;}
.rtable-dialog{border:0;border-radius:12px;padding:18px 22px;width:min(1100px,96vw);max-height:92vh;
  background:var(--surface);color:var(--text);}
.rtable-dialog::backdrop{background:rgba(16,34,42,.55);}
.rtable-dialog-head{display:flex;justify-content:space-between;align-items:center;gap:12px;margin-bottom:8px;}
.rtable-dialog-head h2{margin:0;font-size:18px;color:var(--text);}
.rtable-dialog .rtable-scroll{max-height:calc(92vh - 260px);}
/* Placeholder for a later "open this chart in detail" view. Deliberately
   disabled and greyed for the POC, kept only as a visible reminder. */
.chart-soon{position:absolute;top:16px;right:16px;display:inline-flex;align-items:center;
  background:var(--surface-3);border:1px solid var(--hairline-soft);color:#9AABB0;
  font-family:var(--label);font-size:11.5px;font-weight:600;text-transform:uppercase;letter-spacing:.14em;
  padding:6px 12px;border-radius:7px;cursor:not-allowed;}
.chart-soon::after{
  content:attr(data-soon);position:absolute;bottom:calc(100% + 8px);left:50%;transform:translateX(-50%);
  white-space:nowrap;background:var(--paper);color:#FFFFFF;font-family:var(--ui);
  text-transform:none;letter-spacing:0;font-weight:500;font-size:12px;padding:6px 10px;border-radius:6px;
  box-shadow:0 8px 18px rgba(16,34,42,.25);opacity:0;pointer-events:none;transition:opacity .12s;}
.chart-soon:hover::after{opacity:1;}
.chart-k{font-family:var(--label);text-transform:uppercase;letter-spacing:.14em;font-size:12.5px;font-weight:600;color:var(--muted);margin:0 0 3px;}
.chartcard h4{font-size:16px;color:var(--paper);margin-bottom:4px;}
.chart-sub{margin:0 0 16px;font-size:13px;color:var(--muted);}
.chart-legend{display:flex;gap:16px;margin-top:14px;font-size:12.5px;color:var(--text);}
.chart-legend span{display:inline-flex;align-items:center;gap:6px;}
.cdot{width:11px;height:11px;border-radius:4px;display:inline-block;}
.cdot.teal{background:var(--brand-teal);}
.cdot.red{background:var(--blocked);}
/* chart 1: stacked outcome bars */
.ebar{margin-bottom:13px;}
.ebar-top{display:flex;justify-content:space-between;align-items:baseline;margin-bottom:5px;font-size:13px;color:var(--text);}
.ebar-track{display:flex;height:20px;border-radius:6px;overflow:hidden;background:var(--teal-wash);}
.ebar-stab{background:var(--brand-teal);color:#FFFFFF;font-size:11.5px;font-weight:600;display:flex;align-items:center;padding-left:8px;white-space:nowrap;}
.ebar-det{background:var(--blocked);}
/* chart 3: mechanism bars */
.mbar{display:grid;grid-template-columns:112px 1fr 64px;gap:10px;align-items:center;margin-bottom:11px;}
.mbar-label{font-size:13px;color:var(--text);}
.mbar-track{height:18px;background:var(--teal-wash);border-radius:6px;overflow:hidden;}
.mbar-fill{height:100%;background:#3E8A8E;border-radius:6px;}
.mbar-fill.first{background:var(--brand-teal);}
.mbar-n{font-family:var(--mono);font-size:12.5px;color:var(--paper);text-align:right;}
.mbar-n .mbar-share{display:block;font-style:normal;font-size:11px;color:var(--muted);}

/* A value axis under a horizontal bar style chart. Faint gridlines are drawn as
   a repeating background on each track, self aligning to the axis because a
   track runs zero to the ceiling, so no pixel width has to be known here. */
.plot-h{position:relative;}
.gridded{
  background-image:repeating-linear-gradient(90deg,
    var(--hairline) 0, var(--hairline) 1px, transparent 1px, transparent var(--tick));
  background-position:0 0;
}
.va-h{display:grid;grid-template-columns:112px 1fr 64px;gap:10px;margin-top:6px;}
.va-unit{font-family:var(--label);text-transform:uppercase;letter-spacing:.1em;font-size:10px;
  color:var(--muted);align-self:center;}
.va-track{position:relative;height:16px;}
.va-t{position:absolute;top:0;transform:translateX(-50%);font-family:var(--mono);font-size:10.5px;
  color:var(--muted);white-space:nowrap;}
.va-track .va-t:first-child{transform:none;}
.va-track .va-t:last-child{transform:translateX(-100%);}
.axis-caption{margin:10px 0 0;font-size:11.5px;color:var(--muted);}
.axis-unit-line{margin:8px 0 0;font-family:var(--label);text-transform:uppercase;letter-spacing:.1em;
  font-size:10.5px;color:var(--muted);}
/* chart 4: quartile bars */
.qbars{display:flex;align-items:flex-end;gap:16px;height:170px;padding:0 4px;}
.qbar{flex:1;display:flex;flex-direction:column;align-items:center;gap:8px;height:100%;justify-content:flex-end;}
.qbar-n{font-family:var(--mono);font-size:12.5px;color:var(--paper);font-weight:500;}
.qbar-fill{width:100%;background:#4E9599;border-radius:8px 8px 4px 4px;}
.qbar-fill.hi{background:var(--brand-teal);}
.qbar-label{font-family:var(--label);text-transform:uppercase;letter-spacing:.14em;font-size:12.5px;color:var(--text);}
/* chart hover tooltip */
.ebar,.mbar,.qbar{cursor:default;}
.charts [data-tip]:hover .ebar-track,.charts [data-tip]:hover .mbar-track{filter:brightness(.96);}
.charttip{
  position:fixed;z-index:60;pointer-events:none;background:var(--paper);color:#FFFFFF;
  font-family:var(--ui);font-size:12px;font-weight:500;padding:6px 10px;border-radius:6px;
  white-space:nowrap;box-shadow:0 8px 18px rgba(16,34,42,.25);
}
.charttip[hidden]{display:none;}

/* Value controls in the builder: a set of ticks for "is one of", a pair of
   menus for "is between". Both sit in the value cell of the condition row. */
.vmulti{display:flex;flex-wrap:wrap;gap:6px;}
.vchip{
  display:inline-flex;align-items:center;gap:6px;font-size:12.5px;color:var(--text);
  background:var(--surface);border:1px solid var(--hairline);border-radius:7px;padding:5px 9px;cursor:pointer;
}
.vchip input{accent-color:var(--brand-teal);}
.vbetween{display:flex;align-items:center;gap:8px;}
.vbetween .join{grid-column:auto;padding:0;color:var(--dim);}
.vbetween .sel{flex:1;min-width:0;}

/* A withheld bar is shown as a small hatched stub, so the chart admits a value
   was there without hinting at its size. */
.mbar-fill.withheld-fill{
  background:repeating-linear-gradient(45deg,var(--control) 0,var(--control) 2px,transparent 2px,transparent 5px);
  border-radius:6px;
}

/* Chart controls. Print sends the single chart to its own printable page (and
   to the audit trail); the expand icon takes it to the full window. */
.chart-actions{display:flex;gap:8px;align-items:center;flex:none;}
.chart-print{
  font-family:var(--label);font-size:11.5px;font-weight:600;text-transform:uppercase;letter-spacing:.12em;
  padding:6px 12px;border-radius:7px;text-decoration:none;cursor:pointer;
  border:1px solid var(--hairline);background:var(--surface);color:var(--brand-teal-lift);
}
.chart-print:hover{border-color:var(--brand-teal);}
.chart-expand{
  width:32px;height:32px;display:inline-flex;align-items:center;justify-content:center;
  border:1px solid var(--hairline);background:var(--surface);color:var(--brand-teal-lift);
  border-radius:8px;cursor:pointer;font-size:17px;line-height:1;
}
.chart-expand:hover{border-color:var(--brand-teal);}
.chartbars{min-width:0;}

/* The chart selector, a dropdown at the top right of the charts. It is a display
   preference: it never changes what is counted or suppressed. */
.chartpicker{position:relative;}
.chartpicker > summary{
  list-style:none;cursor:pointer;display:inline-flex;align-items:center;gap:8px;
  border:1px solid var(--hairline);border-radius:8px;padding:7px 13px;background:var(--surface);
  font-family:var(--label);text-transform:uppercase;letter-spacing:.12em;font-size:11.5px;font-weight:600;
  color:var(--brand-teal-lift);
}
.chartpicker > summary::-webkit-details-marker{display:none;}
.chartpicker > summary::after{content:"▾";font-size:11px;color:var(--muted);}
.chartpicker[open] > summary{border-color:var(--brand-teal);}
.chartpicker > form{
  position:absolute;right:0;top:calc(100% + 6px);z-index:30;width:320px;max-width:88vw;
  background:var(--surface);border:1px solid var(--hairline);border-radius:10px;padding:16px;
  box-shadow:0 18px 40px rgba(16,34,42,.18);
}
.picker-row{display:flex;gap:10px;align-items:flex-start;padding:8px 0;border-bottom:1px solid var(--hairline-soft);font-size:13px;color:var(--text);cursor:pointer;}
.picker-row:last-of-type{border-bottom:none;}
.picker-row input{margin-top:3px;accent-color:var(--brand-teal);}

/* The expanded chart takes the full window. It scales to the space it is given,
   so it reads on a small laptop and fills a large monitor without a fixed size,
   and it shows the per band numbers the card only summarises. */
.chart-overlay{
  position:fixed;inset:0;z-index:50;background:rgba(16,34,42,.55);
  display:flex;align-items:flex-start;justify-content:center;padding:min(5vh,48px) 4vw;overflow-y:auto;
}
.chart-overlay[hidden]{display:none;}
.chart-sheet{
  position:relative;background:var(--surface);border-radius:14px;padding:clamp(20px,4vw,44px);
  width:min(1100px,96vw);box-shadow:0 30px 70px rgba(16,34,42,.3);
}
.chart-close{
  position:absolute;top:16px;right:16px;border:1px solid var(--hairline);background:var(--surface);
  color:var(--text);border-radius:8px;padding:7px 12px;cursor:pointer;font-size:13px;
}
.chart-close:hover{border-color:var(--brand-teal);color:var(--paper);}
.expanded-title{font-size:clamp(22px,3vw,32px);color:var(--paper);margin:2px 0 4px;}
.expanded-sub{color:var(--muted);margin:0 0 6px;font-size:15px;}
.expanded-meta{margin:0 0 20px;}
.expanded-bars{display:grid;gap:14px;}
.ebar-row{
  display:grid;grid-template-columns:minmax(140px,1.4fr) 3fr 90px 72px;gap:clamp(10px,1.5vw,22px);align-items:center;
}
.ebar-cat{font-size:clamp(14px,1.4vw,17px);color:var(--text);}
.ebar-track{height:clamp(20px,2.4vw,30px);background:var(--teal-wash);border-radius:7px;overflow:hidden;}
.ebar-fill{height:100%;background:#3E8A8E;border-radius:7px;}
.ebar-fill.first{background:var(--brand-teal);}
.ebar-n{font-family:var(--mono);font-size:clamp(13px,1.3vw,16px);color:var(--paper);text-align:right;}
.ebar-share{font-family:var(--mono);font-size:clamp(12px,1.2vw,15px);color:var(--muted);text-align:right;}
.expanded-note{margin-top:22px;}
.expanded-print{margin-top:24px;}
@media (max-width:560px){
  .ebar-row{grid-template-columns:1fr 60px 56px;}
  .ebar-row .ebar-track{display:none;}
}

/* Standalone print sheet for a single chart. */
.printsheet{max-width:820px;margin:0 auto;padding:28px;color:var(--paper);}
.printhead{border-bottom:2px solid var(--hairline);padding-bottom:14px;margin-bottom:16px;}
.printmeta{display:grid;grid-template-columns:1fr 1fr;gap:24px;}
.printbars{width:100%;border-collapse:collapse;}
.printbars th,.printbars td{text-align:left;padding:8px 10px;border-bottom:1px solid var(--hairline-soft);font-size:13px;}
.printbars th{font-family:var(--label);text-transform:uppercase;letter-spacing:.1em;font-size:11px;color:var(--muted);}
.printbars td.n{font-family:var(--mono);text-align:right;}
.printbars th:not(:first-child){text-align:right;}
@media (max-width:640px){.printmeta{grid-template-columns:1fr;}}

/* Per chart controls: the type and the field or fields it is drawn from, so a
   researcher builds their own analysis rather than picking from a fixed set. */
/* The card header row: chart number on the left, the form, expand and remove on
   the right. One row of a fixed height, so cards line up next to each other
   whatever the form label reads, and the space below it is the same on every
   card. Clear space separates it from the selector strip. */
.chart-head{display:flex;justify-content:space-between;align-items:center;gap:12px;
  min-height:32px;margin-bottom:14px;}
.chart-k{margin:0;}

/* A compact strip of controls, in its own space between the header and the
   chart. The type and field selectors sit side by side at their natural width,
   wrapping only when a narrow card cannot hold them on one row. The visible
   labels are gone; each control keeps an accessible name. */
.chart-editor{display:flex;flex-wrap:wrap;align-items:center;gap:8px;margin:0 0 16px;padding:8px 10px;
  background:var(--surface-2,var(--surface));border:1px solid var(--hairline-soft);border-radius:9px;}
.chart-editor .sel{width:auto;min-width:0;max-width:230px;padding:7px 10px;font-size:13px;}
.chart-editor .ctrl-fields{display:flex;align-items:center;gap:8px;min-width:0;flex-wrap:wrap;}
.chart-editor .ctrl-cap{font-family:var(--label);text-transform:uppercase;letter-spacing:.12em;
  font-size:10.5px;font-weight:600;color:var(--muted);}
.chart-editor .vmulti{max-width:100%;}
.chart-note{margin:0 0 12px;font-size:12.5px;color:var(--amber-deep,#8a6d1f);
  background:var(--amber-wash,#fbf3dd);border:1px solid var(--amber-line,#ecdca6);
  border-radius:7px;padding:7px 10px;}

/* The remove control sits with the expand icon. The last remaining chart cannot
   be removed, so its control is disabled rather than hidden, making the minimum
   of one clear. */
.chart-remove-form{display:inline-flex;margin:0;}
.chart-remove{width:32px;height:32px;display:inline-flex;align-items:center;justify-content:center;
  border:1px solid var(--hairline);background:var(--surface);color:var(--blocked);border-radius:8px;
  cursor:pointer;font-size:17px;line-height:1;}
.chart-remove:hover{border-color:var(--blocked);}
.chart-remove[disabled]{color:#C7D5DA;cursor:not-allowed;border-color:var(--hairline-soft);}

/* Table charts: cross tabulation and the numeric summary. */
.charttable-wrap{overflow-x:auto;border:1px solid var(--hairline);border-radius:10px;}
.charttable{width:100%;border-collapse:collapse;font-size:13px;}
.charttable th,.charttable td{padding:8px 12px;text-align:right;white-space:nowrap;
  border-bottom:1px solid var(--hairline-soft);}
.charttable th:first-child,.charttable td.rowhead{text-align:left;color:var(--text);}
.charttable thead th{font-family:var(--label);text-transform:uppercase;letter-spacing:.1em;
  font-size:10.5px;color:var(--muted);}
.charttable td{font-family:var(--mono);color:var(--paper);}
.charttable td.rowhead{font-family:var(--ui);}

/* The per band share in the expanded and printed bar views. */

/* In place chart updates. The Apply button is the fallback for when JavaScript
   is off; with it on the change applies on its own, so the button is hidden. */
.js .chart-apply{display:none;}
/* While a card redraws it shows a working state and its controls are inert, so a
   second change cannot be submitted for it. The other cards stay usable. */
.chart-busy{
  position:absolute;inset:0;z-index:6;display:none;align-items:center;justify-content:center;gap:9px;
  background:rgba(247,250,250,.72);border-radius:12px;
  font-family:var(--label);text-transform:uppercase;letter-spacing:.12em;font-size:12px;color:var(--muted);
}
.chartcard.is-updating .chart-busy{display:flex;}
.chartcard.is-updating .chart-actions,
.chartcard.is-updating .chart-editor{opacity:.5;pointer-events:none;}
/* A failed redraw leaves the previous chart in place and says so. */
.chart-error{margin:10px 0 0;font-size:12.5px;color:var(--blocked);}

/* Under a tablet width, one chart card fills the row rather than two side by
   side, so the forms stay readable on a narrow screen. */
@media (max-width:900px){.charts{grid-template-columns:1fr;}}

/* The visual form picker, top right of the card before expand and remove. A
   greyed form states on hover, and inline, what the analysis would need. */
.formpicker{position:relative;}
/* Every other control in a card header is the form picker's height, so the
   results table's Wider and Open sit on one row with the chart cards' controls. */
.chart-btn{
  display:inline-flex;align-items:center;height:32px;padding:0 11px;border:1px solid var(--hairline);
  border-radius:8px;background:var(--surface);font-family:var(--ui);font-size:12px;font-weight:600;
  line-height:1;color:var(--brand-teal-lift);text-decoration:none;cursor:pointer;white-space:nowrap;
}
.chart-btn:hover{border-color:var(--brand-teal);}
.chart-actions form{margin:0;display:inline-flex;}
/* About these numbers: one line when something is withheld, the rest behind
   a toggle closed by default. */
.about-numbers{margin-top:10px;}
.about-line{margin:0 0 4px;font-size:12.5px;color:var(--text);}
.about-toggle > summary{list-style:none;cursor:pointer;display:inline-flex;align-items:center;gap:6px;
  font-size:12px;font-weight:600;color:var(--brand-teal-lift);}
.about-toggle > summary::-webkit-details-marker{display:none;}
.about-toggle > summary::after{content:"\25be";font-size:10px;}
.about-toggle[open] > summary::after{content:"\25b4";}
.about-body p{margin:6px 0 0;max-width:72ch;font-size:12px;line-height:1.6;color:var(--text);}
.formpicker > summary{
  list-style:none;cursor:pointer;display:inline-flex;align-items:center;gap:6px;height:32px;
  border:1px solid var(--hairline);border-radius:8px;padding:0 11px;background:var(--surface);
  font-size:12px;font-weight:600;color:var(--brand-teal-lift);white-space:nowrap;
}
.formpicker > summary::-webkit-details-marker{display:none;}
.formpicker > summary::after{content:"\25be";color:var(--muted);font-size:10px;}
.formpicker[open] > summary{border-color:var(--brand-teal);}
.formpicker-menu{
  position:absolute;right:0;top:calc(100% + 6px);z-index:35;width:280px;max-width:86vw;
  background:var(--surface);border:1px solid var(--hairline);border-radius:10px;padding:12px;
  box-shadow:0 18px 40px rgba(16,34,42,.18);display:flex;flex-direction:column;gap:2px;
  animation:menuin .12s ease both;
}
@keyframes menuin{from{opacity:0;transform:translateY(-3px);}to{opacity:1;transform:none;}}

/* Confirming a chart removal. The house error colours, because this is the one
   control on the card that loses work, and cancel is the safe default: it takes
   focus, so a stray Enter cancels, and Escape cancels too. */
.chart-confirm{
  border:1px solid var(--blocked);background:var(--blocked-wash);border-radius:8px;
  padding:12px 14px;margin-bottom:12px;animation:menuin .12s ease both;
}
.chart-confirm-q{margin:0 0 10px;font-size:13px;color:var(--text);}
.chart-confirm-q strong{color:var(--paper);font-weight:600;}
@media (prefers-reduced-motion:reduce){
  .formpicker-menu,.chart-confirm{animation:none;}
}
.formopt{
  text-align:left;border:none;background:none;border-radius:7px;padding:7px 9px;cursor:pointer;
  font-size:13px;color:var(--text);font-family:var(--ui);
}
.formopt:hover{background:var(--teal-wash);}

/* The regression card's boxes as list boxes (static/js/reg-picker.js). A
   greyed field keeps its text at the page's text colour, so it clears the
   7.0 floor: it is set apart by italic, a tint and the "?", not by fading. */
.lbox{position:relative;}
/* Update choices is for JavaScript off; the list boxes update themselves. */
[data-reg-update][hidden],[data-reg-nojs][hidden]{display:none;}
.lbox-button{display:flex;align-items:center;justify-content:space-between;gap:8px;text-align:left;cursor:pointer;}
.lbox-button::after{content:"\25be";font-size:11px;color:var(--text);}
.lbox-list{position:absolute;left:0;right:0;top:calc(100% + 4px);z-index:40;margin:0;padding:4px;list-style:none;
  max-height:300px;overflow:auto;background:var(--surface);border:1px solid var(--hairline);border-radius:10px;
  box-shadow:0 18px 40px rgba(16,34,42,.18);}
.lbox-list:focus{outline:2px solid var(--brand-teal);outline-offset:2px;}
.lbox-list li{display:flex;align-items:center;justify-content:space-between;gap:10px;padding:7px 9px;border-radius:7px;
  font-size:13px;color:var(--paper);cursor:pointer;}
.lbox-list li[aria-selected="true"]{font-weight:600;}
.lbox-list li.lbox-active{background:var(--teal-wash);box-shadow:inset 0 0 0 2px var(--brand-teal);}
.lbox-list li.lbox-off{color:var(--text);font-style:italic;background:#F3F6F7;cursor:not-allowed;}
.lbox-list li.lbox-off.lbox-active{background:#E8EEF0;}
.lbox-q{position:relative;flex:none;display:inline-flex;align-items:center;justify-content:center;width:18px;height:18px;
  border:1px solid var(--text);border-radius:50%;font-size:11px;font-style:normal;font-weight:600;color:var(--text);}
.lbox-q:hover::after{content:attr(data-why);position:absolute;right:0;top:calc(100% + 6px);z-index:41;width:max-content;
  max-width:260px;padding:6px 9px;border-radius:6px;background:var(--paper);color:#FFFFFF;font-size:12px;
  font-weight:400;line-height:1.4;white-space:normal;}
.lbox-why{min-height:1em;margin:4px 0 0;font-size:12px;color:var(--text);}

/* The restrictions panel on a catalogue card: one line closed, every rule open. */
.restrictions-panel{margin-top:14px;border:1px solid var(--hairline);border-radius:10px;background:var(--surface);}
.restrictions-panel > summary{list-style:none;cursor:pointer;display:flex;flex-wrap:wrap;align-items:baseline;gap:6px 10px;padding:10px 14px;font-size:13px;color:var(--text);}
.restrictions-panel > summary::-webkit-details-marker{display:none;}
.restrictions-panel > summary .eyebrow{margin:0;}
.restrictions-more{margin-left:auto;font-size:12.5px;font-weight:600;color:var(--brand-teal-lift);white-space:nowrap;}
.restrictions-more::after{content:" \25be";}
.restrictions-panel[open] .restrictions-more::after{content:" \25b4";}
.restrictions-body{padding:0 14px 14px;}
/* Relative, so the visually hidden labels of the owner's forms, which are
   absolutely positioned, scroll with the table instead of widening the page. */
.restrictions-scroll{overflow-x:auto;position:relative;}

/* The catalogue card's buttons. One line height, so the field dictionary's
   <button>, a dashboard <a> and the dashboard menu's <summary> are one height:
   a button's line height is otherwise "normal" and a link's the body's 1.62. */
.catactions .btn{line-height:20px;}
/* The dashboard menu, when a card offers two or more dashboards. */
.dashpick{position:relative;}
.dashpick > summary{list-style:none;cursor:pointer;display:inline-flex;align-items:center;gap:8px;white-space:nowrap;}
.dashpick > summary::-webkit-details-marker{display:none;}
.dashpick > summary::after{content:"\25be";font-size:11px;}
.dashpick[open] > summary{border-color:var(--brand-teal);}
.dashpick-menu{
  position:absolute;left:0;top:calc(100% + 6px);z-index:35;width:320px;max-width:86vw;
  margin:0;padding:6px;list-style:none;background:var(--surface);border:1px solid var(--hairline);
  border-radius:10px;box-shadow:0 18px 40px rgba(16,34,42,.18);animation:menuin .12s ease both;
}
.dashpick-menu a{display:flex;flex-direction:column;gap:2px;padding:9px 10px;border-radius:7px;text-decoration:none;}
.dashpick-menu a:hover{background:var(--teal-wash);}
.dashpick-menu a:focus-visible{outline:2px solid var(--brand-teal);outline-offset:-2px;}
.dashpick-name{font-size:14px;font-weight:600;color:var(--paper);}
.dashpick-says{font-size:12.5px;color:var(--text);}
@media (prefers-reduced-motion:reduce){.dashpick-menu{animation:none;}}
.formopt.on{background:var(--teal-wash);color:var(--brand-teal-lift);font-weight:600;}
.formopt.disabled{display:flex;flex-direction:column;gap:1px;color:var(--dim);cursor:not-allowed;}
.formopt.disabled em{font-style:normal;font-size:11px;color:#9AABB0;}

/* A small colour cycle for the parts of stacked, grouped and heat map charts. */
.seg-0{background:var(--brand-teal);}
.seg-1{background:#3E8A8E;}
.seg-2{background:#7BB2A6;}
.seg-3{background:#C7A15A;}
.seg-4{background:#9A6DB0;}
.seg-5{background:#5B7C99;}
.chart-legend{display:flex;flex-wrap:wrap;gap:8px 14px;margin-top:12px;font-size:12px;color:var(--text);}
.chart-legend .leg{display:inline-flex;align-items:center;gap:6px;}
.swatch{width:11px;height:11px;border-radius:3px;display:inline-block;}

/* Vertical columns. */
/* Vertical value charts share a y-axis on the left with tick labels and faint
   horizontal gridlines behind the columns, so a height can be read as a number. */
.plot-v{display:flex;gap:8px;align-items:stretch;}
.yaxis{position:relative;width:44px;flex:none;height:200px;}
.ya-tick{position:absolute;right:0;transform:translateY(50%);font-family:var(--mono);font-size:10.5px;
  color:var(--muted);white-space:nowrap;}
.ya-unit{position:absolute;left:0;top:-16px;font-family:var(--label);text-transform:uppercase;
  letter-spacing:.1em;font-size:10px;color:var(--muted);}
.gridded-v{
  background-image:repeating-linear-gradient(0deg,
    var(--hairline) 0, var(--hairline) 1px, transparent 1px, transparent var(--tick));
  background-position:left bottom;
}
.colchart{flex:1;display:flex;align-items:flex-end;gap:10px;height:200px;overflow-x:auto;}
.col{flex:1 0 44px;display:flex;flex-direction:column;align-items:center;gap:6px;height:100%;
  justify-content:flex-end;min-width:44px;}
.col-track{width:100%;flex:1;display:flex;align-items:flex-end;}
.col-fill{width:100%;background:#3E8A8E;border-radius:6px 6px 3px 3px;min-height:2px;}
.col-fill.first,.colchart .col:first-child .col-fill{background:var(--brand-teal);}
.col-n{font-family:var(--mono);font-size:11.5px;color:var(--paper);}
.col-label{font-size:11.5px;color:var(--text);text-align:center;word-break:break-word;}

/* Dot plot: a value on a shared axis, read where bars would crowd. */
.dotplot{display:flex;flex-direction:column;gap:9px;}
.dotrow{display:grid;grid-template-columns:112px 1fr 64px;gap:10px;align-items:center;}
.dot-label{font-size:13px;color:var(--text);}
.dot-track{position:relative;height:20px;border-bottom:1px solid var(--hairline);}
.dot-track.gridded{background-color:var(--surface-2);}
/* Half the radius it was drawn at, matching the line and area marker, so a point
   reads as a point rather than as the main object on the track. */
.dot{position:absolute;top:7px;width:6px;height:6px;border-radius:50%;background:var(--brand-teal);
  transform:translateX(-50%);}
.dot-withheld{font-size:11px;}
/* A chart with nothing left to draw says so, rather than leaving an empty plot
   that reads as a rendering fault. */
.chart-empty{
  margin:8px 0 0;font-size:12px;line-height:1.5;color:var(--muted);
  display:flex;align-items:baseline;gap:8px;
}
.dot-n{font-family:var(--mono);font-size:12px;color:var(--paper);text-align:right;}

/* Line and area over an ordered sequence. */
.linechart{width:100%;height:auto;display:block;overflow:hidden;}
.line-axis{stroke:var(--hairline);stroke-width:1;}
.line-stroke{fill:none;stroke:var(--brand-teal);stroke-width:2.5;stroke-linejoin:round;stroke-linecap:round;}
.area-fill{fill:rgba(22,104,109,.14);stroke:none;}
/* The stroke comes down with the radius. At the old 2.5 a halved marker would
   close up into a solid blob and lose the ring that marks the point. */
.line-dot{fill:#fff;stroke:var(--brand-teal);stroke-width:1.5;}
.withheld-tick{fill:var(--control);}
.line-label{fill:var(--muted);font-size:10px;font-family:var(--ui);}
.line-grid{stroke:var(--hairline);stroke-width:1;}
.axis-label{fill:var(--muted);font-size:10px;font-family:var(--ui);}
.point-value{fill:var(--paper);font-size:10px;font-family:var(--mono);}

/* Comparative statistics panel. A researcher reads figures, an interval, a p
   value and the n, so it is set as plain tables rather than a picture. */
.statspanel{background:var(--surface);border:1px solid var(--hairline);border-radius:12px;
  padding:18px 20px;margin-bottom:16px;min-width:0;}
.stats-head{display:flex;justify-content:space-between;align-items:baseline;gap:12px;margin-bottom:10px;}
.stats-head h4{font-size:15px;color:var(--paper);margin:0;}
.crosstab em{display:block;font-style:normal;font-size:11.5px;color:var(--dim);font-family:var(--mono);}
.stats-figures{width:100%;border-collapse:collapse;margin:14px 0 6px;font-size:13px;}
.stats-figures th{text-align:left;color:var(--muted);font-weight:600;font-size:11px;
  text-transform:uppercase;letter-spacing:.08em;padding:6px 10px;border-bottom:1px solid var(--hairline);}
.stats-figures td{padding:7px 10px;border-bottom:1px solid var(--hairline);color:var(--text);}
.stats-figures td:first-child{color:var(--paper);}
.stats-context{margin-top:12px;border-top:1px solid var(--hairline);padding-top:10px;}
.stats-context ul{margin:0;padding-left:18px;}
.stats-context li{font-size:12px;color:var(--dim);margin:2px 0;}

/* Stacked and proportional bars, one row per group. */
.stackchart{display:flex;flex-direction:column;gap:10px;}
.stackrow{display:grid;grid-template-columns:112px 1fr;gap:10px;align-items:center;}
.stackchart + .va-h,.stackchart ~ .va-h{grid-template-columns:112px 1fr;}
.stack-label{font-size:13px;color:var(--text);}
.stack-track{display:flex;height:22px;border-radius:6px;overflow:hidden;background:var(--teal-wash);}
.stack-seg{height:100%;display:flex;align-items:center;justify-content:center;overflow:hidden;}
.seg-n{font-family:var(--mono);font-size:10.5px;color:#fff;white-space:nowrap;}

/* Grouped bars, a small cluster of columns per group. */
.groupchart{flex:1;display:flex;gap:16px;height:200px;align-items:stretch;overflow-x:auto;}
.grouprow{flex:1 0 60px;display:flex;flex-direction:column;gap:6px;min-width:60px;}
.group-bars{flex:1;display:flex;align-items:flex-end;justify-content:center;gap:5px;
  border-bottom:1px solid var(--hairline);}
.gbar{flex:1;max-width:36px;min-width:8px;border-radius:4px 4px 0 0;min-height:2px;position:relative;
  display:flex;align-items:flex-start;justify-content:center;}
.gbar-n{position:absolute;top:-15px;font-family:var(--mono);font-size:10px;color:var(--paper);white-space:nowrap;}
.group-label{font-size:12.5px;color:var(--text);text-align:center;}

/* Heat map: two categorical fields as shaded cells. A withheld cell is hatched,
   never shaded, so a light shade cannot imply a small number. */
.heatmap{width:100%;border-collapse:separate;border-spacing:3px;font-size:12px;}
.heatmap th{font-family:var(--label);text-transform:uppercase;letter-spacing:.08em;font-size:10px;
  color:var(--muted);font-weight:600;padding:2px 4px;text-align:center;}
.heatmap th.rowhead{text-align:right;color:var(--text);text-transform:none;letter-spacing:0;font-size:12px;}
.heatcell{width:44px;height:30px;border-radius:4px;text-align:center;vertical-align:middle;}
.heat-n{font-family:var(--mono);font-size:10.5px;color:var(--paper);}
.heat-3 .heat-n,.heat-4 .heat-n{color:#fff;}
.heat-0{background:#EAF2F2;}
.heat-1{background:#BEDAD9;}
.heat-2{background:#7FB6B4;}
.heat-3{background:#3E8A8E;}
.heat-4{background:var(--brand-teal);}
.heat-withheld{background:repeating-linear-gradient(45deg,var(--control) 0,var(--control) 2px,transparent 2px,transparent 5px);}
.heat-key{display:flex;align-items:center;gap:5px;margin-top:12px;font-size:11px;color:var(--muted);}
.heat-key i{width:16px;height:12px;border-radius:2px;display:inline-block;}

/* Box plot: median and quartiles across a categorical split. A group below the
   threshold is withheld whole, since quartiles on a few records identify them. */
.boxchart{display:flex;flex-direction:column;gap:12px;}
.boxrow{display:grid;grid-template-columns:112px 1fr;gap:12px;align-items:center;}
.box-label{font-size:13px;color:var(--text);}
.box-track{position:relative;height:24px;}
.box-grid{position:absolute;inset:0;pointer-events:none;}
.box-grid .bg{position:absolute;top:0;bottom:0;width:1px;background:var(--hairline);}
.box-whisker{position:absolute;top:11px;height:2px;background:var(--muted);}
.box-iqr{position:absolute;top:3px;height:18px;background:var(--teal-wash);border:1.5px solid var(--brand-teal);border-radius:3px;}
.box-median{position:absolute;top:3px;height:18px;width:2px;background:var(--brand-teal);}
.box-withheld{font-size:11px;}
.box-axisrow{display:grid;grid-template-columns:112px 1fr;gap:12px;}
.box-ticks{position:relative;height:16px;}
.box-ticks span{position:absolute;top:0;transform:translateX(-50%);font-family:var(--mono);font-size:10.5px;color:var(--muted);}
.box-axis{display:flex;justify-content:space-between;font-family:var(--mono);font-size:11px;
  color:var(--dim);margin-top:4px;padding-left:calc(25% + 12px);}

/* Treemap: nested rectangles by count, area proportional. */
.treemap{position:relative;width:100%;height:280px;background:var(--teal-wash);border-radius:10px;overflow:hidden;}
.tile{position:absolute;border:2px solid var(--surface);border-radius:4px;background:#4E9599;
  padding:5px 7px;overflow:hidden;display:flex;flex-direction:column;gap:1px;color:#fff;}
.tile:nth-child(3n){background:#3E8A8E;}
.tile:nth-child(3n+1){background:var(--brand-teal);}
.tile-label{font-size:11px;line-height:1.15;}
.tile-n{font-family:var(--mono);font-size:11px;opacity:.85;}
.tile.withheld-fill{color:var(--text);}

/* Name-this-analysis modal. */
.modal-overlay{
  position:fixed;inset:0;z-index:40;background:rgba(16,34,42,.45);
  display:flex;align-items:center;justify-content:center;padding:20px;overflow-y:auto;
}
.modal-overlay[hidden]{display:none;}
.modal{
  background:var(--surface);border-radius:12px;padding:26px 28px;max-width:460px;width:100%;
  box-shadow:0 24px 60px rgba(16,34,42,.28);max-height:90vh;overflow-y:auto;margin:auto;
}
.modal h3{font-size:19px;margin-bottom:6px;}

/* Audit trail: filter chips. */
.filterbar{display:flex;flex-wrap:wrap;gap:8px;margin:2px 0 4px;}
.chip{
  display:inline-flex;align-items:center;gap:8px;text-decoration:none;
  border:1px solid var(--hairline);border-radius:20px;padding:6px 14px;
  font-size:13px;color:var(--text);background:var(--surface);
}
.chip:hover{border-color:var(--muted);color:var(--paper);}
.chip.on{background:var(--teal-wash);border-color:var(--brand-teal);color:var(--brand-teal-lift);font-weight:600;}
.chip .chipn{font-family:var(--mono);font-size:11.5px;color:var(--dim);}
.chip.on .chipn{color:var(--brand-teal-lift);}

/* Audit trail: older activity folded away by day. */
.daygroup{border-top:1px solid var(--hairline-soft);}
.daygroup summary{
  list-style:none;cursor:pointer;display:flex;align-items:center;gap:12px;
  padding:14px 2px;font-family:var(--label);text-transform:uppercase;
  letter-spacing:.14em;font-size:12.5px;color:var(--text);font-weight:600;
}
.daygroup summary::-webkit-details-marker{display:none;}
.daygroup summary::before{
  content:"▸";color:var(--muted);font-size:11px;transition:transform .15s;
}
.daygroup[open] summary::before{transform:rotate(90deg);}
.daygroup .daycount{
  margin-left:auto;font-family:var(--mono);text-transform:none;letter-spacing:0;
  font-size:12px;color:var(--dim);font-weight:400;
}
.daygroup[open] summary{color:var(--paper);}
.daygroup > .thread{padding-bottom:8px;}

/* Sign in step indicator (POC design). */
.step{display:flex;align-items:center;gap:9px;margin-bottom:14px;}
.step .num{
  width:22px;height:22px;border-radius:11px;background:var(--brand-teal);color:#fff;
  font-size:12px;font-weight:700;display:flex;align-items:center;justify-content:center;flex:none;
}
.step .lbl{
  font-family:var(--label);font-weight:600;text-transform:uppercase;letter-spacing:.16em;
  font-size:12.5px;color:var(--muted);
}

/* Submitting state (progressive enhancement). These styles are only ever
   applied by submit-guard.js while a form is on its way, so a slow submit
   cannot be pressed twice. Nothing here appears without JavaScript. */
.btn.is-submitting{cursor:progress;}
.btn.is-submitting[disabled]{opacity:.92;color:#FFFFFF;cursor:progress;}
.btn-spinner{
  display:inline-block;vertical-align:-2px;width:13px;height:13px;margin-right:9px;
  border:2px solid rgba(255,255,255,.4);border-top-color:#FFFFFF;border-radius:50%;
  animation:btnspin .7s linear infinite;
}
@keyframes btnspin{to{transform:rotate(360deg);}}
/* Respect a reduced motion preference: hold the ring still so it reads as a
   static indicator, and let the changed label carry the state instead. */
@media (prefers-reduced-motion:reduce){
  .btn-spinner{animation:none;}
}

/* Visually hidden, still read by a screen reader. Used for the polite live
   region that announces the submitting state. */
.sr-only{
  position:absolute;width:1px;height:1px;padding:0;margin:-1px;
  overflow:hidden;clip:rect(0 0 0 0);white-space:nowrap;border:0;
}

/* Printing a result (also the path to "save as PDF"). Strip the chrome so the
   page prints as the charts, the table and the information alone. */
@media print{
  .topbar,.rail,.perimeter,.backlink,.chart-soon,.charttip,.chart-actions{display:none !important;}
  .row .btn,.row a.btn{display:none !important;}
  .app,.body,.main{display:block;background:#FFFFFF;}
  .chartcard,.card{break-inside:avoid;}
}

/* The printed analysis: the whole thing as one document, landscape, a chart to
   a page. It shares the sheet, meta and chart treatment of a single printed
   chart rather than introducing a second one; what is added here is the page
   geometry and the running header, neither of which a one page print needs.

   The header is fixed rather than written into each page, because a header
   repeated by hand drifts the moment a chart changes height, which is the same
   reasoning that removed the editor's second scroller (constraint 22). A fixed
   box in print context is painted on every sheet by the browser. */
.printdoc .printrun{
  display:flex;justify-content:space-between;gap:16px;
  font-size:11px;color:var(--muted);
  padding-bottom:6px;border-bottom:1px solid var(--hairline);
  margin-bottom:14px;
}
.printsigfacts{
  display:grid;grid-template-columns:repeat(auto-fit,minmax(190px,1fr));
  gap:10px 20px;margin:0 0 12px;
}
.printsigfacts dt{font-size:11px;color:var(--muted);text-transform:uppercase;letter-spacing:0.04em;}
.printsigfacts dd{margin:2px 0 0;font-size:14px;color:var(--text);}
.printsig{
  margin-top:18px;padding:16px;
  border:1px solid var(--hairline);border-left:3px solid var(--brand-teal);
  border-radius:10px;background:var(--surface);
}
.printcaveat{
  background:var(--surface-3);border-radius:8px;padding:12px 14px;
  font-size:13px;line-height:1.5;color:var(--text);max-width:66ch;margin:0;
}
.printpara{
  margin:14px 0 0;font-size:13px;line-height:1.6;color:var(--text);max-width:90ch;
}
.printchart{margin-top:14px;}
.printchart svg{width:100%;height:auto;}

@media print{
  /* Landscape, so a wide chart is wide on paper rather than shrunk to fit a
     portrait column. */
  @page{size:A4 landscape;margin:12mm;}
  .printdoc{background:#FFFFFF;}
  /* One chart to a page. `break-after` on every sheet but the last, so the
     document does not end on a blank page. */
  .printdoc .printpage{break-after:page;}
  .printdoc .printpage:last-of-type{break-after:auto;}
  .printdoc .printpage{break-inside:avoid;}
  /* Repeated on every sheet. */
  .printdoc .printrun{
    position:fixed;top:0;left:0;right:0;
    background:#FFFFFF;
  }
  .printdoc .printsheet{padding-top:26px;}
  /* Print colours: the browser drops backgrounds by default, and a caveat that
     loses its panel loses the thing that marks it as a qualification. */
  .printsig,.printcaveat{
    -webkit-print-color-adjust:exact;print-color-adjust:exact;
  }
}

/* The analysis title and its rename control. The form is visible by default so
   the control works with JavaScript off; the script hides it and reveals the
   Rename button, which is the enhancement rather than the baseline. */
.renameform[hidden]{display:none;}
.analysistitle .renameopen{padding:4px 10px;font-size:12px;}

/* The instructions page. Seven collapsed sections, so the shape of what is
   documented fits on a screen before anything is opened.

   details and summary rather than a scripted disclosure: they open and close
   with JavaScript off, they are focusable and operable from the keyboard, and
   they report their own expanded state to a screen reader without any of it
   being added by hand. */
.guide{
  border:1px solid var(--hairline);border-radius:10px;
  margin-bottom:10px;background:var(--surface);
}
.guide > summary{
  cursor:pointer;padding:14px 16px;list-style:none;
  display:flex;flex-wrap:wrap;align-items:baseline;gap:4px 12px;
}
/* The default disclosure triangle is replaced, so the marker has to be removed
   in both the standard and the WebKit spelling or two markers appear. */
.guide > summary::-webkit-details-marker{display:none;}
.guide > summary::marker{content:"";}
.guide > summary::before{
  content:"+";font-family:var(--mono);font-size:14px;line-height:1;
  color:var(--muted);flex:none;width:14px;
}
.guide[open] > summary::before{content:"−";}
.guide > summary:hover{background:var(--hover,rgba(0,0,0,0.02));}
/* Focus has to remain visible: this is the control a keyboard reader uses to
   move through the page. */
.guide > summary:focus-visible{outline:2px solid var(--focus,#2b6cb0);outline-offset:-2px;}
.guide-title{font-weight:600;font-size:15px;color:var(--text);}
.guide-hint{color:var(--muted);font-size:13px;}
.guide-body{padding:0 16px 14px 42px;}
.guide-body .hint{margin:0 0 10px;}
.guide-body .plain{margin:0 0 10px;padding-left:18px;}
.guide-body .plain li{margin-bottom:4px;color:var(--muted);font-size:13px;}
/* A language inside the code section. Indented and quieter than a top level
   section so the nesting reads. */
.guide-nested{margin:10px 0 0;background:transparent;}
.guide-nested > summary{padding:10px 12px;}
.guide-nested .guide-body{padding:0 12px 10px 38px;}


/* The catalogue dataset on a saved result card, under the date. */
.runitem-dataset{
  margin:2px 0 0;font-size:12px;color:var(--muted);
}
/* An analysis whose access has ended. Inert: it explains an absence rather than
   offering a way in, so it is not a link and does not respond to a hover. */
.runitem-lapsed{
  opacity:0.72;cursor:default;
}
.runitem-lapsed:hover{background:inherit;border-color:var(--hairline);}
/* The catalogue dataset filter is a select, sized so the row sits on one line
   with the three fields beside it. It was a fieldset of stacked tick boxes,
   which set its own height and left nothing in the row aligned. */
.savedfilter select.input{
  width:210px;max-width:100%;
  background:var(--surface);color:var(--text);
  border:1px solid var(--hairline);border-radius:7px;
}
/* Below this the row wraps, and a filter whose fields are each a different
   width wraps into a ragged block. Full width from here, so a wrapped row is
   a clean stack. */
@media (max-width:640px){
  .savedfilter{width:100%;margin-left:0;}
  .savedfilter .field{flex:1 1 100%;}
  .savedfilter .input,
  .savedfilter select.input,
  .savedfilter input[type="search"],
  .savedfilter input[type="date"]{width:100%;}
}

/* A chart group that can be narrowed to. It is a button, so it is reachable and
   operable from the keyboard without anything added, and it keeps the bar's own
   layout rather than looking like a form control. */
button.mbar-drill{
  display:grid;grid-template-columns:inherit;align-items:center;gap:inherit;
  width:100%;text-align:left;background:none;border:0;padding:2px 0;
  font:inherit;color:inherit;cursor:pointer;border-radius:4px;
}
button.mbar-drill:hover .mbar-label{text-decoration:underline;}
button.mbar-drill:focus-visible{outline:2px solid var(--focus,#2b6cb0);outline-offset:1px;}
/* A group that cannot be narrowed to. Not a control, and it carries the reason
   on the element so hovering or focusing says why. */
.mbar-nodrill{cursor:help;}

/* The drill path. */
.drillpath{margin:10px 0 16px;}
.drillpath-form{display:flex;flex-wrap:wrap;align-items:center;gap:4px 6px;}
.crumb{
  background:none;border:0;padding:2px 6px;font:inherit;font-size:13px;
  color:var(--link,#2b6cb0);cursor:pointer;border-radius:4px;
}
.crumb:hover{text-decoration:underline;}
.crumb:focus-visible{outline:2px solid var(--focus,#2b6cb0);outline-offset:1px;}
.crumb-here{color:var(--text);cursor:default;font-weight:600;}
.crumb-sep{color:var(--muted);font-size:12px;}

/* The context table on the contact page: four labelled values a researcher
   copies into an email. Monospaced values so a reference is readable and
   selectable as one run of characters. */
.ctxtable{width:100%;border-collapse:collapse;}
.ctxtable th{
  text-align:left;font-weight:500;font-size:13px;color:var(--muted);
  padding:5px 14px 5px 0;white-space:nowrap;vertical-align:top;width:1%;
}
.ctxtable td{
  font-family:var(--mono);font-size:12.5px;color:var(--text);
  padding:5px 0;word-break:break-all;
}

/* The role preview toggle. A demonstration control in the header, shown only to
   the account named in ROLE_PREVIEW_ACCOUNT. Presentation only: it redraws the
   interface as a researcher sees it and changes nothing the server authorises.

   The mode is readable as a word as well as by knob position, because position
   alone is exactly the kind of thing somebody misreads in a screenshot. */
.roleview { margin: 0; display: flex; align-items: center; }
.roleswitch {
  display: inline-flex; align-items: center; gap: 8px;
  background: var(--surface); border: 1px solid var(--hairline);
  border-radius: 999px; padding: 4px 10px 4px 10px; cursor: pointer;
  font: inherit; font-size: 12px; color: var(--paper); line-height: 1;
}
.roleswitch:hover { border-color: var(--brand-teal); }
.roleswitch-label { opacity: .62; }
.roleswitch-mode { font-weight: 600; letter-spacing: .01em; }
.roleswitch-track {
  position: relative; width: 30px; height: 16px; border-radius: 999px;
  background: var(--hairline); flex: 0 0 auto; transition: background .15s;
}
.roleswitch-knob {
  position: absolute; top: 2px; left: 2px; width: 12px; height: 12px;
  border-radius: 50%; background: #fff; transition: transform .15s;
  box-shadow: 0 1px 2px rgba(0,0,0,.25);
}
/* Left for researcher, right for administrator. */
.roleswitch.as-admin .roleswitch-track { background: var(--brand-teal); }
.roleswitch.as-admin .roleswitch-knob { transform: translateX(14px); }
.roleswitch.as-researcher .roleswitch-track { background: var(--hairline); }
.roleswitch.as-researcher .roleswitch-knob { transform: none; }

/* The standing note in the rail while researcher view is on. It says what the
   control does not do, which is the half a reader otherwise supplies wrongly. */
.previewnote {
  margin: 14px 10px 0; padding: 10px 12px; border-radius: 8px;
  border: 1px dashed var(--hairline);
}

/* Tables with a row per thing, as opposed to .ctxtable which is a key and value
   list. Everything is centred on the y axis so a row with a control in it reads
   level with a row without one, and the control is sized to the text rather than
   to the default button padding, which was making the rows tall and uneven. */
.rowtable{width:100%;border-collapse:collapse;}
.rowtable th{
  text-align:left;font-weight:500;font-size:12px;color:var(--muted);
  letter-spacing:.04em;text-transform:uppercase;
  padding:0 14px 6px 0;white-space:nowrap;vertical-align:middle;
}
.rowtable td{
  font-size:13px;color:var(--text);
  padding:7px 14px 7px 0;vertical-align:middle;
  border-top:1px solid var(--hairline);
}
.rowtable td:last-child,.rowtable th:last-child{padding-right:0;text-align:right;}
.rowtable .rt-mono{font-family:var(--mono);font-size:12.5px;}
.rowtable .rt-quiet{color:var(--muted);}
/* The control in a row. Height comes from the line box plus small padding, so it
   sits inside the row rather than setting the row's height. */
.btn.rowbtn{
  padding:4px 10px;font-size:12px;line-height:1.3;border-radius:6px;
  min-height:0;height:auto;white-space:nowrap;
}
.rowtable form{margin:0;display:inline-block;}
/* A control that ends somebody's access reads as heavier than one that
   withdraws an unused invitation, because it is. */
.btn.rowbtn.heavy{border-color:var(--blocked);color:var(--blocked);}
.btn.rowbtn.heavy:hover{background:var(--blocked);color:#fff;}
/* The dataset list under an accepted account. A details element, so it needs no
   script and opens with JavaScript off. */
.seelist{margin:4px 0 0;font-size:12.5px;}
.seelist summary{cursor:pointer;color:var(--muted);}
.seelist ul{margin:6px 0 0 0;padding-left:16px;}
.seelist li{margin:0 0 3px;}

/* The access period editor, inside the Ends cell of the memberships table.
   A details element, so it opens with no script and the table grows in one
   place rather than jumping. Closed by default. */
.endedit > summary{
  cursor:pointer;font-size:13px;color:var(--text);list-style:none;
  display:inline-flex;align-items:center;gap:6px;
}
.endedit > summary::-webkit-details-marker{display:none;}
.endedit > summary::after{content:"edit";font-size:11px;color:var(--muted);
  border:1px solid var(--hairline);border-radius:5px;padding:1px 5px;}
.endedit[open] > summary::after{content:"close";}
.endedit[open] > summary{margin-bottom:8px;}
.endform{
  margin:0;padding:10px 12px;border:1px solid var(--hairline);border-radius:8px;
  background:var(--surface);min-width:260px;
}
.endform label{display:block;font-size:11px;letter-spacing:.04em;
  text-transform:uppercase;color:var(--muted);margin-bottom:4px;}
.endform .input{max-width:190px;}
.endpresets{display:flex;flex-wrap:wrap;gap:6px;margin-top:8px;}

/* Significance testing at the foot of a chart. The sentence first, the
   supporting detail under it, and the p value last rather than first, because a
   researcher reading a p value first reads the rest as decoration. */
.sigtest{margin:14px 0 0;padding:12px 14px;border-top:1px solid var(--hairline);}
.sigtest-sentence{margin:0 0 10px;font-size:14px;line-height:1.55;}
.sigtest-refused{margin:0;font-size:13px;line-height:1.55;color:var(--text);}
.sigtest-detail{display:grid;grid-template-columns:auto 1fr;gap:2px 14px;margin:0 0 10px;font-size:12.5px;}
.sigtest-detail dt{color:var(--muted);}
.sigtest-detail dd{margin:0;font-family:var(--mono);}
/* The half a reader supplies for themselves and gets wrong, so it is not a
   footnote. */
.sigtest-caveat{margin:0;font-size:12.5px;line-height:1.6;color:var(--muted);
  border-left:2px solid var(--hairline);padding-left:10px;}

/* Significance, at the foot of a chart that can carry a test. The sentence
   first and the p value last, because a researcher who reads the p value first
   reads it as the finding. */
.sigfoot{margin-top:14px;padding-top:12px;border-top:1px solid var(--hairline);}
.sigpanel{font-size:13px;}
.sigsentence{margin:0 0 10px;font-size:14px;line-height:1.55;color:var(--text);}
.sigdetail{display:grid;grid-template-columns:auto 1fr;gap:2px 14px;margin:0 0 10px;}
.sigdetail dt{font-size:11px;letter-spacing:.04em;text-transform:uppercase;
  color:var(--muted);align-self:center;}
.sigdetail dd{margin:0;font-family:var(--mono);font-size:12.5px;}
/* The half a reader supplies for themselves and gets wrong, so it is not a
   footnote and is not smaller than the finding. */
.sigcaveat{margin:0;padding:10px 12px;border-radius:8px;
  border:1px dashed var(--hairline);font-size:12.5px;line-height:1.6;color:var(--muted);}
.sigrefused .hint{line-height:1.6;}

/* The naming styles in the run dialogue. Each option shows the name it would
   actually produce, so the choice is made by reading. */
.namestyles{border:none;padding:0;margin:0 0 16px;}
.namestyles legend{font-size:11px;letter-spacing:.04em;text-transform:uppercase;
  color:var(--muted);padding:0;margin-bottom:6px;}
.namestyle{display:grid;grid-template-columns:auto auto 1fr;gap:8px;
  align-items:baseline;padding:6px 8px;border-radius:6px;cursor:pointer;}
.namestyle:hover{background:var(--surface-2);}
.namestyle-key{font-size:11px;letter-spacing:.04em;text-transform:uppercase;
  color:var(--muted);min-width:56px;}
.namestyle-name{font-size:12.5px;font-family:var(--mono);word-break:break-word;}
.namestyle-default{display:flex;gap:8px;align-items:center;margin-top:8px;
  font-size:12.5px;color:var(--muted);}

/* The significance card, ahead of the descriptive ones.

   Same surface, hairline and radius as a chart card so it reads as the first of
   four, with a teal rule down the left and its own heading because it is an
   inference rather than a count.

   It uses its width rather than running everything down the left: the heading
   and the collapse control sit apart, the supporting rows lay out across the
   card, and only the sentence and the caveat keep a reading measure, because
   those are prose and the rest is not. */
.sigcard{
  /* The caveat's columns depend on how wide the card is, not on how wide the
     window is: the card sits inside a main column whose width the viewport
     does not predict. A viewport breakpoint got this wrong at 1024, where the
     window is wide and the card is not. */
  container-type:inline-size;
  --sigcard-pad-x:22px;
  --sigcard-pad-b:20px;
  background:var(--surface);
  border:1px solid var(--hairline);
  border-left:3px solid var(--brand-teal);
  border-radius:12px;
  padding:20px var(--sigcard-pad-x) var(--sigcard-pad-b);
  margin:0 0 18px;
}
.sigcard-head{
  display:flex;align-items:flex-start;justify-content:space-between;
  gap:16px;margin-bottom:6px;
}
.sigcard-eyebrow{color:var(--brand-teal);margin:0 0 4px;}
.sigcard-title{margin:0;font-size:19px;color:var(--paper);}
.sigcard-toggle{margin:0;flex:0 0 auto;}
/* 36em is about 75 characters in this typeface, at any size in this card. It
   replaces the `ch` caps, which were set by eye and measured wrong: `ch` is the
   advance of a zero, which is wider than the average letter here, so a 66ch cap
   produced 88 characters on the line rather than the 71 it was meant to be. */
.sigcard-lede{margin:6px 0 16px;font-size:13px;line-height:1.6;color:var(--muted);max-width:36em;}
.sigcard-body{margin:0 0 16px;}
.sigcard-body:last-child{margin-bottom:0;}
/* The finding, so it takes the largest size in the card. 18px rather than 16
   both suits what it is and buys width: 75 characters occupies 650px at this
   size against 565px at the old one. It still does not reach the card edge, and
   nothing sensible would: a single column filling 992px runs to 128 characters,
   and holding 75 across that width would need 27px type. */
.sigcard-sentence{
  margin:0 0 14px;font-size:18px;line-height:1.5;
  color:var(--paper);max-width:36em;
}
/* Across the card rather than down the left. Auto fitting columns, so four
   items sit in a row on a wide screen and fold to two and then one as the card
   narrows, without a breakpoint for each. */
.sigcard-detail{
  display:grid;grid-template-columns:repeat(auto-fit,minmax(190px,1fr));
  gap:12px 24px;margin:0 0 16px;
  padding:12px 0;border-top:1px solid var(--hairline-soft);
  border-bottom:1px solid var(--hairline-soft);
}
.sigcard-detail dt{
  font-size:11px;letter-spacing:.04em;text-transform:uppercase;
  color:var(--muted);margin:0 0 2px;
}
.sigcard-detail dd{margin:0;font-family:var(--mono);font-size:13px;color:var(--text);}
/* A light panel, not a dark one. The dark treatment measured 3.8 to 1, below
   the 4.5 minimum for body text, and read as something switched off rather
   than something to read. This is 8.7 to 1 with the heading at 14.6, and it
   reads as part of the finding rather than as a warning or an aside. */
/* Part of the card, not an attachment to it. It was an inset box stopping
   short of the card on every side, which read as an aside, and it is half the
   finding. So it runs out to the card's own edges by a negative margin equal to
   the card's padding, and takes that padding back, which puts its text on the
   same left edge as the text above it and closes the card off at the bottom.
   The teal rule on the left runs past it unbroken.

   Two columns, because that is the only way prose fills 992px and stays
   readable. `column-width` rather than a breakpoint, so the browser takes two
   columns where they fit and one where they do not, and the card can be any
   width. At 14px each column holds about 75 characters, which is why the size
   went up rather than down: at 13.5px a 480px column runs to 77. */
.sigcard-caveat{
  /* Out to the card's edges, and the padding taken back, so its text sits on
     the same left edge as the text above it. */
  margin:0 calc(-1 * var(--sigcard-pad-x));
  padding:16px var(--sigcard-pad-x);
  border:0;
  border-top:1px solid var(--hairline-soft);
  border-bottom:1px solid var(--hairline-soft);
  background:var(--surface-3);
  font-size:14px;line-height:1.65;color:var(--text);
  /* The panel itself is never capped: it spans the card at every width, which
     is what makes it read as part of the card rather than an inset note. */
  max-width:none;
}
/* The measure lives on the text, not on the panel. One column with a cap is the
   default and the fallback: a browser without container queries, or a card too
   narrow for two columns, gets readable text rather than a line the width of
   the card, which measured 110 characters and was worse than the inset box it
   replaced. 34em is about 74 characters at this size. */
.sigcard-caveat-text{margin:0;max-width:34em;}
/* Two columns once the card can give each one a proper measure. The number is
   the card's content box, and it is set from the arithmetic rather than picked:
   at 880 each column holds about 65 characters, and below that they get too
   narrow to read comfortably, so one measured column is the better answer.
   920 was wrong by eight pixels, which cost the two column layout at a 1280
   window, where the card's content box is 912. */
@container (min-width:880px){
  .sigcard-caveat-text{max-width:none;columns:2;column-gap:36px;}
}
/* Where the caveat is the last thing in the card, which is the shut state and
   any state with no controls under it, it closes the card off rather than
   ending in a rule with a strip of card below it. The bottom padding is taken
   from the card so the two cannot fall out of step. */
.sigcard-body:last-child .sigcard-caveat{
  margin-bottom:calc(-1 * var(--sigcard-pad-b));
  padding-bottom:var(--sigcard-pad-b);
  border-bottom:0;
  border-radius:0 0 10px 10px;
}
/* A heading orphaned at the foot of the first column reads as a new section. */
.sigcard-caveat strong{color:var(--paper);font-weight:600;break-after:avoid;}
.sigcard-refused{font-size:14px;line-height:1.65;color:var(--text);max-width:36em;}
.sigcard-refused .eyebrow{color:var(--control);}
.sigcard-form{margin:0;}
/* Two ways to save an edited analysis (Sprint Enchilada E02). */
.savechoice{border:1px solid var(--hairline);border-radius:8px;padding:14px 16px;margin:16px 0 0;display:grid;gap:14px;background:var(--surface);}
.savechoice legend{font-weight:600;color:var(--paper);padding:0 4px;}
.savechoice-option{display:grid;gap:6px;margin:0;}
.savechoice-option .btn{justify-self:start;}
.savechoice-note{margin:0;max-width:40em;}
/* The history of an analysis. */
.history{margin-top:18px;border-top:1px solid var(--hairline-soft);padding-top:12px;}
.history summary{cursor:pointer;font-weight:600;color:var(--paper);}
.history-list{list-style:none;margin:12px 0 0;padding:0;display:grid;gap:12px;}
.history-event{border-left:2px solid var(--hairline);padding:2px 0 2px 12px;}
.history-when,.history-what,.history-conditions,.history-diff,.history-reopen{margin:0 0 4px;}
/* The regression card sits under the significance card and reads as its pair. */
.regcard{margin-top:16px;}
.regtable-wrap{overflow-x:auto;margin:0 0 14px;}
.regtable{min-width:520px;}
.regtable th,.regtable td{text-align:left;padding:6px 10px;border-bottom:1px solid var(--hairline-soft);}
.regtable .num{text-align:right;font-family:var(--mono);font-variant-numeric:tabular-nums;white-space:nowrap;}
/* The controls spread across the card, with the menus taking the space rather
   than sitting at their minimum width against the left edge. */
.sigcard-controls{
  display:grid;grid-template-columns:minmax(0,1fr) minmax(0,1fr) auto;
  gap:14px;align-items:end;
}
.sigcard-controls .field{margin:0;min-width:0;}
.sigcard-controls .sel{width:100%;}
.sigcard-controls label{
  display:block;font-size:11px;letter-spacing:.04em;
  text-transform:uppercase;color:var(--muted);margin-bottom:4px;
}
/* The last ch cap on the card. 80ch measured 107 characters, for the same
   reason the others did: ch is the advance of a zero, wider than the average
   letter in this face, so a ch cap runs about a third longer than it reads. */
.sigcard-note{margin:10px 0 0;max-width:36em;}
/* Collapsed. The head stays, and so does anything already found: a result keeps
   its sentence and its caveat, and a refusal keeps its reason. */
.sigcard-shut{--sigcard-pad-b:16px;}
.sigcard-shut .sigcard-body{margin-bottom:0;}

@media (max-width:900px){
  /* Through the variables, so the caveat's bleed stays exactly one padding
     wide. Setting `padding` here directly left it overhanging the card by six
     pixels each side. */
  .sigcard{--sigcard-pad-x:16px;--sigcard-pad-b:16px;padding-top:16px;}
  /* The measure caps stay. Removing them let the sentence run to 95 characters
     on a laptop, which is worse than the desktop measure and the opposite of
     what a narrow rule is for. A ch cap never exceeds the available width. */
  .sigcard-sentence{font-size:15px;}
  .sigcard-controls{grid-template-columns:1fr;}
  .sigcard-head{flex-wrap:wrap;}
}

/* The perimeter banner when a screen shows record level rows. Marked out from
   the ordinary one, because a reader who skims past it has skimmed past the one
   sentence that says this screen is different from every other. */
.perimeter-record{
  border-left:3px solid var(--blocked);
  background:var(--blocked-wash);
}
.perimeter-record .tag{color:var(--blocked);}
.perimeter-record .say strong{color:var(--blocked);}

/* --- the question reader -------------------------------------------------- */
/* House treatment throughout. The tab's development badge is the same shape as
   the other small status pills on the platform rather than an invented style,
   because a marker that looks bespoke reads as decoration and this one is a
   statement of fact: the model is not part of the deployed environment. */
.qtab-dev{
  margin-left:6px;padding:1px 5px;border-radius:4px;
  font-family:var(--label);font-size:9.5px;font-weight:600;
  text-transform:uppercase;letter-spacing:.1em;
  background:var(--surface-3);color:var(--muted);
  border:1px solid var(--hairline-soft);
  vertical-align:middle;
}
.asknote{margin:0 0 16px;}
.askhead{margin-bottom:14px;}
/* The same measure the significance card settled on: 36em is about 75
   characters in this face at any size. */
.askmeasure{max-width:36em;}
.askform{margin:0 0 16px;}
.askinput{
  width:100%;min-height:76px;padding:10px 12px;font-size:14px;line-height:1.5;
  font-family:inherit;resize:vertical;
}
.askrow{gap:10px;align-items:center;margin-top:10px;}
.askreading,.askrefusal{margin:0 0 16px;padding:16px 18px;}
.askreading{border-left:3px solid var(--brand-teal);}
.askrefusal{border-left:3px solid var(--control);}
.askconditions{margin:0;padding-left:18px;}
.askconditions li{margin-bottom:4px;font-size:14px;color:var(--text);}
.askrule{margin:0;font-size:12px;color:var(--muted);}
.askrule code{font-family:var(--mono);font-size:12px;}
.askexamples{margin:0 0 16px;}
.askexamples .plain{margin:0;padding-left:18px;}
.askexamples .plain li{
  margin-bottom:5px;font-size:13px;color:var(--muted);
  /* Copyable, so it reads as text to take rather than prose to skim. */
  font-family:var(--mono);
}
.askhistory{border-top:1px solid var(--hairline-soft);padding-top:14px;}
.askturn{padding:8px 0;border-bottom:1px solid var(--hairline-soft);}
.askturn:last-child{border-bottom:0;}
.askturn-q{margin:0 0 2px;font-size:13.5px;color:var(--text);}
.askturn .hint{margin:0;font-size:12px;}
.askturn-refused .askturn-q{color:var(--muted);}
/* The older half of the history. House details treatment: the summary is a
   control the researcher can see is a control, and nothing above it moves when
   it opens. */
.askolder{margin-top:10px;}
.askolder-summary{cursor:pointer;font-size:12.5px;color:var(--brand-teal);padding:4px 0;}
.askolder-summary:hover{text-decoration:underline;}
.askolder[open] .askolder-summary{margin-bottom:4px;}
.askpager{display:flex;align-items:center;gap:10px;margin-top:10px;}
.askpager .btn{padding:4px 10px;font-size:12.5px;}
.askpager-at{margin:0;}

@media (max-width:900px){
  .askrow{flex-wrap:wrap;}
}
.askcompare{margin-top:14px;padding-top:12px;border-top:1px solid var(--hairline-soft);}
.askcompare-label{
  display:block;font-size:11px;letter-spacing:.04em;text-transform:uppercase;
  color:var(--muted);margin-bottom:6px;
}
.askcompare-input{flex:1 1 260px;min-width:0;height:34px;font-size:13px;}
.asked-note{margin-top:10px;}
/* The version, in the catalogue card's heading. Quieter than the name because
   the name is what a researcher looks for and the version is what tells two
   cards apart once they have found it. */
.cardversion{
  font-family:var(--mono);font-size:13px;font-weight:400;color:var(--muted);
  margin-left:6px;
}
.cardolder{margin:12px 0 0;max-width:36em;}
.askpanel{
  border:1px solid var(--hairline);border-left:3px solid var(--brand-teal);
  border-radius:10px;padding:14px 16px;margin:0 0 16px;background:var(--surface);
}
.askpanel-turn{padding:8px 0;border-bottom:1px solid var(--hairline-soft);}
.askpanel-turn:last-child{border-bottom:0;padding-bottom:0;}
.askpanel-q{margin:0 0 6px;font-size:14px;color:var(--text);}
.askambig{
  margin:14px 0 0;padding:14px 16px;border-radius:10px;
  background:var(--surface-3);border:1px solid var(--hairline-soft);
}
.askambig-set{border:0;padding:0;margin:0 0 12px;}
.askambig-set legend{padding:0;margin-bottom:8px;font-size:13.5px;color:var(--text);max-width:36em;}
.askambig-choice{
  display:flex;align-items:center;gap:8px;padding:5px 0;
  font-family:var(--mono);font-size:12.5px;color:var(--text);
}
.askcontrols{align-items:center;gap:12px;flex-wrap:wrap;}
.askcontrols form{margin:0;}
.askblocked{max-width:36em;}

/* --- how a fragment control looks while it works -------------------------- */
/* House tokens throughout. The spinner is a rotating arc rather than an image
   or a library, and it is decorative: the disabled state and the live region
   carry the meaning for anybody not looking at it. */
.cspin{
  display:inline-block;width:11px;height:11px;margin-right:7px;
  vertical-align:-1px;
  border:2px solid currentColor;border-right-color:transparent;border-radius:50%;
  animation:cspin .6s linear infinite;
}
@keyframes cspin{to{transform:rotate(360deg);}}
.btn.is-working{opacity:.75;cursor:progress;}
.btn:disabled{cursor:not-allowed;}
@media (prefers-reduced-motion:reduce){
  /* A spinner that does not spin still says the control is working, because it
     appears only while it is. Motion is the part that is optional. */
  .cspin{animation:none;}
}

/* The line at the foot of the screen. Fixed, so it does not move the page and
   does not push anything about: it reports and gets out of the way. Never
   modal, never covering a control, and it disappears on its own. */
.panelmessage{
  position:fixed;left:50%;bottom:18px;transform:translateX(-50%);
  z-index:60;max-width:min(46em,calc(100vw - 32px));
  padding:11px 16px;border-radius:9px;
  background:var(--surface);border:1px solid var(--hairline);
  border-left:3px solid var(--brand-teal);
  box-shadow:0 2px 10px rgba(16,34,42,0.10);
  font-size:13.5px;line-height:1.5;color:var(--text);
}
/* Three kinds, and which one is used is the server's decision rather than the
   browser's: success, a refusal the researcher can act on, and a fault in the
   platform. Contrast computed rather than judged: 14.04:1, 15.71:1 and 9.69:1,
   all past AAA for body text. */
.panelmessage-good{
  background:#C8E6C9;color:#111111;border-color:#7CB57F;border-left-color:#2E7D32;
}
.panelmessage-warn{
  background:#FFE9A8;color:#111111;border-color:#D9B860;border-left-color:#B7791F;
}
.panelmessage-bad{
  background:#8A1017;color:#FFFFFF;font-weight:600;
  border-color:#6B0C12;border-left-color:#FFFFFF;
}
/* A reference is a thing to copy, so it is set in the mono face wherever it
   appears in one of these. */
.panelmessage code{font-family:var(--mono);font-size:12.5px;}
@media (max-width:640px){
  .panelmessage{left:16px;right:16px;transform:none;max-width:none;}
}

/* ---- Live feeds and dashboards -------------------------------------------
   Appended. No rule above this line is edited: the dashboard is a new app and
   the brief is that nothing demonstrated may change. */

/* The dashboard takes the whole viewport rather than the reading measure the
   rest of the platform is set to. It is built to be projected, and a map and
   five supplier panels inside a centred column is a screen nobody at the back
   can read.

   Reached with `:has()` rather than by editing base.html, because nothing
   outside the dashboard may change and the shell is shared with every other
   screen. A browser without `:has()` renders the dashboard exactly as every
   other screen renders, centred and boxed, which is a smaller screen rather
   than a broken one. */
body:has(.app-fullbleed) .stage{max-width:none;margin:0;padding:0;}
body:has(.app-fullbleed) footer{display:none;}
.app-fullbleed{
  border:0;border-radius:0;box-shadow:none;
  /* Fixed to the viewport rather than merely at least as tall as it. The
     dashboard's own panels scroll inside themselves; the page does not, because
     a screen built for a projector that has to be scrolled is a screen the back
     of the room cannot read. */
  height:100vh;max-height:100vh;overflow:hidden;
  display:flex;flex-direction:column;
  /* A short ease in, rather than the page appearing at once. The shell's own
     entrance animation only runs at sign in; this one runs whenever the
     dashboard is opened, which is the moment it is put on a projector. */
  animation:dashin .22s cubic-bezier(.33,.68,.28,1) both;
}
.app-fullbleed.leaving{animation:dashout .16s ease-in both;}
@keyframes dashin{from{opacity:0;transform:translateY(6px);}to{opacity:1;transform:none;}}
@keyframes dashout{from{opacity:1;}to{opacity:.4;}}
@media (prefers-reduced-motion: reduce){
  .app-fullbleed,.app-fullbleed.leaving{animation:none;}
}
.app-fullbleed .body{flex:1;min-height:0;overflow:hidden;}
.app-fullbleed .rail{overflow-y:auto;}

/* The dashboard's own layout.

   A grid rather than a stack, because the brief is that on a large display the
   counters, the map, the suppliers, the two charts and the dataset panel are
   visible together without scrolling. Rows that size to their content, and one
   row that takes what is left and gives it to the map.

   `min-height:0` on the grid children is what lets that last row actually
   shrink: a grid item defaults to its content's minimum, so without it the map
   pushes the page taller instead of the panel scrolling inside itself. */
/* Rows that size to their content, and the content area scrolls if it must.
   
   It was a `1fr` row with the whole thing clipped, which worked until the
   panels were given minimum heights: on a 14 inch laptop the map's minimum was
   taller than the row it was in, so it spilled over the regional table and drew
   on top of it. A minimum that a layout can overrule is not a minimum.
   
   So the minimums win and this scrolls instead. The topbar, the perimeter
   banner and the rail stay put either way, because they are outside this box: a
   dashboard whose header scrolls away has stopped being one, and that was the
   thing worth protecting rather than the absence of a scrollbar. */
.app-wide .main.main-wide{
  max-width:none;padding:10px 20px 8px;
  display:grid;grid-template-rows:auto auto auto auto auto;
  gap:8px;min-height:0;overflow-y:auto;
}
.app-wide .main.main-wide > *{min-width:0;}
.fsynthline{
  margin:0;font-size:12.5px;line-height:1.45;color:var(--muted);
  background:#FFF6E0;border-left:3px solid #E0B94A;border-radius:0 6px 6px 0;
  padding:7px 12px;
}
.fsynthline strong{color:var(--text);}

.fhead{display:flex;justify-content:space-between;align-items:center;gap:16px;flex-wrap:wrap;margin:0;}
.flive{display:inline-flex;align-items:center;gap:7px;font-size:12px;letter-spacing:.08em;text-transform:uppercase;color:var(--brand-teal);font-weight:600;}
.flive-idle{color:var(--muted);}
.flive-behind{color:var(--amber-deep,#8a6d1f);}
/* How current the research copy is. Plain text while it is, and a notice in
   the chart-note amber when it is not, because a copy that stopped is a thing
   to read before any figure under it. */
.ffresh{margin:0 0 8px;font-size:12.5px;color:var(--muted);}
.fstale{margin:0 0 10px;font-size:13px;line-height:1.45;color:var(--amber-deep,#8a6d1f);
  background:var(--amber-wash,#fbf3dd);border:1px solid var(--amber-line,#ecdca6);
  border-radius:7px;padding:8px 12px;}
.fdot{width:9px;height:9px;border-radius:50%;background:currentColor;}
.flive:not(.flive-idle) .fdot{animation:fpulse 2s ease-in-out infinite;}
@keyframes fpulse{0%,100%{opacity:1;}50%{opacity:.35;}}
@media (prefers-reduced-motion: reduce){.flive .fdot{animation:none;}}
.fsynth{display:inline-block;font-size:11px;letter-spacing:.08em;text-transform:uppercase;
        background:#FFE9A8;color:#111111;border-radius:4px;padding:2px 7px;font-weight:700;}
.fsynthnote{margin:10px 0 18px;}
.fscope{margin:0 0 12px;}

.fcounters{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:10px;margin:0;}
.fcounter{background:var(--surface);border:1px solid var(--hairline);border-radius:10px;padding:10px 14px;}
.fcounter p{margin:0 0 2px;font-size:10.5px;letter-spacing:.09em;text-transform:uppercase;color:var(--label);}
.fcounter .v{display:block;font-size:clamp(26px,2.2vw,40px);line-height:1.05;font-weight:700;color:var(--text);}
.fcounter .u{font-size:11.5px;color:var(--muted);}
.fasat{margin:0;font-size:11.5px;}
.fallowance{margin:0;font-size:11.5px;}

/* Everything that moves, as its own grid so the row it sits in can shrink. */
/* The query bar, the counters, the as-at line, the panels, then the regional
   table. Five children, five rows.
   
   It had four rows and `min-height:0`, which is what let the table paint
   outside the panel that holds it. `min-height:0` tells a grid item it may
   shrink below its own content, and this box is inside a grid whose height is
   the viewport, so it did: the container came out 523px tall while its rows
   summed to 1124, and everything past the fold drew over what followed. The
   fifth row was missing on top of that, so the table had no row of its own.
   
   Measured before the fix, at 1512: 13 of 20 table rows had bounds outside the
   panel's. After: none. */
#feedlive{display:grid;grid-template-rows:auto auto auto auto auto;gap:8px;}
/* The regional table sits below the three panels, never over them.
   
   At 1512 the table drew on top of the map, the suppliers and the chart. The
   cause was the panels being given minimum heights inside a grid row that was
   shorter than they were, so they spilled out of their row into the next one.
   Grid does not clip an overflowing item, it lets it draw over what follows.
   
   `align-items:start` and a row that sizes to its tallest child is the fix:
   the panel row is as tall as the panels need, and the table starts after it.
   At 1920 the layout is unchanged, which is the requirement. */
/* The panel row has a floor and a ceiling, and each panel scrolls inside it.
   
   Not tidiness: a control that made the row shorter made the whole page
   shorter, and a page shorter than the current scroll position is clamped by
   the browser. Restoring a scroll offset cannot beat that, because there is
   nowhere to restore it to. Measured, before this: changing the window moved
   the reader 65px and picking a supplier moved them 115px, with nothing in the
   handler touching the scroll.
   
   So the height stops changing. Same reasoning as pinning the pages of the
   reader's history: remove the height change and the clamp goes with it
   (constraint 22). */
/* The three panel row.
   
   **One height for the row, and every panel in it clips its own content.**
   
   This row has now produced the same fault three times, each time in a
   different mechanism and each time looking like a new bug: a grid with four
   rows for five children, a flex item shrunk below its content by
   `min-height:0`, and a panel given `overflow-y:visible` so the map would not
   scroll. Every one of them is the same shape underneath, which is a container
   whose height is fixed while its content is taller and which does not clip.
   The content then draws over whatever is below it, and because two panels are
   both painted the rows and tiles read through each other.
   
   So the invariant is stated here rather than restored again: the row has one
   height, and each of the three panels is exactly that height and scrolls what
   does not fit. A panel cannot spill because there is nowhere for the overflow
   to go. That is asserted at four widths in the tests rather than left to be
   noticed (constraint 22).
   
   The height is a viewport measure rather than a fixed one, so the row is
   proportionate on a laptop and on a 24 inch display without a breakpoint
   choosing between them. */
.fgrid{--fgrid-row:clamp(520px,58vh,660px);align-items:stretch;min-height:430px;}
.fcol{align-self:stretch;min-height:0;}
.fmap,.fcol-suppliers,.fcol-charts{
  height:var(--fgrid-row);max-height:var(--fgrid-row);overflow-y:auto;min-height:0;
}
/* The right column holds two panels and they split the row between them, so
   the three columns read as three columns rather than as three panels of
   different lengths. Each scrolls its own content. */
.fcol-charts{display:flex;flex-direction:column;gap:10px;}
.fcol-charts > .fpanel{flex:1 1 0;min-height:0;overflow-y:auto;}

/* Two columns by default, three once there is room for one.
   
   On a 24 inch display the map was tall and half empty while the side column
   scrolled, which is the width being wasted and the height being fought over at
   the same time. Splitting the suppliers from the charts at 1600px uses the
   width the screen actually has and takes the pressure off the height, so
   everything is visible together rather than one panel scrolling inside
   itself. */
.fgrid{
  display:grid;grid-template-columns:minmax(0,1.9fr) minmax(300px,1fr);
  gap:12px;align-items:stretch;
}
@media (min-width: 1400px){
  .fgrid{grid-template-columns:minmax(0,1.5fr) minmax(260px,.85fr) minmax(260px,.95fr);}
}
/* Two columns, with the suppliers above the charts on the right and the map
   spanning both rows. Placed explicitly rather than left to auto flow, which
   wrapped the charts underneath the map and left the right column half empty. */
@media (min-width: 1181px) and (max-width: 1399px){
  .fgrid{grid-template-rows:auto minmax(0,1fr);}
  .fcol-map{grid-column:1;grid-row:1 / span 2;}
  .fcol-suppliers{grid-column:2;grid-row:1;}
  .fcol-charts{grid-column:2;grid-row:2;}
}
.fcol{min-width:0;min-height:0;display:flex;flex-direction:column;gap:10px;}
/* A suppliers panel is as tall as its contents, never as tall as the space it
   was given. The charts column is the other way round, and says so above: its
   two panels share the row rather than taking their own heights.
   
   `.fcol` sets `min-height:0`, which lets a flex item shrink below its own
   content. The suppliers panel was 520px tall holding 586px of list, and
   because it does not clip, the overflow spilled into the scroller around it.
   So the scroll range came from a squashed child rather than from the list,
   and the scroller had no relationship to what was in it. Measured: box 520,
   content 586.
   
   `flex:0 0 auto` makes the panel take its natural height, so the scroller's
   scroll height is the content's height and scrolling stops at the last
   supplier. */
.fcol-suppliers > .fpanel{flex:0 0 auto;}
/* And a wheel that reaches the end of one of these stays there rather than
   carrying on into the page behind it, which is what makes a short scroll feel
   like it never arrives anywhere. */
.fmap,.fcol-suppliers,.fcol-charts,.ftablewrap{overscroll-behavior-y:contain;}
.fcol-suppliers,.fcol-charts{position:relative;}
.fpanel{
  background:var(--surface);border:1px solid var(--hairline);border-radius:10px;
  padding:11px 14px;margin:0;
}
.ffreeze{margin:0;}

.fmap{
  background:var(--surface);border:1px solid var(--hairline);border-radius:10px;
  padding:12px 14px;flex:1;position:relative;
}
.fmap-head{display:flex;justify-content:space-between;align-items:baseline;gap:12px;margin-bottom:8px;}
/* Events by region, the graphic view.
   
   Each nation is its own bordered container, so the nations read as separate
   blocks rather than one continuous grid. The containers are equal width cells
   of one grid and the regions inside are a second grid of fixed height cells,
   so a nation with five regions and one with two are the same width and line
   up on the same rows. */
/* One height per row, so the containers line up across the grid.
   
   `align-items:start` let each container shrink to its own contents, so a
   nation with five regions stood taller than its neighbours and nothing lined
   up. Stretching them makes every container in a row the height of the tallest
   in that row, which is what "consistent sizing" has to mean when nations hold
   different numbers of regions. */
.fmap-nations{
  display:grid;grid-template-columns:repeat(auto-fill,minmax(268px,1fr));
  gap:12px;align-items:stretch;
}
.fmap-nation{
  min-width:0;border:1px solid var(--hairline);border-radius:9px;
  padding:10px 11px 11px;background:var(--surface);
}
.fmap-nation-name{margin:0 0 8px;font-size:12px;letter-spacing:.08em;text-transform:uppercase;color:var(--label);}
.fmap-regions{
  display:grid;grid-template-columns:repeat(auto-fill,minmax(76px,1fr));gap:6px;
}
/* One height for every cell whatever state it is in, so a withheld region does
   not make its row a different size from the counts beside it. */
.fmap-cell{
  display:flex;flex-direction:column;align-items:center;justify-content:center;
  gap:2px;min-height:58px;padding:6px 4px;border-radius:6px;
  border:1px solid var(--hairline);background:var(--teal-wash);text-align:center;
}
/* Both drawn in the same near black rather than the count in the text colour
   and the label in the muted one.

   The hierarchy is size and weight, not colour, because colour is the budget
   that is already spent here: the label sat in --muted, which is 1.7:1 against
   the darker end of the tile range and unreadable. A ratio measured across the
   whole fill range is asserted in the tests rather than judged by eye. */
.fmap-count{font-size:19px;font-weight:700;color:#111111;line-height:1.05;}
.fmap-label{font-size:10px;font-weight:400;color:#111111;letter-spacing:.04em;}
/* Genuinely no reports. A real figure, so it is drawn as one: plain ground and
   a lighter number, never a gap and never the withheld treatment. */
.fmap-cell-zero{background:var(--surface-2);}
/* Darker than --muted, which is 4.1:1 on this ground. The zero is the signal
   that separates "reported nothing" from "not shown", so it is held to 7:1
   like body text rather than to the 3:1 large bold text could get away with. */
.fmap-cell-zero .fmap-count{color:#455860;font-weight:600;}
/* Withheld. The house treatment, the same dashed control-coloured chip the
   cohort table and every chart already use for a withheld cell, so a reader
   who has seen one anywhere else recognises this one. No number in it. */
.fmap-cell-withheld{
  background:var(--control-wash);border-style:dashed;border-color:var(--control);
}
.fmap-cell-withheld .withheld{
  /* The shared chip's ink is --control, which is 3.38:1 on this wash and fails
     (ISSUES.md BIO-51). Darkened for this use rather than left to inherit a
     ratio that does not pass. Measured 6.94:1. */
  font-size:9px;letter-spacing:.1em;padding:1px 6px;border:0;background:none;
  color:#6B4C11;
}
.fmap-cell-withheld .fmap-label{color:#5F450D;}
.fmap-empty,.fmap-withheld{margin:10px 0 0;}

/* The map component. One implementation on two surfaces.
   
   Every colour is a custom property with the light chrome as its default. The
   dark surface redefines the same six and nothing else, which is the whole of
   the theming: no second set of rules, and nothing that can be changed on one
   surface and forgotten on the other. */
.geomap{
  /* The research panel is one of three in a row with a shared height, so the
     map has to leave room for the breadcrumb above it and the withheld note
     below. At 560 the panel overflowed by 62px and the legend was squeezed to
     175px, which wrapped its prose into a ribbon three words wide. */
  --gm-max-height:380px;
  --gm-ink:var(--brand-teal);
  --gm-hover:var(--brand-teal);
  --gm-context:#9DB2BC;
  --gm-context-fill:rgba(255,255,255,.001);
  --gm-edge:#FFFFFF;
  /* Every coastline and land border, one colour on this surface. */
  --gm-line:#8FA3AD;
  /* An area that reported nothing, and one whose count is not shown. Neither
     is invisible and neither is greyed out of the design: they are two facts,
     drawn as two things, in the same palette as everything else here. */
  --gm-control-bg:rgba(255,255,255,.92);
  --gm-control-ink:var(--text);
  --gm-control-line:var(--hairline);
  --gm-zero:#C3D0D7;
  --gm-withheld:#AD8628;
  --gm-withheld-ground:#FAF3E4;
  --gm-base-opacity:.85;
  --gm-base-filter:none;
  position:relative;
}
.geomap-dark{
  /* The operating picture gives the map a panel of its own, so it fills it
     edge to edge rather than sitting in the middle of it. Capping the width at
     what the tallest allowed box would be left 395px of empty panel either
     side of an 801px map in a 1196px column. The frame is fitted to whatever
     shape the panel is, so filling the width costs no letterboxing. */
  --gm-max-height:560px;
  --gm-ink:var(--op-accent);
  --gm-hover:var(--op-accent);
  --gm-context:#48626C;
  --gm-context-fill:rgba(11,20,24,.001);
  --gm-edge:#0B1418;
  /* Every coastline and land border, one colour on this surface. */
  --gm-line:#6A8791;
  --gm-control-bg:rgba(17,30,36,.92);
  --gm-control-ink:var(--op-text);
  --gm-control-line:var(--op-line);
  --gm-zero:#33454C;
  --gm-withheld:#D2A64A;
  --gm-withheld-ground:#3A3020;
  /* The relief, on a dark panel.
     
     Inverting it and then dropping it to 0.4 opacity took it to nothing: the
     map read as flat shapes on flat dark and the terrain the reference gets its
     credibility from was there in the markup and invisible on the screen.
     Darkened and cooled instead of inverted, and left at full opacity so it is
     actually seen. One image still, two surfaces. */
  --gm-base-opacity:1;
  /* Inverted, then lifted rather than dimmed. Inverting the light source image
     and multiplying brightness by 0.42 put the whole relief between 0 and 0.16
     luminance, which is black on a panel that is already nearly black: the
     terrain was in the markup and absent from the screen. Land now sits around
     0.12 to 0.35 and the sea below the panel, so the shapes still read as the
     brightest thing on it. */
  --gm-base-filter:invert(1) hue-rotate(185deg) saturate(.5) brightness(.72) contrast(.95);
  /* Night operations rather than a pale terrain map.
     
     The reference uses satellite tiles, which are ruled out: every tile request
     tells somebody else's server which area is being looked at. What is left is
     to darken the relief until it reads as ground at night and put real vector
     detail on top of it, which is what the layers below are. Brightness came
     down from 0.95 to 0.72 and contrast up, so the relief is present as texture
     and the towns, roads and markers are the bright things on it. */

  /* The bundled detail. Every one of these is drawn under the shapes and the
     markers, so nothing crosses a cluster. */
  --gm-urban:#2E4756;
  --gm-water:#1D4A5E;
  --gm-rail:#4A5A63;
  --gm-road:#5A4B3A;
  --gm-motorway:#8A6C42;
  --gm-label:#C6D6DE;
  --gm-label-ink:#0B1418;
}
/* Panning. The plot scrolls, so a pointer drag and a trackpad both move the
   map, and the scrollbars say there is more to see. */
/* The map fills its panel and the frame moves inside it. It used to be a
   drawing wider and taller than the panel inside a scrolling box, so it opened
   showing about a quarter of itself. */
/* The dark surface's map takes the whole panel. The research view's shares a
   column with a legend, so it keeps the projection's proportions and is
   centred in what is left. */
.geomap-dark .geomap-plot{
  width:100%;max-width:none;aspect-ratio:auto;
  /* Tall enough that the frame crops rather than throws away half of Europe.
     A shallower panel means a harder crop, and at 2.05 to 1 the frame kept only
     fourteen degrees of latitude and Ukraine fell out of the bottom of it. */
  /* Where there is no band (the stacked layout below 1180 wide), a height of
     its own. With a band, the plot takes whatever height the band leaves it
     (the band rules below), and the map fits its frame to that. */
  height:clamp(340px, calc(var(--op-band, 760px) - 82px), 700px);
}
.geomap-plot{
  position:relative;overflow:hidden;cursor:grab;touch-action:none;
  /* A definite height, and the projection's own proportions.
     
     `height:100%` resolved against a parent with no height of its own, so the
     panel grew to the whole drawing and ran off the screen with only Norway in
     view. Given a height but the column's full width it was a 2.5:1 strip,
     which the projection cannot fill: the window is 1.43:1, so a third of the
     panel was empty either side whatever frame was chosen.
     
     So the box takes the projection's shape and is centred in whatever column
     it is given. The map fills it, and the panel is around the map rather than
     the map being a band across the panel. */
  /* One rule for two very different columns.
     
     The operating picture gives the map about 1200px and the research view
     about 600, and a fixed height with a free width letterboxed the narrow one
     exactly as a fixed width letterboxed the wide one. Capping the width at
     what the tallest allowed box would be lets the aspect hold in both: the
     wide column stops at 801 by 560, the narrow one takes 603 by 421, and
     neither has empty bands down its sides. */
  --gm-max-height:560px;
  aspect-ratio:1000/699;
  width:min(100%, calc(var(--gm-max-height) * 1000 / 699));
  height:auto;margin-inline:auto;
}
@media (max-width:900px){
  /* Too narrow to hold the shape. Width first, and let the height follow. */
  .geomap-plot{width:100%;}
}
.geomap-dragging{cursor:grabbing;}
/* Zoom, top left, over the map, on both surfaces. */
.geomap-zoom{
  position:absolute;top:10px;left:10px;z-index:3;
  display:flex;flex-direction:column;gap:4px;
}
/* The layers cog shares the zoom buttons' rule rather than copying it, so the
   two corners read as a pair and cannot drift apart. */
.geomap-zoom button,.geomap-layers-toggle{
  width:40px;height:40px;display:flex;align-items:center;justify-content:center;
  font-size:19px;line-height:1;cursor:pointer;
  background:var(--gm-control-bg);color:var(--gm-control-ink);
  border:1px solid var(--gm-control-line);border-radius:7px;
  font-family:inherit;
}
.geomap-zoom button:last-child{font-size:13px;}
.geomap-zoom button:hover,.geomap-layers-toggle:hover,
.geomap-layers-toggle[aria-expanded="true"]{border-color:var(--gm-hover);color:var(--gm-hover);}
.geomap-zoom button:focus-visible,
.geomap-layers-toggle:focus-visible{outline:2px solid var(--gm-hover);outline-offset:2px;}
/* Map layers, top right, over the map. The column runs the plot's height and
   lets presses through, so only the cog and the open panel take them; the
   panel is positioned inside it and scrolls within the plot, so opening it at
   the smallest band changes nothing about the map, the band or the cards. */
.geomap-layers{
  position:absolute;top:10px;right:10px;bottom:10px;z-index:4;
  width:40px;pointer-events:none;
}
.geomap-layers > *{pointer-events:auto;}
.geomap-layers-panel{
  position:absolute;top:46px;right:0;width:224px;max-height:calc(100% - 46px);
  overflow:auto;box-sizing:border-box;padding:8px 10px 6px;
  background:var(--gm-control-bg);color:var(--gm-control-ink);
  border:1px solid var(--gm-control-line);border-radius:8px;
  box-shadow:0 10px 26px rgba(0,0,0,.34);
}
.geomap-layers-panel[hidden]{display:none;}
/* Placed against the window by static/js/map-layers.js when it opens, so it
   can be taller than the map. Where even the window is too short to hold every
   row, the foot of it says so until the reader has scrolled to the end. */
.geomap-layers-panel.has-more:not(.at-end)::after{
  content:"More below";position:sticky;bottom:-6px;display:block;
  margin:2px -10px -6px;padding:12px 10px 5px;text-align:center;
  font-size:10px;font-weight:600;letter-spacing:.12em;text-transform:uppercase;
  background:linear-gradient(rgba(0,0,0,0),var(--gm-control-bg) 55%);
}
.geomap-layers-head{
  margin:0 0 4px;font-size:10.5px;font-weight:600;letter-spacing:.12em;
  text-transform:uppercase;opacity:.72;
}
.gmlayer{display:flex;align-items:center;gap:8px;padding:3px 0;font-size:12px;cursor:pointer;}
.gmlayer input{margin:0;flex:none;}
.gmlayer-swatch{flex:none;width:18px;height:18px;display:inline-flex;}
.gmlayer-swatch svg{width:18px;height:18px;overflow:visible;}
.gmlayer-glyph{
  font-size:10.5px;font-weight:600;text-anchor:middle;fill:var(--gm-label);
  paint-order:stroke fill;stroke:var(--gm-label-ink);stroke-width:2.4;stroke-linejoin:round;
}
.geomap-plot.geomap-dragging{cursor:grabbing;}
.geomap-svg{display:block;width:100%;height:100%;}
.geomap-base{opacity:var(--gm-base-opacity);filter:var(--gm-base-filter);}
/* A country with no feed. A fill a pointer can find and nothing else: no
   shading, no link and no stroke; its outline is the boundary layer's. It takes
   a pointer so hovering names it, which is the only way the legend's "a bare
   outline has no feed here" is any use to a reader who cannot tell which
   country they are looking at. `fill` is set rather than left transparent so
   the whole shape is hoverable and not just its edge. */
.geomap-context{
  fill:var(--gm-context-fill);stroke:none;
}
.geomap-context.fhovered{fill:var(--gm-hover);fill-opacity:.14;}
/* Fills carry no stroke, on either surface, in any state. Every line is drawn
   once, by the boundary layer, from the same arcs the fills are made of; a
   stroke on a fill is a second copy of a border its neighbour also draws, which
   is the doubling this replaced. Hovering lightens a fill rather than
   outlining it, for the same reason. */
.geomap-shape path,.geomap-zero path,.geomap-withheld path{stroke:none;}
.geomap-shape path.fhovered{filter:brightness(1.3);}
.geomap-pick{cursor:pointer;}
/* The boundary layer. One colour and one weight for every land border and
   every coastline, round at every join and end so an arc meets the next
   without a notch, and one pixel at every zoom. The region lines are the
   compass cuts inside a nation, drawn in the panel's own colour so they read
   as a parting in the shading rather than as a border. */
.gm-lines{pointer-events:none;}
.gm-line{
  fill:none;stroke-linejoin:round;stroke-linecap:round;
  vector-effect:non-scaling-stroke;shape-rendering:geometricPrecision;
}
.gm-line-coast,.gm-line-border{stroke:var(--gm-line);stroke-width:1;}
.gm-line-region{stroke:var(--gm-edge);stroke-width:1;}
.geomap-empty{font-size:12px;color:var(--muted);margin:10px 0 0;}
.geomap-dark .geomap-empty{color:var(--op-muted);}

@media (max-width:640px){
  .fmap-nations{grid-template-columns:1fr;}
  .fmap-regions{grid-template-columns:repeat(auto-fill,minmax(72px,1fr));}
}

.fsuppliers{list-style:none;margin:0;padding:0;}
.fsupplier{padding:6px 0;border-bottom:1px solid var(--hairline-soft);}
.fsupplier:last-child{border-bottom:0;}
.fstate{font-size:11px;letter-spacing:.06em;text-transform:uppercase;color:var(--brand-teal);font-weight:600;}
.fsupplier-off .fstate{color:var(--muted);}
.fsupplier-quiet .fstate{color:#8A6A00;}
/* A rate far above the rest of the feed. Measured against the panel in the
   tests like every other state colour. It reads as elevated without reading as
   an alarm, because what it describes is how much data is arriving. */
.fsupplier-high .fstate{color:#7F4400;}
/* The synthetic marker, small enough to sit inside a panel heading and dark
   enough to survive a screenshot. It is not decoration: a panel that can be
   cropped out of the page has to carry the sentence with it. */
.finvented{
  display:inline-block;margin-left:6px;padding:1px 6px;border-radius:3px;
  background:#F3E7C8;color:#604400;font-size:9px;font-weight:700;
  letter-spacing:.08em;text-transform:uppercase;vertical-align:middle;
}

.fbars{display:flex;align-items:flex-end;gap:3px;height:74px;}
.fbar{flex:1;min-height:2px;background:var(--brand-teal);border-radius:2px 2px 0 0;opacity:.85;}
.fbands{list-style:none;margin:0;padding:0;}
.fbands li{margin-bottom:7px;}
.fband{height:7px;background:var(--hairline-soft);border-radius:4px;overflow:hidden;}
.fband span{display:block;height:100%;background:var(--brand-teal);}

.fqueries{margin:0;}
.fquerybar{display:flex;align-items:center;gap:12px;flex-wrap:wrap;}
.fquerylist{display:flex;flex-wrap:wrap;gap:7px;align-items:center;}
.fpresets{display:flex;flex-wrap:wrap;gap:7px;}
.fpresets form{display:inline;}
.fq-add{cursor:pointer;font-family:inherit;border-style:dashed;}
.fq-add:hover{border-color:var(--brand-teal);border-style:solid;}
.fscope{font-size:12px;color:var(--muted);}
.fq-wrap{display:inline-flex;align-items:center;}
.fq{display:inline-block;padding:5px 11px;border-radius:999px;border:1px solid var(--hairline);
    font-size:13px;text-decoration:none;color:var(--text);background:var(--surface);}
.fq-on{border-color:var(--brand-teal);box-shadow:inset 0 0 0 1px var(--brand-teal);font-weight:600;}
/* The query palette. Fixed, and chosen to stay apart from each other on a
   projector: a researcher typing a hex value could pick one that vanishes on
   the screen this is built for. Text stays var(--text) on a light wash rather
   than being drawn in the colour, so contrast does not depend on the choice. */
.fq-teal{background:#D7ECEA;}
.fq-amber{background:#FFE9A8;}
.fq-violet{background:#E4DCF4;}
.fq-rose{background:#F7DCE2;}
.fq-slate{background:#DEE4EA;}
.fq-moss{background:#DFEBD5;}
.fq-drop{display:inline;}
.fq-x{background:none;border:0;cursor:pointer;color:var(--muted);font-size:16px;line-height:1;padding:0 4px;}
.fq-x:hover{color:var(--text);}
.fqadd{margin-top:12px;}
.fqadd > summary{cursor:pointer;font-size:12.5px;color:var(--brand-teal);}
/* The Add a query row.
   
   Five controls on one baseline. They were aligned on flex-end, which lines up
   the bottom edge of whatever each column happens to contain: the value column
   carries a hint paragraph under its control and the others do not, so its
   control sat a line higher than the rest. Aligning the tops of equal height
   rows and letting the hint hang below fixes it at the cause rather than by
   nudging one column. */
.fqform{display:flex;flex-wrap:wrap;gap:12px;align-items:flex-start;margin-top:12px;}
.fqform .field{margin:0;display:flex;flex-direction:column;min-width:0;}
.fqform .field > label{
  display:block;margin-bottom:5px;line-height:1.2;min-height:15px;
}
/* One height for every control in the row, so the five line up whatever kind
   of input each one is. */
.fqform .sel,.fqform .input{height:38px;box-sizing:border-box;}
.fqform select[multiple].sel{height:auto;min-height:38px;}
.fqvalues{position:relative;}
/* The hint hangs below the row rather than growing the column, which is what
   pushed the value control out of line with the other four. */
.fqform .fqhint{
  position:absolute;top:100%;left:0;margin:4px 0 0;font-size:11px;
  white-space:nowrap;
}
/* The button sits on the controls' baseline, not on the labels'. */
.fqform > .btn{margin-top:20px;height:38px;}
@media (max-width:760px){
  .fqform{flex-direction:column;align-items:stretch;}
  .fqform .field{width:100%;}
  .fqform .fqhint{position:static;white-space:normal;}
  .fqform > .btn{margin-top:4px;width:100%;}
}

/* The Add catalogue dialogue. */
.addcat-panel{padding-top:4px;}
.catlive{display:inline-flex;align-items:center;gap:6px;font-size:11px;letter-spacing:.07em;
         text-transform:uppercase;color:var(--brand-teal);font-weight:700;}

/* A 14 inch laptop is the case that must still work: 1512 wide, and after the
   browser chrome about 860 of usable height. Everything is reachable, with the
   map and the right column scrolling inside themselves where they have to.
   Tighter than the 24 inch case on purpose: the alternative is a page that
   scrolls as a whole, and a dashboard whose header scrolls away is a dashboard
   that has stopped being one. */
/* At 1920 the map is the thing worth looking at, so it gets height. The other
   two panels in the row keep their own ceiling and simply have room to spare. */
/* The map's frame, sized from the row it sits in rather than from a number.
   
   The map view has to fit inside the panel without the panel scrolling, and
   the panel is now one row tall whatever the width, so the frame is the row
   less the breadcrumb, the heading and the note beneath it. Written as one
   subtraction, so changing the row height moves the map with it. */
.geomap-light{--gm-max-height:calc(var(--fgrid-row,520px) - 190px);}

@media (max-width: 1599px){
  .fcounter .v{font-size:clamp(24px,1.9vw,32px);}
  .fcounter{padding:8px 12px;}
  /* 236 rather than 190. The nations are bordered containers now with a grid
     of region cells inside, and at 190 a container was too narrow to hold two
     cells side by side, so a five region nation drew a column of five. */
  .fmap-nations{grid-template-columns:repeat(auto-fill,minmax(236px,1fr));gap:10px;}
  .fbars{height:62px;}
  .fsynthline{font-size:12px;padding:6px 11px;}
}
/* Below this the two columns stack and the page scrolls, which is the right
   trade at that width. */
@media (max-width: 1180px){
  .fgrid{min-height:0;}
  /* Stacked, so each panel takes its natural height and nothing is
     constrained. There is no row to share and nothing to clip against. */
  .fmap,.fcol-suppliers,.fcol-charts{
    height:auto;max-height:none;overflow:visible;
  }
  .fcol-charts > .fpanel{flex:0 0 auto;overflow-y:visible;}
  .app-fullbleed{height:auto;max-height:none;overflow:visible;}
  .app-fullbleed .body{overflow:visible;}
  .app-wide .main.main-wide{overflow:visible;grid-template-rows:none;}
  .fgrid{grid-template-columns:minmax(0,1fr);}
  .fcounters{grid-template-columns:repeat(2,minmax(0,1fr));}
}
@media (max-width: 640px){
  .fcounters{grid-template-columns:minmax(0,1fr);}
  .fmap-nations{grid-template-columns:minmax(0,1fr);}
  .fqform{flex-direction:column;align-items:stretch;}
  .fquerybar{align-items:flex-start;flex-direction:column;gap:8px;}
}
/* The Add catalogue modal is wider than the run modal: it carries a column
   table and two panels of prose, and the run modal's measure would wrap the
   header table into something unreadable. */
.modal-wide{max-width:820px;width:calc(100vw - 48px);max-height:86vh;overflow-y:auto;}
/* A dialogue whose head stays put and whose body scrolls. The whole thing used
   to scroll as one, which carried Close off the top of a long tab. */
.modal-split{display:flex;flex-direction:column;overflow:hidden;padding:0;}
.modal-split .modal-head{
  flex:none;padding:20px 22px 14px;border-bottom:1px solid var(--hairline);
  background:var(--surface);
}
.modal-split .modal-body{
  flex:1 1 auto;min-height:0;overflow-y:auto;overscroll-behavior-y:contain;
  padding:18px 22px 22px;
}
.cardlive{margin-top:10px;}
.ffoot{display:flex;gap:18px;align-items:flex-start;}
.ffoot .hint{margin:0;font-size:11.5px;line-height:1.5;}
.ffoot .hint:first-child{flex:2;}
.ffoot .hint:last-child{flex:1;}
.fbuild{display:inline-block;margin-left:8px;padding:1px 7px;border-radius:999px;
        background:var(--teal-wash);color:var(--brand-teal-lift);font-weight:600;}
@media (max-width: 1180px){ .ffoot{flex-direction:column;gap:8px;} }

/* ---- The significance card runs its prose the full width of the card ------

   WHERE THE CONSTRAINT CAME FROM. There is no single global prose measure. It
   is a repeated idiom, `max-width:36em`, written into nine separate rules, five
   of which belong to this card:

     .sigcard-lede       the introduction
     .sigcard-sentence   the finding
     .sigcard-caveat-text  "What this does not mean", at 34em
     .sigcard-refused    the refusal wording
     .sigcard-note       the three paragraphs under the menus

   The other four are `.askmeasure`, `.cardolder`, `.askblocked` and the
   ambiguity legend, which belong to the AI panel and the catalogue card. Those
   are left exactly as they are, and so is `.lede` at 70ch and `.caption span`
   at 62ch.

   `.sigcard-caveat-text` also had a container query giving it two columns once
   the card passed 880px, which is what split "What this does not mean" into the
   two side by side blocks. That is undone here too, so it is one block.

   THE SCOPE OF THIS OVERRIDE is `.sigcard`, the card element itself. That
   matters for one case: the print sheet renders `.sigcard-sentence` on its own,
   outside any `.sigcard`, inside a `.printsheet` capped at 820px. Scoping to
   the ancestor leaves the printed sentence at its measure, where a full 820px
   line would be about a hundred characters and worse on paper.

   WHY THE MEASURE WAS THERE. A long line is harder to read, and 36em is about
   the right measure for a paragraph somebody reads start to finish. The finding
   is not that: it is one sentence somebody reads once, for an answer, and
   breaking it across five short lines to protect a reading measure made the
   card twice as tall as the answer needed. Nick's decision, recorded here so
   the next person knows this card is deliberately different rather than
   overlooked. */
.sigcard .sigcard-lede,
.sigcard .sigcard-sentence,
.sigcard .sigcard-refused,
.sigcard .sigcard-note,
.sigcard .sigcard-caveat-text,
.sigcard .askmeasure{
  max-width:none;
}
.sigcard .sigcard-caveat-text{
  columns:auto;
  column-gap:normal;
}
@container (min-width:880px){
  .sigcard .sigcard-caveat-text{columns:auto;}
}

/* ---- Dashboard, second round: minimums, axes, table, map -----------------*/

/* Minimum heights.
   
   On a 14 inch laptop the panels compressed until the suppliers showed two of
   five and the chart was a strip. These are floors rather than fixed heights:
   the panels still grow on a larger display, which is what fills the white
   space that used to sit below them.
   
   The numbers are what five rows and a readable chart actually measure, taken
   from the rendered panels rather than guessed. */
/* Measured from the rendered panels rather than guessed: a supplier row is 54px
   once its figures fit on one line, a nation block of region tiles is 149px, and
   the chart needs 96 for its bars plus both axes and the sentence above them. */
.fcol-suppliers .fpanel{min-height:318px;}   /* five supplier rows and a heading */
.fmap{min-height:330px;}                     /* five region tiles at least */
.fchart{min-height:186px;}                   /* bars tall enough to compare, plus both axes */
.ftablepanel{min-height:150px;overflow:hidden;}

/* A panel that scrolls says so, rather than leaving a researcher to guess that
   there is more below the fold. A fading edge at the bottom, and only while
   there is something under it: `scroll-timeline` is not needed for that because
   the mask sits on the scroll container and disappears when it is scrolled to
   the end. */
.fcol-suppliers,.fcol-charts,.fmap,.ftablewrap{scrollbar-width:thin;}

/* The "more below" indicator is gone from both surfaces.
   
   It was removed from the research view for overlapping content, and from the
   operating picture because a white fade on a dark panel reads as a fault and
   it sat over the last row of the casualty feed. A panel that scrolls says so
   with a scrollbar, and every panel clips at its own edge, which is the part
   that mattered. */

/* The over time chart, with both axes. */
.fchart-say{margin:0 0 8px;font-size:11.5px;}
.fplot{display:grid;grid-template-columns:auto minmax(0,1fr);gap:0 8px;}
.fyaxis{
  display:flex;flex-direction:column;justify-content:space-between;
  font-size:10px;color:var(--muted);text-align:right;height:96px;
  border-right:1px solid var(--hairline-soft);padding-right:6px;
}
.fplot-area{min-width:0;}
.fbars{display:flex;align-items:flex-end;gap:3px;height:96px;
       border-bottom:1px solid var(--hairline);}
.fbar-slot{flex:1;height:100%;display:flex;align-items:flex-end;min-width:0;}
.fbar{width:100%;min-height:2px;background:var(--brand-teal);border-radius:2px 2px 0 0;opacity:.85;}
.fxaxis{display:flex;gap:3px;margin-top:4px;}
.fxtick{flex:1;min-width:0;font-size:10px;color:var(--muted);text-align:center;
        white-space:nowrap;overflow:visible;}
.fyname{margin:4px 0 0;font-size:10px;letter-spacing:.06em;text-transform:uppercase;
        color:var(--muted);}

/* Regional activity. */
/* A fixed height, not just a ceiling. Fewer rows made the table shorter, which
   made the page shorter, which clamped the reader's scroll position: choosing a
   window moved them 47px and choosing a supplier 29px. A table that is always
   the same height cannot do that. */
/* Fixed height, and the rows are clipped by it.
   
   `overflow-y:auto` clips vertically but leaves the horizontal axis visible, so
   a sticky header and a wide row could still paint outside the rounded corner
   of the panel. `overflow:hidden auto` clips both, and the radius on the
   wrapper is what actually rounds the clip: a radius on the panel alone does
   not clip a scrolling child. */
.ftablewrap{
  height:190px;overflow:hidden auto;margin-top:8px;position:relative;
  border-radius:8px;
}
/* Twice the height on a laptop, so five rows are readable before scrolling.
   1920 and mobile are unchanged. */
@media (min-width: 1181px) and (max-width: 1599px){
  .ftablewrap{height:380px;}
  .ftablepanel{min-height:300px;}
}
.ftable{width:100%;border-collapse:collapse;font-size:12.5px;}
.ftable th{
  position:sticky;top:0;background:var(--surface);text-align:left;
  font-family:var(--label);text-transform:uppercase;letter-spacing:.06em;
  font-size:10.5px;color:var(--label);font-weight:600;
  padding:4px 8px 5px;border-bottom:1px solid var(--hairline);
}
.ftable td{padding:4px 8px;border-bottom:1px solid var(--hairline-soft);color:var(--text);}
.ftable tr:last-child td{border-bottom:0;}
.ftable .num{text-align:right;font-family:var(--mono);}
/* A row that moved at the last refresh, marked quietly.
   
   A left edge that fades, not a flash and not a colour that pulls the eye to a
   particular region the instant it moves. It marks the whole refresh, not an
   arrival: several records inside one bucket produce one mark. */
.ftable-moved td:first-child{box-shadow:inset 3px 0 0 var(--brand-teal);}
.fmovedsay{margin:6px 0 0;font-size:11px;}
/* Halved, so it reads as quick rather than as a state the row is in. The delay
   is set per row by the shim, spread across about a second, so the table looks
   alive rather than updating in one block. */
@media (prefers-reduced-motion: no-preference){
  .ftable-moved td{animation:fmoved 3s ease-out both;}
}
@keyframes fmoved{from{background:var(--teal-wash);}to{background:transparent;}}

/* The Graphic and Map control. A segmented pair, obviously a choice. */
.fseg{display:inline-flex;border:1px solid var(--hairline);border-radius:999px;
      overflow:hidden;background:var(--surface-2);}
.fseg-opt{
  padding:4px 14px;font-size:12px;font-weight:600;text-decoration:none;
  color:var(--muted);letter-spacing:.02em;
}
.fseg-opt + .fseg-opt{border-left:1px solid var(--hairline);}
.fseg-opt:hover{color:var(--text);}
.fseg-opt.on{background:var(--brand-teal);color:#FFFFFF;}
.fmap-note{margin:0 0 8px;font-size:11.5px;}

/* The geographic view. */
/* The legend column is given a width rather than left to size itself. `auto`
   takes max-content, and the legend carries two explanatory sentences whose
   max-content width is 566px, so the map got 137 of a 746px panel. */
/* The legend column. It was a fixed 168px, which is a thin strip on a 24 inch
   display and wrapped its prose into a column two or three words wide. It now
   takes a share of the room and stops growing once the text is comfortable. */
.fgeo{
  display:grid;grid-template-columns:minmax(0,1fr) clamp(200px,26%,300px);
  gap:16px;align-items:start;
}
.fgeo-plot{min-width:0;width:100%;}
.fgeo-crumb{margin:0 0 6px;font-size:12px;color:var(--muted);}
.fgeo-crumb strong{color:var(--text);}
.fgeo-crumb a{color:var(--brand-teal);}
/* Each map eases in, so moving between the world and a nation reads as a
   change of view rather than a jump. It is an animation on arrival rather than
   a transition between two states, because the two states are two renders from
   the server and there is nothing to tween between. */
.fgeomap{width:100%;height:auto;min-height:230px;max-height:400px;display:block;
         animation:fgeoin .22s cubic-bezier(.33,.68,.28,1) both;}
@keyframes fgeoin{from{opacity:0;transform:scale(.985);}to{opacity:1;transform:none;}}
.fgeo-shape path{stroke:var(--surface);stroke-width:1.2;vector-effect:non-scaling-stroke;}
.fgeo-pick{cursor:pointer;}
.fgeo-pick:hover path,.fgeo-pick:focus path{stroke:var(--brand-teal);stroke-width:2;}
.fgeo-legend{min-width:0;}
.flegend{list-style:none;margin:0;padding:0;display:flex;flex-direction:column;gap:2px;}
.flegend li{display:flex;align-items:center;gap:7px;}
.flegend-swatch{width:26px;height:13px;border-radius:3px;border:1px solid var(--hairline);}
.flegend-text{font-size:10.5px;color:var(--text);}
@media (prefers-reduced-motion: reduce){
  .fgeomap{animation:none;}
  .ftable-moved td{animation:none;}
}
@media (max-width: 1180px){
  .fgeo{grid-template-columns:minmax(0,1fr);}
  .fgeo-legend{display:flex;gap:14px;align-items:flex-start;}
  .flegend{flex-direction:row;}
}
.fsupplier-figs{
  margin:2px 0 0;display:flex;flex-wrap:wrap;gap:2px 10px;
  font-size:11px;color:var(--muted);line-height:1.4;
}
.fsupplier-figs strong{color:var(--text);font-weight:600;}
/* The hover box. Immediate, clear of the mark, and never over it. */
.ftip{
  position:fixed;z-index:60;max-width:280px;pointer-events:none;
  background:#12333A;color:#FFFFFF;font-size:12px;line-height:1.45;
  padding:7px 10px;border-radius:7px;box-shadow:0 6px 20px rgba(16,34,42,.22);
}
/* The map key's box, which opens under the key and never over the map
   (tooltip.js `placeBelow`). Wider, so the long ones wrap into a block, with
   an arrow up at the row it explains. */
.ftip.ftip-below{max-width:320px;}
.ftip.ftip-below::before{
  content:"";position:absolute;top:-6px;left:var(--ftip-arrow,14px);margin-left:-6px;
  border:6px solid transparent;border-top:0;border-bottom-color:#12333A;
}
/* The mark being read. Subtle: enough to say which one, not enough to redraw
   the chart. */
.fbar.fhovered{opacity:1;filter:brightness(.86);}
/* The region cells are HTML now rather than SVG rects, so the mark is a
   border rather than a stroke. Same restraint: enough to say which one. */
.fmap-cell.fhovered{border-color:var(--brand-teal);box-shadow:inset 0 0 0 1px var(--brand-teal);}
/* One hover weight for the whole map, at country level and region level
   alike, halved from the two it was. `vector-effect` keeps it one pixel
   whatever the zoom transform is doing, which is what "the same weight" has to
   mean on a map that scales. */
.fgeo-shape.fhovered path,[data-tip].fhovered > path{
  stroke:var(--gm-hover,var(--brand-teal));stroke-width:1;
  vector-effect:non-scaling-stroke;
}
.fbands li.fhovered .fband span{filter:brightness(.86);}
.buildhead{justify-content:space-between;align-items:flex-start;gap:16px;margin-bottom:8px;}
.dsswitch{display:flex;align-items:center;gap:8px;flex:none;}
.dsswitch .sel{max-width:260px;}
.catdefault-on{font-size:11px;letter-spacing:.07em;text-transform:uppercase;
               color:var(--brand-teal-lift);background:var(--teal-wash);
               border-radius:999px;padding:3px 10px;font-weight:700;}

/* The catalogue card's heading row.
   
   The text takes the space that is left and wraps inside itself; the badges
   keep their own width and stay on the title line. `min-width:0` on the text is
   what lets it shrink: without it a flex item is at least its content's minimum
   width, which for a long summary is the whole card, and the badges wrapped
   below and went left.
   
   Three badges fit beside the title at every desktop width because the group
   does not wrap internally. Below 720 the whole group drops under the title,
   which is where it belongs on a phone rather than squeezed alongside. */
.cardhead{display:flex;justify-content:space-between;align-items:flex-start;gap:16px;}
.cardhead-text{flex:1 1 auto;min-width:0;}
.cardbadges{
  display:flex;align-items:center;gap:10px;flex:0 0 auto;flex-wrap:nowrap;
  margin-top:2px;
}
.cardbadges form{display:flex;}
@media (max-width: 720px){
  .cardhead{flex-direction:column;gap:10px;}
  .cardbadges{flex-wrap:wrap;}
}

/* ---- The operational picture --------------------------------------------
   
   A different surface with a different purpose, and its own palette. Scoped to
   `.opview` so nothing here reaches the research screens: those keep the light
   theme, the perimeter banner and every control they have.
   
   **Contrast is measured, not judged.** Every ink below was composited against
   all three surfaces and the worst pair is 7.09:1, which is AAA for body text.
   A test recomputes it from this file, so changing a colour means re-measuring
   rather than eyeballing. The screen is built to be read across a room. */
.opview{
  --op-bg:#0B1418;
  --op-panel:#111E24;
  --op-row:#16262E;
  --op-line:#22383F;
  --op-text:#E6EEF2;
  --op-muted:#9FB3BD;
  /* No severity reported, which is a different thing from a severity and is not
     a point on the scale.
     Its own token rather than the muted ink it used to borrow. #9FB3BD sits at
     0.433 relative luminance and critical at 0.429, so an unknown cluster and a
     critical one were 1.01:1 apart: the same grey to anybody reading the map in
     greyscale, and the same grey to a red deficiency. That is two different
     things looking identical on the screen a commander reads first.
     This is 1.81:1 against critical and no closer than that to any other band,
     and it still carries the chip's ink at 4.61:1. The ramp is otherwise
     untouched. `tests_triage_bands` recomputes both figures from this file, so
     changing a token here means re-measuring rather than eyeballing. */
  --op-unknown:#6E838C;
  --op-accent:#67DCD3;
  --op-critical:#FF8F8F;
  --op-severe:#FFB86B;
  --op-moderate:#FFE066;
  --op-minor:#7FEFC8;
  --op-expectant:#000000;
  --op-text-on-expectant:#E6EDF0;
  /* The triage chips, exactly as the ATAK plugin draws them and as Chris
     gave them on 30 September (docs/design/atak-triage-chips.png). Fills are
     never adjusted; only a label may change, black or white, to meet the 7.0
     floor. T1's black label is 4.97:1, the one recorded exception
     (ISSUES.md BIO-63, `tests_triage_chips.ChipContrast`). */
  --op-t1:#E53935;
  --op-t2:#F1C40F;
  --op-t3:#27AE60;
  --op-t4:#000000;
  --op-t4-border:#78838D;
  --op-t4-ink:#FFFFFF;
  --op-chip-label:#000000;
  --op-sos:#FF8585;
  --op-ok:#22D3EE;
  --op-t-none:#9DB0B8;
  --op-good:#7FEFC8;
  --op-bad:#FF9E9E;
  /* Dark ink on a light chip, as the silent chip carries it. */
  --op-chip-ink:#0B1418;
  /* The readings' icons, in the Biostream ATAK plugin's colours. Graphics, not text, so
     the 3:1 non-text floor applies; each clears 5.5:1 on the panel. */
  --op-hr:#FF5A4E;
  --op-spo2:#4DA3FF;
  --op-rr:#A58BFF;
  --op-battery:#D0D5D8;
  /* The plugin's alert orange, 8.33:1 on the panel. The brief's starting
     point, #F57C00, is 6.29:1 and fails the floor for the count beside it. */
  --op-alert:#FF9D42;
  background:var(--op-bg);color:var(--op-text);
  min-height:100vh;display:flex;flex-direction:column;
}
body:has(.opview) .stage{max-width:none;margin:0;padding:0;}
body:has(.opview) footer{display:none;}
body:has(.opview){background:#0B1418;}

/* Five pixels above and below rather than ten: at 1512 by 860 the header,
   the tabs, the banner and the headline cards took 291 pixels before any
   data. The title over its subtitle is the tallest thing here and sets the
   height; side by side they do not fit at 1512 and the bar wraps. */
.opbar{
  display:flex;align-items:center;gap:20px;flex-wrap:wrap;
  padding:5px 18px;background:#0E1A20;border-bottom:1px solid var(--op-line);
}
/* The wordmark, a link home. The white logo in the line the word "Biostream"
   stood in (15px at the body's 1.62, 24.3px), drawn at the height the research
   header draws the same logo (`.wordmark`, 15.4px), so the block and the bar
   keep their height. Padding with a matching negative margin gives the hover
   room without moving anything. */
.opident{
  display:flex;flex-direction:column;gap:1px;
  color:inherit;text-decoration:none;
  padding:2px 6px;margin:-2px -6px;border-radius:7px;
  transition:background-color .15s ease-out;
}
.opident-mark{display:flex;align-items:center;height:calc(15px * 1.62);}
.opident-mark img{display:block;height:15.4px;width:auto;}
.opident-line{font-size:10.5px;letter-spacing:.16em;text-transform:uppercase;color:var(--op-muted);}
/* The same hover as the view switch beside it: the accent, faintly, behind. */
a.opident:hover{background:color-mix(in srgb,var(--op-accent) 14%,transparent);}
a.opident:hover .opident-line{color:var(--op-text);}
a.opident:focus-visible{outline:2px solid var(--op-accent);outline-offset:2px;}

/* The fade between the Operational View and the home page, 450ms, opacity
   only, easing out (static/js/page-fade.js). Every page opts in to the
   browser's cross-document transition, and only when motion is allowed; the
   script skips it for every navigation a marked link did not start. The root
   cross-fade is opacity alone, old into new, so a dark to light change does not
   pass through white or black. */
@media (prefers-reduced-motion: no-preference){
  @view-transition{navigation:auto;}
}
::view-transition-group(root),
::view-transition-old(root),
::view-transition-new(root){animation-duration:450ms;animation-timing-function:ease-out;}
/* Where the browser has no such transition, the outgoing page fades its own
   opacity over the ground of the page it is going to. */
html.tre-leaving-light{background:var(--base);}
html.tre-leaving-dark{background:#0B1418;}
html.tre-leaving body{opacity:0;transition:opacity 450ms ease-out;}
@media (prefers-reduced-motion: reduce){
  ::view-transition-group(*),::view-transition-old(*),::view-transition-new(*){animation:none !important;}
  html.tre-leaving body{opacity:1;transition:none;}
}
.optitle{flex:1;min-width:0;text-align:center;}
/* Stated, not inherited. It was taking the light chrome's ink from a global
   heading rule and rendering at 1.08:1 against this bar, which is the title of
   the screen being invisible on it. */
.optitle h1{
  margin:0;font-size:19px;font-weight:600;letter-spacing:.01em;
  color:var(--op-text);
}
.optitle p{margin:2px 0 0;font-size:10.5px;letter-spacing:.16em;text-transform:uppercase;color:var(--op-muted);}
/* EXERCISE, not a classification marking. Nothing on this platform is
   classified and printing a caveat on invented data would be a false claim
   about what it is (constraint 19). */
.opexercise{
  font-size:11px;letter-spacing:.18em;text-transform:uppercase;font-weight:700;
  color:#0B1418;background:var(--op-moderate);border-radius:4px;padding:4px 10px;
}
.oplive{display:inline-flex;align-items:center;gap:7px;font-size:11px;
        letter-spacing:.12em;text-transform:uppercase;color:var(--op-good);font-weight:700;}
.oplive .fdot{width:8px;height:8px;border-radius:50%;background:currentColor;}
@media (prefers-reduced-motion: no-preference){
  .oplive[data-state="live"] .fdot{animation:fpulse 2s ease-in-out infinite;}
}
/* The badge's other states. Only live pulses, and each of the rest has its own
   colour and its own words, so a screen that is not updating cannot pass for
   one that is. The words carry the time the figures were last current. */
.oplive[data-state="slowed"]{color:var(--op-severe);}
.oplive[data-state="paused"],.oplive[data-state="stopped"]{color:var(--op-muted);}
.oplive[data-state="refused"],.oplive[data-state="error"]{color:var(--op-critical);}
.oplive[data-oplive-badge]{cursor:help;white-space:nowrap;}
.opclock{font-family:var(--mono);font-size:15px;letter-spacing:.02em;}
.opclock span{display:block;font-size:10.5px;color:var(--op-muted);letter-spacing:.06em;}
.opwho{display:flex;flex-direction:column;text-align:right;font-size:12.5px;}
.opwho span{font-size:10.5px;color:var(--op-muted);}

.optabs{display:flex;gap:2px;padding:0 12px;background:#0E1A20;
        border-bottom:1px solid var(--op-line);overflow-x:auto;}
.optab{
  padding:6px 15px;font-size:12.5px;font-weight:600;color:var(--op-muted);
  border-bottom:2px solid transparent;white-space:nowrap;text-decoration:none;
}
.optab.on{color:var(--op-accent);border-bottom-color:var(--op-accent);}
/* Visibly not available, and it is not a link: there is no route behind it, so
   there is nothing to reach by typing a URL either. */
.optab[aria-disabled="true"]{opacity:.42;cursor:not-allowed;}

.opnote{
  margin:0;padding:7px 18px;background:#2A2410;border-bottom:1px solid var(--op-line);
  font-size:12px;color:var(--op-moderate);
}
.opnote strong{color:var(--op-moderate);}

.opcounters{display:grid;gap:10px;padding:6px 16px 0;
            grid-template-columns:repeat(auto-fit,minmax(190px,1fr));}
.opcounter{background:var(--op-panel);border:1px solid var(--op-line);
           border-radius:9px;padding:5px 14px;
           display:flex;align-items:center;gap:var(--op-icon-gap);min-width:0;}
/* The icon beside each headline figure, centred on the label and figure as a
   block. One box for all six, one stroke, one colour: the muted ink the card
   labels use, and the critical chip's red for the one filled glyph. The box
   is a variable so the smaller screen can shrink the icon, never the figure. */
.opcounters{--op-icon:40px;--op-icon-gap:12px;}
.opcounter-body{min-width:0;}
.opicon{flex:none;width:var(--op-icon);height:var(--op-icon);
        fill:none;stroke:var(--op-muted);stroke-width:1.6;
        stroke-linecap:round;stroke-linejoin:round;}
.opicon-alert{fill:var(--op-critical);stroke:none;}
/* The smaller screen: 1512 wide gives each card 208 pixels inside its padding,
   and "Evacuations in progress" needs 162 of them on one line. A 40 pixel icon
   and its gap leave 156, so the label broke; 32 and 10 leave 166. The icon
   gives way, never the figure (measured in DASHBOARD.md). */
@media (max-width:1600px){.opcounters{--op-icon:32px;--op-icon-gap:10px;}}
.opcounter p{margin:0 0 3px;font-size:10px;letter-spacing:.1em;text-transform:uppercase;color:var(--op-muted);}
.opcounter .v,.opsince-headline .v{font-size:clamp(22px,2vw,32px);font-weight:700;line-height:1.05;}
.opcounter .u{font-size:11px;color:var(--op-muted);}
.opdelta{font-size:11.5px;font-weight:700;margin-left:8px;}
.opdelta-up{color:var(--op-bad);}
.opdelta-down{color:var(--op-good);}
.opdelta-flat{color:var(--op-muted);}

/* The rail is 420 wide, 160 more than it was, taken from the map and not from
   the lists on the right, which are already tight for the feed's table. The
   width is what lets the nation pills go two to a row. */
.opgrid{display:grid;gap:12px;padding:4px 16px 16px;
        grid-template-columns:420px minmax(0,1fr) clamp(380px,24vw,470px);align-items:start;}
/* The right hand column fills the row and no more: the feed and the timeline
   share its height, each scrolling inside itself, so neither runs below the
   fold on a laptop. The rail does the same and scrolls rather than clipping. */
.opview .opgrid:not(.opgrid-tab) > .opcol:last-child{
  display:flex;flex-direction:column;gap:12px;height:var(--op-band,auto);
}
.opview .opgrid:not(.opgrid-tab) > .opcol:last-child > .oppanel{
  flex:1 1 0;min-height:0;margin:0;display:flex;flex-direction:column;
}
.opview .opgrid:not(.opgrid-tab) > .opcol:last-child .opscroll{
  max-height:none;flex:1 1 auto;min-height:0;
}
.opview .opgrid:not(.opgrid-tab) > .oprail > .oppanel{
  height:var(--op-band,auto);overflow:auto;margin:0;
}
.oppanel{background:var(--op-panel);border:1px solid var(--op-line);
         border-radius:9px;padding:11px 13px;margin-bottom:12px;}
.oppanel h2{margin:0 0 8px;font-size:10.5px;letter-spacing:.12em;
            text-transform:uppercase;color:var(--op-muted);font-weight:700;}
.opchips{display:flex;flex-wrap:wrap;gap:6px;}
.opchip{
  display:inline-flex;align-items:center;gap:6px;padding:4px 9px;border-radius:999px;
  border:1px solid var(--op-line);background:var(--op-row);color:var(--op-text);
  font-size:11.5px;text-decoration:none;
}
.opchip.on{border-color:var(--op-accent);color:var(--op-accent);font-weight:600;}
/* The Nation heading, with the one control that puts both filters back to
   All beside it, offered only when either is set. */
.opfilterhead{display:flex;align-items:baseline;justify-content:space-between;gap:8px;}
.opclear{font-size:11px;color:var(--op-accent);text-decoration:none;
         border:1px solid var(--op-line);border-radius:999px;padding:2px 9px;}
.opclear:hover{border-color:var(--op-accent);}
/* A combination with no reporting in the window, said in words where a panel
   would otherwise draw an empty chart or a nought. */
.opempty{margin:0;padding:10px 0;font-size:12px;color:var(--op-muted);}
.opcounter .v.opnone{font-size:15px;font-weight:600;color:var(--op-muted);}
/* A panel that takes the territory filter only, saying so under its title. */
.opexempt{margin:-4px 0 8px;font-size:10.5px;line-height:1.35;color:var(--op-muted);}
/* A filter that does not apply on this section: still a control, because it
   carries to the others, but drawn as not doing anything here. */
.opinert{opacity:.45;}
.opfilternote{margin:2px 0 6px;}
/* A casualty row that opens its record: the whole row is the target. */
.oprow-link{cursor:pointer;}
.oprow-link:hover td{background:var(--op-row);}
/* Keyboard focus: dotted and in the text colour, so it cannot be taken for
   the open record's solid green border below. */
.oprow-link:focus-within{outline:2px dotted var(--op-text);outline-offset:-4px;}
.oprowlink{color:inherit;text-decoration:none;}
.oprowlink:hover,.oprowlink:focus-visible{text-decoration:underline;}
/* Where each value on an opened record came from, drawn quietly: a rule to
   the left of the value, read by the key at the top of the record. From the
   device is a solid rule; from the record platform a dotted one; not
   recorded has none, and its words already say so. */
.opsrc-key{display:flex;flex-wrap:wrap;gap:4px 14px;margin:0 0 10px;font-size:10.5px;color:var(--op-muted);}
.opsrc{padding-left:7px;border-left:2px solid transparent;}
.opsrc-dd{padding-left:7px;border-left:2px solid transparent;}
.opsrc-device{border-left-color:var(--op-accent);}
.opsrc-platform{border-left:2px dotted var(--op-muted);}
.opsrc-none{border-left-color:transparent;}
.opsrc-key .opsrc-none{font-style:italic;}
.opdetail-views{display:flex;flex-wrap:wrap;gap:4px 14px;margin:-2px 0 10px;font-size:11.5px;}
.opdetail-view{color:var(--op-muted);text-decoration:none;padding-bottom:2px;border-bottom:2px solid transparent;}
.opdetail-view:hover,.opdetail-view:focus-visible{color:var(--op-text);}
.opdetail-view.on{color:var(--op-text);border-bottom-color:var(--op-accent);}
.opdetail-back{margin:4px 0 8px;font-size:11.5px;}
.opdetail-back a,.opdetail-list a,.opdetail-facts dd a{color:var(--op-text);}
.opdetail-list{margin:0 0 8px;padding-left:16px;font-size:11.5px;}
.opwidened{display:flex;flex-wrap:wrap;gap:4px 10px;margin:6px 0 0;padding:6px 10px;
  border:1px solid var(--op-line);border-radius:4px;font-size:11.5px;color:var(--op-text);}
/* The selection in words in the map header. */
.opselection{font-size:11px;font-weight:600;color:var(--op-text);}
.opflag{font-size:13px;line-height:1;}
/* Round, because a flag in a list is an identifier rather than a flag: the
   circle keeps every one the same shape whatever its proportions, so a row of
   them reads as a column of marks. Served from this repository. */
.opflagimg{
  width:16px;height:16px;border-radius:50%;object-fit:cover;flex:none;
  box-shadow:0 0 0 1px rgba(255,255,255,.14);
}
.opcell-nat .opflagimg{width:14px;height:14px;vertical-align:-3px;}
.opcell-nat{text-align:center;}
/* The two letter code beside each flag, as the rail's chips carry it, so the
   Nation and Nationality columns side by side can be read without the flags. */
.opnat-code{margin-left:4px;font-size:10.5px;font-weight:600;letter-spacing:.03em;color:var(--op-text);}
.opwindows{display:flex;gap:4px;}
.opwindow{flex:1;text-align:center;padding:5px 0;border-radius:6px;font-size:11.5px;
          border:1px solid var(--op-line);background:var(--op-row);color:var(--op-muted);
          text-decoration:none;}
.opwindow.on{background:var(--op-accent);border-color:var(--op-accent);color:#08171A;font-weight:700;}

.optable{width:100%;border-collapse:collapse;font-size:11.5px;}
.optable th{
  position:sticky;top:0;background:var(--op-panel);text-align:left;
  font-size:9.5px;letter-spacing:.1em;text-transform:uppercase;color:var(--op-muted);
  padding:5px 7px;border-bottom:1px solid var(--op-line);font-weight:700;
}
/* The feed's own ink, stated rather than inherited.
   
   Location, injury, severity and nation were arriving at 1.74:1 against the
   panel, from the light chrome's muted colour. This is the most operationally
   interesting table on the screen and it was the least readable thing on it.
   Measured after: 7.0:1 for the supporting columns and 14:1 for the place,
   which is the one a reader scans for. */
.optable td{
  padding:5px 6px;border-bottom:1px solid var(--op-line);color:var(--op-muted);
}
.optable td.opcell-key{color:var(--op-text);font-weight:500;}
/* Every column visible. The table was 367px in a 352px panel, so the evacuation
   status was clipped at the edge. */
.optable{table-layout:fixed;}
/* Column widths, so nothing is clipped at the panel edge and the two columns a
   reader scans get the room. */
/* Widths set from the values, measured at 1512 where the column is 380px:
   "12:19" in tabular figures, a "MODERATE" chip, a flag, and the evacuation
   stage in its short form ("En route" rather than "MEDEVAC en route", the
   whole label in its hover box). The place takes what is left. Every one of
   these was clipping: the time read "12..", the severity "MODERA". */
/* Since 11 September: Nation and Nationality, flag and code each, third and
   fourth, and the place fifth. */
.optable th:nth-child(1),.optable td:nth-child(1){width:46px;}
.optable th:nth-child(2),.optable td:nth-child(2){width:74px;}
.optable th:nth-child(3),.optable td:nth-child(3){width:42px;text-align:center;}
.optable th:nth-child(4),.optable td:nth-child(4){width:42px;text-align:center;}
.optable th:nth-child(6),.optable td:nth-child(6){width:56px;}
.optable th:nth-child(7),.optable td:nth-child(7){width:66px;}
.optable th,.optable td{padding-left:4px;padding-right:4px;}
/* The severity chip in the narrow feed, measured: "MODERATE" at 10px with
   0.6px tracking and 7px padding needed 93px in a 74px column. Slightly
   tighter here, and the whole word still. */
.opgrid:not(.opgrid-tab) .optable .opsev{font-size:9.5px;letter-spacing:.3px;padding:1px 5px;}
.optable th,.optable td{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;}
.optable tr:last-child td{border-bottom:0;}
/* Positioned so it is the containing block of what is absolutely placed inside
   it. Without that, the screen reader labels in a wide table (position
   absolute, `opvisually-hidden`) were placed against an ancestor outside the
   scroll box, escaped its clipping, and stretched a 320 wide phone's page to
   1195 (ISSUES.md BIO-84). */
.opscroll{position:relative;max-height:290px;overflow:hidden auto;border-radius:7px;}
/* The synthetic marker on this surface. Every panel showing nation data carries
   one, because a screenshot cropped to a single panel is how a figure travels
   and it must not travel without the sentence that the records are invented
   (constraint 24). Measured against the panel like every other ink here. */
.opinvented,.oprange{
  display:inline-block;margin-left:6px;padding:1px 5px;border-radius:3px;
  font-size:8.5px;font-weight:700;
  letter-spacing:.08em;text-transform:uppercase;vertical-align:middle;
}
.opinvented{background:#3A2E18;color:#F0D089;}
/* The window a card is showing, on the three cards that carried the marker
   above until 10 September. The same pill to the pixel, sharing the rule, in
   the neutral colours of the panel furniture (the chips and tags): the row
   ground and the muted ink, 7.15:1. Their hairline is drawn inside rather
   than as a border, so the box is exactly the marker's size. */
.oprange{background:var(--op-row);color:var(--op-muted);box-shadow:inset 0 0 0 1px var(--op-line);}
/* On Total casualties the pill has its own line under the title, at every
   width, so the card reads the same on both screens. Beside the title it
   needed 207 of the 208 pixels a card has at 1512 and wrapped there anyway.
   A flex row so the pill sits at its own height with no text strut around
   it, and no indent, because nothing is beside it. */
.opcounter-range{display:flex;margin:0 0 4px;}
.opcounter-range .oprange{margin-left:0;}
.opsev{
  display:inline-block;padding:1px 7px;border-radius:4px;font-size:10px;
  font-weight:700;letter-spacing:.06em;text-transform:uppercase;color:#08171A;
}
.opsev-critical{background:var(--op-critical);}
.opsev-severe{background:var(--op-severe);}
.opsev-moderate{background:var(--op-moderate);}
.opsev-minor{background:var(--op-minor);}
.opsev-unknown{background:var(--op-unknown);}
/* The triage chip and its SOS label, one size in every table
   (feeds/_triage_chip.html, apps/feeds/triagechip.py). The label is turned on
   its side to read bottom to top, left of the chip with a small gap, and as
   tall as the chip because the cell stretches both. */
.optriage-cell{display:inline-flex;align-items:stretch;gap:3px;vertical-align:middle;}
.optchip{display:inline-flex;align-items:center;justify-content:center;min-width:28px;padding:1px 7px;border-radius:4px;font-size:11px;font-weight:800;line-height:16px;letter-spacing:.02em;color:var(--op-chip-label);}
.optchip-T1{background:var(--op-t1);}
.optchip-T2{background:var(--op-t2);}
.optchip-T3{background:var(--op-t3);}
/* T4's 1px border stands in for a pixel of padding, so it is the same size
   as the other chips and stays visible on the dark panel. */
.optchip-T4{background:var(--op-t4);color:var(--op-t4-ink);border:1px solid var(--op-t4-border);padding:0 6px;}
.optchip-none{background:var(--op-t-none);}
.opsos{display:inline-flex;align-items:center;justify-content:center;writing-mode:vertical-rl;transform:rotate(180deg);background:var(--op-sos);color:var(--op-chip-label);font-size:7.5px;font-weight:800;line-height:1;padding:0 1px;border-radius:2px;letter-spacing:.03em;}
/* Expectant, a triage category rather than a severity. Black, as a T4 tag is,
   with a light edge so it still reads on the dark surface, and light text
   where it is a chip. */
.opsev-expectant{background:var(--op-expectant);color:var(--op-text-on-expectant);
  box-shadow:inset 0 0 0 1px var(--op-muted);}
/* Whether reports are arriving from the field, on the live feed's heading.

   Right of the heading so it costs no vertical space on a dense screen, and at
   the panel furniture's weight rather than the heading's, because it is a note
   and not a title.

   Nothing here reddens and nothing here moves. Silence is not an error: a
   rehearsal with nobody posting for twenty minutes is normal, and a screen that
   reddened at it would be teaching the reader to ignore it. The only state with
   an accent is the one where reports are arriving; the two quiet states take
   the muted ink the rest of the furniture uses. The dot is filled in the first
   and a hollow ring in the others, so the state survives any colour vision
   deficiency and a greyscale print. */
.opfield{
  float:right;display:inline-flex;align-items:center;gap:5px;
  font-size:10px;font-weight:600;letter-spacing:.04em;text-transform:uppercase;
  color:var(--op-muted);
}
.opfield-dot{
  width:7px;height:7px;border-radius:50%;flex:0 0 auto;
  box-shadow:inset 0 0 0 1.5px currentColor;
}
.opfield-receiving{color:var(--op-accent);}
.opfield-receiving .opfield-dot{background:currentColor;}
/* The device's confidence in the reading it sits beside, 0 to 1.

   **The bar carries it by length and not by colour.** It is the same muted ink
   as the label and only its width changes, so it reads on a greyscale print
   and with any colour vision deficiency, and the number beside it is real text
   in both cases.

   **There is no threshold here, deliberately.** Anything applied to the
   reading itself, fading it or marking it, becomes noticeable at some value,
   and that point is a clinical threshold whether or not anybody wrote it down.
   A bar has no such point. If a threshold is ever wanted it belongs to the
   clinical owner and not to this file.

   Nought is an empty bar with the number 0.00 beside it, which is a
   measurement. A confidence nobody sent says so in words instead, because the
   two are different facts and the panel keeps them apart the way it keeps
   Not recorded apart from a zero. */
.opconf{
  display:inline-flex;align-items:center;gap:5px;margin-left:9px;
  font-size:10px;letter-spacing:.04em;color:var(--op-muted);vertical-align:1px;
}
.opconf-n{font-variant-numeric:tabular-nums;}
.opconf-bar{
  display:inline-block;width:30px;height:5px;border-radius:3px;overflow:hidden;
  box-shadow:inset 0 0 0 1px var(--op-muted);
}
.opconf-bar span{display:block;height:100%;background:var(--op-muted);}
.opconf-none{font-style:italic;}
/* Where the screen is looking, on the map's own head.

   A trail rather than a Back button alone: it says where you ARE as well as
   where you have been, and on a command screen that is the question that must
   never be in doubt. The last step is plain text, because it is here and there
   is nowhere for it to go; Back sits beside it because it is what people reach
   for and the trail's own last step offers no way up.

   Every step is a real button, so the whole thing is reachable from the
   keyboard. Focus used to be a double click and nothing else, which is
   undiscoverable, and on a trackpad or a touch screen barely a gesture. */
.gmtrail{
  display:flex;flex-wrap:wrap;align-items:center;gap:5px;margin:3px 0 0;
  font-size:11px;letter-spacing:.04em;
}
.gmtrail-step{
  background:none;border:0;padding:0;margin:0;cursor:pointer;font:inherit;
  color:var(--op-accent);text-decoration:underline;text-underline-offset:2px;
}
.gmtrail-step:hover{color:var(--op-text);}
.gmtrail-step:focus-visible{outline:2px solid var(--op-accent);outline-offset:2px;}
.gmtrail-sep{color:var(--op-muted);}
.gmtrail-here{color:var(--op-text);font-weight:600;}
/* Back, beside the trail and not on the map's own overlay. It carried
   `geomap-return` and inherited its absolute position, which put it in the
   page header while the trail sat on the map: one glance could not tell you
   both where you are and how to leave. Its own rules now, so there is no
   cascade to lose. */
.gmtrail-back{
  margin-left:8px;height:22px;padding:0 10px;border:1px solid var(--op-accent);
  border-radius:999px;background:var(--op-panel);color:var(--op-accent);
  font:inherit;font-size:10.5px;font-weight:600;cursor:pointer;
}
.gmtrail-back:hover{background:var(--op-row);}
.gmtrail-back:focus-visible{outline:2px solid var(--op-accent);outline-offset:2px;}
.gmtrail-unplaced{margin:2px 0 0;font-size:10.5px;color:var(--op-muted);}
.gmtrail-unplacedlink{
  background:none;border:0;padding:0;margin:0;cursor:pointer;font:inherit;
  color:var(--op-muted);text-decoration:underline;text-underline-offset:2px;
}
.gmtrail-unplacedlink:hover{color:var(--op-text);}
/* The unplaced, under a panel whose figures deliberately exclude them. A note
   and not a row: a distribution's rows are one dimension and must add up to
   its own total, and "not placed" is a different dimension. */
.opunplaced{margin:4px 0 0;font-size:10px;color:var(--op-muted);}
.opstatus-awaiting{color:var(--op-severe);}
.opstatus-en_route,.opstatus-in_transit{color:var(--op-accent);}
.opstatus-evacuated{color:var(--op-good);}
/* A row that has just arrived. Brief, and on a surface with no interval to
   protect: there is nothing to difference here that the reader is not entitled
   to see. */
@media (prefers-reduced-motion: no-preference){
  .oprow-new td{animation:opnew 2.4s ease-out both;}
}
@keyframes opnew{from{background:#1D3A3F;}to{background:transparent;}}

.optimeline{list-style:none;margin:0;padding:0;}
.optimeline li{display:flex;gap:9px;align-items:flex-start;padding:5px 0;
               border-bottom:1px solid var(--op-line);font-size:11.5px;}
.optimeline li:last-child{border-bottom:0;}
.optime{font-family:var(--mono);color:var(--op-muted);flex:none;}
.opkind{
  flex:none;padding:1px 7px;border-radius:4px;font-size:9.5px;font-weight:700;
  letter-spacing:.06em;text-transform:uppercase;color:#08171A;
}
.opkind-strike,.opkind-mass_casualty{background:var(--op-critical);}
.opkind-medevac_launch,.opkind-medevac_complete{background:var(--op-accent);}
.opkind-hospital_alert{background:var(--op-severe);}
.opkind-front_line{background:#B2A6F0;}
.opkind-line_update{background:var(--op-muted);}

.opbottom{display:grid;gap:12px;padding:0 16px 16px;
          grid-template-columns:repeat(auto-fit,minmax(280px,1fr));}
.opbars{display:flex;flex-direction:column;gap:7px;}
.opbar-row{display:grid;grid-template-columns:64px 1fr 44px 38px;gap:8px;align-items:center;font-size:11.5px;}
/* Both are spans, so both have to be told to be boxes. `.opbar-fill` was left
   inline, which ignores width and height entirely: every bar on the triage and
   facility panels was 0 by 0, so the numbers were there and nothing was drawn
   beside them. */
.opbar-track{display:block;height:12px;background:var(--op-row);border-radius:3px;overflow:hidden;}
.opbar-fill{display:block;height:100%;border-radius:3px;min-width:2px;}
/* The occupancy bands, named by operational.FACILITY_BANDS. The colour is here
   rather than in the template so the band a bar draws and the band the Medical
   facilities tab counts are one decision. */
.opbar-fill.opbar-at-capacity,.opbar-fill.opbar-pressure{background:var(--op-critical);}
.opbar-fill.opbar-busy{background:var(--op-severe);}
.opbar-fill.opbar-normal{background:var(--op-accent);}
/* The ring takes the room it is given rather than a fixed 132px, which left it
   small in the corner of its card with the legend floating beside it.

   It wraps, so on a narrow card the legend goes under the ring instead of the
   two fighting over a width that will not hold both. */
.opdonut{display:flex;gap:16px;align-items:center;flex-wrap:wrap;}
.opdonut-ring{
  /* Never wider than the card and never wider than it is useful. `min()`
     rather than a `max-width`, so the cap holds even where a flex line has
     told the element how wide it is. */
  flex:0 1 auto;width:min(100%,210px);min-width:0;height:auto;aspect-ratio:1;
}
/* No font size here on purpose.
   
   The centre label's size depends on how many characters the total has, which
   a stylesheet cannot know, and these two rules used to state one anyway: 19
   and 8 user units in a 42 unit box, which is a numeral almost half the height
   of the whole drawing. It overflowed the hole at every value and was simply
   less obvious while the ring was pinned at 132px. The size is computed in
   `operational.donut_label` and written inline on the element. */
.opdonut-total{font-weight:700;fill:var(--op-text);text-anchor:middle;}
.opdonut-cap{fill:var(--op-muted);text-anchor:middle;letter-spacing:.12em;
             text-transform:uppercase;}
.oplegend{list-style:none;margin:0;padding:0;font-size:11.5px;flex:1;min-width:130px;}
.oplegend li{display:flex;align-items:center;gap:7px;padding:2px 0;}
.oplegend i{width:10px;height:10px;border-radius:2px;flex:none;}
.oplegend b{margin-left:auto;font-variant-numeric:tabular-nums;}
/* The trend chart: legend on top, a y axis from zero beside a plot that takes
   every pixel left, and the x axis under the plot. The drawing is stretched to
   the plot (preserveAspectRatio none) with strokes that do not scale, and the
   markers sit over it as HTML so they stay round. On its own, in the analytics
   tab, the plot is given a height; in the bottom row the card gives it one. */
.optrendbox{display:flex;flex-direction:column;flex:1 1 auto;min-height:0;}
.optrend-legend{display:flex;flex-wrap:wrap;gap:3px 12px;list-style:none;margin:0 0 6px;
                padding:0;font-size:11px;color:var(--op-text);}
.optrend-legend li{display:flex;align-items:center;gap:6px;}
.optrend-legend i{width:8px;height:8px;border-radius:50%;flex:none;}
.optrend-body{display:grid;grid-template-columns:auto minmax(0,1fr);
              grid-template-rows:minmax(0,1fr) auto;column-gap:6px;
              flex:1 1 auto;min-height:150px;}
.optrend-yaxis{position:relative;grid-row:1;grid-column:1;font-size:9.5px;
               color:var(--op-muted);font-variant-numeric:tabular-nums;}
.optrend-yaxis span{position:absolute;right:0;transform:translateY(50%);white-space:nowrap;}
.optrend-yaxis .optrend-ysizer{position:static;display:block;visibility:hidden;height:0;transform:none;}
.optrend-plot{position:relative;grid-row:1;grid-column:2;min-height:0;}
.optrend{position:absolute;inset:0;width:100%;height:100%;display:block;overflow:visible;}
/* Faint, well behind the lines. */
.optrend-grid{stroke:var(--op-line);stroke-width:1;vector-effect:non-scaling-stroke;opacity:.6;}
.optrend-dots{position:absolute;inset:0;pointer-events:none;}
.optrend-dot{position:absolute;width:5px;height:5px;margin:-2.5px 0 0 -2.5px;border-radius:50%;}
.optrend-body .optrend-axis{grid-row:2;grid-column:2;}
/* Axis labels placed under their own bucket. The first sits flush left and the
   last flush right, so neither hangs off the panel. */
.optrend-axis{position:relative;height:13px;margin-top:3px;font-size:9.5px;color:var(--op-muted);}
.optrend-axis span{position:absolute;top:0;transform:translateX(-50%);white-space:nowrap;}
.optrend-axis span:first-child{transform:none;}
.optrend-axis span:last-child:not(:first-child){transform:translateX(-100%);}
/* Invisible hover bands, one per bucket. A 1.8px line is not a pointer target,
   so the band is the whole height of the chart and the reader points at a
   moment rather than at a stroke. */
.optrend-hit{fill:transparent;}
.optrend-hit.fhovered{fill:rgba(255,255,255,.05);}
.opfac{font-size:11.5px;margin-bottom:9px;}
.opfac-name{display:flex;justify-content:space-between;gap:8px;margin-bottom:3px;}

/* --- the four cards under the map ------------------------------------------
   The content fills the card. The cards share their height with the map row
   above, and the band's handle moves the line between them
   (static/js/picture-band.js sets --op-bottom). Every load and every section
   switch starts the cards at their minimum and gives the rest to the map row
   (BIO-85): `--op-bottom-min`, the least height at which every card is whole,
   measured in a browser at 1181 to 1920 wide (at 180 the facility list starts
   to scroll). The script raises it where a card's contents need more. Below
   1180 the cards stack and every row has that same minimum, so a chart that
   fills its card has a height to fill; without JavaScript the rows are the
   minimum too. */
.opbottom{--op-bottom-min:200px;grid-auto-rows:var(--op-bottom, var(--op-bottom-min));}
.opbottom > .oppanel{display:flex;flex-direction:column;min-height:0;margin:0;}
.opbottom > .oppanel{container-type:size;}
.opbottom .optrend-body{min-height:0;}
.opcardhead{display:flex;align-items:center;justify-content:space-between;gap:8px;margin:0 0 8px;}
.opcardhead h2{margin:0;}
/* The page's range, in the place and the look of the reference's dropdown,
   and plainly not one: no chevron, no pointer, nothing to focus. */
.oprangebox{font-size:11px;color:var(--op-text);background:var(--op-row);
            border:1px solid var(--op-line);border-radius:6px;padding:3px 10px;
            white-space:nowrap;cursor:default;user-select:none;}
/* Triage: bars about three times the weight they were, spread down the card. */
.opbottom .optriage{flex:1 1 auto;min-height:0;justify-content:space-between;gap:6px;}
.opbottom .optriage .opbar-row{flex:1 1 0;min-height:0;}
.opbottom .optriage .opbar-track{height:min(62%,40px);min-height:12px;}
/* Facility status: the same rows, spread down the card. */
.opfacs{display:flex;flex-direction:column;justify-content:space-between;flex:1 1 auto;min-height:0;}
.opfacs .opfac{margin-bottom:0;}
/* Injury pattern: the ring beside its legend, taking the card's height, and
   never so much width that the legend, whose text does not change size, has
   less than its 130 pixels. At 1512 it used to wrap, legend under ring, which
   is what made that row 411 tall. */
.opbottom .opdonut{flex:1 1 auto;min-height:0;flex-wrap:nowrap;}
.opbottom .opdonut-ring{width:min(calc(100% - 146px), calc(100cqh - 25px));}
/* As a card shortens, its contents close up rather than clip. Measured on the
   card's own inside height, because the reader sets it with the band's
   handle; each figure below is that inside height, which is the card less 24.
   Each step is the least that keeps everything whole, and the text never gets
   smaller than the trend legend's 10 pixels. At the default at 1512 (cards
   323) only the first applies. */
@container (max-height:300px){
  .opfacs .opfac-name{margin-bottom:2px;}
  .opfacs .opbar-track{height:9px;}
}
@container (max-height:240px){
  .opfacs .opfac-name{margin-bottom:1px;}
  .opfacs .opbar-track{height:6px;}
  .opbottom .opdonut .oplegend li{padding:0;}
}
@container (max-height:190px){
  .opfacs .opfac{line-height:1.2;}
  .optrend-legend{font-size:10px;margin-bottom:3px;gap:2px 9px;}
  /* Every other label on the y axis, keeping nought and the top, so the
     labels do not crowd a short plot. The gridlines all stay. */
  .optrend-yaxis span:nth-child(2n+3):not(:last-child){visibility:hidden;}
}
/* Anything a filter note pushes past the card scrolls inside it. */
.opfacs{overflow:auto;}

/* --- the map row, one band ------------------------------------------------
   One height, `--op-band`, set by static/js/picture-band.js from what is on
   the screen and dragged by the reader. The rail, the map and the two lists
   take it together, so the three columns start and end on the same two lines,
   and each scrolls inside itself so none of them can grow the band. The grid's
   bottom padding is the handle, so nothing below moves. Below 1180 the row
   stacks and there is no band. */
.opbandhandle{display:none;}
@media (min-width:1181px){
  .opview .opgrid:not(.opgrid-tab){padding-bottom:0;}
  .opview .oppanel-map{height:var(--op-band,auto);display:flex;flex-direction:column;}
  .opview .oppanel-map > .opmap{flex:1 1 auto;min-height:0;display:flex;flex-direction:column;}
  .opview .oppanel-map .geomap{flex:1 1 auto;min-height:0;display:flex;flex-direction:column;}
  .opview .oppanel-map .geomap-dark .geomap-plot{height:auto;max-height:none;flex:1 1 0;min-height:0;}
  .opbandhandle{
    display:block;position:relative;height:16px;margin:0 16px;
    cursor:row-resize;touch-action:none;outline:none;
  }
  /* A line along the edge and a grip in the middle, faint until the pointer
     or the keyboard is on it. */
  .opbandhandle::before{content:"";position:absolute;left:0;right:0;top:7px;height:2px;
                        border-radius:2px;background:transparent;}
  .opbandhandle::after{content:"";position:absolute;left:50%;top:5px;width:48px;height:6px;
                       margin-left:-24px;border-radius:3px;background:var(--op-line);}
  .opbandhandle:hover::before,.opbandhandle:focus-visible::before,
  .opband-dragging .opbandhandle::before{background:var(--op-accent);}
  .opbandhandle:hover::after,.opbandhandle:focus-visible::after,
  .opband-dragging .opbandhandle::after{background:var(--op-accent);}
  .opbandhandle:focus-visible{box-shadow:inset 0 0 0 2px var(--op-accent);border-radius:4px;}
  .opband-dragging,.opband-dragging *{cursor:row-resize !important;user-select:none;}
}

/* Nation and Nationality, two to a row in equal columns, with All alone at the
   top of each list at a column's width. The rail's width is what makes room.
   A name that did not fit would be cut at the pill's edge rather than wrapped
   or shrunk; at 420 none needs to be (measured in DASHBOARD.md). */
.opview:not(.oprail-shut) .oprail .opchips{
  display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:6px;
}
.opview:not(.oprail-shut) .oprail .opchips > .opchip:first-child{
  grid-column:1 / -1;width:calc(50% - 3px);
}
.opview:not(.oprail-shut) .oprail .opchip{min-width:0;white-space:nowrap;overflow:hidden;}
/* At narrow widths the name gives way, with an ellipsis, and the flag and the
   two letter code always show: the code is the identifier, the name is help. */
.opview:not(.oprail-shut) .oprail .opchip > .opflagimg,
.opview:not(.oprail-shut) .oprail .opchip > .opflag{flex:none;}
.opview:not(.oprail-shut) .oprail .opchip > span:not(.opflag){min-width:0;overflow:hidden;text-overflow:ellipsis;}

/* --- the operating picture's map layers ---------------------------------
   
   Record level, and only on this surface. The research view's map is the same
   component with no layers passed to it, so none of the rules below have
   anything to match there.
   
   Every colour is measured against the darkened relief rather than judged, and
   the measurements are in DASHBOARD.md. */

/* The bundled Natural Earth detail. Filled in by the script as the reader
   zooms, one band at a time, fetched from this platform. */
.gm-urban path{fill:var(--gm-urban);fill-opacity:.5;stroke:none;}
.gm-lakes path{fill:var(--gm-water);fill-opacity:.75;stroke:none;}
.gm-rivers path{fill:none;stroke:var(--gm-water);stroke-width:.9;stroke-opacity:.85;}
.gm-rail path{fill:none;stroke:var(--gm-rail);stroke-width:.5;stroke-opacity:.75;
              stroke-dasharray:2.4 1.6;}
.gm-roads path{fill:none;stroke:var(--gm-road);stroke-width:.55;stroke-opacity:.7;}
.gm-motorways path{fill:none;stroke:var(--gm-motorway);stroke-width:.9;stroke-opacity:.85;}

/* Names. Sized in projection units so they scale with the map, and given a
   dark halo rather than a box, because a box over a town is a box over the
   thing being named. */
.gm-placenames text,.gm-countrynames text{
  fill:var(--gm-label);text-anchor:middle;pointer-events:none;
  paint-order:stroke fill;stroke:var(--gm-label-ink);stroke-width:2.4;
  stroke-linejoin:round;font-weight:600;
}
/* Relative to the group, whose size the script sets against the frame. A
   number here would be a number in the drawing's units, which is a name that
   doubles in size every time somebody zooms in. */
/* Relative to the group, whose size the script sets to 11 screen pixels in
   the map's own units. The proportions give 10.5px country names, 11px towns,
   11.5px busier places and 12px capitals, and 12px is the most any name on the
   map may be. A number in px here would be a number in map units, which is a
   name that doubles every time somebody zooms in. */
.gm-countrynames text{
  font-size:.9545em;letter-spacing:.16em;text-transform:uppercase;fill-opacity:.62;
  stroke-width:.5em;
}
.gm-placenames text{font-size:1em;stroke-width:.4em;}
.gm-placenames text[data-weight="1"]{font-size:1.0909em;fill:#EAF3F7;}
.gm-placenames text[data-weight="2"]{font-size:1.0455em;}

/* Where reporting is concentrated. A soft edge, because a hard one reads as a
   line somebody drew on the ground, and this is not that. */
.gm-areas circle{
  fill:var(--op-severe);fill-opacity:.10;stroke:var(--op-severe);
  stroke-opacity:.30;stroke-width:.8;stroke-dasharray:5 4;
}
/* ARRIVALS, AS A PULSE. A ring expanding from the place's own circle and
   fading, so somebody watching sees where things are developing without
   reading the feed.

   THE COLOUR IS THE TRIAGE BAND OF WHAT ARRIVED, taken from the same
   `--op-*` tokens and the same `gm-sev-*` classes the cluster markers and the
   triage bars use. Red was asked for and refused: red is T1 on this map, and
   a red ring for a minor casualty would tell a commander something false in
   the half second before they read the count. A burst shows its worst.

   NO FILL. The ring is a stroke only, so it never tints what is under it and
   never reads as a shape on the ground. `gm-sev-*` sets a fill, so it is
   turned off here rather than a second set of colour rules being written. */
.gm-pulses{pointer-events:none;}
.gm-pulse{
  fill:none;stroke:currentColor;vector-effect:non-scaling-stroke;
  transform-box:fill-box;transform-origin:center;
  animation:gmpulse 2.4s cubic-bezier(.16,.7,.3,1) forwards;
}
.gm-pulse-critical{color:var(--op-critical);}
.gm-pulse-severe{color:var(--op-severe);}
.gm-pulse-moderate{color:var(--op-moderate);}
.gm-pulse-minor{color:var(--op-minor);}
.gm-pulse-expectant{color:var(--op-expectant);}
.gm-pulse-unknown{color:var(--op-unknown);}
/* One ring per place per eight second refresh, running for 2.4 seconds: 0.125
   pulses a second at the very most, against a photosensitivity limit of three.
   Slow on purpose, so several areas pulsing together read as a picture rather
   than as an alarm. */
@keyframes gmpulse{
  from{transform:scale(.25);}
  to{transform:scale(1);opacity:0;}
}
/* A REGION ARRIVAL washes the region's own shape rather than expanding a ring
   from a point, because a posted report names a region and carries no place.
   A stroke and a very light fill, so the shape brightens and fades without
   ever reading as a new boundary. */
/* FILL ONLY, NEVER A STROKE. A stroked region reads as a line somebody drew
   on the ground, which is the thing this map refuses everywhere else
   (`tests_boundaries`), and a wash that outlined a region for two seconds
   would be making that claim for two seconds. The region lights up instead. */
.gm-wash{
  fill:currentColor;fill-opacity:.30;stroke:none;
  animation:gmwash 2.4s cubic-bezier(.16,.7,.3,1) forwards;
}
/* NO `from`. An animated property takes the keyframes' value over the
   element's own, so a `from{opacity:1}` here threw away the strength the
   script had just set and every wave washed at the same weight however many
   had arrived. With only a `to`, the animation starts from what is on the
   element, which is where the count is carried. */
@keyframes gmwash{
  to{opacity:0;}
}

/* REDUCED MOTION: nothing moves. The arrival is a state on the place's own
   circle, which the next refresh clears, rather than a ring going anywhere.
   Motion was never the only signal: the counts, the live feed and the
   timeline all change as they did before. */
@media (prefers-reduced-motion: reduce){
  .gm-pulse,.gm-wash{display:none;animation:none;}
}
.gm-areas circle.gm-arrived{
  fill-opacity:.20;stroke-opacity:.75;stroke-width:1.6;stroke-dasharray:none;
}
/* The still state for a region, for reduced motion. A fill again, for the
   reason the wash above is a fill. */
.geomap-shape.gm-arrived path{fill-opacity:.42;}

.gmkey .gm-areakey,.gmlayer-swatch .gm-areakey{
  fill:var(--op-severe);fill-opacity:.14;stroke:var(--op-severe);
  stroke-opacity:.5;stroke-width:1.4;stroke-dasharray:3 2.4;
}

/* Population centres: a small light dot with a dark ring, the same in the
   layers panel as on the map, sized in screen pixels by the script like the
   facility markers. Gridlines: hairlines that stay a hairline at any zoom. */
.gm-town{fill:var(--gm-label);fill-opacity:.9;stroke:var(--gm-label-ink);stroke-width:1.2;}
.gm-towns g{pointer-events:auto;}
.gm-grid line,.gm-grid path{
  fill:none;stroke:var(--gm-label);stroke-opacity:.22;stroke-width:1;
  vector-effect:non-scaling-stroke;pointer-events:none;
}

/* Movements. Dashed and animated so they read as movement rather than as
   boundaries, and the animation stops for anybody who has asked it to. */
.gm-route{
  stroke-width:1;stroke-opacity:.55;stroke-dasharray:6 5;fill:none;
  stroke-linecap:round;
}
.gm-route-en_route{stroke:var(--op-accent);}
.gm-route-in_transit{stroke:var(--op-moderate);}
.gm-routes line:hover{stroke-opacity:1;stroke-width:1.8;}
.gm-asset{stroke:var(--gm-edge);stroke-width:.6;}
.gm-asset-en_route{fill:var(--op-accent);}
.gm-asset-in_transit{fill:var(--op-moderate);}
@media (prefers-reduced-motion: no-preference){
  .gm-routes line{animation:gmflow 1.6s linear infinite;}
}
@keyframes gmflow{to{stroke-dashoffset:-11;}}

/* Facilities. A square with a cross in it, so it is told apart from a cluster
   by shape and not only by colour. */
.gm-facility{stroke:var(--gm-edge);stroke-width:1;}
.gm-facility-at-capacity,.gm-facility-pressure{fill:var(--op-critical);}
.gm-facility-busy{fill:var(--op-severe);}
.gm-facility-normal{fill:var(--op-accent);}
.gm-cross{fill:var(--gm-edge);}
.gm-facilities g{cursor:pointer;}

/* Incident clusters. The halo is what makes a marker readable over relief and
   over another marker; the disc carries the count. */
.gm-halo{fill-opacity:.16;stroke:none;}
.gm-cluster{fill-opacity:.85;stroke:var(--gm-edge);stroke-width:1;}
.gm-sev-critical{fill:var(--op-critical);stroke-opacity:.9;}
.gm-sev-severe{fill:var(--op-severe);}
.gm-sev-moderate{fill:var(--op-moderate);}
.gm-sev-minor{fill:var(--op-minor);}
.gm-sev-unknown{fill:var(--op-unknown);}
/* Expectant on the map, which is the one that needs care.

   Black is the treatment a T4 tag has, and on the dark theme `--gm-edge` is
   #0B1418: a black disc drawn the ordinary way has a near black rim and near
   black digits on it and disappears into the ground. Measured rather than
   assumed, the fill is 1.11:1 against that ground, so the fill cannot be the
   thing that makes this readable.

   So the rim is. It is the mid grey the expectant chip already uses for the
   same job, which reads against black on both themes and against both map
   grounds, and it is heavier than every other marker's, so the difference is a
   shape as well as a colour and survives any colour vision deficiency. The halo
   is that grey rather than a tint of the fill: a black halo at 16 per cent is
   nothing at all, and a pale one puts the disc in a surround instead of a
   hole. The band is on the wrapping group because the count is a sibling of
   the circles and a class on a circle cannot reach it. */
.gm-sev-expectant{fill:var(--op-expectant);}
.gm-band-expectant .gm-cluster{stroke:var(--op-muted);stroke-width:1.6;
  stroke-opacity:1;fill-opacity:1;}
.gm-band-expectant .gm-halo{fill:var(--op-muted);fill-opacity:.22;}
.gm-band-expectant .gm-count{fill:var(--op-text-on-expectant);}
.gm-count{
  fill:var(--gm-edge);font-size:7px;font-weight:700;text-anchor:middle;
  pointer-events:none;
}
.gm-incidents g{cursor:pointer;}
.gm-incidents g:hover .gm-cluster,.gm-facilities g:hover .gm-facility{
  stroke:var(--op-text);stroke-width:1.6;
}

/* The key, as a strip under the map. It sat over the map bottom right and
   that corner held the three busiest clusters on the screen. */
.gmkey{
  margin-top:6px;background:var(--gm-control-bg);border:1px solid var(--gm-control-line);
  border-radius:7px;color:var(--op-text);font-size:10.5px;line-height:1.3;
  display:flex;align-items:stretch;
}
.gmkey summary{
  cursor:pointer;list-style:none;padding:6px 10px;font-size:9.5px;
  letter-spacing:.12em;text-transform:uppercase;color:var(--op-muted);
  font-weight:700;display:flex;align-items:center;gap:8px;flex:none;
}
.gmkey summary::-webkit-details-marker{display:none;}
.gmkey-mark{
  width:0;height:0;border-top:4px solid transparent;border-bottom:4px solid transparent;
  border-left:5px solid currentColor;transition:transform .12s ease;
}
.gmkey[open] .gmkey-mark{transform:rotate(180deg);}
.gmkey summary:hover{color:var(--op-accent);}
.gmkey-body{
  display:flex;flex-wrap:wrap;align-items:center;gap:4px 16px;padding:5px 10px;
  border-left:1px solid var(--gm-control-line);
}
/* Each row is a button to press and hold (static/js/map-solo.js), reset so it
   reads as the label it was. The padding is taken back by the margin, so the
   strip is laid out exactly as before. No long press menu or text selection
   on a touch screen, and a finger that moves still scrolls the page. */
.gmkey-row{display:inline-flex;align-items:center;gap:6px;cursor:help;
  font:inherit;color:inherit;background:none;border:0;border-radius:5px;
  padding:2px 5px;margin:-2px -5px;text-align:left;
  touch-action:manipulation;-webkit-touch-callout:none;-webkit-user-select:none;user-select:none;}
.gmkey-row:focus-visible{outline:2px solid var(--op-accent);outline-offset:1px;}
/* The row being held. A ring as well as a fill, so colour is not the only
   thing that says which one. */
.gmkey-row.gmkey-held{background:var(--op-panel);color:var(--op-text);box-shadow:inset 0 0 0 1.5px var(--op-accent);}
.gmkey-swatch{flex:none;width:15px;height:15px;}
.gmkey-swatch svg{width:15px;height:15px;overflow:visible;}
.gmkey-scale{display:flex;gap:1.5px;align-items:center;}
.gmkey-scale i{width:2.5px;height:10px;background:var(--gm-ink);border-radius:1px;}
.gmkey-scale i[data-band="1"]{opacity:.25;}
.gmkey-scale i[data-band="2"]{opacity:.4;}
.gmkey-scale i[data-band="3"]{opacity:.58;}
.gmkey-scale i[data-band="4"]{opacity:.78;}
.gmkey-scale i[data-band="5"]{opacity:1;}
.gmkey-note{color:var(--op-muted);margin-left:auto;}

/* The map's panel, with less of it between the map and the edge. The header
   is one line and the padding is enough that the panel's own rounded corner
   never cuts into the plot. */
.oppanel-map{padding:7px 8px 7px;}
.opmap-head{
  display:flex;justify-content:space-between;align-items:baseline;gap:10px;
  margin-bottom:5px;
}
.opmap-head h2{margin:0;}
.opmap-head span{font-size:10.5px;color:var(--op-muted);}

/* Lines that stay lines. Drawn in the map's units, a motorway was 0.9 units
   wide, which is 2px at the world view and 25px at the closest: zooming in
   thickened every road into a band. `non-scaling-stroke` holds a stroke at its
   screen width whatever the zoom, so the widths below are pixels. */
.geomap-dark [data-geomap-detail] path,
.gm-routes line,.gm-areas circle{vector-effect:non-scaling-stroke;}
.gm-rivers path{stroke-width:1.1;}
.gm-rail path{stroke-width:.9;stroke-dasharray:4 3;}
.gm-roads path{stroke-width:.8;}
.gm-motorways path{stroke-width:1.4;}
.gm-routes .gm-route{stroke-width:1.5;stroke-dasharray:7 5;}
.gm-areas circle{stroke-width:1;stroke-dasharray:6 4;}

/* The count on a cluster is sized by the script, in the marker's own pixel
   space, and hidden when it would not fit inside its disc. */
.gm-incidents .gm-count{
  fill:var(--gm-edge);font-weight:700;text-anchor:middle;pointer-events:none;
}
/* A count too big for its disc, drawn beside it, with a dark halo so it
   reads over relief and roads. */
.gm-incidents .gm-count-out{
  text-anchor:start;fill:var(--op-text);paint-order:stroke fill;
  stroke:var(--gm-edge);stroke-width:3px;stroke-linejoin:round;
}
/* The region shading fades as the reader zooms in; the script sets how much.
   Only on this surface: the research view never sets the property, so its
   shading is drawn exactly as before. */
/* Region shading. The script sets --gm-shade from the zoom (1 far out, a
   quarter close in); the counted regions are drawn at half of that on top, in
   every state. One multiplier on the whole shape, so hovered and not, selected
   and not, keep exactly the ratio they had and only the whole gets lighter. */
.geomap-dark{--gm-shade-base:.5;}
.geomap-dark [data-geomap-shapes] > .geomap-shape{opacity:var(--gm-shade,1);}
.geomap-dark [data-geomap-shapes] > [data-band]{
  opacity:calc(var(--gm-shade,1) * var(--gm-shade-base));
}
/* The close zoom relief: shaded hills only, mid grey where the ground is flat
   or wet, so an overlay blend changes nothing but the slopes. */
.geomap-dark .geomap-relief{
  mix-blend-mode:overlay;opacity:0;pointer-events:none;transition:opacity .25s ease;
}
.gm-merged .gm-cluster{stroke-dasharray:3 2;}
.gm-merged{cursor:zoom-in;}

/* --- the sections beside the operating picture --------------------------
   
   One visual language, not five. Every rule below reuses the surface's own
   properties (`--op-panel`, `--op-line`, `--op-text`, `--op-muted`, the
   severity colours) rather than introducing a palette, so a change to the
   picture's theme moves the sections with it. Where a section needed a shape
   the picture did not have, the shape is new and the colours are not. */

/* The tab, when it is a real link. Same chip as the disabled one, so the two
   sit on one line without the built ones jumping. */
a.optab{text-decoration:none;}
a.optab:hover{background:var(--op-row);color:var(--op-text);}
.optab.opbusy{opacity:.6;}
/* While a section is being fetched. Deliberately slight: a reader pressing a
   column heading should see that something is happening without the table
   flashing. */
#optab.opbusy{opacity:.72;transition:opacity .12s ease;}

.opgrid-tab{grid-template-columns:260px minmax(0,1fr);}

/* The filter bar. */
.opfilters{padding:8px 16px 0;}
.opfilterbar{
  display:flex;flex-wrap:wrap;gap:10px;align-items:flex-end;
  background:var(--op-panel);border:1px solid var(--op-line);
  border-radius:9px;padding:11px 13px;
}
.opfield{display:flex;flex-direction:column;gap:4px;font-size:11.5px;min-width:150px;}
.opfield > span{
  font-size:10px;letter-spacing:.1em;text-transform:uppercase;
  color:var(--op-muted);font-weight:700;
}
.opfield-wide{flex:1 1 200px;}
.opfield select,.opfield input{
  background:var(--op-row);border:1px solid var(--op-line);color:var(--op-text);
  border-radius:6px;padding:6px 8px;font:inherit;font-size:12px;width:100%;
}
.opfield select:focus,.opfield input:focus{
  outline:2px solid var(--op-accent);outline-offset:1px;
}
.opbtn{
  background:var(--op-row);border:1px solid var(--op-line);color:var(--op-text);
  border-radius:6px;padding:7px 14px;font:inherit;font-size:12px;font-weight:600;
  cursor:pointer;text-decoration:none;display:inline-block;
}
.opbtn:hover{border-color:var(--op-accent);color:var(--op-accent);}
/* The refresh control: Refresh, the same button as Apply with an icon, then
   how long ago the screen was brought up to date (feeds/_refresh_control.html),
   in the menu row after the "?" since BIO-84.
   The status is body text; failed is the alert colour; New reports is a pill
   in it. Each measured against the panel in tests_live_refresh. */
.oprefresh{display:inline-flex;align-items:center;gap:10px;white-space:nowrap;}
.oprefresh-btn{display:inline-flex;align-items:center;gap:6px;}
/* In the menu row it reads as a menu item (1 October): no border in any
   state, nothing behind it at rest, and the accent on hover as before. The
   border's pixel goes into the padding, so the button keeps its size. Focus
   keeps the browser's own ring, as the section tabs beside it do. */
.optabs .oprefresh-btn,.optabs .oprefresh-btn:hover,.optabs .oprefresh-btn:focus,
.optabs .oprefresh-btn:active{border:0;background:transparent;}
.optabs .oprefresh-btn{padding:8px 15px;}
.optabs .oprefresh-btn:hover{color:var(--op-accent);}
.oprefresh-status{font-size:12px;color:var(--op-text);font-variant-numeric:tabular-nums;}
.oprefresh-status[data-state="failed"]{color:var(--op-alert);font-weight:600;}
.oprefresh-new{font-size:11px;font-weight:700;color:#08171A;background:var(--op-alert);border-radius:4px;padding:2px 7px;}
.opbtn-quiet{font-weight:500;}
.opbtn-off{opacity:.45;cursor:default;}
.opbtn-off:hover{border-color:var(--op-line);color:var(--op-text);}

/* What is currently applied, and how to take it off. */
.opapplied{display:flex;flex-wrap:wrap;gap:7px;align-items:center;padding:9px 2px 0;}
.opapplied-say{font-size:11.5px;color:var(--op-muted);}
.optag{
  display:inline-flex;align-items:center;gap:6px;font-size:11.5px;
  background:var(--op-row);border:1px solid var(--op-line);color:var(--op-text);
  border-radius:999px;padding:3px 10px;text-decoration:none;
}
.optag:hover{border-color:var(--op-accent);color:var(--op-accent);}
.optag b{font-weight:700;}
.optag-clear{color:var(--op-muted);}
.opdropped{
  margin:8px 2px 0;font-size:11.5px;color:var(--op-severe);
}

/* The table and its record panel, side by side. */
.opsplit{display:grid;gap:12px;padding:12px 16px 16px;grid-template-columns:minmax(0,1fr);}
/* The record list is the work on this section, so it takes the height the
   panels on the operating picture take rather than the 290px a panel sharing a
   column with three others gets. A viewport measure, like the picture's own row,
   so it is proportionate on a laptop and on a wall display without choosing
   between them. */
.opcasualties .opscroll{max-height:none;height:clamp(320px,calc(100vh - 430px),820px);overflow:auto;}
/* The count floats right of the heading and drops below it when the panel is
   narrow. Contained here, or the scrolling table beside it shrinks to fit
   round the float (123px at 375px wide). */
.opcasualties > h2{display:flow-root;}
.opsplit-open{grid-template-columns:minmax(0,1fr) 10px min(var(--oprecord-w,340px),60vw,calc(100% - 378px));column-gap:4px;}
.opcasualties{margin-bottom:0;}
.oppanel h2 .opcount{
  float:right;font-size:10.5px;letter-spacing:.06em;text-transform:none;
  color:var(--op-muted);font-weight:500;
}
/* The record list is a table in a whole column rather than in a 380px panel,
   so it undoes the picture's column widths rather than inheriting them.
   
   Those widths exist because the live feed sits in a narrow panel beside the
   map and every column had to be told what to give up. Inherited here they
   truncated the time to "16..", the severity to "CRITICA" and the status to
   "MEDEVA...", in a table with four hundred spare pixels beside it. */
.optable-wide{table-layout:auto;font-size:12px;}
.optable-wide th:nth-child(n),.optable-wide td:nth-child(n){width:auto;}
.optable-wide th,.optable-wide td{
  white-space:nowrap;overflow:visible;text-overflow:clip;padding:6px 10px;
}
.optable-wide td.opcell-key{white-space:normal;}
/* With a record open beside the list at 1512 the list is 31px wider than its
   panel once Nation and Nationality are both there, and the panel clips it. The
   stage and the facility, the two longest phrases, wrap there instead. */
.opsplit-open .optable-wide td.opcell-yield{white-space:normal;}
.optable-wide th:nth-child(3),.optable-wide td:nth-child(3){
  width:44px;text-align:center;
}
.opsortable a{
  color:inherit;text-decoration:none;display:inline-flex;align-items:center;gap:4px;
}
.opsortable a:hover{color:var(--op-accent);}
.opsorted{color:var(--op-text);}
.opsortmark{font-size:8px;}
.opcell-time{font-variant-numeric:tabular-nums;}
.opcell-open{text-align:right;}
.opopenlink{color:var(--op-accent);text-decoration:none;font-weight:600;}
.opopenlink:hover{text-decoration:underline;}
.oprow.opopen{background:var(--op-row);}
/* The open record's row, marked two ways so neither depends on the other: a
   2px border in the Open link's green, and an arrowhead after the link that
   is a shape before it is a colour. The border is drawn as inset shadows on
   the cells, which take no space, and the arrow's slot is reserved on every
   row, so opening or closing a record moves nothing. */
.oprow.opopen td{box-shadow:inset 0 2px 0 var(--op-accent),inset 0 -2px 0 var(--op-accent);}
.oprow.opopen td:first-child{box-shadow:inset 2px 0 0 var(--op-accent),inset 0 2px 0 var(--op-accent),inset 0 -2px 0 var(--op-accent);}
.oprow.opopen td:last-child{box-shadow:inset -2px 0 0 var(--op-accent),inset 0 2px 0 var(--op-accent),inset 0 -2px 0 var(--op-accent);}
.opcell-open::after{
  content:"";display:inline-block;width:0;height:0;margin-left:7px;vertical-align:-1px;
  border-top:6px solid transparent;border-bottom:6px solid transparent;
  border-left:8px solid transparent;
}
.oprow.opopen .opcell-open::after{border-left-color:var(--op-accent);}
.opempty{padding:18px 0;text-align:center;color:var(--op-muted);}
.opnone{color:var(--op-muted);font-style:italic;}
.oppager{display:flex;gap:10px;align-items:center;justify-content:flex-end;padding-top:10px;}
.oppager-say{font-size:11.5px;color:var(--op-muted);}

/* One casualty, in full. */
.opdetail{margin-bottom:0;align-self:start;}
.opdetail h2{display:flex;justify-content:space-between;align-items:center;gap:8px;}
.opclose{
  display:inline-flex;align-items:center;gap:5px;color:var(--op-muted);
  text-decoration:none;font-size:10.5px;letter-spacing:.06em;
}
.opclose:hover{color:var(--op-accent);}
.opdetail-head{display:flex;flex-wrap:wrap;align-items:center;gap:8px 12px;margin-bottom:10px;}
.opdetail-nat{display:inline-flex;align-items:center;gap:6px;font-size:12px;}
.opdetail-nat .opnat-code{margin-left:0;}
.opdetail-natlabel{
  font-size:9.5px;letter-spacing:.1em;text-transform:uppercase;font-weight:700;
  color:var(--op-muted);
}
.opdetail-head3{
  margin:14px 0 6px;font-size:10px;letter-spacing:.12em;text-transform:uppercase;
  color:var(--op-muted);font-weight:700;
  border-top:1px solid var(--op-line);padding-top:10px;
}
.opdetail-facts{
  display:grid;grid-template-columns:auto 1fr;gap:4px 12px;margin:0;font-size:11.5px;
}
.opdetail-facts dt{color:var(--op-muted);}
.opdetail-facts dd{margin:0;color:var(--op-text);overflow-wrap:anywhere;}
/* A posted report's card: one label width for every section, so the values
   line up down the card rather than moving with each section's longest label. */
.opdetail-facts[data-section]{grid-template-columns:11em minmax(0,1fr);}
.opdetail-blank{
  margin:14px 0 0;padding-top:10px;border-top:1px solid var(--op-line);
  font-size:11px;color:var(--op-muted);
}

/* The summary row on a section, which is the picture's counter row in a
   narrower column rather than a second kind of card. */
.opcounters-tab{padding:8px 0 0;grid-template-columns:repeat(4,minmax(0,1fr));}

.optable-facilities td.opnum,.optable-facilities th:nth-child(n+4){
  text-align:right;font-variant-numeric:tabular-nums;
}
.optable-facilities th:nth-child(n+4) a{justify-content:flex-end;}
.opsub{color:var(--op-muted);font-size:10.5px;margin-left:4px;}
.opcell-bar{min-width:150px;}
.opcell-bar .opbar-track{width:100%;margin-bottom:3px;}
.opcell-bar b{font-size:10.5px;color:var(--op-text);}
/* One definition of pressure, drawn the same way the picture draws it. */
.opfac-at-capacity{background:var(--op-critical);}
.opfac-pressure{background:var(--op-critical);}
.opfac-busy{background:var(--op-severe);}
.opfac-normal{background:var(--op-accent);}
.opstate{
  display:inline-block;padding:2px 8px;border-radius:999px;font-size:10.5px;
  font-weight:700;color:#0B1418;
}
.opstate-at-capacity{background:var(--op-critical);}
.opstate-pressure{background:var(--op-critical);}
.opstate-busy{background:var(--op-severe);}
.opstate-normal{background:var(--op-accent);}
.optag-on{border-color:var(--op-accent);color:var(--op-accent);font-weight:600;}

.optable-evac td.opnum,.optable-evac th:nth-child(6),.optable-evac th:nth-child(9){
  text-align:right;font-variant-numeric:tabular-nums;
}
.opcell-mix{display:flex;gap:4px;flex-wrap:wrap;}
/* The mix, as one chip per severity present. Same colours as the severity
   chips everywhere else on this surface, so a reader already knows them. */
.opmix{
  display:inline-block;min-width:22px;text-align:center;padding:1px 5px;
  border-radius:4px;font-size:10.5px;font-weight:700;color:#0B1418;
}

.optable-admin td.opnum,.optable-admin th{white-space:nowrap;}
.optable-admin td.opcell-key{display:table-cell;}
.optable-admin td.opcell-key img{vertical-align:-2px;margin-right:5px;}
.oprow-warn td{background:rgba(240,85,90,.08);}
.opadmin-rule{margin:0 0 10px;font-size:11.5px;color:var(--op-muted);line-height:1.5;}
.opadmin-absent{margin:0;padding-left:18px;font-size:11.5px;color:var(--op-muted);line-height:1.55;}
.opadmin-absent li{margin-bottom:7px;}
.opadmin-absent b{color:var(--op-text);}
.opscroll-short{max-height:250px;}
/* A table that may be wider than a phone scrolls sideways inside its panel,
   and positioned so its screen reader labels stay inside it too. */
.opwide{position:relative;overflow-x:auto;}
.oppanel h2 .opopenlink{float:right;font-size:10.5px;text-transform:none;letter-spacing:0;}

/* The summary charts. Two to a row on a wide screen, one on a narrow one, and
   every one of them is the picture's own panel with a chart in it. */
.opanalytics{
  display:grid;gap:12px;padding:12px 16px 16px;
  grid-template-columns:repeat(auto-fit,minmax(min(430px,100%),1fr));
}
.opanalytics .oppanel{margin-bottom:0;}
.opnote-inline{margin:8px 0 0;border-radius:9px;}
.opnationbar{
  display:grid;grid-template-columns:58px minmax(0,1fr) 54px;gap:9px;
  align-items:center;font-size:11.5px;
}
.opnationbar-on .opnationbar-name{color:var(--op-accent);font-weight:700;}
.opnationbar-name{
  display:inline-flex;align-items:center;gap:5px;color:var(--op-text);
  white-space:nowrap;overflow:hidden;text-overflow:ellipsis;
}
.opnationbar-wide{grid-column:1;}
.opnationbar b{text-align:right;font-variant-numeric:tabular-nums;}
/* A track holding several fills side by side rather than one. */
.opnationbar .opbar-track{display:flex;}
.opnationbar .opbar-fill{border-radius:0;}
.opnationbar .opbar-fill:first-child{border-radius:3px 0 0 3px;}
.opnationbar .opbar-fill:last-child{border-radius:0 3px 3px 0;}
.oplegend-row{display:flex;flex-wrap:wrap;gap:10px;margin-bottom:10px;}
.oplegend-row li{padding:0;}
/* The distribution. Columns, because the shape is the point. */
.opspread{display:flex;align-items:flex-end;gap:4px;height:130px;}
.opspread-col{
  flex:1;display:flex;flex-direction:column;justify-content:flex-end;
  align-items:center;height:100%;gap:3px;
}
.opspread-bar{
  display:block;width:100%;background:var(--op-accent);border-radius:3px 3px 0 0;
  min-height:2px;
}
.opspread-col.fhovered .opspread-bar{background:var(--op-moderate);}
.opspread-tick{font-size:9px;color:var(--op-muted);}

.opvisually-hidden{
  position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;
  clip:rect(0 0 0 0);white-space:nowrap;border:0;
}

/* Below the rail's own breakpoint the record stacks under the list, and the
   handle goes: there is no width to share. Moved from 1400px on 29 September
   2026, so a 1400px screen keeps the record beside the list. */
@media (max-width:1180px){
  .opsplit-open{grid-template-columns:minmax(0,1fr);}
  .opsplit-handle{display:none;}
}

/* A map layer that has been switched off.
   
   The class is on the view, which is outside the fragment the picture replaces
   every eight seconds, so unticking a box survives a refresh. It used to be the
   `checked` attribute the template writes plus a `hidden` flag on each drawn
   layer, both inside that fragment, so the box ticked itself again on the next
   refresh. There is nothing to restore now because nothing is thrown away. */
.opview.oplayer-off-incidents [data-oplayer-target="incidents"],
.opview.oplayer-off-facilities [data-oplayer-target="facilities"],
.opview.oplayer-off-routes [data-oplayer-target="routes"],
.opview.oplayer-off-assets [data-oplayer-target="assets"],
.opview.oplayer-off-areas [data-oplayer-target="areas"],
.opview.oplayer-off-labels [data-oplayer-target="labels"],
.opview.oplayer-off-towns [data-oplayer-target="towns"],
.opview.oplayer-off-grid [data-oplayer-target="grid"],
.opview.oplayer-off-air [data-oplayer-target="air"],
.opview.oplayer-off-detail [data-geomap-detail]{display:none;}

/* Pressing and holding a key row shows only that kind of item
   (static/js/map-solo.js). Everything else a key row names, and the arrival
   rings and the focused area, fade to a tenth and take no pointer; the base
   map, the lines and the names stay as the cog has them, so the items keep
   their context.

   One rule per kind, naming everything but that kind, so the held kind's own
   opacity is never set and never has to be put back: the shading draws its
   counted regions at half the zoom's shade, and a rule restoring "full" would
   have drawn them twice as dark while held. Only inside the map's drawing, so
   the same symbols in the key and the layers list are never faded.

   The held kind shows even where the cog has it off, for the length of the
   hold only: these classes are the hold's and are removed on release, and the
   cog's classes are never touched. Later than the rule that hides a layer and
   more specific than it, so it wins on both counts. */
.opview.opsolo-clusters [data-geomap-shapes] :is(.geomap-shape,.gm-facilities,.gm-routes,.gm-assets,.gm-air,.gm-areas,.gm-pulses,.gm-asked),
.opview.opsolo-shading [data-geomap-shapes] :is(.gm-incidents,.gm-facilities,.gm-routes,.gm-assets,.gm-air,.gm-areas,.gm-pulses,.gm-asked),
.opview.opsolo-facilities [data-geomap-shapes] :is(.gm-incidents,.geomap-shape,.gm-routes,.gm-assets,.gm-air,.gm-areas,.gm-pulses,.gm-asked),
.opview.opsolo-air [data-geomap-shapes] :is(.gm-incidents,.geomap-shape,.gm-facilities,.gm-routes,.gm-assets,.gm-areas,.gm-pulses,.gm-asked),
.opview.opsolo-evacuations [data-geomap-shapes] :is(.gm-incidents,.geomap-shape,.gm-facilities,.gm-air,.gm-areas,.gm-pulses,.gm-asked),
.opview.opsolo-areas [data-geomap-shapes] :is(.gm-incidents,.geomap-shape,.gm-facilities,.gm-routes,.gm-assets,.gm-air,.gm-pulses,.gm-asked){opacity:.1;pointer-events:none;transition:opacity .12s ease;}
.opview.opsolo-clusters.oplayer-off-incidents [data-geomap-shapes] .gm-incidents,
.opview.opsolo-shading.oplayer-off-incidents [data-geomap-shapes] .geomap-shape,
.opview.opsolo-facilities.oplayer-off-facilities [data-geomap-shapes] .gm-facilities,
.opview.opsolo-air.oplayer-off-air [data-geomap-shapes] .gm-air,
.opview.opsolo-evacuations.oplayer-off-routes [data-geomap-shapes] .gm-routes,
.opview.opsolo-evacuations.oplayer-off-assets [data-geomap-shapes] .gm-assets,
.opview.opsolo-areas.oplayer-off-areas [data-geomap-shapes] .gm-areas{display:inline;}
@media (prefers-reduced-motion:reduce){
  .opview.opsolo [data-geomap-shapes] :is(.gm-incidents,.geomap-shape,.gm-facilities,.gm-routes,.gm-assets,.gm-air,.gm-areas,.gm-pulses,.gm-asked){transition:none;}
}
/* What the hold is showing, over the top of the map between the zoom buttons
   and the cog. Solid, so its contrast does not depend on the map under it.
   Empty, and drawn as nothing, while nothing is held; never display:none, so
   the live region stays in place for a screen reader. */
.gmsolo-say{
  position:absolute;top:10px;left:50%;transform:translateX(-50%);z-index:3;
  margin:0;max-width:calc(100% - 140px);pointer-events:none;
  background:var(--op-panel);color:var(--op-text);border:1px solid var(--op-accent);
  border-radius:7px;padding:5px 10px;font-size:12px;line-height:1.35;text-align:center;
}
.gmsolo-say:empty{padding:0;border:0;}

/* Air assets. One symbol for each kind, defined once in the map's own defs
   and drawn with `use` both on the map and in the layers panel, so the two are
   the same drawing. Light at base, the accent while flying, with a dark edge
   so a symbol reads over the relief and the shading alike. */
.gm-air use{
  color:var(--gm-label);stroke:var(--gm-label-ink);stroke-width:1.1px;
  paint-order:stroke fill;stroke-linejoin:round;
}
.gm-air-outbound use,.gm-air-inbound use{color:var(--op-accent);}
.gm-air g[data-tip]{cursor:help;}

/* The rail collapses to a narrow strip so the content can take the width.
   
   The class sits on the document element rather than on anything inside the
   page, so the state is applied by a script in the head before the first paint.
   Put on a wrapper inside <body> it would have to be applied after the rail had
   already been laid out, and the rail would flash open on every navigation.
   
   The width transitions. The rail's own contents are switched rather than
   faded, because a label crossfading to a letter reads as a glitch. */
.body{transition:grid-template-columns .18s ease;}
.railtoggle{
  display:flex;align-items:center;gap:8px;width:100%;background:none;border:0;
  color:var(--muted);font-size:11px;letter-spacing:.1em;text-transform:uppercase;
  padding:2px 18px 12px;cursor:pointer;font-family:var(--label);
}
.railtoggle:hover{color:var(--text);}
.railtoggle:focus-visible{outline:2px solid var(--brand-teal);outline-offset:-2px;}
/* A chevron, pointing the way the click goes. */
.railtoggle-arrow{
  width:7px;height:7px;flex:none;border-left:1.5px solid currentColor;
  border-bottom:1.5px solid currentColor;transform:rotate(45deg);
  transition:transform .18s ease;
}
.app-railshut .railtoggle-arrow{transform:rotate(-135deg);}
.app-railshut .body{grid-template-columns:44px 1fr;}
.app-railshut .rail-h,.app-railshut .scope,.app-railshut .previewnote,
.app-railshut .rail-version,.app-railshut .railtoggle-word{display:none;}
.app-railshut .rail hr{margin:8px 10px;}
.app-railshut .railtoggle{justify-content:center;padding:2px 0 12px;}
/* The letter stands in for the label. Every item stays present and clickable:
   a menu that empties when it is collapsed is a menu somebody has to remember
   the shape of. The title carries the full label for a pointer, and the link
   text is still read in full by a screen reader because it is the font size
   that is zero rather than the text that is gone. */
.app-railshut .rail a{padding:10px 0;text-align:center;font-size:0;}
.app-railshut .rail a::first-letter{font-size:14px;}
@media (prefers-reduced-motion: reduce){
  .body,.railtoggle-arrow{transition:none;}
}
/* Below 1040 the rail is already a band above the content rather than a left
   column, so there is no width to reclaim and nothing to collapse. The rule
   that lays it out that way is at line 569; this follows it rather than
   choosing a number of its own. */
@media (max-width:1040px){
  .railtoggle{display:none;}
  .app-railshut .body{grid-template-columns:1fr;}
  .app-railshut .rail-h,.app-railshut .scope,.app-railshut .previewnote,
  .app-railshut .rail-version{display:revert;}
  .app-railshut .rail a{padding:10px 18px;text-align:left;font-size:14px;}
}

/* The rail collapse control. The class lives on .opview, which is outside the
   fragment that refreshes, so the collapsed state survives a refresh without
   anything having to restore it. Collapsed narrows the rail rather than
   removing it: every control stays reachable, with its label carried by the
   tip instead of the text. */
.oprailtoggle{
  margin-left:auto;align-self:center;display:inline-flex;align-items:center;gap:7px;
  background:transparent;border:1px solid var(--op-line);border-radius:6px;
  color:var(--op-muted);font:inherit;font-size:11px;padding:4px 10px;cursor:pointer;
}
.oprailtoggle:hover{color:var(--op-text);border-color:var(--op-accent);}

/* The question mark right of Admin. Filled in the accent, bold, round, and the
   height of the tab labels' line with their own vertical padding, centred on
   them. A clear ring on keyboard focus. */
.ophelp{
  align-self:center;flex:none;width:26px;height:26px;margin-left:8px;padding:0;
  border:0;border-radius:50%;background:var(--op-accent);color:#08171A;
  font:inherit;font-size:15px;font-weight:800;line-height:26px;text-align:center;cursor:pointer;
}
.ophelp:hover{filter:brightness(1.1);}
.ophelp:focus-visible{outline:2px solid var(--op-text);outline-offset:2px;}

/* The "since your last visit" dialog. It never grows past the window: the
   header with its close button and the footer with its labelling stay put and
   the middle scrolls. No padding on the dialog itself, so a click that lands
   on it and not on its contents is a click on the backdrop. */
.opsince{
  padding:0;border:1px solid var(--op-line);border-radius:12px;
  background:var(--op-panel);color:var(--op-text);
  width:min(560px, calc(100vw - 32px));max-height:calc(100vh - 48px);overflow:hidden;
}
.opsince::backdrop{background:rgba(4,10,12,.62);}
.opsince-box{display:flex;flex-direction:column;max-height:calc(100vh - 48px);}
.opsince-head{display:flex;align-items:center;justify-content:space-between;gap:12px;
              padding:14px 18px 12px;border-bottom:1px solid var(--op-line);flex:none;}
.opsince-head h2{margin:0;font-size:12px;letter-spacing:.12em;text-transform:uppercase;
                 color:var(--op-muted);font-weight:700;}
.opsince-close{display:inline-flex;align-items:center;gap:6px;background:var(--op-row);
               border:1px solid var(--op-line);border-radius:6px;color:var(--op-text);
               font:inherit;font-size:12px;padding:4px 10px;cursor:pointer;}
.opsince-close:hover{border-color:var(--op-accent);}
.opsince-close:focus-visible{outline:2px solid var(--op-accent);outline-offset:2px;}
.opsince-body{flex:1 1 auto;min-height:0;overflow:auto;padding:14px 18px;}
.opsince-lede{margin:0;font-size:13px;line-height:1.5;}
.opsince-block{margin:0 0 16px;}
.opsince-block:last-child{margin-bottom:0;}
.opsince-block h3{margin:0 0 8px;font-size:10.5px;letter-spacing:.12em;text-transform:uppercase;
                  color:var(--op-muted);font-weight:700;}
.opsince-headline p{margin:4px 0 0;font-size:12px;color:var(--op-muted);}
.opsince-list{list-style:none;margin:0;padding:0;}
.opsince-list li{display:flex;align-items:center;justify-content:space-between;gap:10px;
                 padding:4px 0;font-size:12.5px;border-bottom:1px solid var(--op-line);}
.opsince-list li:last-child{border-bottom:0;}
.opsince-list b{font-variant-numeric:tabular-nums;}
.opsince-rest,.opsince-change{margin:6px 0 0;font-size:12px;color:var(--op-muted);line-height:1.45;}
.opsince-change{color:var(--op-text);margin:0;}
.opsince-foot{flex:none;padding:10px 18px 14px;border-top:1px solid var(--op-line);}
.opsince-quiet{display:flex;align-items:center;gap:8px;font-size:12px;color:var(--op-text);}
.opsince-note{margin:8px 0 0;font-size:11px;color:var(--op-muted);line-height:1.45;}
.oprailicon{width:11px;height:11px;border:1.5px solid currentColor;border-radius:2px;
            border-left-width:4px;}
.opview.oprail-shut .oprailicon{border-left-width:1.5px;border-right-width:4px;}

.opview.oprail-shut .opgrid{grid-template-columns:58px minmax(0,1fr) 380px;}
.opview.oprail-shut .oprail .oppanel{padding:10px 6px;}
.opview.oprail-shut .oprail h2{display:none;}
.opview.oprail-shut .oprail .opchips{gap:5px;}
.opview.oprail-shut .oprail .opchip{
  justify-content:center;padding:4px 0;width:100%;gap:0;
}
.opview.oprail-shut .oprail .opchip > span:not(.opflag){display:none;}
.opview.oprail-shut .oprail .opflag{font-size:11px;}
.opview.oprail-shut .oprail .opwindows{flex-direction:column;}

@media (max-width: 1500px){
  .opgrid{grid-template-columns:230px minmax(0,1fr) 330px;}
  .opview.oprail-shut .opgrid{grid-template-columns:58px minmax(0,1fr) 330px;}
}
@media (max-width: 1180px){
  .opgrid,.opview.oprail-shut .opgrid{grid-template-columns:minmax(0,1fr);}
  /* One column, so there is no width to reclaim and nothing to collapse. */
  .oprailtoggle{display:none;}
  .opview.oprail-shut .oprail h2,
  .opview.oprail-shut .oprail .opchip > span:not(.opflag){display:revert;}
  .opview.oprail-shut .oprail .opchip{width:auto;padding:4px 9px;gap:6px;}
  .opview.oprail-shut .oprail .opwindows{flex-direction:row;}
  .opbottom{grid-template-columns:repeat(auto-fit,minmax(260px,1fr));}
}

/* The Add a catalogue dialogue.
   
   Every control in it is a drawing of a control. They are filled in and styled
   normally rather than disabled, because a row of greyed out inputs reads as a
   screen that is broken rather than one that is not connected yet, and the
   notice at the top is the thing that has to be believed. What makes them inert
   is structural: there is no form element in the dialogue at all, so nothing
   can submit even with the script gone, the buttons are type="button", and
   every group carries a Mockup chip. Pressing one says so.
   
   The text inputs are `readonly` rather than `disabled`: readonly keeps normal
   ink and stays focusable and selectable, so a reader can copy the endpoint,
   and it cannot be typed into or submitted. */
.addcat-tabs{display:grid;grid-template-columns:1fr 1fr;gap:8px;margin:0 0 18px;}
.addcat-tab{
  display:flex;align-items:center;justify-content:center;gap:8px;
  padding:11px 14px;border:1px solid var(--hairline);border-radius:9px;
  background:var(--surface-2);color:var(--muted);text-decoration:none;
  font-family:var(--label);letter-spacing:.12em;text-transform:uppercase;
  font-size:11.5px;font-weight:600;
}
.addcat-tab:hover{color:var(--text);border-color:var(--brand-teal);}
.addcat-tab.on{
  background:var(--teal-wash);border-color:var(--brand-teal);
  color:var(--brand-teal-lift);box-shadow:inset 0 0 0 1px var(--brand-teal);
}
.addcat-tab:focus-visible{outline:2px solid var(--brand-teal);outline-offset:2px;}

/* The sixty columns, in their own scroller, so the sequence is checkable
   without the dialogue running past the bottom of the screen. */
.addcat-columns{max-height:260px;overflow:hidden auto;overscroll-behavior-y:contain;}
.addcat-columns table{width:100%;}
.addcat-columns thead th{position:sticky;top:0;background:var(--surface);z-index:1;}
.addcat-pos{font-family:var(--mono);color:var(--muted);width:70px;}
.addcat-columns:focus-visible{outline:2px solid var(--brand-teal);outline-offset:2px;}

.addcat-controls{
  margin-top:22px;padding:16px;border:1px solid var(--hairline);
  border-radius:10px;background:var(--surface-2);
}
.addcat-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(210px,1fr));gap:12px;}
.addcat-field{display:flex;flex-direction:column;min-width:0;}
.addcat-field > label{
  font-size:11px;letter-spacing:.06em;text-transform:uppercase;
  color:var(--muted);margin-bottom:5px;font-family:var(--label);
}
.addcat-field .input,.addcat-field .sel{height:38px;box-sizing:border-box;width:100%;}
.addcat-mono{font-family:var(--mono);font-size:12px;}
.addcat-file{display:flex;gap:8px;align-items:center;min-width:0;}
/* Not a button. It looks like the control a real file input would carry, and it
   is a span so there is nothing to press and nothing to fail. */
.addcat-fake{flex:none;pointer-events:none;opacity:.75;}
.addcat-file .input{flex:1;min-width:0;}
.addcat-suffix{display:flex;align-items:center;gap:8px;}
.addcat-suffix span{font-size:12px;color:var(--muted);white-space:nowrap;}
.addcat-actions{display:flex;gap:10px;flex-wrap:wrap;margin-top:14px;}
/* A value the platform shows rather than asks for. Greyed, so it reads as an
   example of what would be issued rather than as something to fill in, and
   still selectable so it can be copied. */
.addcat-shown{color:var(--muted);background:var(--surface-2);}

/* The nation picker. */
.addcat-picker{position:relative;}
.addcat-hidden-label{
  position:absolute;width:1px;height:1px;overflow:hidden;clip:rect(0 0 0 0);
  white-space:nowrap;
}
.addcat-picker > .input{width:100%;height:38px;box-sizing:border-box;}
.addcat-options{
  list-style:none;margin:4px 0 0;padding:4px;position:absolute;z-index:5;
  left:0;right:0;background:var(--surface);border:1px solid var(--hairline);
  border-radius:9px;box-shadow:0 12px 28px rgba(16,34,42,.14);
  max-height:220px;overflow:auto;
}
.addcat-options[hidden]{display:none;}
.addcat-option{
  display:block;width:100%;text-align:left;padding:7px 9px;border:0;
  border-radius:6px;background:none;cursor:pointer;font:inherit;font-size:13px;
  color:var(--text);
}
.addcat-option b{font-family:var(--mono);color:var(--muted);margin-right:7px;}
.addcat-option:hover,.addcat-option:focus-visible{background:var(--teal-wash);}
.addcat-check{
  display:flex;align-items:center;gap:8px;margin:10px 0 0;font-size:12.5px;
  color:var(--text);cursor:pointer;
}
.addcat-chosen{
  list-style:none;margin:10px 0 0;padding:0;display:flex;flex-wrap:wrap;gap:6px;
}
.addcat-chip{
  display:inline-flex;align-items:center;gap:7px;padding:3px 4px 3px 9px;
  border:1px solid var(--hairline);border-radius:999px;background:var(--surface);
  font-size:12px;
}
.addcat-chip b{font-family:var(--mono);color:var(--muted);margin-right:3px;}
.addcat-chip-x{
  width:19px;height:19px;border:0;border-radius:50%;background:var(--surface-2);
  color:var(--muted);cursor:pointer;font-size:13px;line-height:1;
  display:flex;align-items:center;justify-content:center;font-family:inherit;
}
.addcat-chip-x:hover{background:var(--control-wash);color:#604400;}
.addcat-chip-x:focus-visible{outline:2px solid var(--brand-teal);outline-offset:1px;}
.addcat-empty{margin:10px 0 0;}
.addcat-empty[hidden]{display:none;}
.addcat-said{
  margin:16px 0 0;padding:10px 13px;border-radius:9px;
  border:1px solid var(--control);background:var(--control-wash);
  color:#4A3600;font-size:12.5px;line-height:1.5;
}
.addcat-said[hidden]{display:none;}
@media (max-width:620px){
  .addcat-tabs{grid-template-columns:1fr;}
  .addcat-actions .btn{width:100%;}
}

/* --- the Ai panel on the Operational View --------------------------------
   The button sits beside the view switch at the switch's own height, 29px,
   and the EXERCISE pill is set to the same so the three share one line. Its
   corners are an app icon's rather than the switch's: the switch is already a
   full pill, so nothing can be rounder than it, and the brief's reference for
   the letterform was an icon. The panel is fixed over the right hand column,
   under the bar, and never over the map it moves; where it sits was measured
   at 1920 and at 1512 by 860 (DASHBOARD.md). */
.opexercise{height:29px;box-sizing:border-box;display:inline-flex;align-items:center;}
.opask-toggle{
  height:29px;min-width:38px;padding:0 10px;box-sizing:border-box;flex:none;
  border:0;border-radius:9px;background:var(--op-accent);color:#08171A;
  font:inherit;font-size:14px;font-weight:700;letter-spacing:.01em;line-height:1;
  display:inline-flex;align-items:center;justify-content:center;cursor:pointer;
}
.opask-toggle:hover{filter:brightness(1.08);}
.opask-toggle:focus-visible{outline:2px solid var(--op-accent);outline-offset:2px;}
.opask-toggle[aria-expanded="true"]{box-shadow:0 0 0 2px var(--op-bg),0 0 0 4px var(--op-accent);}
.opask-toggle.is-busy{animation:opask-breathe 1.2s ease-in-out infinite;}
.opask{
  position:fixed;right:16px;top:70px;z-index:60;
  width:360px;max-width:calc(100vw - 32px);max-height:calc(100vh - 86px);overflow:auto;
  box-sizing:border-box;padding:14px 16px 16px;
  background:var(--op-panel);color:var(--op-text);
  border:1px solid var(--op-line);border-radius:10px;
  box-shadow:0 18px 40px rgba(0,0,0,.45);
  display:flex;flex-direction:column;gap:10px;
}
.opask[hidden]{display:none;}
.opask-head{display:flex;align-items:center;justify-content:space-between;gap:10px;}
.opask-head h2{margin:0;font-size:14px;font-weight:600;letter-spacing:.02em;color:var(--op-text);}
.opask-close{border:0;background:transparent;color:var(--op-muted);font-size:20px;line-height:1;
  padding:2px 6px;border-radius:6px;cursor:pointer;}
.opask-close:hover{color:var(--op-text);background:var(--op-row);}
.opask-close:focus-visible{outline:2px solid var(--op-accent);outline-offset:1px;}
.opask-preview{margin:0;font-size:11.5px;color:var(--op-muted);}
.opask-form{margin:0;}
.opask-label{display:block;margin:0 0 5px;font-size:10.5px;letter-spacing:.14em;
  text-transform:uppercase;color:var(--op-muted);}
.opask-row{display:flex;gap:8px;}
.opask-input{flex:1;min-width:0;height:32px;box-sizing:border-box;padding:0 10px;
  border:1px solid var(--op-line);border-radius:7px;background:var(--op-bg);color:var(--op-text);
  font:inherit;font-size:13px;}
.opask-input:focus{outline:none;border-color:var(--op-accent);}
.opask-go{height:32px;padding:0 14px;border:0;border-radius:7px;background:var(--op-accent);
  color:#08171A;font:inherit;font-size:13px;font-weight:700;cursor:pointer;
  display:inline-flex;align-items:center;gap:7px;}
.opask-go:disabled{cursor:progress;opacity:.85;}
.opask-go:focus-visible{outline:2px solid var(--op-accent);outline-offset:2px;}
.opask-go.is-busy::before{content:"";width:11px;height:11px;border-radius:50%;box-sizing:border-box;
  border:2px solid currentColor;border-right-color:transparent;animation:opask-spin .8s linear infinite;}
.opask-examples,.opask-offer{display:flex;flex-direction:column;gap:6px;}
.opask-example{text-align:left;padding:7px 10px;border:1px solid var(--op-line);border-radius:7px;
  background:var(--op-row);color:var(--op-text);font:inherit;font-size:12.5px;line-height:1.35;cursor:pointer;}
.opask-example:hover{border-color:var(--op-accent);}
.opask-example:disabled{opacity:.6;cursor:progress;}
.opask-example:focus-visible{outline:2px solid var(--op-accent);outline-offset:1px;}
.opask-note{margin:2px 0 0;font-size:11.5px;color:var(--op-muted);}
.opask-answer{border-top:1px solid var(--op-line);padding-top:10px;}
.opask-answer:empty{display:none;}
.opask-reply{display:flex;flex-direction:column;gap:8px;}
.opask-asked{margin:0;font-size:12px;color:var(--op-muted);overflow-wrap:anywhere;}
.opask-asked-label{font-size:10.5px;letter-spacing:.14em;text-transform:uppercase;margin-right:6px;}
.opask-sentence{margin:0;font-size:13.5px;line-height:1.5;color:var(--op-text);}
.opask-figures{margin:0;display:grid;gap:0;font-size:12px;}
.opask-figure{display:flex;justify-content:space-between;gap:12px;padding:4px 0;
  border-bottom:1px solid var(--op-line);}
.opask-figure dt{color:var(--op-muted);}
.opask-figure dd{margin:0;font-variant-numeric:tabular-nums;font-weight:600;text-align:right;}
.opask-mapnote{margin:0;font-size:11.5px;color:var(--op-muted);}
/* The catching up screen (feeds/picture_catching_up.html): the picture's own
   ground, and one plain statement of what is happening. The picture's colour
   tokens live on .opview, which this page does not have, so each colour carries
   its value as a fallback. */
body:has(.opcatchup){background:#0B1418;}
body:has(.opcatchup) .stage{max-width:none;margin:0;padding:0;}
body:has(.opcatchup) footer{display:none;}
.opcatchup{
  min-height:100vh;box-sizing:border-box;padding:24px;
  display:flex;align-items:center;justify-content:center;
}
.opcatchup-box{max-width:460px;display:flex;flex-direction:column;align-items:flex-start;gap:10px;}
.opcatchup h1{margin:0;font-size:18px;font-weight:600;color:var(--op-text,#E6EEF2);}
.opcatchup p{margin:0;font-size:13px;line-height:1.5;color:var(--op-text,#E6EEF2);}
.opcatchup .opcatchup-note{font-size:12px;color:var(--op-muted,#9FB3BD);}
.opcatchup a{color:var(--op-accent,#67DCD3);}
.opcatchup-spin{
  width:18px;height:18px;border-radius:50%;box-sizing:border-box;
  border:2px solid var(--op-line,#22383F);border-top-color:var(--op-accent,#67DCD3);
  animation:btnspin .9s linear infinite;
}
@media (prefers-reduced-motion: reduce){.opcatchup-spin{animation:none;}}
/* A long list of figures, two to a row, so what comes after it stays in view. */
.opask-figures-two{grid-template-columns:1fr 1fr;column-gap:16px;}
/* What the picture cannot answer, set apart from what it counted. */
.opask-limits{
  margin:0;padding:2px 0 2px 10px;border-left:2px solid var(--op-line);
  font-size:12.5px;line-height:1.5;color:var(--op-text);
}
.opask-handoff{display:flex;flex-direction:column;align-items:flex-start;gap:8px;}
/* The panel's own control shape, outlined rather than filled so it does not
   read as the Ask button. */
.opask-tre{
  height:30px;padding:0 12px;border:1px solid var(--op-accent);border-radius:7px;
  background:transparent;color:var(--op-accent);font:inherit;font-size:12.5px;
  font-weight:600;cursor:pointer;
}
.opask-tre:hover{background:color-mix(in srgb,var(--op-accent) 14%,transparent);}
.opask-tre:focus-visible{outline:2px solid var(--op-accent);outline-offset:2px;}
.opask-tre:disabled{cursor:progress;opacity:.85;}
.opask-pathway{
  width:100%;box-sizing:border-box;padding:10px 12px;
  border:1px solid var(--op-line);border-radius:8px;background:var(--op-row);
}
.opask-pathway[hidden]{display:none;}
.opask-pathway .opask-label{margin-bottom:8px;}
.opask-steps{
  margin:0;padding:0;list-style:none;display:flex;flex-direction:column;gap:6px;
  font-size:12.5px;line-height:1.45;color:var(--op-text);
}
/* The answer's regions on the map: an outline in the accent that stays the
   same width however far in the map is, over a faint fill, and never in the
   way of a pointer. */
/* The focused area's outline. It was the assistant's alone; it is the
   focus's now, whichever way in set it, and it comes from the server so a
   refresh keeps it. */
/* The focused area NEVER TAKES A POINTER. With pointer events it swallowed
   the hover and the click of every marker, label and facility icon beneath it,
   so the tooltips stopped and the markers stopped opening. Stated on the group
   so it holds whatever the paint order later becomes. */
.gm-asked{pointer-events:none;}
.gm-asked path{fill:var(--op-accent);fill-opacity:.10;stroke:var(--op-accent);stroke-width:1.6;
  vector-effect:non-scaling-stroke;pointer-events:none;}
.geomap-return{position:absolute;left:50%;top:10px;transform:translateX(-50%);z-index:5;
  height:28px;padding:0 12px;border:1px solid var(--op-accent);border-radius:999px;
  background:var(--op-panel);color:var(--op-accent);font:inherit;font-size:12px;font-weight:600;cursor:pointer;}
.geomap-return[hidden]{display:none;}
.geomap-return:hover{background:var(--op-row);}
.geomap-return:focus-visible{outline:2px solid var(--op-accent);outline-offset:2px;}
@keyframes opask-spin{to{transform:rotate(360deg);}}
@keyframes opask-breathe{50%{filter:brightness(1.25);}}
@media (prefers-reduced-motion: reduce){
  .opask-go.is-busy::before,.opask-toggle.is-busy{animation:none;}
}

/* ---- Readings on the Casualties list -------------------------------------
   The ALERTS bar, the four reading columns, the history dialog and its chart,
   and the record panel's drag handle. Colours are the tokens above, and the
   triage chips are the rows' own `opsev` classes, so a T code looks the same
   everywhere on the screen. */
.opalerts{display:flex;gap:10px;align-items:stretch;flex-wrap:wrap;margin:0 0 12px;}
.optri-bar{flex:1 1 auto;display:flex;align-items:center;gap:12px;flex-wrap:wrap;background:var(--op-panel);border:1px solid var(--op-line);border-top:3px solid var(--op-alert);border-radius:6px;padding:8px 14px;}
.optri-bar-count{color:var(--op-alert);font-size:18px;font-weight:600;font-variant-numeric:tabular-nums;}
.optri-bar-word{font-family:var(--label);letter-spacing:.06em;font-size:18px;font-weight:600;color:var(--op-text);}
.optri-chips{display:flex;gap:6px;margin-left:auto;flex-wrap:wrap;}
.opwear-tri{width:20px;height:18px;flex:0 0 auto;vertical-align:-3px;}
.opwear-tri path{fill:none;stroke:var(--op-alert);stroke-width:2;stroke-linejoin:round;}
.opwear-tri .opwear-tri-mark{fill:var(--op-alert);stroke:none;}
/* A chip: the label in the rows' own triage colour, the count beneath it. */
.optri{display:inline-grid;grid-template-rows:auto auto;min-width:40px;text-align:center;border:1px solid var(--op-line);border-radius:4px;overflow:hidden;font-variant-numeric:tabular-nums;}
.optri .optri-label{display:block;margin:0;border-radius:0;padding:1px 6px;font-size:11px;font-weight:700;}
.optri .optri-label.optchip-T4{padding:0 5px;}
.optri-count{padding:2px 6px;font-size:16px;font-weight:600;color:var(--op-text);background:var(--op-row);}
.optri-silent{align-self:center;}
.optri-silent .optri-label{background:var(--op-muted);color:var(--op-chip-ink);}
/* The four readings: the value, then its icon at the height of the text. */
.opcell-read{white-space:nowrap;}
.opread{display:inline-flex;align-items:center;gap:5px;font-weight:600;font-variant-numeric:tabular-nums;color:var(--op-text);white-space:nowrap;text-decoration:none;}
.opread svg{width:1em;height:1em;flex:0 0 auto;}
.opread-hr svg{color:var(--op-hr);}
.opread-spo2 svg{color:var(--op-spo2);}
.opread-rr svg{color:var(--op-rr);}
.opread-battery svg{color:var(--op-battery);}
.opread-btn{cursor:pointer;border-bottom:1px dotted var(--op-muted);}
.opread-btn:hover .opread-value,.opread-btn:focus-visible .opread-value{color:var(--op-accent);}
.opread:focus-visible{outline:2px solid var(--op-accent);outline-offset:2px;border-radius:2px;}
.opread-low .opread-value{color:var(--op-severe);}
.opread-sim .opread-value{color:var(--op-muted);font-weight:400;}
.opread-simtag{margin-left:4px;font-size:10px;font-weight:600;letter-spacing:.05em;text-transform:uppercase;color:var(--op-muted);border:1px solid var(--op-line);border-radius:3px;padding:0 3px;}
.opread-notsent{color:var(--op-muted);font-size:11px;}
.opcell-sev{white-space:nowrap;}
.opsilentmark{margin-left:6px;padding:1px 4px;font-size:10px;font-weight:600;color:var(--op-chip-ink);background:var(--op-muted);border-radius:3px;vertical-align:middle;}
/* Time and severity stay in view while the wide table scrolls sideways
   inside its own area. */
.opcasualties .optable th:nth-child(1),.opcasualties .optable td:nth-child(1){position:sticky;left:0;z-index:1;width:52px;min-width:52px;background:var(--op-panel);}
.opcasualties .optable th:nth-child(2),.opcasualties .optable td:nth-child(2){position:sticky;left:52px;z-index:1;background:var(--op-panel);}
/* The header sticks to the top and the two pinned columns to the left, so they
   meet. The header goes over the rows scrolling up under it, and the two
   pinned header cells over everything, so the headings scrolling sideways go
   under them. With both at 1 the rows came later in the page and painted over
   the header: scrolled down, Time and Severity were covered (browser
   walkthrough, walk 6, 29 September). */
.opcasualties .optable thead th{z-index:2;}
.opcasualties .optable thead th:nth-child(-n+2){z-index:3;}
.opcasualties .oprow.opopen td:nth-child(-n+2),.opcasualties .oprow-link:hover td:nth-child(-n+2){background:var(--op-row);}
/* The history dialog: the page dims behind it at about sixty percent. */
.opdialog{width:min(640px,calc(100vw - 32px));max-height:calc(100vh - 48px);overflow:auto;padding:18px 20px 16px;border:1px solid var(--op-line);border-radius:10px;background:var(--op-panel);color:var(--op-text);}
.opdialog::backdrop{background:rgba(0,0,0,.6);}
.opdialog-x{position:absolute;top:10px;right:12px;width:30px;height:30px;border:1px solid var(--op-line);border-radius:50%;background:var(--op-row);color:var(--op-text);font-size:18px;line-height:1;cursor:pointer;}
.opdialog-x:focus-visible{outline:2px solid var(--op-accent);outline-offset:2px;}
.opreadings-head{display:flex;align-items:center;gap:10px;flex-wrap:wrap;padding-right:36px;}
.opreadings-title{margin:0;font-size:18px;font-weight:600;color:var(--op-text);}
.opreadings-device{color:var(--op-muted);font-size:12px;}
.opreadings-tabs{display:flex;gap:6px;flex-wrap:wrap;margin:12px 0 6px;}
.opreadings-tab{padding:3px 10px;border:1px solid var(--op-line);border-radius:999px;color:var(--op-text);font-size:12px;text-decoration:none;background:var(--op-row);}
.opreadings-tab.on{border-color:var(--op-accent);color:var(--op-accent);font-weight:600;}
.opreadings-latest{margin:8px 0 4px;}
.opreadings-value{font-size:26px;font-weight:600;font-variant-numeric:tabular-nums;}
.opreadings-spans{display:flex;gap:6px;flex-wrap:wrap;margin:8px 0;}
.opreadings-count{margin:4px 0 8px;font-size:12px;}
.opreadings-one{margin:14px 0;}
/* The view's own heading ink: the page's heading rule is the research theme's
   dark ink, which vanished on this panel. */
.opreadings-one .opdetail-h{margin:0 0 6px;font-size:11px;font-weight:700;letter-spacing:.08em;text-transform:uppercase;color:var(--op-text);}
.opreadings-one .opdetail-h .opnone{margin-left:6px;font-weight:400;letter-spacing:0;text-transform:none;font-style:normal;}
.opreadings-table td,.opreadings-table th{font-size:12px;}
.opchart{display:block;width:100%;height:auto;margin:6px 0;}
.opchart-grid{stroke:var(--op-line);stroke-width:1;}
.opchart-tick{fill:var(--op-muted);font-size:10px;}
.opchart-line{fill:none;stroke:currentColor;stroke-width:2;stroke-linejoin:round;}
.opchart-band{fill:currentColor;opacity:.15;}
.opchart-dot{fill:currentColor;}
.opchart-hr{color:var(--op-hr);}
.opchart-spo2{color:var(--op-spo2);}
.opchart-rr{color:var(--op-rr);}
.opchart-battery{color:var(--op-battery);}
/* The record panel's width: its saved value, never above sixty percent of the
   window, with a handle between the list and the record. */
/* Two caps, because one was not enough: 60 percent of the window alone left a
   1400px screen's list 50px wide beside the rail. The record also leaves the
   list at least 360px, measured against this grid, never the window. */
.opsplit-handle{position:relative;cursor:col-resize;border-radius:4px;touch-action:none;}
.opsplit-handle::before{content:"";position:absolute;top:0;bottom:0;left:4px;width:2px;background:var(--op-line);border-radius:1px;}
.opsplit-handle:hover::before,.opsplit-handle.dragging::before{background:var(--op-accent);}
.opsplit-handle:focus-visible{outline:2px solid var(--op-accent);outline-offset:2px;}
/* The same orange triangle on the map, on a place holding somebody in need. */
.gm-need path{fill:var(--op-alert);stroke:var(--op-bg);stroke-width:1.5;}

/* --- The Operational View on a tablet and a phone (ISSUES.md BIO-84) -------
   Measured at 1600, 1400, 1024, 768, 390 and 320 wide: the page itself never
   scrolls sideways. What is wider than the screen scrolls inside its own box:
   the section tabs in their strip, the tables in theirs, a dialog inside
   itself. Below 600 every control is at least 44 pixels tall to touch. */

/* The menu row: the tabs and the "?" in a strip that scrolls inside itself,
   then the refresh control, 24 pixels after the "?" while there is room and
   pinned at the right end when there is not, because the strip gives way and
   the control does not. Then the rail toggle, at the far right. */
.optabs{overflow:visible;align-items:center;gap:0;}
.optabs-strip{display:flex;gap:2px;align-items:center;flex:0 1 auto;min-width:0;
              overflow-x:auto;scrollbar-width:thin;}
.optabs > .oprefresh{flex:none;margin-left:24px;margin-right:12px;}
.optabs > .oprailtoggle{margin-left:auto;}

/* The refresh control's short form, used below 768: the icon, and "0 s".
   The words go out of sight and stay in the accessibility tree, so a screen
   reader still hears "Refresh now" and "Updated 0 s ago". */
.oprefresh-short{display:none;}
@media (max-width:767px){
  .oprefresh-btn-text,
  .oprefresh-full{position:absolute;width:1px;height:1px;margin:-1px;padding:0;
                  overflow:hidden;clip:rect(0 0 0 0);white-space:nowrap;border:0;}
  .oprefresh-short{display:inline;}
  .optabs > .oprefresh{margin-left:12px;gap:6px;}
  .optabs .oprefresh-btn{padding:8px 11px;}
}

/* The filter rail at 1180 and below, where it stops being a column and
   becomes a band above the panels: a panel the Filters button opens. Only when the
   page's script is running (`op-js`, set by it): without JavaScript there is
   no button to press, so the rail stays shown, stacked above the panels, and
   every filter stays reachable. */
@media (max-width:1180px){
  .oprailtoggle{display:inline-flex;}
  .opview.op-js:not(.oprail-open) .oprail{display:none;}
  .opview .opgrid:not(.opgrid-tab) > .opcol{height:auto;}
  .oppanel-map{min-height:min(70vh,520px);}
  .oppanel-map .opmap{min-height:min(70vh,520px);}
  /* The live feed scrolls sideways inside its panel, with the time and the
     triage chip pinned, as the Casualty Records table already does. */
  .opscroll{overflow:auto;}
  .opgrid:not(.opgrid-tab) .optable{min-width:640px;}
  .opgrid:not(.opgrid-tab) .optable th:nth-child(1),
  .opgrid:not(.opgrid-tab) .optable td:nth-child(1){position:sticky;left:0;z-index:1;width:52px;min-width:52px;background:var(--op-panel);}
  .opgrid:not(.opgrid-tab) .optable th:nth-child(2),
  .opgrid:not(.opgrid-tab) .optable td:nth-child(2){position:sticky;left:52px;z-index:1;background:var(--op-panel);}
  /* With no band the right hand column has no height to share, and its two
     panels, which share it at flex 1 1 0, closed to their headings (BIO-85).
     Each takes its own height here, its rows scrolling inside at the height
     the lists have everywhere else. */
  .opview .opgrid:not(.opgrid-tab) > .opcol:last-child > .oppanel{flex:none;}
  .opview .opgrid:not(.opgrid-tab) > .opcol:last-child .opscroll{flex:none;max-height:290px;}
  .gmkey{flex-wrap:wrap;}
  .gmkey-body{overflow-x:auto;}
  .opfilterbar{flex-wrap:wrap;}
}

/* Two columns of headline cards on a tablet, 1024 wide included. */
@media (max-width:1024px){
  .opcounters{grid-template-columns:repeat(2,minmax(0,1fr));}
}

/* A phone. One column of cards, the header's furniture allowed to wrap, and
   every control at least 44 pixels to touch. */
@media (max-width:599px){
  /* The title takes a line of its own and wraps, rather than squeezing to
     a sliver beside the logo and pushing the header past the screen. */
  .optitle{flex-basis:100%;order:3;}
  .opcounters{grid-template-columns:minmax(0,1fr);}
  .opbar{gap:8px 12px;padding:5px 12px;}
  .optab{min-height:44px;display:inline-flex;align-items:center;}
  .ophelp{width:44px;height:44px;line-height:44px;}
  .oprefresh-btn,.oprailtoggle,.opbtn{min-height:44px;}
  .opwindow{min-height:44px;display:inline-flex;align-items:center;justify-content:center;}
  /* The nation chips stay 29 pixel pills (BIO-64) and are 45 to touch: an
     invisible area 8 pixels above and below each, measured from the outside
     of the border (so 9 from the padding box it is placed against), and 16
     between rows, so two chips' areas meet and never overlap. The name's ellipsis is on its
     own span, so the chip no longer needs to clip. */
  .opview:not(.oprail-shut) .oprail .opchips{row-gap:16px;}
  .opview:not(.oprail-shut) .oprail .opchip{position:relative;overflow:visible;}
  .opview:not(.oprail-shut) .oprail .opchip::before{content:"";position:absolute;left:-1px;right:-1px;top:-9px;bottom:-9px;}
  .opgrid{padding:4px 10px 12px;}
  .opcounters{padding:6px 10px 0;}
}

/* Dialogs fit the screen and scroll inside themselves, with the close button
   kept in reach at the top while the rest scrolls. */
.opdialog{max-height:calc(100dvh - 24px);}
.opdialog-x{position:sticky;top:0;z-index:2;}
@media (max-width:599px){
  .opdialog{width:calc(100vw - 16px);padding:12px;}
  .opdialog-x{min-width:44px;min-height:44px;}
}
