/* SiteSurvey — approved design system.
   Tokens and layout ported verbatim from the approved index.html shell:
   light background, calm teal accent, dark mode, 860px breakpoint
   (sidebar on web, bottom nav on mobile). Admin screens extend the same tokens. */

/* 2026-07-26 — readability pass, measured with scripts/audit_ui.py.
   --ink-3 was #93a0aa in light (2.68:1 on --panel) and #6f7d87 in dark
   (3.88:1), so every caption, status line, timestamp and the Edit/Delete
   controls sat under the 4.5:1 minimum on seven screens in both themes.
   --ok / --warn / --bad were carrying small bold text on their own -soft
   backgrounds at 3.27 to 4.30, and those pills say draft / approved / sent /
   failed, which is the one thing on the card a surveyor must be able to read.
   Values below were computed against the real panel colours, then verified by
   re-running the probe. --on-accent exists because the accent is lightened for
   dark mode: white on it measured 2.48:1. */
:root{
  --bg:#f7f8f9; --panel:#ffffff; --panel-2:#f1f3f5; --line:#e6e9ec;
  /* The gap the phone's own controls need under the fixed bar. ONE definition,
     because five different places sit above that bar and five hand-tuned
     numbers is how they drifted apart before. Anything anchored to the bottom
     adds `var(--nav-gap)` rather than inventing its own figure. */
  --nav-gap:max(18px, env(safe-area-inset-bottom));
  --ink:#1c2126; --ink-2:#5b656e; --ink-3:#68737d;
  --accent:#2f7d92; --accent-ink:#215d6e; --accent-soft:#e3f0f2;
  --on-accent:#ffffff;
  --ok:#276a4e; --ok-soft:#e4f1eb; --warn:#8a5911; --warn-soft:#f7eede;
  --bad:#a94040; --bad-soft:#f6e6e6;
  --shadow:0 6px 22px -12px rgba(20,30,40,.18);
  --sans:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Helvetica,Arial,sans-serif;
  --mono:ui-monospace,SFMono-Regular,Menlo,monospace;

  /* ---------- TYPE SCALE (2026-07-26) ----------
     Colour has been tokenised since day one; type never was. There were 100
     hard-coded font-size declarations across 17 values and 43 weights, so a
     screen could drift from the rest of the app and nothing would catch it —
     which is exactly what happened to Regulatory links, where the browser's
     own 16px body and 18.72px h3 went unnoticed because no rule claimed them.

     Every value below is the size that was ALREADY rendering; this changed
     nothing on screen. What it changes is where the decision lives: edit a
     token here and every screen moves together.

     Named by role, not by size, so the name survives a change of value.
     Consolidated 2026-07-26 with the operator: 17 steps down to 14. */
  --fs-badge:0.656rem;      /* status badges */
  --fs-pill:0.688rem;         /* pills, counts, sub-labels */
  --fs-body:0.781rem;       /* body, meta, hints, timestamps — the workhorse.
                               2026-07-26: --fs-meta (0.719) and --fs-hint
                               (0.75) folded in here. Three sizes inside one
                               pixel of each other is not a scale, it is three
                               people guessing, and a new screen had to pick
                               one of three indistinguishable "small" tokens.
                               That is how the drift started. */
  --fs-label:0.813rem;        /* field labels, segmented controls */
  --fs-control-sm:0.844rem; /* compact buttons and cells */
  --fs-control:0.875rem;    /* buttons. --fs-control-lg (0.906) folded in. */
  --fs-heading:0.938rem;      /* card and section headings */
  --fs-heading-sm:0.969rem; /* option-card heading */
  --fs-sheet:1rem;        /* full-screen sheet heading */
  --fs-brand:1.063rem;        /* wordmark, modal title */
  --fs-heading-lg:1.125rem;   /* report + analyse headings */
  --fs-title:1.188rem;        /* screen title */
  --fs-title-lg:1.438rem;     /* screen title, desktop */
  --fs-stat:1.5rem;         /* the big number on a KPI */
  --fs-input:1rem;        /* fields — below 16 iOS zooms the page on focus */

  --fw-normal:400;
  --fw-medium:500;
  --fw-semi:600;
  --fw-bold:650;
  --fw-heavy:700;
  --fw-stat:680;

  /* Body measure. Prose past ~70 characters is hard to track back to the next
     line; the Regulatory links paragraphs were running to 97. */
  --measure:68ch;

  /* How many lines a clamped meta line may use. It scales with text size on
     purpose: a fixed two-line clamp hides MORE the larger the type gets, so
     someone who turned text up to read it would end up seeing less of it.
     Bigger text, more room — otherwise the setting fights itself. */
  --clamp-meta:2;
}
:root[data-theme="dark"]{
  --bg:#12161a; --panel:#1a2026; --panel-2:#212a31; --line:#2b353d;
  --ink:#eef2f4; --ink-2:#a7b3bc; --ink-3:#808d97;
  --accent:#5cb0c4; --accent-ink:#8fd0e0; --accent-soft:#183138;
  --on-accent:#12161a;
  --ok:#5cc493; --ok-soft:#15281f; --warn:#e0a95a; --warn-soft:#2c2413;
  --bad:#e08585; --bad-soft:#2c1a1a; --shadow:0 8px 26px -14px rgba(0,0,0,.6);
}
@media (prefers-color-scheme:dark){:root:not([data-theme="light"]){
  --bg:#12161a; --panel:#1a2026; --panel-2:#212a31; --line:#2b353d;
  --ink:#eef2f4; --ink-2:#a7b3bc; --ink-3:#808d97;
  --accent:#5cb0c4; --accent-ink:#8fd0e0; --accent-soft:#183138;
  --on-accent:#12161a;
  --ok:#5cc493; --ok-soft:#15281f; --warn:#e0a95a; --warn-soft:#2c2413;
  --bad:#e08585; --bad-soft:#2c1a1a; --shadow:0 8px 26px -14px rgba(0,0,0,.6);
}}
*{box-sizing:border-box}
/* ---------- TEXT SIZE (2026-07-26, operator) ----------
   Surveyors and homeowners include people who cannot read small type, so text
   size is a setting rather than a fixed decision.

   Two things happen here, and the first matters more than the second.

   1. `font-size:100%` means the root follows the BROWSER's own text-size
      preference. Someone who has already set large text system-wide gets it
      here without touching this app. Every --fs-* token is in rem, so they
      all scale from this one value.
   2. The data-text steps below are for everyone else — the people who need
      bigger type but do not know where that setting lives.

   The two compound, which is correct: a browser set to 125% plus "Large"
   here lands larger still. */
/* Mobile browsers default `text-size-adjust` to `auto`, which INFLATES text by
   a factor they compute from each screen's own layout. Two screens with byte
   identical markup therefore render that markup at different sizes — which is
   how the bottom bar came to show two items on one screen and four on another
   (item 84, reported 2026-07-27 with a DOM paste from each screen proving the
   markup was identical). We set our own type scale; nothing may rescale it. */
html{font-size:100%;-webkit-text-size-adjust:100%;text-size-adjust:100%}
:root[data-text="large"]{font-size:115%;--clamp-meta:3}
:root[data-text="larger"]{font-size:130%;--clamp-meta:4}
:root[data-text="largest"]{font-size:145%;--clamp-meta:5}
html,body{margin:0;height:100%}
/* 2026-07-26 — the app never set a base font-size, so anything without an
   explicit rule fell through to the browser's 16px. That is the root cause
   behind Regulatory links rendering larger than the rest of the app: its
   markup is built in JS and carried no classes, so nothing claimed it. The
   base is now the app's own body size, which means an unclassed element
   inherits the theme instead of the user agent. */
body{background:var(--bg);color:var(--ink);font-family:var(--sans);
  font-size:var(--fs-body);-webkit-font-smoothing:antialiased;line-height:1.5}
svg{fill:none;stroke:currentColor;stroke-width:1.8}
button{font:inherit;cursor:pointer}
/* Fields stay at 16px on purpose: iOS zooms the whole page when a field
   smaller than that takes focus. This was previously inherited from the
   browser default and would have silently shrunk when the base above was set,
   so it is now stated as a token with the reason attached. */
input,select,textarea{font-family:inherit;font-size:var(--fs-input);
  color:var(--ink);background:var(--panel);
  border:1px solid var(--line);border-radius:11px;padding:11px 12px;width:100%}
input:focus,select:focus,textarea:focus{outline:2px solid var(--accent);
  outline-offset:1px;border-color:var(--accent)}
a{color:var(--accent-ink)}
:focus-visible{outline:2px solid var(--accent);outline-offset:2px}

/* ---------- APP SHELL (mobile-first) ---------- */
.app{min-height:100dvh;display:flex;flex-direction:column}
.side{display:none}
.main{flex:1;display:flex;flex-direction:column;min-width:0}
.top{display:flex;align-items:center;gap:12px;padding:16px 18px;position:sticky;
  top:0;background:var(--bg);z-index:5}
.top h1{margin:0;font-size:var(--fs-title);letter-spacing:-.01em;font-weight:var(--fw-bold)}
.top .sub{font-size:var(--fs-body);color:var(--ink-3);margin-top:1px}
.top .sp{flex:1}
.tbtn{color:var(--ink-2);background:var(--panel);border:1px solid var(--line);
  border-radius:999px;padding:8px;width:38px;height:38px;display:grid;place-items:center}
.tbtn svg{width:17px;height:17px}
.content{flex:1;padding:4px 18px calc(100px + var(--nav-gap));max-width:1180px;width:100%;margin:0 auto}

/* The BOTTOM inset was handled and the SIDES were not (found 2026-07-27). On a
   notched phone in landscape, `env(safe-area-inset-left/right)` is the rounded
   corner and the camera housing — without padding for them the first and last
   items sit under the bezel and are unreachable. Portrait is unaffected, which
   is why it survived every portrait measurement. */
.nav{position:fixed;left:0;right:0;bottom:0;display:flex;background:var(--panel);
  border-top:1px solid var(--line);
  padding-top:8px;
  /* ⚠️ `env(safe-area-inset-bottom)` IS ZERO on most of the devices that still
     need a gap (operator, 2026-09-01, from a real screen: "you have to give a
     gap for the device controls at the device bottom edge"). It reports the
     iPhone home indicator and nothing else — an Android gesture bar, a
     browser's own bottom chrome and every desktop window all return 0, so the
     bar was left with 8px and the labels sat on the very edge.
     `max()` makes the inset a FLOOR, not the whole answer: 18px everywhere,
     and more than that where the device actually declares a bigger unsafe
     area. Removing the env() term instead would have re-broken the notched
     phones it was added for on 2026-07-27. */
  padding-bottom:calc(8px + var(--nav-gap));
  padding-left:max(6px, env(safe-area-inset-left));
  padding-right:max(6px, env(safe-area-inset-right));
  z-index:10}
/* `flex:1` alone is `flex:1 1 0%` with `min-width:AUTO` — a flex item will not
   shrink below its own min-content width, so each label acted as a floor. Once
   the labels no longer fitted, the flex line overflowed the viewport and the
   LAST buttons were pushed off the right edge and clipped: still in the DOM,
   still unhidden, simply not on screen. That is the whole of item 84, fixed
   2026-07-27 — `min-width:0` lets them actually shrink, `overflow:hidden`
   clips a label instead of letting it shove a sibling off the bar, and
   `nowrap` keeps every item one line tall so the bar cannot grow a second row.
   The bar must survive five items on the narrowest phone; there is no width at
   which silently dropping a destination is acceptable. */
/* Type up, icon down (operator, 2026-09-01, from a real screen: "Make the menu
   fonts bigger and icons smaller, the gap is ok for now").
   ★ THE LABEL IS THE DESTINATION; the icon only decorates it, so the icon was
     twice the height of the word naming it. Label --fs-badge (0.656rem, the
     status-badge token — the SMALLEST type in the app) → --fs-body (0.781rem),
     the workhorse token already used for every hint and timestamp. Icon 22px →
     19px, which is NOT a new figure: it is what `.side nav button svg` already
     uses for the same icons in the desktop sidebar, so the two navs now agree.
   ★ WHY NOT --fs-label (0.813rem): this bar must survive FIVE items on a 320px
     phone, and `overflow:hidden` above clips a label rather than pushing a
     sibling off the bar — so an over-wide label loses a letter SILENTLY. At
     0.781rem "My team" and "Surveys" still fit the narrowest slot; 0.813rem
     was measured too tight to be worth the extra 0.4px.
   ★ THE GAP IS DELIBERATELY UNTOUCHED — `--nav-gap` and the 3px icon-to-label
     gap both stand, per the same instruction. Net button height moves ~-1px
     (icon -3, larger line-height +2), so the 44px touch minimum is unaffected;
     `audit_ui.py` re-run to prove it, not assumed. */
.nav button{flex:1 1 0;min-width:0;overflow:hidden;white-space:nowrap;
  background:none;border:0;color:var(--ink-3);display:flex;
  flex-direction:column;align-items:center;gap:3px;font-size:var(--fs-body);font-weight:var(--fw-semi);padding:4px}
.nav button svg{width:19px;height:19px}
.nav button[aria-current="true"]{color:var(--accent)}

/* ---------- shared ---------- */
.brand{display:flex;align-items:center;gap:10px;font-weight:var(--fw-bold);font-size:var(--fs-brand);
  letter-spacing:-.01em}
.brand .m{width:30px;height:30px;border-radius:9px;background:var(--accent);
  display:grid;place-items:center;flex:0 0 auto}
.brand .m svg{width:17px;height:17px;stroke:var(--on-accent);stroke-width:2}
.view{display:none;animation:f .25s ease}
.view.on{display:block}
@keyframes f{from{opacity:0;transform:translateY(6px)}to{opacity:1;transform:none}}
.h2{font-size:var(--fs-heading);letter-spacing:-.01em;margin:22px 0 10px;font-weight:var(--fw-bold)}
.h2:first-child{margin-top:8px}
/* 2026-07-26 — two shared patterns, deliberately generic and named by
   structure so any screen can use them. They exist because screens that build
   their own markup in JS had no titled-block or body-copy pattern to reach
   for, so they emitted bare <h3> and <p> and got the browser's defaults —
   18.72px headings and 16px body, larger than anything else in the app.
   .h3 is a heading INSIDE a card, one step under .h2.
   .prose is body copy, capped at a readable measure. */
.h3{font-size:var(--fs-heading);letter-spacing:-.01em;margin:0 0 2px;
  font-weight:var(--fw-heavy);color:var(--ink)}
.prose{font-size:var(--fs-body);color:var(--ink);margin:6px 0 0;
  max-width:var(--measure)}
.prose:first-child{margin-top:0}
.muted{color:var(--ink-3);font-size:var(--fs-body)}
.card{background:var(--panel);border:1px solid var(--line);border-radius:18px;padding:18px}
/* 2026-07-26 (operator) — a single column of cards on the same 18px rhythm the
   report grid uses. Screens that build their cards in JS dropped them straight
   into a bare <div>, so they sat edge to edge with no gap between them. */
.stack{display:grid;gap:18px}
/* A card that carries a warning. Replaces a 3px coloured left border that was
   being set inline in JS — a side-stripe, which this app does not use
   anywhere else, and which put a design decision in a script where no theme
   change could reach it. Tinted surface + full border instead, from the same
   --warn pair every other warning on the app already uses. */
.card.warn{background:var(--warn-soft);border-color:var(--warn)}
.card.warn .h3{color:var(--warn)}
/* ISSUE (measured 2026-07-27, item 74): `.hint` is --ink-3, which was chosen
   against --panel. On the warn TINT it measures 4.21:1 in light mode — under
   the 4.5 floor — so every explanatory note inside a caution card was failing,
   not only the new ones. Found by check_counters.py running audit_ui's own
   probe over a report screen with counters on it.
   FIX: one step darker on the tinted surface. Same shape as the .h3 rule
   above, which exists for the same reason. A colour is chosen against the
   background it actually lands on, not against the default panel. */
.card.warn .hint{color:var(--ink-2)}
/* An item that has not been read yet. Replaces a second inline side-stripe,
   this one on the admin inbox. The border is reinforcement only — an unread
   message also carries a "Mark read" button that a read one does not, so the
   state is never colour alone. The background is deliberately NOT tinted: an
   --accent-soft surface pulled --ink-3 down to 4.02:1 and failed the readable
   minimum on the very card it was meant to highlight (measured 2026-07-26). */
.card.unread{border-color:var(--accent);box-shadow:0 0 0 1px var(--accent)}

/* ---------- the evidence behind a counter-suggestion (item 74, 2026-07-27) ---
   A <details> block, because the platform's own disclosure control is already
   keyboard-operable, already announced to a screen reader, and already keeps
   itself open while the surveyor looks at the photograph again. The summary is
   a 44px target in its own right at phone width, where the whole card is.
   Tokens only — no size, weight or colour decided here. */
.evid{margin-top:10px;border-top:1px solid var(--line);padding-top:10px}
.evid summary{font-size:var(--fs-body);font-weight:var(--fw-semi);
  color:var(--ink);cursor:pointer;padding:10px 0;min-height:44px;
  display:flex;align-items:center}
.evid summary::marker{color:var(--ink-2)}
.evid .prose{color:var(--ink)}
/* The card is already tinted for a caution, so the divider has to read against
   the tint rather than against the panel. */
.card.warn .evid{border-top-color:var(--warn)}

/* ---------- empty · loading · error (item 63, 2026-07-27) ----------
   One pattern for the three things every screen does between "asked" and
   "showing something". Before this each screen invented its own, and two of
   the three were genuinely broken:

     * an ERROR set `body.textContent = e.message` on a container carrying
       .muted, so the one message the user most needs to read rendered in
       --ink-3, the FAINTEST ink in the app — the exact thing DESIGN_SYSTEM.md
       forbids ("never the main message");
     * most screens showed no error at all. They toasted and left the body
       empty, so once the toast faded the user was looking at a blank screen
       with no explanation and no way to retry.

   .statebox is deliberately NOT a .card: it is what stands in place of the
   content, so it carries no surface of its own and does not read as an item
   in a list of cards. */
.statebox{display:flex;flex-direction:column;align-items:center;
  justify-content:center;gap:10px;text-align:center;padding:40px 20px}
.statebox .h3{color:var(--ink)}
.statebox .prose{color:var(--ink-2);margin:0;text-align:center}
/* The error variant tints, so a failure is not carried by wording alone. It
   pairs the colour with a heading and a retry button — state by colour alone
   is banned. */
.statebox.bad{background:var(--bad-soft);border:1px solid var(--bad);
  border-radius:18px}
.statebox.bad .h3{color:var(--bad)}
.statebox.bad .prose{color:var(--bad)}
/* A small spinner for the loading variant, on the same keyframes the analysis
   ring already uses rather than a second animation that could drift from it. */
.spinner{width:26px;height:26px;border-radius:50%;border:3px solid var(--panel-2);
  border-top-color:var(--accent);animation:spin 1s linear infinite}
/* Someone who has asked the browser to stop moving things should not be given
   a spinner that never stops. The ring still shows progress, it just holds
   still — the measured "N min left" line is what carries the reassurance. */
@media (prefers-reduced-motion:reduce){
  .spinner,.ring,.tick .now .d{animation:none}
}

/* ---------- layout utilities ----------
   Small, shared, and documented. They exist because the markup was carrying
   25 inline style="" attributes doing exactly these things, which put spacing
   and sizing decisions outside the theme where nothing could reach them. */
.w-full{width:100%}
.w-auto{width:auto}
.mt-0{margin-top:0}
.mb-0{margin-bottom:0}
.mt-md{margin-top:16px}
.mb-md{margin-bottom:18px}
.mt-lg{margin-top:18px}
.centered{text-align:center}
.stack-sm{display:grid;gap:12px}
.row.wrap{flex-wrap:wrap;gap:12px}
.minw-field{min-width:180px}
.measure{max-width:var(--measure)}
.pad-empty{text-align:center;padding:40px 0}
/* A label with a control beside it — checkbox plus wording, on one line. */
.checkrow{display:flex;gap:9px;align-items:center;font-size:var(--fs-label);
  margin-top:12px;cursor:pointer}
.checkrow input{width:auto}
/* Sizing and alignment utilities the JS-built screens were doing inline. */
.mt-sm{margin-top:9px}
.mb-sm{margin-bottom:11px}
.self-end{align-self:end}
.noshrink{flex:0 0 auto}
.minw-sm{min-width:150px}
.maxw-xs{max-width:110px}
.maxw-sm{max-width:230px}
/* A compact ghost button, for a control that sits beside a field. */
.ghost.compact{padding:8px 12px;font-size:var(--fs-body)}
/* The colour swatch in a chart legend. The series colour is data, so it stays
   an attribute — but which token it uses is a theme decision, so it is named
   here rather than written as a literal. */
.legend i.series-a{background:var(--accent)}
.legend i.series-b{background:var(--ok)}
.row{display:flex;gap:10px;align-items:center}
/* A heading laid out inside a row is a flex item, so its block margins only
   push the row around. Shared, because every screen that puts a title beside
   a badge hits this. */
.row > .h2,.row > .h3{margin:0}
/* A link that is also a tap target. Inline links inherit the line box and come
   out around 24px tall, under the 44px minimum, which is a problem on any
   screen used on a phone — not just the one where it was first measured. */
.taplink{display:inline-flex;align-items:center;min-height:44px}
.grow{flex:1}
.field{margin-bottom:12px}
/* `.field .lbl` is for a group of controls that has no single control to point
   at — a radiogroup, say — where `<label for>` has nothing valid to name.
   Added 2026-07-27 with the feedback rating row; same rule, so the two cannot
   drift apart the way a second hand-written label style would. */
.field label,.field .lbl{display:block;font-size:var(--fs-body);color:var(--ink-2);
  margin-bottom:5px;font-weight:var(--fw-semi)}
.hide{display:none !important}

.primary{background:var(--accent);color:var(--on-accent);border:0;border-radius:15px;
  padding:15px 20px;font-size:var(--fs-heading);font-weight:var(--fw-semi);display:inline-flex;
  align-items:center;justify-content:center;gap:9px;
  box-shadow:0 8px 18px -10px var(--accent)}
.primary svg{width:18px;height:18px;stroke:var(--on-accent);stroke-width:2.2}
.primary:disabled{opacity:.5;box-shadow:none;cursor:not-allowed}
.ghost{background:var(--panel-2);border:1px solid var(--line);color:var(--ink);
  border-radius:14px;padding:13px 16px;font-weight:var(--fw-semi);font-size:var(--fs-control)}
.go{background:var(--accent);border:0;color:var(--on-accent);border-radius:14px;padding:13px 16px;
  font-weight:var(--fw-semi);font-size:var(--fs-control)}
.newbtn{position:fixed;right:18px;bottom:calc(84px + var(--nav-gap));z-index:9}
/* The floating New button sits over the page. On Capture — now a long form of
   checklist, chips, readings and notes — it was covering a field, so that
   screen scrolls clear of it. */
#v-capture{padding-bottom:72px}

/* filter bar + survey pickers
   2026-07-25: the bar was flex rows with fixed wrapping, which between roughly
   860px and 1150px broke into four ragged lines with single controls stretched
   full-width. Now one auto-fit grid: the column count follows the space that is
   actually available, so it degrades evenly at every width with no dead zone. */
/* Collapsed height is one row. The surveys are the point of the screen; the
   filters are a tool for finding them. (2026-07-25) */
.filterbar{margin-bottom:14px;padding:11px 12px}
.ftop{display:flex;gap:9px;align-items:center}
/* 16px, NOT the smaller control size — see the note on `input,select,textarea`.
   Any field under 16px makes iOS zoom the whole page on focus, and the zoom
   STAYS after the field is left: from then on the viewport shows only part of
   the layout, so the bottom bar loses its last items off the right edge and
   `position:fixed` detaches while scrolling. Reported 2026-07-27 as "the bar
   shows 3 of 5 and disappears when I scroll" on the Edit/Approve screen; the
   base rule already guarded this and these three rules were undoing it. A
   compact field is not worth trapping someone at 1.7x zoom (item 84). */
.ftop input{padding:9px 11px;font-size:var(--fs-input);border-radius:10px;flex:1;min-width:0}
.ftop .ghost{flex:0 0 auto;padding:9px 13px;font-size:var(--fs-label);white-space:nowrap}
.fbadge{display:inline-grid;place-items:center;min-width:17px;height:17px;
  padding:0 4px;margin-left:6px;border-radius:9px;background:var(--accent);
  color:var(--on-accent);font-size:var(--fs-badge);font-weight:var(--fw-heavy)}
.fgrid{display:grid;gap:10px;margin-top:11px;padding-top:11px;
  border-top:1px solid var(--line);
  grid-template-columns:repeat(auto-fit,minmax(160px,1fr))}
.fcell{min-width:0}
.fcell label{display:block;font-size:var(--fs-body);color:var(--ink-3);font-weight:var(--fw-semi);
  margin-bottom:4px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.fcell input,.fcell select{padding:9px 10px;font-size:var(--fs-input);border-radius:10px;
  max-width:100%}   /* 16px — iOS zoom guard, see .ftop input above (item 84) */
.fbtns{display:flex;align-items:center;gap:10px;flex-wrap:wrap;margin-top:12px}
.fbtns .ghost{padding:9px 14px;font-size:var(--fs-label);flex:0 0 auto;margin-left:auto}
#filterCount{font-size:var(--fs-body)}
.ftoggle{display:none}
.ftoggle[aria-expanded="true"]{border-color:var(--accent);
  background:var(--accent-soft);color:var(--accent-ink)}

/* Below two usable columns the search box spans the full width instead. */
@media (max-width:420px){ .fcell.wide{grid-column:span 1} }

/* On a phone, nine stacked controls filled the whole screen and pushed the
   survey list below the fold. Keep search visible, collapse the rest behind a
   Filters button. (2026-07-25) */
@media (max-width:719px){
  .fgrid > .fcell:not(.wide){display:none}
  .filterbar.open .fgrid > .fcell{display:block}
  .ftoggle{display:inline-flex;margin-left:auto}
  .fbtns .ghost#clearFilters{margin-left:0}
  .filterbar:not(.open) #clearFilters{display:none}
  /* keep the floating New button clear of the button row */
  .fbtns{padding-right:76px}
}

.picker{display:flex;flex-wrap:wrap;align-items:center;gap:10px;
  margin-bottom:16px;padding:14px}
.picker label{font-size:var(--fs-label);font-weight:var(--fw-semi);color:var(--ink-2);flex:0 0 auto}
/* This one is on the Edit/Approve screen, which is where the zoom was actually
   reported — it is the only focusable field there. 16px, see .ftop input. */
.picker select{flex:1 1 220px;padding:10px 11px;font-size:var(--fs-input)}
.picker p{margin:0;flex:0 0 auto;font-size:var(--fs-body)}

.choose.upload .opt{background:var(--panel-2)}
.choose.upload .ci{background:var(--panel)}

/* surveys — column count follows available width, never a fixed count */
.jobs{display:grid;gap:11px;
  grid-template-columns:repeat(auto-fill,minmax(330px,1fr))}
/* 2026-07-26 — `color` is not optional on a <button>. Without it the browser
   keeps its own `buttontext` default (black) and every child inherits that
   instead of the theme. In light mode black on white looked deliberate; in
   dark mode the card title measured 1.28:1 — invisible. `.job p` set --ink-3
   explicitly, which is why only the title broke. Same fix on .dcell below. */
.job{display:flex;gap:13px;align-items:center;padding:15px;border-radius:16px;
  background:var(--panel);border:1px solid var(--line);text-align:left;width:100%;
  color:var(--ink)}
.job .ic{width:42px;height:42px;border-radius:12px;background:var(--accent-soft);
  display:grid;place-items:center;flex:0 0 auto}
.job .ic svg{width:20px;height:20px;stroke:var(--accent-ink)}
/* The text block must be allowed to shrink, or a long "client · address ·
   date · time · captures" line forces the card wider than its column and wraps
   into four cramped lines. min-width:0 lets it truncate instead. */
.job > div{min-width:0;flex:1}
.job h3{margin:0;font-size:var(--fs-heading);letter-spacing:-.01em;
  overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
/* Two lines, then ellipsis: enough for "address · date · time · captures" in a
   narrow column without letting a long line blow the card out to four lines. */
.job p{margin:2px 0 0;font-size:var(--fs-body);color:var(--ink-3);
  display:-webkit-box;-webkit-line-clamp:var(--clamp-meta);
  -webkit-box-orient:vertical;line-clamp:var(--clamp-meta);overflow:hidden}
.pillr{margin-left:auto;font-size:var(--fs-pill);font-weight:var(--fw-semi);padding:4px 11px;
  border-radius:999px;white-space:nowrap}
.pillr.done{background:var(--ok-soft);color:var(--ok)}
.pillr.draft{background:var(--warn-soft);color:var(--warn)}
.pillr.bad{background:var(--bad-soft);color:var(--bad)}

/* capture */
.choose{display:grid;gap:13px;margin-top:4px;
  grid-template-columns:repeat(auto-fit,minmax(230px,1fr))}
/* ★ 2026-07-27 (item 62) — `color` is LOAD-BEARING here, do not remove.
   `.opt` is a <button>. A button that sets a background but no colour lets
   its children inherit the user agent's `buttontext`, which is BLACK — so all
   six capture headings rendered black on a dark panel at 1.28:1 and were
   effectively invisible in dark mode. Light mode masked it completely.
   This is the THIRD instance of the same bug: `.job` and `.dcell` were both
   fixed on 2026-07-26. This one survived only because Capture had no nav
   button, so audit_ui.py had never once measured the screen. */
.opt{display:flex;align-items:center;gap:15px;padding:18px;border-radius:18px;
  border:1px solid var(--line);background:var(--panel);color:var(--ink);
  text-align:left;width:100%}
.opt.hi{border-color:var(--accent);background:var(--accent-soft)}
.opt .ci{width:48px;height:48px;border-radius:14px;background:var(--panel-2);
  display:grid;place-items:center;flex:0 0 auto}
.opt.hi .ci{background:var(--panel)}
.opt .ci svg{width:23px;height:23px;stroke:var(--accent-ink)}
.opt h3{margin:0;font-size:var(--fs-heading-sm)}
.opt p{margin:2px 0 0;font-size:var(--fs-body);color:var(--ink-2)}
.offline{margin-top:16px;font-size:var(--fs-body);color:var(--ink-3);display:flex;gap:8px;
  align-items:center}
.offline i{width:8px;height:8px;border-radius:50%;background:var(--ok);
  display:inline-block;flex:0 0 auto}
.offline.off i{background:var(--warn)}

/* speed picker — surveyor-selectable analysis depth */
.speeds{display:grid;grid-template-columns:repeat(3,1fr);gap:9px;margin-top:6px}
.speed{border:1px solid var(--line);background:var(--panel);border-radius:14px;
  padding:12px 10px;text-align:left}
.speed.sel{border-color:var(--accent);background:var(--accent-soft)}
.speed b{display:block;font-size:var(--fs-control-sm);color:var(--ink)}
/* 2026-07-27 (item 62): was --ink-3, which measured 4.15:1 light and 4.02:1
   dark against the panel — under the 4.5:1 readable minimum, on the line that
   tells a surveyor which speed to pick. --ink-3 is for metadata; this is the
   explanation. 4.02 is the same number --ink-3 produced on the unread inbox
   card, recorded in failed-fixes.md — the token simply does not clear the bar
   at this size. */
.speed span{display:block;font-size:var(--fs-pill);color:var(--ink-2);margin-top:2px;line-height:1.35}
.speed.sel b{color:var(--accent-ink)}

/* Playback for an uploaded capture (item 68, 2026-07-27). A named class, not
   an inline style: a raw <video> would inherit nothing and sit at whatever
   width the browser felt like. Capped so a portrait phone video does not push
   the whole card off a laptop screen. */
.mediaview{margin-top:11px}
.mediaview img,.mediaview video{display:block;width:100%;max-height:62vh;
  object-fit:contain;border-radius:12px;background:var(--panel-2)}
.mediaview audio{width:100%}

.filelist{margin-top:12px;display:grid;gap:7px}
.fileitem{display:flex;gap:9px;align-items:center;font-size:var(--fs-body);
  background:var(--panel-2);border-radius:10px;padding:9px 11px}
.fileitem .x{margin-left:auto;color:var(--ink-3);font-size:var(--fs-pill)}
.queued{color:var(--warn)}

/* analysing */
.analyse{display:flex;flex-direction:column;align-items:center;justify-content:center;
  gap:20px;text-align:center;padding:52px 20px}
.ring{width:80px;height:80px;border-radius:50%;border:5px solid var(--panel-2);
  border-top-color:var(--accent);animation:spin 1s linear infinite}
@keyframes spin{to{transform:rotate(360deg)}}
.analyse h3{margin:0;font-size:var(--fs-heading-lg)}.analyse p{margin:0;font-size:var(--fs-control-sm);color:var(--ink-2)}
/* The measured time remaining, under the bar. Quieter than the stage list —
   it is reassurance, not instruction — but still above the readable floor. */
.analyse .anleft{font-size:var(--fs-body);color:var(--ink-3);min-height:1.2em}
.tick{display:flex;flex-direction:column;gap:9px;margin-top:6px;text-align:left}
.tick div{display:flex;gap:10px;align-items:center;font-size:var(--fs-label);color:var(--ink-3)}
.tick svg{width:16px;height:16px;stroke:var(--ok);stroke-width:2.4;flex:0 0 auto}
.tick .done{color:var(--ink-2)}
.tick .now{color:var(--ink);font-weight:var(--fw-semi)}
.tick .now .d{width:15px;height:15px;border-radius:50%;border:2px solid var(--accent);
  border-top-color:transparent;animation:spin .8s linear infinite;flex:0 0 auto}
.tick .pend .d{width:15px;height:15px;border-radius:50%;border:2px solid var(--line);
  flex:0 0 auto}
.bar{width:100%;max-width:320px;height:6px;background:var(--panel-2);border-radius:99px;
  overflow:hidden}
.bar i{display:block;height:100%;background:var(--accent);width:0;transition:width .4s ease}

/* report */
.rgrid{display:grid;gap:18px;
  grid-template-columns:repeat(auto-fit,minmax(300px,1fr))}
.rhead h3{margin:0;font-size:var(--fs-heading-lg);letter-spacing:-.01em}
.rhead p{margin:3px 0 0;font-size:var(--fs-body);color:var(--ink-3)}
.rhead .draftpill{display:inline-block;margin-top:10px;font-size:var(--fs-pill);font-weight:var(--fw-semi);
  color:var(--warn);background:var(--warn-soft);padding:4px 11px;border-radius:999px}
.find{display:flex;gap:11px;padding:11px 0;border-top:1px solid var(--line)}
.find:first-of-type{border-top:0}
.find .st{width:9px;height:9px;border-radius:50%;margin-top:6px;flex:0 0 auto}
.st.ok{background:var(--ok)}.st.attention{background:var(--warn)}.st.repair{background:var(--bad)}
.find h4{margin:0;font-size:var(--fs-control)}
.find p{margin:1px 0 0;font-size:var(--fs-body);color:var(--ink-2)}
.find .edit{margin-left:auto;background:none;border:0;color:var(--ink-3);font-size:var(--fs-body);
  font-weight:var(--fw-semi);flex:0 0 auto}
.rec{display:flex;gap:11px;padding:9px 0;font-size:var(--fs-label);align-items:baseline}
.rec b{color:var(--on-accent);background:var(--accent);font-size:var(--fs-pill);width:20px;height:20px;
  border-radius:6px;display:grid;place-items:center;flex:0 0 auto}
/* Priority 1-4. The number alone means nothing to a client, so the timeframe
   rides underneath it. */
.rec b.p1{background:var(--bad)}.rec b.p2{background:var(--warn)}
.rec b.p3{background:var(--accent)}.rec b.p4{background:var(--ink-3)}
.rec .rtext{display:block}
.rec .rwhen{display:block;font-style:normal;font-size:var(--fs-body);color:var(--ink-3);
  margin-top:2px}
.notecard textarea{min-height:92px;resize:vertical}
.asset{display:flex;justify-content:space-between;gap:10px;padding:9px 0;
  border-top:1px solid var(--line);font-size:var(--fs-body)}
.asset:first-of-type{border-top:0}.asset span{color:var(--ink-2)}
/* Version list: one row per report version, each viewable and printable. */
.vrow{display:flex;gap:11px;align-items:center;padding:9px 0;font-size:var(--fs-body);
  border-top:1px solid var(--line);flex-wrap:wrap}
.vrow:first-of-type{border-top:0}
.vrow b{color:var(--on-accent);background:var(--accent);font-size:var(--fs-pill);padding:3px 8px;
  border-radius:7px;flex:0 0 auto}
.vrow .grow{flex:1;min-width:140px;color:var(--ink-2)}
.vrow .ts{font-size:var(--fs-body);color:var(--ink-3);flex:0 0 auto}

/* ISSUE [botnav-short], found by MEASUREMENT 2026-07-28 after three wrong
   diagnoses. This row holds up to SEVEN buttons — View captures · Preview ·
   Download DOCX · Download PDF · Approve, plus Export and Delete inserted at
   runtime — and it had `display:flex` with no `flex-wrap`, while `flex:1`
   leaves `min-width:auto` so no button could shrink below its own label. On a
   390px phone the row therefore laid out **713px wide**.

   ★ THE DAMAGE IS NOT ON THIS ROW. A mobile browser widens the LAYOUT VIEWPORT
     to fit overflowing content, and `position:fixed` resolves against the
     layout viewport — so the bottom navigation bar grew to 714px on a 390px
     screen, put its five buttons at 140px each, and pushed the last three
     outside the visible area. It was reported as "the bottom bar shows 3 items
     of 5, and disappears when I scroll", on this screen only. Three diagnoses
     blamed the bar itself; the bar was never wrong.

   The lesson worth keeping: horizontal overflow anywhere on a screen breaks
   every `position:fixed` element on it. Never let a button row overflow. */
.rbtns{display:flex;flex-wrap:wrap;gap:11px;margin-top:6px}
.rbtns button{flex:1 1 140px;min-width:0;font-size:var(--fs-control);font-weight:var(--fw-semi);border-radius:14px;padding:14px}

/* deliver + settings */
.dgrid{display:grid;gap:11px;
  grid-template-columns:repeat(auto-fit,minmax(165px,1fr))}
.dcell{display:flex;align-items:center;gap:11px;padding:15px;border-radius:15px;
  border:1px solid var(--line);font-size:var(--fs-control-sm);font-weight:var(--fw-medium);background:var(--panel);
  text-align:left;color:var(--ink)}
.dcell.on{border-color:var(--accent);background:var(--accent-soft);color:var(--accent-ink)}
.dcell svg{width:19px;height:19px;flex:0 0 auto}
.lane{padding:15px 0;border-top:1px solid var(--line)}.lane:first-child{border-top:0}
.lane h4{margin:0 0 9px;font-size:var(--fs-control)}
.seg{display:flex;gap:8px}
.seg button{flex:1;border:1px solid var(--line);background:var(--panel);color:var(--ink-2);
  border-radius:11px;padding:11px;font-size:var(--fs-label);font-weight:var(--fw-semi)}
.seg button.sel{border-color:var(--accent);background:var(--accent-soft);color:var(--accent-ink)}
.note{font-size:var(--fs-body);color:var(--ink-3);margin-top:14px}

/* login */
.login{min-height:100dvh;display:grid;place-items:center;padding:24px}
.login .card{width:100%;max-width:380px}
.login .brand{justify-content:center;margin-bottom:6px}
.err{background:var(--bad-soft);color:var(--bad);border-radius:11px;padding:11px 13px;
  font-size:var(--fs-label);margin-bottom:12px}
.okmsg{background:var(--ok-soft);color:var(--ok);border-radius:11px;padding:11px 13px;
  font-size:var(--fs-label);margin-bottom:12px}

/* ---------- admin: analytics + ops ---------- */
.tabs{display:flex;gap:7px;overflow-x:auto;padding-bottom:4px;margin-bottom:14px}
.tabs button{border:1px solid var(--line);background:var(--panel);color:var(--ink-2);
  border-radius:999px;padding:8px 15px;font-size:var(--fs-body);font-weight:var(--fw-semi);white-space:nowrap}
.tabs button.sel{border-color:var(--accent);background:var(--accent-soft);color:var(--accent-ink)}
.kpis{display:grid;gap:11px;margin-bottom:16px;
  grid-template-columns:repeat(auto-fit,minmax(155px,1fr))}
.kpi{background:var(--panel);border:1px solid var(--line);border-radius:15px;padding:14px}
.kpi .v{font-size:var(--fs-stat);font-weight:var(--fw-stat);letter-spacing:-.02em;line-height:1.15}
.kpi .k{font-size:var(--fs-body);color:var(--ink-3);margin-top:3px;font-weight:var(--fw-semi)}
.kpi .sub{font-size:var(--fs-pill);color:var(--ink-3);margin-top:4px}
.kpi.live .v{color:var(--accent)}
/* A stat tile that GOES somewhere (item 71, 2026-07-27). Interactive has to
   look interactive — a card you can click with no affordance is a card nobody
   clicks. Left-aligned to match the static tiles it sits beside, so the only
   difference is the hover, the pointer and the focus ring. */
/* ★ `color:inherit` IS LOAD-BEARING. DO NOT REMOVE.
   A <button> that sets a background but no color makes its children inherit
   the user agent's `buttontext` — BLACK — which is invisible in dark mode and
   measured 1.28:1 here. This is the FOURTH time this exact bug has landed in
   this app: `.job`, `.dcell` and `.opt` were the first three. Any time an
   element becomes a button, it needs its colour stated. Caught by audit_ui.py
   on the first run after the change (2026-07-27). */
.kpi-go{text-align:left;font:inherit;color:inherit;width:100%;cursor:pointer;
  transition:border-color .15s ease,background .15s ease}
.kpi-go:hover:not(:disabled){border-color:var(--accent);background:var(--accent-soft)}
/* The label and hint step up to --ink-2 on an interactive tile. --ink-3 does
   not clear 4.5:1 at 11-12.5px — 4.15 light / 4.02 dark, the same pair already
   recorded against `.speed span` and the unread inbox card — and the hover
   tint makes it worse, not better. Measured on the hovered tile, 2026-07-27. */
.kpi-go .k,.kpi-go .sub{color:var(--ink-2)}
/* No opacity on the disabled state: fading the whole tile dropped its label
   and hint to 4.02:1, under the 4.5 minimum. A tile with nothing to show is
   marked by the ABSENCE of the hover and pointer, not by being harder to
   read — DESIGN_SYSTEM.md forbids state carried by dimming text. */
.kpi-go:disabled{cursor:default}
/* Tone on a stat tile, following .kpi.live exactly (item 67, 2026-07-27).
   The value is tinted; the meaning is ALSO written in the .sub line beneath —
   DESIGN_SYSTEM.md forbids state carried by colour alone.

   ISSUE (measured 2026-07-27, item 75): these were `.kpi.ok` / `.warn` / `.bad`,
   and `.ok` / `.err` are the app's GLOBAL message-box classes — `.ok` sets
   `background:var(--ok-soft)` and its own padding. So every toned tile silently
   picked up the success-box tint, and `--ink-3` on that tint measures 4.17:1 —
   under the 4.5 floor. It had been true of item 67's own tiles since the day
   they shipped and was never caught, because this tab is OPERATOR-only and
   `audit_ui.py` walks Analytics as an admin. Found by check_sentiment.py
   running audit_ui's own probe as the operator.
   FIX: tone classes that belong to the tile and cannot collide with a message
   box. A one-word class name shared between a box and a modifier is a cascade
   collision waiting for its first measurement. */
.kpi.tone-ok .v{color:var(--ok)}
.kpi.tone-warn .v{color:var(--warn)}
.kpi.tone-bad .v{color:var(--bad)}

.panelgrid{display:grid;gap:16px}
table.data{width:100%;border-collapse:collapse;font-size:var(--fs-body)}
table.data th{text-align:left;color:var(--ink-3);font-weight:var(--fw-semi);font-size:var(--fs-body);
  padding:7px 8px;border-bottom:1px solid var(--line)}
table.data td{padding:8px;border-bottom:1px solid var(--line);vertical-align:top}
table.data tr:last-child td{border-bottom:0}
table.data td.num{text-align:right;font-variant-numeric:tabular-nums}

/* one-line explainer under a dashboard heading or inside a card */
.hint{font-size:var(--fs-body);color:var(--ink-3);margin:0 0 12px;line-height:1.45;max-width:62ch}
.hint.section{margin:-4px 0 12px}

.funnel{display:grid;gap:8px}
/* ---------- CHART ROWS ----------
   FIXED outer columns, and that is the whole point (fixed 2026-07-27).

   ISSUE: this was `minmax(88px,120px) 1fr auto`. Every .fstep is its OWN grid
   container, so the label column sized to that row's own word and the last
   column to whether that row had a drop-off figure. Two rows with the SAME
   count and the same computed width therefore rendered different lengths,
   because the percentage was a share of a track that changed size per row.
   Measured on the live Ops funnel: `upload` 36 and `transcribe` 36 both at
   78% came out 217px and 240px — a 23px difference on identical numbers.
   A funnel you cannot compare by eye is worse than no funnel; it invites a
   confident wrong reading.

   FIX: both outer columns are fixed, so every track in a chart is exactly the
   same width and a bar's length is comparable to every other bar. 44px on the
   drop column fits "-100%" at --fs-pill; the label column scales with the
   container rather than the text. */
.fstep{display:grid;grid-template-columns:clamp(72px,26%,120px) 1fr 44px;gap:10px;
  align-items:center;font-size:var(--fs-body)}
.fstep .track{background:var(--panel-2);border-radius:99px;height:22px;overflow:hidden;
  position:relative}
.fstep .fill{height:100%;background:var(--accent);border-radius:99px;min-width:2px;
  transition:width .5s ease}
.fstep .drop{font-size:var(--fs-pill);color:var(--bad);font-weight:var(--fw-semi);
  white-space:nowrap;text-align:right}
.fstep .lbl{color:var(--ink-2);text-transform:capitalize}
/* `capitalize` is right for the token-shaped data these charts were built for
   — "web", "macos", "report_ready". It is wrong for a written label, which it
   renders as "Something On A Screen". Charts.bars({rawLabels:true}) opts out.
   Added 2026-07-27 (item 67). */
.chart-rawlabels .fstep .lbl{text-transform:none}
.fstep .cnt{position:absolute;right:8px;top:50%;transform:translateY(-50%);
  font-size:var(--fs-pill);font-weight:var(--fw-semi);color:var(--ink-2)}

.errlog{display:grid;gap:8px;max-height:340px;overflow-y:auto}
.errrow{background:var(--bad-soft);border-radius:11px;padding:10px 12px;font-size:var(--fs-body)}
.errrow b{color:var(--bad)}
.errrow code{font-family:var(--mono);font-size:var(--fs-pill);color:var(--ink-2);
  display:block;margin-top:3px;word-break:break-word}

.chartwrap{width:100%;overflow:hidden}
.chartwrap svg{width:100%;height:auto;display:block}
.legend{display:flex;gap:14px;flex-wrap:wrap;font-size:var(--fs-body);color:var(--ink-3);
  margin-top:8px}
.legend i{width:9px;height:9px;border-radius:3px;display:inline-block;margin-right:5px}

.keyrow{display:flex;gap:9px;align-items:center;padding:11px 0;
  border-top:1px solid var(--line);flex-wrap:wrap}
.keyrow:first-of-type{border-top:0}
.keyrow .nm{font-size:var(--fs-control-sm);font-weight:var(--fw-semi);flex:1;min-width:130px}
.dot{width:8px;height:8px;border-radius:50%;background:var(--ink-3);flex:0 0 auto}
.dot.on{background:var(--ok)}
.badge{font-size:var(--fs-badge);font-weight:var(--fw-heavy);padding:3px 8px;border-radius:999px;
  background:var(--panel-2);color:var(--ink-2)}
.badge.on{background:var(--ok-soft);color:var(--ok)}
/* --ink-2, not --ink-3: this badge sits on --panel-2, which is darker than the
   panel the token was tuned against, so it landed at 4.3 where everything else
   cleared 4.5. It is also the badge that admits a source is NOT watched — the
   one users most need to read. */
.badge.off{background:var(--panel-2);color:var(--ink-2)}
/* 2026-07-26 (item 51): a "problem" inbox message was rendering in the same
   grey as "link only", so the two read as equally unimportant. */
.badge.bad{background:var(--bad-soft);color:var(--bad)}
.badge.warn{background:var(--warn-soft);color:var(--warn)}

/* ---------- modal ---------- */
.modalwrap{position:fixed;inset:0;z-index:80;display:grid;place-items:center;
  background:rgba(12,18,24,.5);padding:18px;overflow-y:auto}
.modalbox{background:var(--panel);border:1px solid var(--line);border-radius:18px;
  padding:20px;width:100%;max-width:460px;box-shadow:var(--shadow);
  max-height:90dvh;overflow-y:auto}
.modalbox h3{margin:0 0 14px;font-size:var(--fs-brand);letter-spacing:-.01em}
.modalbtns{display:flex;gap:10px;margin-top:18px}
.modalbtns button{flex:1;padding:12px;font-size:var(--fs-control);font-weight:var(--fw-semi)}
.mfield{margin-bottom:12px}
.mfield label{display:block;font-size:var(--fs-body);color:var(--ink-2);font-weight:var(--fw-semi);
  margin-bottom:5px}
.mfield textarea{min-height:88px;resize:vertical}
.mrow{display:flex;gap:10px;flex-wrap:wrap}
.mrow .mfield{flex:1 1 140px;min-width:0}

/* ---------- server status light ----------
   Colour alone never carries the meaning: the dot has an aria-label and the
   line underneath it says the same thing in words.
   State classes are namespaced `slight--*`: a bare `.ok` collided with the
   success-banner class and inflated the dot to 26x22 with padding. */
.login .card{position:relative}
.slight{position:absolute;top:14px;right:14px;
  width:9px;height:9px;border-radius:50%;
  background:var(--ink-3);box-shadow:0 0 0 0 transparent;
  transition:background .25s ease,box-shadow .25s ease}
.slight--ok{background:var(--ok);box-shadow:0 0 0 3px var(--ok-soft)}
.slight--degraded{background:var(--warn);box-shadow:0 0 0 3px var(--warn-soft)}
.slight--down{background:var(--bad);box-shadow:0 0 0 3px var(--bad-soft)}
.slight--checking{animation:pulse 1.1s ease-in-out infinite}
@keyframes pulse{0%,100%{opacity:.35}50%{opacity:1}}
@media (prefers-reduced-motion:reduce){.slight--checking{animation:none}}

/* ---------- login extras: forgot / reset ---------- */
.loginlinks{text-align:center;margin:14px 0 0;font-size:var(--fs-body)}
.linkbtn{background:none;border:0;color:var(--accent-ink);font-weight:var(--fw-semi);
  font-size:var(--fs-body);padding:4px 6px;cursor:pointer}
.linkbtn:hover{text-decoration:underline}
.ok{background:var(--ok-soft);color:var(--ok);border-radius:11px;padding:11px 13px;
  font-size:var(--fs-label);margin-bottom:14px}

/* ---------- settings: account, branding, people ---------- */
.kv{display:flex;justify-content:space-between;gap:12px;padding:8px 0;
  border-top:1px solid var(--line);font-size:var(--fs-label)}
.kv:first-of-type{border-top:0}
.kv span{color:var(--ink-2)}
.hdrimg{display:block;max-width:100%;max-height:110px;object-fit:contain;
  background:#fff;border:1px solid var(--line);border-radius:11px;padding:8px;
  margin-bottom:11px}

/* ---------- capture: safety, coverage, readings ---------- */
.safety{padding:12px 0;border-top:1px solid var(--line)}
.safety:first-child{border-top:0;padding-top:0}
.sfhead b{display:block;font-size:var(--fs-control-sm)}
.sfhead .hint{display:block;margin:2px 0 8px}
.sfseg{display:flex;flex-wrap:wrap;gap:7px}
.sfseg button{flex:1 1 130px;padding:9px 11px;font-size:var(--fs-body);font-weight:var(--fw-semi);
  border:1px solid var(--line);border-radius:11px;background:var(--panel-2);
  color:var(--ink-2)}
.sfseg button.sel{border-color:var(--accent);background:var(--accent-soft);
  color:var(--accent-ink)}

/* Coverage chips: tap targets stay ≥40px high for a gloved hand on site. */
.chips{display:flex;flex-wrap:wrap;gap:8px}
.chip{padding:10px 13px;min-height:40px;font-size:var(--fs-body);font-weight:var(--fw-semi);
  border:1px solid var(--line);border-radius:20px;background:var(--panel-2);
  color:var(--ink-2)}
.chip.on{border-color:var(--ok);background:var(--ok-soft);color:var(--ok)}
.chip.on::before{content:"✓ "}

.rgroup{margin-bottom:14px}
.rgroup h4{margin:0 0 8px;font-size:var(--fs-body);text-transform:uppercase;
  letter-spacing:.05em;color:var(--ink-3)}
.rfields{display:grid;gap:11px;grid-template-columns:repeat(auto-fit,minmax(180px,1fr))}
.rfield .hint{margin:3px 0 0}
.runit{display:flex;align-items:center;gap:8px}
.runit input{flex:1;min-width:0}
.runit span{font-size:var(--fs-body);color:var(--ink-3);flex:0 0 auto;min-width:34px}
.runit input.derived{background:var(--panel-2);color:var(--ink-2);font-weight:var(--fw-semi)}
.runit input.bad{border-color:var(--bad);background:var(--bad-soft)}

/* ---------- legal screens + recording notice (item 48) ---------- */
.recnotice{margin:10px 0 0;padding:11px 13px;border-radius:12px;
  background:var(--warn-soft);color:var(--warn);font-size:var(--fs-body);font-weight:var(--fw-semi)}
.draftbanner{margin:0 0 16px;padding:11px 13px;border-radius:12px;
  background:var(--warn-soft);color:var(--warn);font-size:var(--fs-body);font-weight:var(--fw-semi)}
.legaltabs{padding:12px 16px 0;flex-wrap:wrap}
.legalbody{flex:1;overflow-y:auto;padding:16px}
.legalsec{margin-bottom:16px}
.legalsec h4{margin:0 0 5px;font-size:var(--fs-control-sm)}
.legalsec p{margin:0;font-size:var(--fs-label);color:var(--ink-2);line-height:1.6}
.loginlinks .sep{color:var(--ink-3);margin:0 4px}

/* ---------- advisory notice ----------
   Wider than a plain toast because it carries a suggestion, and it stays until
   dismissed or timed out. It never blocks the edit it is commenting on. */

/* ---------- report flow steps ----------
   State is never colour alone: the current step is bold with a filled marker
   and carries aria-current, and completed steps get a tick.

   2026-07-26 (item 52) — two changes.
   1. The pills used to sit in a wrapped row with nothing joining them, so they
      read as three separate tags rather than one flow. They are now joined by
      a connector line that fills up to the current step. A connector across a
      wrapped row looks broken, so the row no longer wraps: on a narrow screen
      it scrolls sideways instead. The line is decoration on top of state that
      is already readable, and carries aria-hidden in the markup.
   2. The pill itself is now a <button> so it can be clicked and tabbed to.
      The marker moved from li::before onto the button, so the whole pill is
      one target; the counter still increments on the li and is inherited. */
.steps{display:flex;list-style:none;gap:0;margin:0 0 14px;padding:0 0 2px;
  flex-wrap:nowrap;overflow-x:auto;counter-reset:step;
  scrollbar-width:none;-ms-overflow-style:none}
.steps::-webkit-scrollbar{display:none}
.steps li{counter-increment:step;display:flex;align-items:center;flex:0 0 auto}

/* The join between two pills. Filled once the step it leads into is reached,
   so the fill and the pills always tell the same story. */
.stepline{display:block;width:22px;height:2px;border-radius:2px;
  background:var(--line);flex:0 0 auto}
.steps li.done .stepline,.steps li.now .stepline{background:var(--accent)}

.steppill{display:flex;align-items:center;gap:7px;
  font-family:inherit;font-size:var(--fs-body);font-weight:var(--fw-semi);color:var(--ink-3);
  background:var(--panel);border:1px solid var(--line);border-radius:999px;
  padding:7px 14px 7px 8px;cursor:pointer;white-space:nowrap;
  transition:background .12s ease,border-color .12s ease}
.steppill::before{content:counter(step);display:grid;place-items:center;
  width:19px;height:19px;border-radius:50%;background:var(--panel-2);
  color:var(--ink-3);font-size:var(--fs-pill);flex:0 0 auto}
.steps li.done .steppill{color:var(--ok);border-color:var(--ok-soft)}
.steps li.done .steppill::before{content:"✓";background:var(--ok-soft);
  color:var(--ok)}
.steps li.now .steppill{color:var(--accent-ink);border-color:var(--accent);
  background:var(--accent-soft)}
.steps li.now .steppill::before{background:var(--accent);color:var(--on-accent)}
.steppill:hover:not([aria-disabled="true"]){border-color:var(--accent)}

/* A stage this report has nothing behind. Kept focusable and announced with
   aria-disabled rather than removed, so a keyboard or screen-reader user meets
   the same three stages everyone else does — and gets told why it is closed.
   The dashed border says "not available" without relying on colour. */
.steppill[aria-disabled="true"]{cursor:default;border-style:dashed;
  opacity:.72}

/* ---------- report preview ----------
   Full-screen sheet. The iframe carries the report's own stylesheet, so nothing
   here styles the document itself — only the frame around it. */
.pvwrap{position:fixed;inset:0;z-index:90;display:grid;place-items:center;
  background:rgba(12,18,24,.62);padding:14px}
.pvbox{background:var(--panel);border:1px solid var(--line);border-radius:18px;
  box-shadow:var(--shadow);width:100%;max-width:860px;height:92dvh;
  display:flex;flex-direction:column;overflow:hidden}
.pvhead{display:flex;align-items:center;gap:10px;padding:14px 16px;
  border-bottom:1px solid var(--line)}
.pvhead h3{margin:0;font-size:var(--fs-sheet);letter-spacing:-.01em}
.pvhead .pvnote{flex:1;font-size:var(--fs-body)}
.pvbox iframe{flex:1;width:100%;border:0;background:#fff}
.pvbtns{display:flex;gap:10px;padding:12px 16px;border-top:1px solid var(--line)}
.pvbtns button{flex:1;padding:12px;font-size:var(--fs-control);font-weight:var(--fw-semi)}
@media (max-width:600px){
  .pvwrap{padding:0}
  .pvbox{height:100dvh;max-width:none;border-radius:0;border:0}
  .pvhead .pvnote{display:none}
}

/* ---------- searchable report chooser ---------- */
.chooser .ftop{margin-bottom:11px}
.chooserlist{display:grid;gap:7px;max-height:330px;overflow-y:auto;
  border:1px solid var(--line);border-radius:13px;padding:7px;
  background:var(--panel-2)}
.crow{display:flex;gap:11px;align-items:center;width:100%;text-align:left;
  background:var(--panel);border:1px solid var(--line);border-radius:11px;
  padding:11px 13px}
.crow:hover{border-color:var(--accent)}
.crow.sel{border-color:var(--accent);background:var(--accent-soft)}
.crow .cmain{flex:1;min-width:0}
.crow b{display:block;font-size:var(--fs-control-sm);font-weight:var(--fw-bold);
  overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.crow span{display:block;font-size:var(--fs-body);color:var(--ink-3);
  overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.crow .tick{width:18px;height:18px;flex:0 0 auto;color:var(--accent)}

/* ---------- editable lists ---------- */
.addrow{display:flex;justify-content:flex-end;margin-top:11px}
.addbtn{background:var(--panel-2);border:1px dashed var(--line);color:var(--ink-2);
  border-radius:11px;padding:9px 14px;font-size:var(--fs-label);font-weight:var(--fw-semi);width:100%}
.addbtn:hover{border-color:var(--accent);color:var(--accent-ink)}
.rowacts{display:flex;gap:4px;flex:0 0 auto;margin-left:auto}
.iconbtn{background:none;border:0;color:var(--ink-3);font-size:var(--fs-body);
  font-weight:var(--fw-semi);padding:6px 7px;border-radius:8px}
.iconbtn:hover{background:var(--panel-2);color:var(--ink)}
.iconbtn.danger:hover{background:var(--bad-soft);color:var(--bad)}

/* 2026-07-26 (operator): two notices were both fixed at the same coordinates,
   so the second covered the first and one of them went unread. Every notice
   now goes into one stack that lays them out in a column.
   column-reverse means the first toast keeps the position a single toast has
   always had, and a later one appears ABOVE it — nothing already on screen
   moves under the reader's eye. */
.toaststack{position:fixed;left:50%;transform:translateX(-50%);bottom:calc(96px + var(--nav-gap));
  z-index:60;display:flex;flex-direction:column-reverse;align-items:center;
  gap:10px;max-width:90vw;pointer-events:none}
.toaststack > *{pointer-events:auto}
.toast{display:flex;align-items:flex-start;gap:9px;
  background:var(--ink);color:var(--bg);padding:12px 18px;border-radius:12px;
  font-size:var(--fs-label);font-weight:var(--fw-medium);box-shadow:var(--shadow);max-width:90vw}

/* 2026-07-26 — one style served all 102 notices, so "Approved" and "Analysis
   failed" looked identical and the only way to tell them apart was to read.
   Success and failure now differ by a leading mark AND a colour, never colour
   alone, and they carry role=status / role=alert so a screen reader is told
   which it is. Neutral notices keep the original dark pill. */
.toast .tmark{flex:0 0 auto;font-weight:var(--fw-heavy);line-height:1.45}
/* ---------- BANNER ----------
   Advice, not a receipt: it persists, it carries actions, and it belongs above
   the thing it is talking about. It was a toast until 2026-07-26, and in the
   centre-aligned shrink-to-fit toast stack it collapsed to a 62px text column
   at phone width. Full width of the content rail solves that by construction —
   there is nothing to shrink to. */
.bannerrail{display:grid;gap:10px;margin:0 0 14px}
.bannerrail:empty{display:none}
.banner{display:flex;align-items:flex-start;gap:12px;flex-wrap:wrap;
  background:var(--warn-soft);color:var(--warn);border:1px solid var(--warn);
  border-radius:14px;padding:12px 14px;line-height:1.45}
.banner .atext{flex:1 1 16rem;min-width:0}
.banner .bactions{display:flex;gap:14px;align-items:center;flex:0 0 auto}
.banner .linkbtn,.banner .taplink{color:var(--warn)}
/* The same notice, floating near the action instead of at the top of the
   page. A fixed width rather than shrink-to-fit: the toast stack is centre
   aligned, so a flex row with two buttons collapsed the text to a 62px column
   at phone width. Nothing to shrink to means nothing to collapse. */
.banner.floating{width:min(26rem,90vw);box-shadow:var(--shadow)}

/* The marker on a link that opens a new tab. Decoration — the accessible name
   says "opens in a new tab", so this is never the only signal. */
.extmark{margin-left:4px;font-size:var(--fs-badge);opacity:.8}

.toast.ok{background:var(--ok-soft);color:var(--ok);
  border:1px solid color-mix(in srgb,var(--ok) 30%,transparent)}
.toast.bad{background:var(--bad-soft);color:var(--bad);
  border:1px solid color-mix(in srgb,var(--bad) 34%,transparent)}

/* A toast that can be ACTED ON — the analysis-finished notice (item 78,
   2026-07-27). Its button is a real 44px target even inside the pill, because
   this is the one toast a surveyor is meant to press rather than read: they
   walked away from the screen and this is the way back to the draft.
   `align-items:center` overrides the base pill's flex-start so the button sits
   on the text baseline rather than hanging below it. */
.toast.toast-action{align-items:center;gap:12px}
.toast.toast-action .linkbtn{color:inherit;font-weight:var(--fw-bold);
  text-decoration:underline;min-height:44px;padding:0 4px;flex:0 0 auto}

/* ---------- TOUCH TARGETS ----------
   44px is the phone minimum (Apple HIG; Material asks 48dp). It is applied
   ONLY below the touch breakpoint: on a desktop with a mouse, 31px inline
   Edit/Delete buttons beside each finding are fine, and forcing them to 44
   would stretch a dense list for no one's benefit.

   Found by scripts/audit_ui.py once it stopped silently skipping the screens
   it could not click at 390px — 182 undersized targets on four screens that
   had never been measured at phone width, the worst at 26px.

   Checkboxes are deliberately absent: they sit inside a .checkrow label, and
   the whole row is the tap target, so the row carries the minimum instead. */
@media (max-width:859px){
  /* .chip added 2026-07-27 (item 62): the fourteen coverage chips sat at
     40px and measured 41 — three short of the minimum, on the control a
     surveyor taps most while standing in a plantroom holding a phone. They
     were missed because Capture had never been audited. */
  /* `.ftop .ghost` added 2026-07-27 (item 66c): the filter bar shrinks its
     ghost buttons to 9px padding, which lands them at 40px. `.ftoggle` was
     listed here but its siblings were not, so "Mark all read" measured 40 the
     moment the Inbox toolbar moved into the shared filter panel — and the
     Surveys "Clear" button had the same 40px all along, unmeasured because it
     is hidden until a filter is set. Fixed on the CLASS, not the screen. */
  /* H3 (2026-08-31): the input list used to be
     `input[type="search"],input[type="text"],input[type="number"]`, and it
     MISSED MOST OF THE APP'S FIELDS. Two ways, both measured in a real
     browser at 390px, not reasoned about:
       · `input[type="text"]` does not match a BARE `<input>`. The browser
         treats it as a text field; the SELECTOR does not. So `#siteName`,
         `#surveyName`, `#siteAddr`, `#siteOcc` (the new-survey form) and
         `#chatText` (item 88) all sat at 42px with `min-height: 0px`.
       · `email`, `password` and `date` were never in the list at all — so
         the SIGN-IN SCREEN's two fields were 42px, on the first surface
         anybody touches.
     Enumerating types is the bug. Exclude the two that are deliberately
     handled elsewhere (a checkbox's tap target is its `.checkrow` label, see
     the note above) and cover everything else, so a field added tomorrow with
     a type nobody thought of is already right. */
  .iconbtn,.tbtn,.linkbtn,.steppill,.addbtn,.edit,.taplink,
  .seg button,.tabs button,.ftoggle,.ftop .ghost,.dcell,.chip,.omenubtn,
  select,textarea,
  input:not([type="checkbox"]):not([type="radio"]):not([type="hidden"]){
    min-height:44px}
  /* Width matters as much as height: an inline "Edit" is 38px wide, so a
     44px-tall button is still a 38px target. */
  .iconbtn,.tbtn,.linkbtn,.taplink,.tabs button,.seg button,
  .omenubtn{min-width:44px}
  .checkrow{min-height:44px}
}

/* ---------- DESKTOP / WEB LAYOUT ---------- */
@media (min-width:860px){
  .app{flex-direction:row}
  .side{display:flex;flex-direction:column;width:240px;flex:0 0 240px;
    border-right:1px solid var(--line);background:var(--panel);padding:20px 14px;
    position:sticky;top:0;height:100dvh;overflow-y:auto}
  .side .brand{padding:4px 10px 20px}
  .side nav{display:flex;flex-direction:column;gap:3px}
  .side nav button{display:flex;align-items:center;gap:12px;background:none;border:0;
    color:var(--ink-2);padding:11px 12px;border-radius:11px;font-size:var(--fs-control);
    font-weight:var(--fw-semi);text-align:left;width:100%}
  .side nav button svg{width:19px;height:19px;flex:0 0 auto}
  .side nav button[aria-current="true"]{background:var(--accent-soft);color:var(--accent-ink)}
  .side .newbtn2{margin-top:auto}
  .side .whoami{font-size:var(--fs-body);color:var(--ink-3);padding:12px 10px 0;
    border-top:1px solid var(--line);margin-top:12px}
  .nav{display:none}
  .newbtn{display:none}
  .content{padding:8px 26px 44px}
  /* 2026-07-25: this was background:transparent, so the sticky header let
     scrolled content show straight through it — the page title sat on top of
     whatever card was passing underneath. It must stay opaque. */
  .top{padding:22px 26px 10px;background:var(--bg)}
  .top h1{font-size:var(--fs-title-lg)}
  .opt{flex-direction:column;align-items:flex-start;gap:12px;min-height:150px}
  .rspan{grid-column:1 / -1}
  .seg button{padding:12px}
  .panelgrid.two{grid-template-columns:repeat(auto-fit,minmax(340px,1fr))}
}

/* The sidebar costs 240px. Between the nav breakpoint and roughly 1150px that
   left too little for the content grids, which is where the layout used to
   look broken. Give the space back until the window is genuinely wide. */
@media (min-width:860px) and (max-width:1150px){
  .side{width:196px;flex:0 0 196px;padding:18px 10px}
  .side nav button{font-size:var(--fs-control-sm);padding:10px}
  .content{padding:8px 18px 44px}
  .top{padding:20px 18px 10px}
}

/* Only give the report its asymmetric two-column form when there is room for
   both columns to breathe. */
@media (min-width:1100px){
  .rgrid{grid-template-columns:1.6fr 1fr;align-items:start}
}
@media (prefers-reduced-motion:reduce){
  *{animation-duration:.01ms !important;transition-duration:.01ms !important}
}

/* ---------- a survey row with its own delete (item 79, 2026-07-27) ----------
   `.job` is itself a <button> so the whole card is focusable and keyboard
   operable. A second button cannot live inside it — invalid, and it swallows its
   own clicks — so the grid cell becomes this wrapper holding two real buttons
   side by side. The card takes the space; the delete takes what it needs. */
/* 🔴 SUPERSEDED 2026-07-27 by `.swiperow` (item 82): the actions are no longer
   a permanent column beside the card, they are hidden behind a swipe. The rule
   is kept only so `.job` still fills its row inside the new wrapper. */
.swipemain .job{width:100%}

/* ---------- SWIPE TO REVEAL ROW ACTIONS (item 82, 2026-07-27) ----------
   Operator directive: row actions are HIDDEN, and a left swipe reveals Edit and
   Delete. A permanently visible Delete beside every row is both noise and an
   invitation to a mis-tap.

   ★ SWIPE IS TOUCH-ONLY, SO IT CANNOT BE THE ONLY WAY IN. There is no swipe with
     a mouse and none with a keyboard, and this app is used at 1280px by admins
     as well as on a phone. The actions are therefore revealed by ANY of:
       · a left swipe            (touch — the requested gesture)
       · hovering the row        (mouse, via @media (hover:hover))
       · focusing anything in it (keyboard, via :focus-within)
     Hidden by default in every case, which is the point of the request.

   ★ NOTHING IS DESTRUCTIVE ON REVEAL. Revealing is not acting: both buttons
     still go through their own confirmation, and the delete still types the word
     for an approved or sent survey.

   The actions sit BEHIND the row and the row slides over them, rather than the
   actions animating in — one transform, no layout thrash, and the row keeps its
   full width so the text never reflows mid-gesture. */
/* `--swipe-x` holds the offset ALREADY NEGATIVE, set from JS once the tray has
   been measured, so this rule does no arithmetic.
   Correction to an earlier note here (2026-07-27): that note claimed
   `translateX(calc(var(--swipe-w) * -1))` silently resolved to zero. It did not.
   The transform was correct all along and the measurement was wrong — computed
   style was read in the same frame the class was added, so it sampled the 0.18s
   transition at its start. The pre-negated variable is kept because it is
   simpler to read, NOT because calc was broken. Anything measuring this must
   wait for the transition to settle first. */
.swiperow{position:relative;overflow:hidden;border-radius:16px;
  --swipe-x:0px}
/* ⚠️ `background` IS LOAD-BEARING, not decoration (operator, 2026-09-01, from a
   real screen). The tray sits BEHIND this element and the element slides over
   it — but a `z-index` above something TRANSPARENT hides nothing. `.find`,
   `.rec` and the asset rows are bare rows on a card with no surface of their
   own, so Edit and Delete were legible THROUGH the finding text at rest, on
   every screen size, reading as buttons overlapping the words.
   It looked correct on the survey list only because `.job` there is a card
   that brings its own background. The fix belongs HERE, on the slider, so it
   is true for every row this pattern is ever wrapped around — not on each
   list, which is how three of them came to differ in the first place.
   `--panel` because that is `.card`'s background, and these rows sit on a
   card; anything else would show as a stripe of the wrong shade. */
.swipemain{position:relative;z-index:1;transition:transform .18s ease;
  will-change:transform;background:var(--panel)}
.swipeacts{position:absolute;top:0;right:0;bottom:0;display:flex;align-items:stretch;
  gap:6px;padding:6px;z-index:0}
/* The revealed actions need a SURFACE of their own. Sitting bare on the page
   background they read as loose text rather than controls, and — because they
   stay in the DOM when hidden — their contrast would be measured against
   whatever happened to be behind them. A tinted surface fixes both, and states
   its own colour, which this repo has now been caught not doing five times. */
.swipeacts button{min-width:64px;border-radius:12px;background:var(--panel-2);
  color:var(--ink);border:1px solid var(--line);font-weight:var(--fw-semi)}
.swipeacts button.danger{background:var(--bad-soft);color:var(--bad);
  border-color:color-mix(in srgb,var(--bad) 34%,transparent)}
/* Revealed. One rule, three triggers. */
.swiperow.swipe-open .swipemain{transform:translateX(var(--swipe-x))}
.swiperow:focus-within .swipemain{transform:translateX(var(--swipe-x))}
@media (hover:hover){
  .swiperow:hover .swipemain{transform:translateX(var(--swipe-x))}
}
/* Mid-gesture the row follows the finger, so the transition is switched off or
   it fights the drag. */
.swiperow.swipe-drag .swipemain{transition:none}
/* A hint that there is something there, on touch only — a row that looks
   identical to a plain one teaches nobody the gesture exists. */
.swiperow::after{content:"";position:absolute;right:6px;top:50%;
  transform:translateY(-50%);width:3px;height:18px;border-radius:99px;
  background:var(--line);opacity:1;transition:opacity .18s ease;z-index:2;
  pointer-events:none}
.swiperow.swipe-open::after,.swiperow:focus-within::after{opacity:0}
@media (hover:hover){.swiperow:hover::after{opacity:0}}
@media (prefers-reduced-motion:reduce){
  .swipemain{transition:none}
}

/* ---------- the on-site chat (item 88) ----------
   Built on the sheet pattern the report preview already uses (`.pvwrap`), not
   on a new one: same overlay, same full-screen behaviour under 600px, same
   dismissal. A surveyor who has opened a report preview has already learnt
   how this closes.

   The FAB sits LEFT. `.newbtn` owns right:18px / bottom:84px, and two round
   floating buttons a thumb apart is how somebody starts a survey they did not
   want while reaching for a question. */
.chatfab{position:fixed;left:18px;bottom:calc(84px + var(--nav-gap));z-index:9;display:flex;
  align-items:center;gap:8px;min-height:48px;padding:12px 18px;border:0;
  border-radius:999px;background:var(--accent);color:var(--on-accent);
  font-size:var(--fs-control);font-weight:var(--fw-semi);
  box-shadow:0 8px 20px -10px rgba(20,30,40,.5)}
.chatfab svg{width:20px;height:20px;fill:none;stroke:currentColor;
  stroke-width:1.9;stroke-linecap:round;stroke-linejoin:round}

/* ⚠️ z-index 50, and every neighbour matters. Found by check_chat_screen.py,
   2026-08-29: at 90 the panel sat ON TOP of the first-use modal, so the one
   thing a new person MUST be able to press was behind a scrim they could not
   reach. The order is: bottom nav (10) < this sheet (50) < toasts (60) <
   modal (80) < full-screen preview (90). A modal is the app's interruption and
   outranks a sheet; a toast about this panel has to be readable over it. */
.chatwrap{position:fixed;inset:0;z-index:50;display:grid;place-items:end center;
  background:rgba(12,18,24,.62);padding:14px}
.chatbox{background:var(--panel);border:1px solid var(--line);border-radius:18px;
  box-shadow:var(--shadow);width:100%;max-width:680px;height:min(88dvh,760px);
  display:flex;flex-direction:column;overflow:hidden}
.chathead{display:flex;align-items:center;gap:10px;padding:14px 16px;
  border-bottom:1px solid var(--line)}
.chathead h3{margin:0;flex:1;font-size:var(--fs-sheet);letter-spacing:-.01em}
/* The permanent sentence. Quiet, never hidden, never dismissible — it is what
   remains when the first-use modal is long forgotten. */
.chatline{margin:0;padding:9px 16px;font-size:var(--fs-body);color:var(--ink-2);
  background:var(--panel-2);border-bottom:1px solid var(--line);line-height:1.45}
.chatlog{flex:1;overflow-y:auto;padding:14px 16px;display:flex;
  flex-direction:column;gap:10px}
.cturn{max-width:88%;padding:10px 13px;border-radius:14px;
  font-size:var(--fs-body);line-height:1.5;white-space:pre-wrap}
.cturn.me{align-self:flex-end;background:var(--accent-soft);color:var(--ink)}
.cturn.ai{align-self:flex-start;background:var(--panel-2);color:var(--ink);
  border:1px solid var(--line)}
/* A question waiting for signal. Dashed, so it does not read as sent. */
.cturn.held{align-self:flex-end;background:var(--panel);color:var(--ink-2);
  border:1px dashed var(--line)}
.cturn .cwho{display:block;font-size:var(--fs-badge);font-weight:var(--fw-heavy);
  letter-spacing:.04em;text-transform:uppercase;color:var(--ink-3);
  margin-bottom:4px}
/* A figure the answer stated that this survey has no evidence for (item 74).
   It is a warning ON the opinion, never a badge that could be mistaken for the
   checked-evidence badge the report screen uses. */
.cturn .cflag{display:block;margin-top:7px;padding-top:7px;
  border-top:1px solid var(--line);font-size:var(--fs-body);color:var(--warn)}
.chatfoot{border-top:1px solid var(--line);padding:12px 16px}
.chatask{gap:8px;align-items:flex-end}
.chatask .field{margin:0}
.chatnote{font-size:var(--fs-body);color:var(--ink-2);background:var(--panel-2);
  border:1px solid var(--line);border-radius:12px;padding:10px 12px;
  margin-bottom:10px;line-height:1.45}
.chatnote.bad{background:var(--bad-soft);border-color:var(--bad-soft);
  color:var(--bad)}
/* Hold to speak. Deliberately the biggest control in the panel: it is pressed
   with a glove on, in a plant room, while holding a torch. */
.ptt{width:100%;margin-top:10px;min-height:56px;border-radius:14px;
  border:1px solid var(--line);background:var(--panel-2);color:var(--ink);
  font-size:var(--fs-control);font-weight:var(--fw-semi);
  touch-action:none;user-select:none;-webkit-user-select:none}
.ptt[aria-pressed="true"]{background:var(--bad-soft);border-color:var(--bad);
  color:var(--bad)}
.ptt[disabled]{opacity:.55}
.chatfoot .hint{margin:8px 0 0}
@media (max-width:600px){
  .chatwrap{padding:0}
  .chatbox{height:100dvh;max-width:none;border-radius:0;border:0}
}

/* ---------- overflow (⋯) menu on a CARD — H2b (2026-09-01) ----------
   `DECIDED [card-swipe-tray]`: a card does not get the swipe tray, it gets
   this. The whole reasoning is on `overflowMenu()` in `app.js`; the short of
   it is that a card holds a media player, and scrubbing a player IS a
   horizontal drag.

   ⚠️ z-index 55, and the neighbours matter (the ladder in the chat block
   below): bottom nav (10) < sheet (50) < THIS (55) < toasts (60) < modal (80)
   < full-screen preview (90). Above a sheet, because a card can be rendered
   inside one and its menu has to be reachable there. Below a toast and below a
   modal, because pressing an item in here OPENS a modal and it would be absurd
   for the menu to sit on top of the confirmation it just raised — the exact
   mistake the chat panel made at 90 on 2026-08-29. */
/* ⚠️ `margin-left:auto`, NOT the `.sp` spacer the card headers already carry.
   Seen in a screenshot, 2026-09-01: `.sp{flex:1}` is scoped to `.top` in this
   stylesheet, so the spacer inside a CARD header does nothing and the header's
   items sit bunched at the left. The ⋯ landed mid-card, and a menu
   right-aligned to a button that is itself mid-card hung off the card's LEFT
   edge onto the page. This puts the button hard right, which is where the
   panel is anchored from. */
.omenu{position:relative;display:inline-flex;margin-left:auto}
/* States its own colour. A button that sets a background and not a `color`
   inherits the user agent's black and measures 1.28:1 in dark mode — this repo
   has been caught doing it four times (DESIGN_SYSTEM, "Banned"). */
/* ⚠️ SIZED BY PADDING, NOT BY `min-height` — measured, 2026-09-01. It first
   read `min-width:34px;min-height:34px` and the button came out 36x34 at
   390px, UNDER the 44px minimum, even though `.omenubtn` is in the shared
   44px list above. Cause: this block is appended at the END of the file, so a
   base declaration here LANDS AFTER the mobile media query and beats it. The
   44px rule stays in one place; nothing here contradicts it. */
.omenubtn{padding:8px 11px;border-radius:12px;
  border:1px solid var(--line);background:var(--panel-2);color:var(--ink-2);
  font-size:var(--fs-heading-lg);line-height:1;font-weight:var(--fw-bold)}
.omenubtn:hover{background:var(--panel);color:var(--ink)}
.omenubtn[aria-expanded="true"]{background:var(--panel);color:var(--ink);
  border-color:var(--accent)}
/* Right-aligned to the button so the panel grows INWARDS over the card. A menu
   that grew to the right would push the page wide, and horizontal overflow
   anywhere on a screen breaks every `position:fixed` element on it — see the
   note further up this file. */
.omenulist{position:absolute;top:calc(100% + 6px);right:0;z-index:55;
  min-width:186px;display:grid;gap:2px;padding:6px;border-radius:14px;
  background:var(--panel);border:1px solid var(--line);box-shadow:var(--shadow)}
/* Flipped above when the card is near the bottom of the viewport. Set from JS
   after measuring, as a class. */
.omenu.omenu-up .omenulist{top:auto;bottom:calc(100% + 6px)}
.omenulist button{width:100%;min-height:44px;padding:0 12px;text-align:left;
  border:0;border-radius:10px;background:var(--panel);color:var(--ink);
  font-size:var(--fs-control);font-weight:var(--fw-semi)}
.omenulist button:hover{background:var(--panel-2)}
.omenulist button.danger{color:var(--bad)}
.omenulist button.danger:hover{background:var(--bad-soft)}
