/* ============================================================
   Bug reporting dashboard — BugTrack multi-page visual system.

   Ported from refrence_file/bug-dashboard-8-bugtrack-pages.html
   (gitignored; on the owner's machine only), the approved design.

   Severity colours are SOW-specified and documented in the user
   manual — do not change them here.
   ============================================================ */
/* ── typefaces, served by this app ──────────────────────────────────────
   Both faces used to load from fonts.googleapis.com, which made a render-
   blocking request to a third party on every page load — and on a firewalled
   company server it fails silently and the whole dashboard falls back to a
   system font. Same two faces, same weights, latin subset (its unicode-range
   already covers every accent in the agency, employee and QA names here):
   225kB across 8 files, cached after the first load, no outside dependency.
   Refresh them with scripts/ nothing — they are static; re-download from
   Google Fonts only if a weight is added to the design. */
@font-face{font-family:'Plus Jakarta Sans';font-style:normal;font-weight:400;font-display:swap;src:url('fonts/plus-jakarta-sans-400.woff2') format('woff2')}
@font-face{font-family:'Plus Jakarta Sans';font-style:normal;font-weight:500;font-display:swap;src:url('fonts/plus-jakarta-sans-500.woff2') format('woff2')}
@font-face{font-family:'Plus Jakarta Sans';font-style:normal;font-weight:600;font-display:swap;src:url('fonts/plus-jakarta-sans-600.woff2') format('woff2')}
@font-face{font-family:'Plus Jakarta Sans';font-style:normal;font-weight:700;font-display:swap;src:url('fonts/plus-jakarta-sans-700.woff2') format('woff2')}
@font-face{font-family:'Plus Jakarta Sans';font-style:normal;font-weight:800;font-display:swap;src:url('fonts/plus-jakarta-sans-800.woff2') format('woff2')}
@font-face{font-family:'JetBrains Mono';font-style:normal;font-weight:400;font-display:swap;src:url('fonts/jetbrains-mono-400.woff2') format('woff2')}
@font-face{font-family:'JetBrains Mono';font-style:normal;font-weight:500;font-display:swap;src:url('fonts/jetbrains-mono-500.woff2') format('woff2')}
@font-face{font-family:'JetBrains Mono';font-style:normal;font-weight:600;font-display:swap;src:url('fonts/jetbrains-mono-600.woff2') format('woff2')}

:root{
  --bg:#f7f8fb; --card:#fff; --line:#e8eaf2; --line-soft:#f2f4fa;
  /* --ink-3 is the quiet grey behind every hint, percentage, sub-label and
     footnote — about 130 elements per page. At #9aa0b0 it scored 2.62:1 on
     white, well under the 4.5:1 floor for normal text, so the least-read text
     on the dashboard was also the least readable.

     #666f7d is the LIGHTEST value that clears 4.5:1 on all three surfaces this
     grey actually sits on — 5.08 on a white card, 4.78 on the page background,
     4.62 on --line-soft (heatmap cells, hovered rows). Measuring against white
     alone is the trap: #6e7686 passes there at 4.57 and still fails at 4.15 on
     a heatmap cell. Lightest-that-passes matters because every step darker
     collapses the gap to --ink-2 and the text hierarchy with it.
     test/dashboard.test.js computes the ratio against --line-soft. */
  --ink:#171a22; --ink-2:#5f6472; --ink-3:#666f7d;
  --brand:#2549d8; --brand-deep:#1e3a8a; --brand-tint:#eff3ff; --brand-tint-2:#dbe4fe;
  /* --brand decomposed to r,g,b for rgba(var(--brand-rgb), alpha) heat scales,
     and a distinct darker "ink" tone (NOT --brand-deep) used as heat-cell text
     colour on a light cell background — one home for both, so the overview
     and reports heat scales cannot drift from the palette or each other. */
  --brand-rgb:37, 73, 216; --brand-ink:#1a2f7a;
  /* severity — semantic, SOW-specified, documented */
  --crit:#c0392b; --imp:#d99019; --norm:#3b74c4; --min:#9aa3ad; --unset:#cdd2d8;
  --good:#15803d; --warn:#b45309;
  --up:#b23a30; --up-bg:#fbeceb; --down:#1c7a58; --down-bg:#e4f2ec;
  --flat:#666f7d; --flat-bg:#f2f4fa;
  /* Chart palette. These six were written into overview.js as raw hex — the
     only colours on the dashboard that lived outside this block, so the one
     place you would go to change a colour was the one place they were not.
     Ordered as the donut consumes them, most-used category first. */
  --cat-1:#2549d8; --cat-2:#7d9bf5; --cat-3:#bcccfd;
  --cat-4:#0f766e; --cat-5:#c0392b; --cat-6:#d6d3d0;
  /* KPI tile tints: a wash and the ink that sits legibly on it. Same reason —
     eight of the eleven stray hex values were these pairs. */
  --tile-bug:#eff3ff;    --tile-bug-ink:#1e3a8a;
  --tile-warn:#fdeceb;   --tile-warn-ink:var(--crit);
  --tile-flag:#fff7e8;   --tile-flag-ink:var(--warn);
  --tile-agency:#e4f6f4; --tile-agency-ink:#0f766e;
  --tile-qa:#e8f5ec;     --tile-qa-ink:var(--good);
  --tile-team:#f3f0fb;   --tile-team-ink:#7c5cbf;
  --sans:'Plus Jakarta Sans',-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,Helvetica,Arial,sans-serif;
  --mono:'JetBrains Mono',ui-monospace,'Cascadia Code',Menlo,Consolas,monospace;
  --r:14px;
  --shadow:0 1px 2px rgba(23,26,34,.04), 0 8px 24px -12px rgba(23,26,34,.10);
  /* Card-row geometry (owner, 2026-08-04): a chart box must be the SAME SIZE
     under every filter, so the page does not re-flow into a different shape
     each time a menu changes. These two numbers are the whole mechanism — the
     rows below set them as grid-auto-rows, so a card's height stops being a
     function of how much data it happens to hold.
       --tile-h  one card in a .g2/.g3/.g4 row. Sized to the tallest bounded
                 body any of them renders: a 12-row bar list (341px) + the
                 card's own 71px of header and padding = 412.
       --hero-h  the whole Overview hero row: Bug Trend (318, fixed) + the 16px
                 column gap + 226 for the bars beneath it, which is also
                 2 x 272 for the two donuts stacked in the right column. */
  --tile-h:412px;
  --hero-h:560px;
}
*{box-sizing:border-box;margin:0;padding:0}
body{background:var(--bg);color:var(--ink);font-family:var(--sans);font-size:13.5px;
  line-height:1.5;-webkit-font-smoothing:antialiased;display:flex;min-height:100vh}
a{color:inherit;text-decoration:none}

/* ── topbar controls ─────────────────────────────────────── */
.topbar{display:flex;align-items:flex-start;gap:16px;flex-wrap:wrap;padding:18px 26px 14px;
  background:rgba(247,248,251,.92);backdrop-filter:blur(10px);position:sticky;top:0;z-index:20;
  border-bottom:1px solid var(--line)}
.tb-title h1{font-size:22px;font-weight:800;letter-spacing:-.03em}
.tb-title .sub{color:var(--ink-2);font-size:12.5px;margin-top:2px}
/* The title doubles as "start over" — back to the Overview with every filter
   cleared. Deliberately quiet: it must read as the page heading first and a
   control second, so the affordance only appears under the cursor. The
   negative margin keeps the larger hit area from shifting the heading. */
.home-title{display:block;cursor:pointer;border-radius:9px;margin:-4px -8px;padding:4px 8px;
  transition:background .16s,transform .1s}
.home-title:hover{background:var(--brand-tint)}
.home-title:hover h1{color:var(--brand-deep)}
.home-title:active{transform:scale(.995)}
.home-title:focus-visible{outline:2px solid var(--brand);outline-offset:2px}
.tb-title .stamp{font-family:var(--mono);font-size:10px;color:var(--ink-3);margin-top:6px}
.crumb{font-family:var(--mono);font-size:10px;letter-spacing:.1em;text-transform:uppercase;
  color:var(--ink-3);margin-bottom:3px;min-height:13px}
.crumb a{color:var(--brand-deep);font-weight:600}
.crumb a:hover{text-decoration:underline}
.crumb a:active{opacity:.6}
/* There are now ten labelled controls (eight filters plus Trend and Export).
   Laid out as a wrapping flex row they need ~1615px, so at every width this
   dashboard is actually read at they spill onto a second line — and greedy
   flex wrapping made that line a stubby, left-aligned remnant under a full
   first line: a ragged two-row block, not a control panel.
   The strip is a uniform column grid instead. auto-fit means the column COUNT
   is the thing that changes with width (8 tracks at 1600, 7 at 1440, 6 at
   1280, 2 on a phone) while every control stays exactly as wide as its
   neighbours, so a second row reads as the second rank of one matrix rather
   than as an overflow. flex:1 1 100% claims a row of the topbar outright,
   which is what already happened by accident — the flex container wrapped it
   there once the title no longer left room — so this costs no extra height,
   it only makes the intent explicit and gives the grid the full width to
   divide. align-items:end keeps every input on one baseline the way
   align-items:flex-end did. */
.tools{flex:1 1 100%;margin-left:0;display:grid;
  grid-template-columns:repeat(auto-fit,minmax(148px,1fr));gap:10px;align-items:end}
.ctl{display:flex;flex-direction:column;gap:4px;min-width:0}
.ctl-wide{grid-column:span 2}
.ctl>span:first-child{font-family:var(--mono);font-size:8.5px;letter-spacing:.14em;
  text-transform:uppercase;color:var(--ink-3);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.ctl select,.ctl input{font-family:var(--sans);font-size:12.5px;padding:7px 10px;border-radius:9px;
  border:1px solid var(--line);background:var(--card);color:var(--ink)}
/* Fill the track. min-width:0 is what actually lets a <select> shrink below
   the width of its longest option — agency names run to "Product Architecture
   and Structure" and without this one declaration that select alone sets the
   width of every track in the grid. */
.tools .ctl select,.tools .ctl input{width:100%;min-width:0}
.ctl select:focus,.ctl input:focus{outline:none;border-color:var(--brand);box-shadow:0 0 0 3px var(--brand-tint)}
.ctl select:disabled{background:var(--brand-tint);color:var(--brand-deep);font-weight:700}
.dates{display:flex;align-items:center;gap:5px}
/* Two date inputs sharing one two-track cell: a grid so they split it evenly
   and the arrow keeps its intrinsic width, rather than each input insisting
   on the browser's default date-field width and overflowing the cell. */
.tools .dates{display:grid;grid-template-columns:minmax(0,1fr) auto minmax(0,1fr);gap:5px;align-items:center}
.dates i{color:var(--ink-3);font-style:normal}
.seg{display:inline-flex;border:1px solid var(--line);border-radius:9px;overflow:hidden;background:var(--card)}
.seg button{border:none;background:none;font-family:var(--sans);font-size:12.5px;font-weight:600;
  padding:7px 12px;cursor:pointer;color:var(--ink-2);transition:background .15s,transform .12s}
.seg button.on{background:var(--brand);color:#fff}
.seg button:not(.on):hover{background:var(--line-soft)}
/* In the grid a segmented control is one track like any select, so it fills
   the track and its two buttons split it — otherwise Trend and Export would
   sit at their intrinsic width inside a wider cell and be the two controls
   that visibly do not line up with the rest. */
.tools .seg{display:flex;width:100%}
.tools .seg button{flex:1 1 0;min-width:0;padding:7px 6px}

/* ── the Untyped flag: a tagging gap, not a category ─────── */
.untyped{color:var(--crit);font-weight:700}

/* ── deltas ──────────────────────────────────────────────── */
.delta{font-family:var(--mono);font-size:10px;font-weight:700;padding:1px 6px;border-radius:20px}
.delta.up{color:var(--up);background:var(--up-bg)}
.delta.down{color:var(--down);background:var(--down-bg)}
.delta.flat{color:var(--flat);background:var(--flat-bg)}

/* ── states ──────────────────────────────────────────────── */
.content{padding:16px 26px 40px;display:flex;flex-direction:column;gap:16px;flex:1}
.content.dimmed{opacity:.45;transition:opacity .15s}
/* Filter-change continuity. Changing a filter used to swap the whole view out
   in one frame with nothing connecting the old page to the new one, so a click
   on a donut slice read as a flicker rather than as cause and effect.
   .content.dimmed above already covers the WAIT (main.js dims #view while a
   request is in flight and undims before painting); this covers the ARRIVAL,
   which is the half that was missing — and it extends that same mechanism
   rather than adding a second one, since both live on .content.
   No JS at all: paint() assigns #view.innerHTML wholesale, so every block here
   is a brand-new element on every render and its entry animation re-fires by
   itself. That also means it costs nothing extra on a filter change that fires
   no request (severity and type re-render from rows already in memory).
   The stagger is by position, so the page assembles top-down; it is capped at
   four steps because the last block must not still be arriving once someone
   has begun reading the first. Worst case 160ms delay + 260ms. */
@keyframes viewIn{from{opacity:0;transform:translateY(6px)}}
.content>*:not(.skel){animation:viewIn .26s cubic-bezier(.2,.8,.3,1) both}
.content>*:nth-child(2){animation-delay:.04s}
.content>*:nth-child(3){animation-delay:.08s}
.content>*:nth-child(4){animation-delay:.12s}
.content>*:nth-child(n+5){animation-delay:.16s}
.msg{background:var(--card);border:1px solid var(--line);border-radius:var(--r);padding:26px;
  text-align:center;color:var(--ink-2);box-shadow:var(--shadow)}
.msg button{margin-top:12px;border:none;background:var(--brand);color:#fff;font-weight:700;
  font-family:var(--sans);font-size:12.5px;padding:8px 16px;border-radius:9px;cursor:pointer;
  transition:transform .12s}
.empty{padding:26px;text-align:center;color:var(--ink-3);font-family:var(--mono);font-size:11.5px}
.skel{background:var(--card);border:1px solid var(--line);border-radius:var(--r);height:120px;
  box-shadow:var(--shadow);animation:pulse 1.2s ease-in-out infinite}
@keyframes pulse{0%,100%{opacity:1}50%{opacity:.55}}

/* ── responsive ──────────────────────────────────────────────
   The breakpoints live at the BOTTOM of this file, after every rule they
   override. They used to sit here, above the sidebar's own definition, and a
   media query adds no specificity — so `.side` at line ~199 silently beat the
   `.side` inside `@media(max-width:1100px)`, and the entire mobile sidebar
   rule was dead. On a phone the sidebar stayed 236px wide and a full viewport
   tall, pushing the whole dashboard down by one blank screen.
   See the end of this file. */

/* ── print / save-as-PDF ─────────────────────────────────── */
@media print{
  .side,.tools,.crumb{display:none!important}
  body{display:block;background:#fff}
  .topbar{position:static;border-bottom:2px solid var(--ink);backdrop-filter:none;background:#fff}
  .content{padding:0}
  .card,.kpi,.strip,.ecard{break-inside:avoid;page-break-inside:avoid;box-shadow:none}
  /* Paper has no scrollbar. The fixed row heights that keep the screen steady
     under a filter change would silently print only the first screenful of a
     scrolled card — so on paper every card goes back to its own content
     height and nothing is cut off. */
  .g2,.g3,.g4,.g-hero{grid-auto-rows:auto!important}
  .hero-col>.card{flex:0 0 auto!important}
  .card-b{overflow:visible!important}
  *{-webkit-print-color-adjust:exact;print-color-adjust:exact}
  /* Print the finished state, never a frame of an animation. Printing is not
     reliably timed against them: a chart caught mid-`grow` prints a zero-width
     bar, and viewIn's `both` fill would print the first frame of a block that
     had not started yet. Stating the end state makes the page deterministic on
     paper, which is what a print stylesheet is for. The donut mask goes with
     them for the same reason as in the reduced-motion block — a mask that
     outlives the animation driving it is a chart-shaped hole. */
  *{animation:none!important;transition:none!important}
  .content>*{opacity:1;transform:none}
  .trend .ln{stroke-dashoffset:0}.trend .ar{opacity:1}
  .donut svg{-webkit-mask-image:none;mask-image:none}
}

/* ============================================================
   Below: ported verbatim from refrence_file/bug-dashboard-8-
   bugtrack-pages.html's <style> block (the approved design),
   selector groups named in the Task 7 brief. .side-foot and .me
   (the design's static signed-in-user block) and .note and
   .pill-ctl (the design-concept banner and its inert control
   pills) are dropped — this dashboard is public/read-only and
   the topbar above already carries real controls. .main is
   added (not named in the brief's list, but required for the
   sidebar + content two-column shell layout to render at all).
   ============================================================ */

/* ── sidebar ─────────────────────────────────────────── */
.side{width:236px;flex:0 0 236px;background:var(--card);border-right:1px solid var(--line);
  display:flex;flex-direction:column;position:sticky;top:0;height:100vh;overflow:hidden}
.side::after{content:'';position:absolute;left:-70px;bottom:-90px;width:280px;height:280px;border-radius:50%;
  background:radial-gradient(circle at 30% 30%,#dbe4fe,#f4f6ff 60%,transparent 70%);opacity:.85;pointer-events:none}
/* Also "start over" (see .home-title). It is an <a> now, so the colour reset
   matters: without it the wordmark would inherit the link colour. */
.brand{display:flex;align-items:center;gap:11px;padding:22px 20px 20px;color:inherit;
  cursor:pointer;transition:transform .1s}
.brand:active{transform:scale(.985)}
.brand:hover .wm{color:var(--brand-deep)}
.brand:focus-visible{outline:2px solid var(--brand);outline-offset:-2px;border-radius:12px}
.brand .mark{width:34px;height:34px;border-radius:10px;flex:0 0 auto;display:grid;place-items:center;
  background:linear-gradient(145deg,#2549d8,#1e3a8a);box-shadow:0 4px 12px -4px rgba(65,105,225,.6)}
.brand .mark svg{width:18px;height:18px;fill:#fff}
.brand .wm{font-weight:800;font-size:17px;letter-spacing:-.02em}
.brand .wm small{display:block;font-family:var(--mono);font-size:8.5px;font-weight:500;letter-spacing:.14em;
  text-transform:uppercase;color:var(--ink-3);margin-top:1px}
.nav{padding:4px 12px;display:flex;flex-direction:column;gap:2px;position:relative;z-index:1;overflow-y:auto}
.nav a{display:flex;align-items:center;gap:11px;padding:9px 12px;border-radius:9px;color:var(--ink-2);
  font-weight:500;font-size:13px;transition:background .16s,color .16s,transform .12s;cursor:pointer}
.nav a svg{width:16px;height:16px;stroke:currentColor;fill:none;stroke-width:1.7;flex:0 0 auto}
.nav a:hover{background:var(--line-soft);color:var(--ink)}
.nav a.on{background:var(--brand-tint);color:var(--brand-deep);font-weight:700;box-shadow:inset 2px 0 0 var(--brand)}
.nav a .badge{margin-left:auto;font-family:var(--mono);font-size:9.5px;font-weight:600;padding:1px 6px;
  border-radius:20px;background:var(--line-soft);color:var(--ink-2)}
.nav a.on .badge{background:var(--brand);color:#fff}
.nav-sep{margin:12px 12px 6px;font-family:var(--mono);font-size:8.5px;letter-spacing:.16em;
  text-transform:uppercase;color:var(--ink-3);position:relative;z-index:1}
/* The sections menu button exists only where the sidebar is a top bar — see
   the 1100px block at the end of this file, which turns it on. Hidden here at
   the base so a desktop never sees it, whatever order the rules load in. */
.menu-btn{display:none}

/* ── main column (sidebar + main are the two flex children of body) ── */
.main{flex:1;min-width:0;display:flex;flex-direction:column}

/* ── cards / primitives ──────────────────────────────── */
/* A flex column so a card stretched by its grid row (see .g3 / .g-hero) hands
   the spare height to its body rather than leaving it under the body as dead
   space inside the border. */
.card{background:var(--card);border:1px solid var(--line);border-radius:var(--r);box-shadow:var(--shadow);
  display:flex;flex-direction:column;min-width:0}
.card-h{display:flex;align-items:center;gap:10px;padding:15px 17px 0;flex:0 0 auto}
.card-h h3{font-size:13.5px;font-weight:700;letter-spacing:-.01em}
.card-h .hint{font-size:11.5px;color:var(--ink-3);font-weight:500}
.card-h .right{margin-left:auto;display:flex;align-items:center;gap:10px}
.card-b{padding:14px 17px 17px;flex:1 1 auto;min-width:0}
/* The three card bodies whose content has a fixed intrinsic height — a donut
   and its legend, the month heatmap, the team columns. A list or a table fills
   whatever height it is given, but these do not, so in a stretched row they
   would sit at the top with all the spare height pooled beneath them: the
   ragged bottom edge moved inside the border rather than removed. Centring
   splits that space above and below, which reads as a deliberately composed
   card instead of a short one. :has() rather than a new class because these
   bodies are built by ui.js's shared card()/donut()/columns() helpers — the
   shape of the content is the only thing that distinguishes them, and this
   round is not allowed to change what those helpers emit.

   `safe center` rather than plain `center`: safe alignment falls back to
   start-alignment the moment the content is TALLER than the body, and centred
   overflow is unreachable — the overflowing top half scrolls out of the box
   with no way to scroll back to it. A heatmap in a month with six week-rows
   and a wide legend is close enough to the limit to be worth the guard. */
.card-b:has(>.donut-wrap),.card-b:has(>.hm),.card-b:has(>.cols),.card-b:has(>.trend){
  display:flex;flex-direction:column;justify-content:safe center}
/* "All 40 →" measured 17x20px — the smallest target on the page. The padding
   buys a 29px one; the negative margin means the card header does not move. */
.link{color:var(--brand-deep);font-weight:700;font-size:11.5px;cursor:pointer;
  display:inline-block;padding:6px 8px;margin:-6px -8px}
.link:hover{text-decoration:underline}

/* Visible only to a screen reader: the status line that replaced aria-live on
   the whole view. Not display:none — that would hide it from the reader too. */
.sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;
  clip-path:inset(50%);white-space:nowrap;border:0}

/* ── keyboard ──────────────────────────────────────────────────────────
   The heatmap days and the week rows are <button> now (they filter the whole
   dashboard, and as <div>s the Tab key walked straight past all 35 of them).
   A button brings the browser's own font, border and centring with it, so it
   has to be reset back to the element it is replacing. */
button.cell,button.brow{font:inherit;color:inherit;background:none;border:0;
  text-align:left;width:100%;-webkit-appearance:none;appearance:none}
/* One ring, one colour, everywhere focus can land — matching .home-title. */
.hm .cell:focus-visible,.brow:focus-visible,.card-b:focus-visible,
.col:focus-visible,.kpi:focus-visible,.li.clickable:focus-visible,
.arc-link:focus-visible{outline:2px solid var(--brand);outline-offset:2px;border-radius:8px}

.kpis{display:grid;grid-template-columns:repeat(8,minmax(0,1fr));gap:12px}
@media(max-width:1500px){.kpis{grid-template-columns:repeat(4,minmax(0,1fr))}}
@media(max-width:820px){.kpis{grid-template-columns:repeat(2,minmax(0,1fr))}}
.kpi{background:var(--card);border:1px solid var(--line);border-radius:var(--r);padding:14px;
  box-shadow:var(--shadow);position:relative;overflow:hidden;transition:transform .16s,box-shadow .16s,border-color .16s}
a.kpi{cursor:pointer}
a.kpi:hover{transform:translateY(-2px);border-color:var(--brand);box-shadow:0 10px 26px -14px rgba(65,105,225,.55)}
.kpi .ic{width:34px;height:34px;border-radius:10px;display:grid;place-items:center;margin-bottom:10px}
.kpi .ic svg{width:17px;height:17px;stroke-width:1.8;fill:none}
.kpi .k{font-size:11.5px;color:var(--ink-2);font-weight:600}
.kpi .v{font-size:27px;font-weight:800;letter-spacing:-.035em;line-height:1.1;margin-top:3px;font-variant-numeric:tabular-nums}
.kpi .d{font-family:var(--mono);font-size:10px;font-weight:600;margin-top:5px}
.kpi .d.up{color:var(--crit)}.kpi .d.down{color:var(--good)}.kpi .d.flat{color:var(--ink-3)}

/* stat strip on detail pages */
.strip{display:grid;grid-template-columns:repeat(auto-fit,minmax(130px,1fr));gap:0;background:var(--card);
  border:1px solid var(--line);border-radius:var(--r);box-shadow:var(--shadow);overflow:hidden}
.strip .s{padding:15px 17px;border-right:1px solid var(--line-soft)}
.strip .s:last-child{border-right:none}
.strip .s .k{font-family:var(--mono);font-size:9px;letter-spacing:.13em;text-transform:uppercase;color:var(--ink-3)}
.strip .s .v{font-size:24px;font-weight:800;letter-spacing:-.03em;margin-top:4px;font-variant-numeric:tabular-nums}
.strip .s .sub{font-size:11px;color:var(--ink-3);margin-top:1px}

/* bar list */
.blist{display:flex;flex-direction:column;gap:11px}
/* padding 3px, not 2px: 18px of content plus 3px top and bottom is exactly the
   24px minimum tap target. The matching negative margin keeps the row rhythm
   identical to the 22px version, so nothing else on the card moves. */
.brow{display:grid;grid-template-columns:1fr 1fr auto;gap:10px;align-items:center;border-radius:7px;
  padding:3px 4px;margin:-3px -4px;min-height:24px;transition:background .14s,transform .12s}
a.brow{cursor:pointer}
a.brow:hover{background:var(--line-soft)}
.brow .nm{font-size:12.2px;color:var(--ink-2);font-weight:500;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
a.brow:hover .nm{color:var(--brand-deep);font-weight:600}
/* barList's kind:'qa' rows wrap the avatar dot and name in class="nm qa" —
   the new-naming counterpart of .bar-nm.qa below. Without this the qa-dot
   (itself display:flex, so block-level) stacks above the name instead of
   sitting beside it. */
.nm.qa{display:flex;align-items:center;gap:7px}
.brow .track{height:8px;border-radius:20px;background:var(--line-soft);overflow:hidden}
.brow .fill{display:block;height:100%;border-radius:20px;background:linear-gradient(90deg,#bcccfd,#2549d8);
  transform-origin:left;animation:grow .8s cubic-bezier(.2,.8,.3,1) both}
.brow .ends{display:flex;align-items:baseline;gap:6px}
.brow .n{font-family:var(--mono);font-size:11.5px;font-weight:700;text-align:right;min-width:26px;font-variant-numeric:tabular-nums}
.brow .pct{font-family:var(--mono);font-size:10.5px;color:var(--ink-3);min-width:34px;text-align:right}

/* Overview "Bugs per week/month" rows (overview.js's weekBar) — clickable
   date-range drill-down, not a link: cursor is set explicitly since these
   are plain divs, and .active marks the bucket the dashboard is currently
   scoped to with the same tinted/bordered language as .legend .li.active. */
/* Their labels are short and uniform ("15 Jul"), so the generic 1fr name
   column left half the row empty and pushed every bar into the right half.
   A fixed label track gives the bar the whole remaining width — the only
   part of the row that carries a magnitude. Taller bars too: these are the
   card's subject, not a sparkline beside a name. */
.brow.wk{cursor:pointer;grid-template-columns:58px minmax(0,1fr) auto;gap:14px}
.brow.wk .track{height:13px}
.brow.wk .nm{font-family:var(--mono);font-size:11px;color:var(--ink-2)}
.brow.wk:hover{background:var(--line-soft)}
.brow.wk.active{background:var(--brand-tint);box-shadow:inset 2px 0 0 var(--brand)}
.brow.wk.active .nm{color:var(--brand-deep);font-weight:700}

/* donut */
.donut-wrap{display:flex;align-items:center;gap:18px;flex-wrap:wrap}
.donut{position:relative;flex:0 0 auto}
.donut svg{transform:rotate(-90deg);display:block}
.donut circle{fill:none}
/* Sweep-in, the donut's counterpart to the trend line's `draw`: the ring is
   revealed clockwise from twelve o'clock, in the same direction the slices are
   laid out, instead of appearing whole.
   Done with an animated conic mask rather than stroke-dashoffset because each
   arc's dash pair is computed per slice and written as an inline presentation
   attribute — CSS cannot animate towards a value it does not know, and this
   round may not change what ui.js's donut() emits. `from 90deg` because a mask
   is applied in the element's OWN coordinate space, which the -90deg above has
   already rotated; without it the sweep would start at nine o'clock while the
   arcs start at twelve. Masks do not affect hit-testing (unlike clip-path), so
   an arc stays clickable throughout.
   --sweep is registered at 360deg, not 0deg, so the failure mode is safe in
   both directions: a browser with no @property support leaves it unresolved,
   mask-image falls back to none and the ring is simply whole; and anywhere the
   animation does not run (reduced motion, print) --sweep holds at its initial
   360deg, which is again a whole ring. A 0deg initial value would have hidden
   every donut on the page instead — the one failure in this file that would be
   invisible in review and total on screen. */
@property --sweep{syntax:'<angle>';inherits:false;initial-value:360deg}
@keyframes sweep{from{--sweep:0deg}}
.donut svg{-webkit-mask-image:conic-gradient(from 90deg,#000 var(--sweep),transparent 0);
  mask-image:conic-gradient(from 90deg,#000 var(--sweep),transparent 0);
  animation:sweep .38s cubic-bezier(.3,.7,.3,1) both}
/* pointer-events:none is load-bearing, not a tidy-up. inset:0 makes this
   centre label a transparent lid over the ENTIRE donut, and a div with no
   background still hit-tests, so it was swallowing every pointer event aimed
   at an arc: round UI-D1's clickable slices never once navigated, their
   data-tip tooltips never fired, and neither the pointer nor the help cursor
   ever appeared — the arcs' own rules below were all correct and simply
   unreachable. Verified in the browser before and after: a real click on a By
   Severity arc left the hash at #/overview, and elementFromPoint over three
   separate points on the ring answered DIV.ctr every time; both now resolve
   to the arc. The label is a total and a caption — inert text with nothing to
   click, nothing to hover and no reason to be selectable — so it has no
   business intercepting anything. */
.donut .ctr{position:absolute;inset:0;display:grid;place-content:center;text-align:center;
  pointer-events:none}
.donut .ctr b{display:block;font-size:23px;font-weight:800;letter-spacing:-.03em;line-height:1;font-variant-numeric:tabular-nums}
.donut .ctr span{font-family:var(--mono);font-size:8.5px;letter-spacing:.14em;text-transform:uppercase;color:var(--ink-3)}
.legend{display:flex;flex-direction:column;gap:7px;flex:1;min-width:150px}
.legend .li{display:flex;align-items:center;gap:8px;font-size:12px;text-decoration:none;color:inherit}
.legend .dot{width:8px;height:8px;border-radius:3px;flex:0 0 auto}
.legend .lb{color:var(--ink-2);font-weight:500}
.legend .vv{margin-left:auto;font-family:var(--mono);font-size:11px;font-weight:600;font-variant-numeric:tabular-nums}
.legend .pc{font-family:var(--mono);font-size:10px;color:var(--ink-3);min-width:36px;text-align:right}
/* Clickable donut slices — By Severity / Top Bug Categories on Overview
   (spec 2026-07-30, ui-b). arc-link/li.clickable are only ever emitted when a
   slice carries an href, so a plain donut (every pre-existing caller) is
   unaffected by these rules. */
.donut .arc-link{cursor:pointer;transition:opacity .12s}
.donut .arc-link circle{transition:opacity .15s}
.legend a.li.clickable{cursor:pointer;border-radius:6px;padding:3px 6px;margin:-3px -6px;transition:background .15s,opacity .15s,transform .12s}
.legend a.li.clickable:hover{background:var(--line-soft)}
.legend .li.active{background:var(--brand-tint);border-radius:6px;padding:3px 6px;margin:-3px -6px}
.legend .li.active .lb{color:var(--brand-deep);font-weight:700}
.legend .li.dim{opacity:.5}
/* A slice carrying data-tip explains itself through sevtip.js's shared
   tooltip (round UI-D1). The arc and a legend row that is NOT a link get the
   help cursor; a legend row that IS a link keeps .clickable's pointer, since
   its primary gesture is still navigation. */
.donut circle[data-tip]{cursor:help}
.legend .li[data-tip]:not(a){cursor:help}
.legend .li[data-tip]:not(a):hover{background:var(--line-soft);border-radius:6px;padding:3px 6px;margin:-3px -6px}

/* columns */
/* A column cannot shrink below its own label, so a roster wide enough for the
   card spills past its border rather than reflowing — measured at 390px, seven
   teams needed 326px of a 307px card and the QA column was cut in half by the
   card's edge. Teams are data, added from the admin panel, so this is a matter
   of when rather than if, at any width.

   `overflow:auto hidden` — auto on X, hidden on Y. The Y half is load-bearing:
   left visible, the browser promotes it to auto and this becomes a VERTICAL
   scroll container too, which is precisely what stopped the page scrolling
   over cards two rounds ago. Measured before clipping it — .cols has no
   vertical overflow at 360, 390 or 430 (scrollHeight equals clientHeight), so
   hiding that axis costs nothing today and guards the axis that matters. */
.cols{display:flex;align-items:flex-end;gap:14px;height:236px;padding-top:18px;
  overflow:auto hidden;-webkit-overflow-scrolling:touch}
/* min-width is the whole negotiation. A flex item defaults to min-width:auto —
   it refuses to shrink below its own label — which is what pushed the seventh
   team past the card's edge. A floor of 28px lets the columns give way instead,
   down to a width where the bar still reads and the label still wraps rather
   than clips (measured at 29px: no clipping, still two lines at most). Past
   that floor .cols scrolls, so a large roster gets narrow columns and then a
   swipe, never a column sliced in half by the card border. */
.col{flex:1;min-width:28px;display:flex;flex-direction:column;align-items:center;gap:7px;height:100%;
  justify-content:flex-end;border-radius:9px;padding:4px;transition:background .15s,transform .12s}
a.col{cursor:pointer}
a.col:hover{background:var(--line-soft)}
/* A column carrying a severity breakdown pins sevtip.js's shared tooltip on
   hover (Overview's "Bugs by Team"). When it is not also a link it still has
   to look reactive, or the tooltip appears with nothing having acknowledged
   the pointer. */
.col[data-sev]:not(a){cursor:help}
.col[data-sev]:hover{background:var(--line-soft)}
.col .cv{font-family:var(--mono);font-size:11.5px;font-weight:700;font-variant-numeric:tabular-nums}
/* transform-origin is what makes the scaleY entrance below grow from the axis
   rather than from the middle of the column. */
.col .cb{width:100%;max-width:52px;border-radius:8px 8px 3px 3px;min-height:3px;
  background:linear-gradient(180deg,var(--cat-2),var(--cat-1));transform-origin:bottom;
  animation:rise .7s cubic-bezier(.2,.8,.3,1) both}
.col .cb.zero{background:var(--line)}
.col .cl{font-size:11px;color:var(--ink-2);font-weight:600;text-align:center}
.col .cm{font-family:var(--mono);font-size:9px;color:var(--ink-3)}

/* trend */
.trend svg{width:100%;height:250px;display:block;overflow:visible}
.trend .gl{stroke:var(--line-soft);stroke-width:1}
.trend .ax{font-family:var(--mono);font-size:11px;fill:var(--ink-3)}
.trend .ln{fill:none;stroke:var(--brand);stroke-width:2.2;stroke-linejoin:round;stroke-linecap:round;
  stroke-dasharray:1;stroke-dashoffset:1;animation:draw 1.4s cubic-bezier(.3,.7,.3,1) .2s forwards}
/* A dash of 1 is the whole line because the path carries pathLength="1"
   (overview.js). A fixed unit count here was outgrown once: at 2000, the
   65-day range of 2026-09-17 measured 3,481 units and the stroke fell into
   the dash gap from 17 Aug on. Keep the two 1s together. */
.trend .ar{fill:url(#tg);opacity:0;animation:fade .8s .9s forwards}
.trend .pt{fill:#fff;stroke:var(--brand);stroke-width:2}
/* Wider invisible hover target per day (Fix 3) — the visible .pt dot is only
   3.2px, too fiddly to hover accurately on its own. fill:transparent (not
   "none") keeps it in the "visiblePainted" hit-test so pointer-events still
   register despite being invisible. */
.trend .pt-hit{fill:transparent;cursor:pointer}
.trend .lbl{font-family:var(--mono);font-size:9.5px;font-weight:600;fill:var(--ink-2)}

/* heatmap — fixed-size (not 1fr) tracks so cells stay ~34px regardless of the
   card's width; a grid container with fixed-px tracks does not stretch them
   to fill available space, so the block packs to the left of its card
   instead of ballooning to dominate the page (was ~130px cells before). */
/* Centred in its card: at 268px it is narrower than the g3 column it sits in,
   and left-aligning left an obvious void down the card's right half. */
.hm{display:grid;grid-template-columns:repeat(7,34px);gap:5px;max-width:268px;margin:0 auto}
.hm .hd{font-family:var(--mono);font-size:9px;color:var(--ink-3);text-align:center;padding-bottom:2px}
.hm .cell{aspect-ratio:1;border-radius:7px;display:grid;place-items:center;font-family:var(--mono);
  font-size:10.5px;font-weight:600;background:var(--line-soft);color:var(--ink-3);transition:transform .14s}
.hm .cell:not(.pad):hover{transform:scale(1.09)}
.hm .cell.out{opacity:.35}
/* Leading pad cells (no date) opt out of the click affordance every real day
   gets — a zero-bug or out-of-range day is still honestly clickable, but a
   blank filler before day 1 is not a date at all. */
.hm .cell:not(.pad){cursor:pointer}
/* Active day (from=to=this day): a ring, not just the heat colour, so it
   reads even on a zero-count cell with no background of its own. */
.hm .cell.active{box-shadow:0 0 0 1px #fff,0 0 0 3px var(--brand)}
.hm-scale{display:flex;align-items:center;gap:5px;font-family:var(--mono);font-size:9.5px;color:var(--ink-3);
  margin:11px auto 0;max-width:268px;justify-content:flex-end}
.hm-scale i{width:13px;height:13px;border-radius:4px;display:block}

/* tables */
.tbl-wrap{overflow-x:auto}
.tbl{width:100%;border-collapse:collapse;font-size:12.3px}
.tbl th{font-family:var(--mono);font-size:9px;letter-spacing:.13em;text-transform:uppercase;color:var(--ink-3);
  text-align:left;padding:8px 10px;border-bottom:1px solid var(--line);font-weight:600;white-space:nowrap;
  position:sticky;top:0;background:var(--card);z-index:1}
.tbl td{padding:9px 10px;border-bottom:1px solid var(--line-soft);vertical-align:middle}
.tbl tbody tr:last-child td{border-bottom:none}
.tbl tbody tr{transition:background .14s}
/* Row hover: the old value was a near-white one step off the card background,
   invisible on most screens, so a reader tracking one row across eight
   columns had nothing to hold on to. This is the same token the bar rows and
   columns use for their hover, so all three read as one gesture. */
.tbl tbody tr:hover{background:var(--line-soft)}
.tbl tbody tr.clk{cursor:pointer}
.tid{font-family:var(--mono);font-size:10.5px;color:var(--ink-3)}
/* Reports page: team-by-period matrix cell. Background/colour are set inline
   per-cell (heat intensity relative to the matrix peak) — this rule supplies
   the shape and type so an unstyled number doesn't collapse to a bare inline
   digit next to the "·" of empty cells. */
.heat{display:inline-flex;align-items:center;justify-content:center;min-width:26px;
  padding:3px 8px;border-radius:6px;font-family:var(--mono);font-size:11px;font-weight:700;
  font-variant-numeric:tabular-nums}
/* min-height, not more padding: a chip is a pill, and padding would stretch it
   into a lozenge. The not-a-bug chips are links, so they need the 24px floor
   too — they measured 22px. */
.chip{display:inline-flex;align-items:center;gap:5px;padding:3px 9px;border-radius:20px;font-size:10.5px;
  font-weight:700;white-space:nowrap;min-height:24px}
.chip i{width:6px;height:6px;border-radius:50%;display:block}

/* ── overview page: severity legend strip ─────────────────── */
.legend-bar{display:flex;align-items:center;flex-wrap:wrap;gap:14px;margin-top:4px;
  padding:12px 17px;background:var(--card);border:1px solid var(--line);border-radius:var(--r);
  box-shadow:var(--shadow);font-size:11px;color:var(--ink-2)}
.legend-bar span{display:inline-flex;align-items:center;gap:6px}
.legend-bar .sw{width:8px;height:8px;border-radius:2px;display:inline-block;flex:0 0 auto}

/* ── overview page: not-a-bug pills ──────────────────────── */
.chips{display:flex;flex-wrap:wrap;gap:8px}
/* A chip that links to the Bugs page filtered to its type (round UI-D1).
   Chips have no background of their own, so the hover affordance is the
   border-box tint the rest of the dashboard uses for a hovered target.
   Resting state is left exactly as the shipped chip: only the hover adds a
   tint, so nothing about the card's look changes until you point at it.
   (This comment used to close one line early; the stray prose that followed
   was parsed as part of the next selector, so `a.chip.chip-lk{cursor:pointer}`
   never applied and the chips had no pointer cursor at all.) */
a.chip.chip-lk{cursor:pointer;transition:background .15s,color .15s,transform .12s}
a.chip.chip-lk:hover{background:var(--brand-tint);color:var(--brand-deep)}

/* ── overview page: inline caveat note ───────────────────── */
.note-inline{font-size:11.5px;line-height:1.5;color:var(--ink-2);background:var(--brand-tint);
  border-left:2px solid var(--brand);border-radius:6px;padding:9px 12px;margin-bottom:10px}
/* The developer/reporter cells in a bug table are links, and their 23px avatar
   set the whole target height. One pixel under the minimum is still under. */
.who{display:flex;align-items:center;gap:7px;white-space:nowrap;min-height:24px}
.who .av{width:23px;height:23px;border-radius:50%;display:grid;place-items:center;font-size:9px;font-weight:800;
  background:var(--line-soft);color:var(--ink-2);flex:0 0 auto}
.who .av.qa{background:var(--brand-tint);color:var(--brand-deep)}
.who.lk{cursor:pointer}
.who.lk:hover span:last-child{color:var(--brand-deep);text-decoration:underline}
.tag{display:inline-block;font-family:var(--mono);font-size:10px;padding:2px 7px;border-radius:5px;
  background:var(--line-soft);color:var(--ink-2);margin:1px 2px 1px 0}
.tag.warn{background:#fdeceb;color:var(--crit)}
.agency{display:inline-block;font-size:12px;color:var(--ink);
  border-left:2px solid var(--brand);padding-left:8px;line-height:1.3}
/* bugTable's `links` option (round UI-D1): the agency / developer / reporter
   cells become anchors to that entity's page. `.lk` is the marker the option
   adds — a caller that passes no links renders the same <span> as before and
   is untouched by these rules. .who.lk already exists above; this is its
   agency-cell counterpart, which has no inner span to underline. */
a.agency.lk{cursor:pointer}
a.agency.lk:hover{color:var(--brand-deep);text-decoration:underline}
.dt{font-family:var(--mono);font-size:11px;color:var(--ink-3);white-space:nowrap}
/* Ticket number — monospaced so RIV-1023 and a BugHerd task id line up in a
   column people scan vertically, and never wrapped mid-identifier. */
.tick{font-family:var(--mono);font-size:11px;font-weight:600;color:var(--brand-deep);white-space:nowrap}
a.tick:hover{text-decoration:underline}
/* A bug table taller than this scrolls inside itself, which keeps the sticky
   header useful on 409 rows. Viewport-relative rather than a flat 640px: on a
   laptop that was a letterbox showing seven rows of a long list while most of
   the screen sat empty, and the smaller the window the more of the reading
   happened through a nested scroller. `min()` keeps it sane on a tall monitor.
   No overscroll containment — reaching the last row must hand the wheel back
   to the page rather than dead-end there. */
.scroll-y{max-height:min(78vh,1000px);overflow-y:auto}

/* list-page grid of entity cards */
.ecards{display:grid;grid-template-columns:repeat(auto-fill,minmax(268px,1fr));gap:12px}
.ecard{background:var(--card);border:1px solid var(--line);border-radius:var(--r);padding:15px;
  box-shadow:var(--shadow);cursor:pointer;transition:transform .16s,border-color .16s,box-shadow .16s;display:block}
.ecard:hover{transform:translateY(-2px);border-color:var(--brand);box-shadow:0 10px 26px -14px rgba(65,105,225,.5)}
.ecard .top{display:flex;align-items:center;gap:10px}
.ecard .av{width:36px;height:36px;border-radius:11px;display:grid;place-items:center;font-size:11.5px;
  font-weight:800;background:var(--brand-tint);color:var(--brand-deep);flex:0 0 auto}
.ecard .av.g{background:var(--line-soft);color:var(--ink-2)}
.ecard b{font-size:13.5px;font-weight:700;display:block;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.ecard small{font-family:var(--mono);font-size:9.5px;color:var(--ink-3)}
.ecard .big{margin-left:auto;font-size:22px;font-weight:800;letter-spacing:-.03em;font-variant-numeric:tabular-nums}
.mix{display:flex;height:6px;border-radius:20px;overflow:hidden;background:var(--line-soft)}
.mix i{display:block}
.ecard .mix{margin-top:12px}
.ecard .meta{display:flex;gap:12px;margin-top:9px;font-family:var(--mono);font-size:9.5px;color:var(--ink-3)}
/* An agency or developer with nothing in this period. Present in the grid,
   in roster order, and still clickable — a zero is an answer, not an absence.
   Dimmed rather than hidden so scanning for activity stays fast, and it comes
   back to full strength on hover so reading one costs nothing. No colour of
   its own: this is the same card at lower emphasis. */
.ecard.quiet{opacity:.55}
.ecard.quiet:hover,.ecard.quiet:focus-visible{opacity:1}
.ecard.quiet .big{color:var(--ink-3);font-weight:700}
.ecard.quiet .mix{opacity:.5}

/* list-page entity-card extras: teams.js's period-over-period delta line and
   developers.js's per-period sparkline row (both spliced into entityCard's
   markup with .replace, between .mix and .meta / after .meta respectively) */
.ecard-delta{margin-top:9px;font-family:var(--mono);font-size:10px;color:var(--ink-3);
  display:flex;align-items:center;gap:6px}
.ecard-spark{display:flex;align-items:center;gap:8px;margin-top:10px}
.ecard-spark .spark{flex:0 0 auto;justify-content:flex-start}

/* entityCard() with a falsy href — no server-addressable identity to link to
   (e.g. a QA reporter with no email). Same body, no hover affordance. */
.ecard-static{cursor:default}
.ecard-static:hover{transform:none;border-color:var(--line);box-shadow:var(--shadow)}

/* filters bar */
.fbar{display:flex;gap:8px;flex-wrap:wrap;align-items:center}
.fbar input,.fbar select{font-family:var(--sans);font-size:12.5px;padding:8px 11px;border-radius:9px;
  border:1px solid var(--line);background:var(--card);color:var(--ink)}
.fbar input{min-width:230px}
.fbar input:focus,.fbar select:focus{outline:none;border-color:var(--brand);box-shadow:0 0 0 3px var(--brand-tint)}
.fbar .cnt{font-family:var(--mono);font-size:11px;color:var(--ink-3);margin-left:auto}

/* group blocks (missing tags) */
.gblock{margin-top:4px}
/* A <button> (missing.js) — and until this round the only clickable element on
   the dashboard with no hover state at all, never mind a press one. It gets
   the same tint every other hovered target uses and the same press squeeze.
   The rest is the <button> reset it never had, because a button inherits none
   of it: cursor, text-align, colour, and — measured in the browser, not
   assumed — font-family, which was computing to Arial, Chrome's UA default
   for a button. These three headers were the only text on the dashboard not
   set in the design's own typeface. That is the existing --sans being applied
   where it always should have been, not a new font. */
.gh{display:flex;align-items:center;gap:10px;padding:12px 14px;border-radius:11px;background:#f9fafd;
  border:1px solid var(--line);width:100%;text-align:left;font-family:var(--sans);color:inherit;
  cursor:pointer;transition:background .15s,border-color .15s,transform .12s}
.gh:hover{background:var(--line-soft);border-color:var(--brand-tint-2)}
.gh:active{transform:scale(.995);background:var(--brand-tint)}
.gh b{font-size:13.5px;font-weight:700}
.gh .n{font-family:var(--mono);font-size:10.5px;font-weight:700;padding:2px 9px;border-radius:20px;
  background:var(--brand-tint);color:var(--brand-deep)}
.gh .why{font-family:var(--mono);font-size:10px;color:var(--ink-3);margin-left:auto}
.gh .chev{font-size:10px;color:var(--ink-3);flex:0 0 auto;transition:transform .18s,color .18s}
.gh[aria-expanded="true"] .chev{transform:rotate(180deg);color:var(--brand-deep)}

/* intro line above an accordion / list — smaller than .card-h hint text,
   larger than .empty, sits directly in page flow rather than inside a card */
.lede-sm{font-size:12px;color:var(--ink-2);margin:2px 0 12px}

/* "how to fix" callout inside an open group panel — same tint/border
   language as .note-inline so both read as the same kind of aside */
.fix{font-size:12px;line-height:1.5;color:var(--ink-2);background:var(--brand-tint);
  border-left:2px solid var(--brand);border-radius:6px;padding:9px 12px;margin-bottom:12px}
.fix b{color:var(--ink)}

/* CARD ROWS ARE A FIXED GRID (owner, 2026-08-04). Every card in a row is the
   same size, and that size does not move when a filter changes the data.

   The two earlier attempts both tied height to content, and both failed the
   same way. align-items:start sized each card to itself: 350/211/304 in the
   first .g3, ragged notches of page background between them. Stretching fixed
   the notches but made the row as tall as its tallest card, so the row still
   resized every time a filter changed — pick a developer and Top Agencies
   drops from ten bars to one, the row collapses, and everything below it
   jumps up the page. The exception below it (align-self:start for list and
   table cards) then reintroduced the ragged edge for exactly the cards that
   vary most.

   grid-auto-rows cuts the dependency: the row is --tile-h whatever it holds.
   A card with less data is partly empty, which the owner explicitly accepts;
   a card with more scrolls inside its own body (below). Nothing outside the
   card moves, ever. */
.g4{display:grid;grid-template-columns:minmax(0,1.35fr) minmax(0,1fr) minmax(0,1fr) minmax(0,1.1fr);
  gap:16px;grid-auto-rows:var(--tile-h)}
@media(max-width:1500px){.g4{grid-template-columns:repeat(2,minmax(0,1fr))}}
@media(max-width:900px){.g4{grid-template-columns:minmax(0,1fr)}}
.g3{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:16px;grid-auto-rows:var(--tile-h)}
@media(max-width:1300px){.g3{grid-template-columns:minmax(0,1fr)}}
.g2{display:grid;grid-template-columns:minmax(0,1.5fr) minmax(0,1fr);gap:16px;grid-auto-rows:var(--tile-h)}
@media(max-width:1300px){.g2{grid-template-columns:minmax(0,1fr)}}

/* …with one exception, and it is a judgement about WHY the height is fixed.
   A fixed height buys stability: the row does not resize when a filter changes
   how much data it holds, so nothing below it jumps. That is worth paying for
   on Overview and Reports, where the cards are charts of comparable size and
   the reader is filtering constantly.

   It buys nothing on an entity page. Measured on /agency/Ochsner Health - Dev:
   "By developer" held ONE bar — 18px of content in a 412px card, 326px of
   white — and "Trend by severity" and "Severity mix" wasted 184 and 204. An
   agency with one developer is not a filter state to stay stable across, it is
   what that agency IS; the next agency is a different page, not a re-filter.
   So these rows size to their tallest card. Cards still stretch to match each
   other, so the row keeps its shared bottom edge — that was never the part in
   question. */
.g2.fit,.g3.fit,.g4.fit{grid-auto-rows:auto}

/* A row may also declare a smaller fixed tile. Same mechanism as --tile-h,
   just overridden on the section, so there is still one way heights are set.

   --tile-h is 412 because a 12-row roster needs 409. Overview's FIRST row
   holds none: measured across six filter states at 1600/1440/1366/1301, its
   tallest card never exceeds 369 (Top Agencies at 10 bars needs 350; the type
   donut reaches 369 only at 1366, where its legend labels wrap to two lines).
   Inheriting 412 there was 62px of white under the agency list and 201 under
   the donut, for a constraint belonging to a different row. 372 covers every
   measured state with 3px to spare — do not cut it to the 350 the widest
   window suggests, or the type legend clips at 1366. */
.g2.tile-sm,.g3.tile-sm,.g4.tile-sm{--tile-h:372px}

/* The other half of the mechanism, and it is not optional: a fixed box needs
   somewhere for surplus content to go, or Reports' 33 bug types would simply
   be cut off at the border with no way to reach them. `min-height:0` is what
   makes it work at all — a flex item's default min-height:auto refuses to
   shrink below its content, so the body would push the card taller than the
   grid row instead of scrolling.

   `overflow:hidden auto` — hidden on X, auto on Y — and the X half is not
   cosmetic. With overflow-x left visible the browser promotes it to auto, so
   one stray pixel of horizontal overflow (a bar list at a 1366px window is a
   pixel wide) raises a horizontal scrollbar, that scrollbar eats 14px of
   height, and the card starts scrolling VERTICALLY for content that fit
   perfectly well. Measured: Developers scrolled at 1366 and not at 1600 for
   exactly this reason. Anything that genuinely needs to scroll sideways — a
   bug table — carries its own .tbl-wrap and is unaffected.

   TWO RULES, and the split is the whole point. A card body is `hidden` by
   default — it clips, it is NOT a scroll container, and the wheel passes
   straight through to the page. Only a body that genuinely overflows gets
   `.scrolls` (main.js measures after every render) and becomes scrollable.

   Making every card a scroll container is what broke scrolling: Overview had
   ELEVEN of them, NINE with nothing to scroll, and `overscroll-behavior:
   contain` on all of them told the browser never to hand the wheel back to
   the page. The cards cover most of the page, so for most of the page the
   wheel did nothing, and moving the pointer into a gap between cards made it
   work again — "the scroller stops working sometimes". Containment is gone
   too: a list that reaches its end must let the page carry on. */
.g2>.card>.card-b,.g3>.card>.card-b,.g4>.card>.card-b,.hero-col>.card>.card-b{
  min-height:0;overflow:hidden}
.g2>.card>.card-b.scrolls,.g3>.card>.card-b.scrolls,
.g4>.card>.card-b.scrolls,.hero-col>.card>.card-b.scrolls{overflow:hidden auto}
/* A thin, quiet scrollbar. The default one is wide enough to squeeze a bar
   list's three columns and reads as a browser artefact rather than part of
   the card. */
.card-b{scrollbar-width:thin;scrollbar-color:var(--line) transparent}
.card-b::-webkit-scrollbar{width:8px}
.card-b::-webkit-scrollbar-track{background:transparent}
.card-b::-webkit-scrollbar-thumb{background:var(--line);border-radius:20px;
  border:2px solid var(--card);background-clip:padding-box}
.card-b::-webkit-scrollbar-thumb:hover{background:var(--line-soft)}

/* Overview-only hero row (owner's mockup): two columns, 2fr:1fr, each a flex
   column of stacked cards — Bug Trend + Bugs per week on the left, By
   Severity + Tagging Health on the right. Fixed at --hero-h for the same
   reason as the rows above; the left column's split is what needs stating.

   Bug Trend is `flex:0 0 auto` and everything else `flex:1 1 0`. That is
   deliberate and not interchangeable: the trend's SVG is a fixed 250px, so a
   card that shrinks around it would clip the chart, while the bar list below
   it is the one thing in the row that genuinely grows with the data (a
   six-month weekly range is 26 rows) and is therefore the right card to
   absorb the remainder and scroll. In the right column both donuts are
   `flex:1 1 0`, so they split the row exactly in half — equal by
   construction rather than by whichever legend happens to be longer. */
.g-hero{display:grid;grid-template-columns:minmax(0,2fr) minmax(0,1fr);gap:16px;
  grid-auto-rows:var(--hero-h)}
.hero-col{display:flex;flex-direction:column;gap:16px;min-width:0;min-height:0}
.hero-col>.card{flex:1 1 0;min-height:0}
.hero-col>.card:has(.trend){flex:0 0 auto}
@media(max-width:1300px){.g-hero{grid-template-columns:minmax(0,1fr)}}

.mini{display:grid;grid-template-columns:repeat(5,minmax(0,1fr));gap:12px}
@media(max-width:1200px){.mini{grid-template-columns:repeat(2,minmax(0,1fr))}}
.mcard{background:var(--card);border:1px solid var(--line);border-radius:var(--r);padding:15px 16px 0;
  box-shadow:var(--shadow);overflow:hidden;display:flex;flex-direction:column}
.mcard .mk{font-size:12px;font-weight:700}
.mcard .mv{font-size:25px;font-weight:800;letter-spacing:-.035em;margin-top:5px;font-variant-numeric:tabular-nums}
.mcard .ms{font-family:var(--mono);font-size:10px;color:var(--ink-3);margin-top:2px}
.mcard svg{width:100%;height:44px;margin-top:auto;display:block}

footer{padding:22px 26px 34px;color:var(--ink-3);font-size:11.5px;line-height:1.75;border-top:1px solid var(--line)}
footer b{color:var(--ink-2)}

/* ── press feedback ──────────────────────────────────────────
   Hover was styled on every clickable thing here; the press itself was
   silent. On this dashboard almost every click is a filter change, and a
   filter change takes a beat to land, so until now the only acknowledgement
   that a click had registered at all was the page eventually changing —
   which is exactly the moment someone clicks a second time.

   It is one gesture everywhere: the target gives very slightly under the
   pointer and returns the instant the button is released. Each rule expresses
   that in whatever property the element already animates, so nothing new
   enters the visual vocabulary and no colour is added — the tints are the
   same --brand-tint the hover states already use. The scales are deliberately
   tiny (.985-.995 on a card, .96 on a 34px heatmap cell, since the same
   proportional squeeze has to read at both sizes): a press is confirmation,
   not an event. :active only, so none of it costs anything until a pointer is
   down. */
a.kpi:active{transform:translateY(0) scale(.985)}
.ecard:active{transform:translateY(0) scale(.99)}
a.brow:active,.brow.wk:active{background:var(--brand-tint);transform:scale(.995)}
a.col:active,.col[data-sev]:active{background:var(--brand-tint);transform:scale(.98)}
.hm .cell:not(.pad):active{transform:scale(.96)}
/* SVG: the opacity goes on the <a> wrapper, never the <circle>. A dimmed
   slice carries its opacity as an inline presentation attribute, and a CSS
   opacity on the circle would override it — every slice would press to the
   same value and the "which one is selected" dimming would blink off under
   the finger. On the wrapper the two multiply, so a dim slice stays dim. */
.donut .arc-link:active{opacity:.62}
.legend a.li.clickable:active{background:var(--brand-tint);transform:scale(.99)}
a.chip.chip-lk:active{transform:scale(.97)}
/* Rows, not cells: a transform on a <tr> is not reliable across browsers (a
   table row is not a transformable box everywhere), so a row's press is the
   tint its hover already uses, one step stronger. The linked cells inside
   press on their own, since a click usually lands on one of them. */
.tbl tbody tr:active,.ltable tbody tr:active{background:var(--brand-tint)}
a.agency.lk:active,.who.lk:active,a.tick:active,.link:active{opacity:.6}
.scol:active .sstack{opacity:.72}
.nav a:active{transform:scale(.98)}
.seg button:active,.msg button:active,.scope-all:active{transform:scale(.96)}

@keyframes grow{from{transform:scaleX(0)}to{transform:scaleX(1)}}
/* scaleY, not height: height is a layout property, so the old version made the
   browser re-measure the page on every frame of all seven team columns.
   transform is composited and costs nothing. Visually identical. */
@keyframes rise{from{transform:scaleY(0);opacity:0}}
@keyframes fade{from{opacity:0}to{opacity:1}}
@keyframes draw{to{stroke-dashoffset:0}}
/* Someone who asked the OS for less motion gets a static page. The blanket
   animation:none handles everything that ANIMATES, but three things also need
   their finished state stated outright, because with the animation gone they
   would otherwise sit at whatever resting value the markup gives them: the
   trend line (drawn via stroke-dashoffset, which is 1 in the rule), the
   area fill beneath it, and viewIn's opacity/transform. The donut's conic
   mask strictly needs no line here — --sweep is registered at 360deg so a
   ring that never animates is a whole ring — but it is spelled out anyway,
   because a mask that silently hides a chart is not a failure worth leaving
   to one property registration. */
@media(prefers-reduced-motion:reduce){*{animation:none!important;transition:none!important}
  .trend .ln{stroke-dashoffset:0}.trend .ar{opacity:1}
  .content>*{opacity:1;transform:none}
  .donut svg{-webkit-mask-image:none;mask-image:none}}

/* ============================================================
   Below: carried over from the current (pre-shell) dashboard.css,
   unchanged. The design file has no equivalent — these carry the
   severity tooltip, sparkline, trend candles and severity-stacked
   bars that Task 9 and Task 12 need. Both the old .bar-row names
   (from here) and the new .brow names (ported above, from the
   design file) exist side by side, so markup using either is
   already styled.
   ============================================================ */

.sevtip{position:fixed;z-index:60;background:var(--card);border:1px solid var(--line);border-radius:2px;
  padding:9px 12px;box-shadow:0 8px 24px rgba(22,26,32,.14);pointer-events:none;font-size:12px;
  font-family:var(--sans);white-space:nowrap}
.sevtip[hidden]{display:none}
.sevtip .t-h{font-weight:650;display:flex;justify-content:space-between;gap:14px;margin-bottom:6px;
  padding-bottom:6px;border-bottom:1px solid var(--line)}
.sevtip .t-r{display:flex;align-items:center;gap:7px;margin:3px 0;color:var(--ink-2)}
.sevtip .t-r .sw{width:9px;height:9px;border-radius:2px;display:inline-block}
.sevtip .t-r b{margin-left:auto;color:var(--ink);font-variant-numeric:tabular-nums;font-weight:600}
/* An explanatory tooltip body (data-tip) is a sentence, not a severity row —
   .sevtip is white-space:nowrap for the numeric case, so it would otherwise
   run off the viewport in one line. */
.sevtip .t-note{white-space:normal;max-width:260px;display:block;line-height:1.45}

.spark{display:flex;align-items:flex-end;gap:4px;height:34px;flex:0 0 150px;justify-content:center}
.spark .sb{width:15px;border-radius:1px 1px 0 0;overflow:hidden;display:flex;flex-direction:column-reverse;min-height:2px}
.spark .sb i{display:block;width:100%}

.strend{display:flex;align-items:flex-end;gap:10px;height:144px}
.scol{flex:1;height:100%;display:flex;flex-direction:column;align-items:center;justify-content:flex-end;
  gap:6px;background:none;border:none;padding:0;cursor:pointer;font-family:var(--sans)}
.scol .n{font-family:var(--mono);font-size:11px;color:var(--ink-2);font-variant-numeric:tabular-nums}
.sstack{width:100%;max-width:44px;border-radius:2px;overflow:hidden;display:flex;flex-direction:column-reverse;
  min-height:3px;transition:outline-color .18s,opacity .12s}
.sstack i{display:block;width:100%}
.scol .lb{font-family:var(--mono);font-size:10px;color:var(--ink-3)}
.scol:hover .sstack{outline:2px solid var(--line);outline-offset:3px}
.scol.sel .sstack{outline:2px solid var(--brand);outline-offset:3px}
.scol.sel .lb{color:var(--brand-deep);font-weight:600}

/* ── detail-page period scoping (Task 12): the line under the trend that
   says what a click narrowed the page to, and the button that clears it ── */
.scope-line{margin-top:10px;font-family:var(--mono);font-size:10.5px;color:var(--ink-3);
  display:flex;align-items:center;gap:8px}
.scope-line b{color:var(--ink-2);font-weight:700}
.scope-all{border:none;font-family:var(--sans);font-size:11px;font-weight:700;
  color:var(--brand-deep);background:var(--brand-tint);cursor:pointer;padding:3px 9px;border-radius:20px;
  transition:background .15s,transform .12s}
.scope-all:hover{background:var(--brand-tint-2)}

.bar-row{display:flex;align-items:center;gap:11px;margin:7px 0}
.bar-nm{flex:0 0 175px;font-size:12.5px;color:var(--ink-2);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.bar-nm.qa{display:flex;align-items:center;gap:7px}
.qa-dot{width:18px;height:18px;border-radius:50%;flex:0 0 auto;display:flex;align-items:center;justify-content:center;
  font-family:var(--mono);font-size:8px;font-weight:600;background:var(--line-soft);color:var(--ink-2);border:1px solid var(--line)}
.bar-track{flex:1;height:15px;background:var(--line-soft);border-radius:2px;overflow:hidden}
.bar-fill{height:100%;display:flex;border-radius:2px;overflow:hidden}
.bar-fill i{display:block;height:100%}
.bar-n{flex:0 0 26px;text-align:right;font-family:var(--mono);font-size:12px;font-weight:500;font-variant-numeric:tabular-nums}

.qa-cell{display:flex;align-items:center;gap:8px;white-space:nowrap}
.qa-cell b{font-weight:500;font-size:12.5px}
.qa-cell small{font-family:var(--mono);font-size:9.5px;color:var(--ink-3)}

.sev-chip{display:inline-flex;align-items:center;gap:5px;font-family:var(--mono);font-size:10px;
  letter-spacing:.04em;text-transform:uppercase;color:var(--ink-2);white-space:nowrap}
.sev-chip .sw{width:8px;height:8px;border-radius:2px;display:inline-block}

.ltable{width:100%;border-collapse:collapse;font-size:12.5px}
.ltable th{font-family:var(--mono);font-size:9.5px;letter-spacing:.12em;text-transform:uppercase;color:var(--ink-3);
  text-align:left;padding:7px 10px;border-bottom:1px solid var(--line);font-weight:500}
.ltable td{padding:9px 10px;border-bottom:1px solid var(--line-soft);vertical-align:middle}
.ltable tr:last-child td{border-bottom:none}
.ltable tbody tr{transition:background .15s}
.ltable tbody tr:hover{background:var(--line-soft)}

.tag-chip{display:inline-block;font-family:var(--mono);font-size:10.5px;padding:2px 7px;margin:1px 3px 1px 0;
  border-radius:2px;background:var(--line-soft);color:var(--ink-2);border:1px solid var(--line)}
.tag-none{font-family:var(--mono);font-size:10.5px;color:var(--ink-3)}

.sevbar{display:flex;height:7px;width:116px;flex:0 0 auto;gap:2px;border-radius:1px;overflow:hidden}
.sevbar span{display:block}

/* ══════════════════════════════════════════════════════════════
   RESPONSIVE — last in the file on purpose.

   A media query adds no specificity, so an override must come AFTER the rule
   it overrides. These lived near the top until 2026-08-04, above `.side`'s own
   definition, which meant the mobile sidebar rule never applied at all: on a
   phone the sidebar kept its 236px width and 100vh height, and because
   `body{display:block}` DID apply, the dashboard began one full blank screen
   below the fold. Keep new breakpoints here.
   ══════════════════════════════════════════════════════════════ */

/* Fixed card heights exist so the cards ACROSS a row match each other and stay
   put when a filter changes the data. At these widths every one of these grids
   has already collapsed to a single column, so there is no row left to match —
   and a fixed height there would only buy a scrollbar inside a card inside the
   scrolling page, which on a touch screen is a trap. Each card goes back to its
   own content height, one under the next. */
@media(max-width:1300px){
  .g2,.g3,.g-hero{grid-auto-rows:auto}
  .g2>.card>.card-b,.g3>.card>.card-b,.hero-col>.card>.card-b{overflow:visible}
  .hero-col>.card,.hero-col>.card:has(.trend){flex:0 0 auto}
}
@media(max-width:900px){
  .g4{grid-auto-rows:auto}
  .g4>.card>.card-b{overflow:visible}
}

@media(max-width:1100px){
  body{display:block}
  /* A bar across the top instead of a full-height column: brand on the left,
     the sections button on the right, and the nav itself closed until asked
     for. The bar is sticky (it is ~62px, unlike the topbar, which was made
     static at 560px for being taller than the screen) so the button is always
     one tap away; z-index above the topbar's 20 so the panel opens over it.
     overflow must be visible here — the panel hangs below the bar's box. */
  .side{width:auto;flex:none;height:auto;position:sticky;top:0;z-index:30;flex-direction:row;
    align-items:center;border-right:none;border-bottom:1px solid var(--line);overflow:visible}
  .side::after{display:none}
  .brand{padding:12px 16px;flex:0 0 auto}

  /* The button: a 40px square at the end of the bar, drawn in the same ink,
     radius and border as every other control. Three lines that fold into a
     cross while open, so the same spot both opens and closes and reads as
     such. Transform-only, so it costs no layout. */
  .menu-btn{display:grid;place-items:center;width:40px;height:40px;margin-left:auto;margin-right:14px;
    flex:0 0 auto;background:var(--card);color:var(--ink-2);border:1px solid var(--line);border-radius:10px;
    cursor:pointer;transition:background .16s,color .16s,border-color .16s,transform .12s}
  .menu-btn svg{width:20px;height:20px;stroke:currentColor;fill:none;stroke-width:2;stroke-linecap:round}
  .menu-btn svg path{transition:transform .22s cubic-bezier(.3,.7,.3,1),opacity .16s;transform-origin:12px 12px}
  .menu-btn:hover{background:var(--brand-tint);color:var(--brand-deep);border-color:var(--brand-tint-2)}
  .menu-btn:active{transform:scale(.95)}
  .menu-btn:focus-visible{outline:2px solid var(--brand);outline-offset:2px}
  body.nav-open .menu-btn{background:var(--brand-tint);color:var(--brand-deep);border-color:var(--brand-tint-2)}
  body.nav-open .menu-btn .l1{transform:translateY(5px) rotate(45deg)}
  body.nav-open .menu-btn .l2{opacity:0;transform:scaleX(.2)}
  body.nav-open .menu-btn .l3{transform:translateY(-5px) rotate(-45deg)}

  /* The panel: the desktop sidebar's list, dropped beneath the bar over the
     page rather than pushing it down, so opening and closing never shifts the
     figures. Section labels return (they were hidden in the strip), rows grow
     to a 44px tap target, and the list scrolls inside itself only if the
     screen is shorter than eight rows — both axes stated, no overscroll
     containment, so a panel that fits hands the wheel back to the page. */
  .side .nav{display:none;position:absolute;top:100%;left:0;right:0;flex-direction:column;gap:2px;
    padding:8px 12px 14px;background:var(--card);border-bottom:1px solid var(--line);
    box-shadow:0 24px 40px -24px rgba(23,26,34,.35);max-height:calc(100vh - 64px);overflow:hidden auto}
  body.nav-open .side .nav{display:flex;animation:menuIn .18s cubic-bezier(.3,.7,.3,1)}
  .side .nav a{padding:12px 14px;font-size:14px;min-height:44px}
  .side .nav-sep{display:block;margin:12px 14px 4px}
  /* A quiet scrim under the panel: it says "this is a layer", and a tap on it
     lands on the page, which main.js reads as "close". Below the bar's 30. */
  body.nav-open::before{content:'';position:fixed;inset:0;background:rgba(23,26,34,.28);z-index:25}
}
@keyframes menuIn{from{opacity:0;transform:translateY(-6px)}}

@media(max-width:820px){
  .kpis{grid-template-columns:repeat(2,minmax(0,1fr))}
  /* Below ~820px a 148px floor drops auto-fit to a single column and the
     strip becomes ten full-width rows. A lower floor keeps two or three
     tracks; Period still spans two, which on the narrowest screens simply
     means it takes a row of its own — the honest layout for two date
     fields there. */
  .tools{grid-template-columns:repeat(auto-fit,minmax(124px,1fr))}
}

@media(max-width:560px){
  /* The topbar is sticky and, with ten controls, was taller than the phone
     screen — so the sticky header covered the page it was heading. Let it
     scroll away here; the controls are still one swipe up. */
  .topbar{position:static;padding:14px 16px 10px;gap:10px}
  .content{padding:12px 16px 32px}
  /* Ten stacked controls put the first figure below the fold. A 100px floor
     gives three tracks at 375px instead of two, and the refresh stamp — useful
     on a desktop, noise on a phone — gives up its line. Together the strip
     loses roughly a third of its height, so the KPIs are visible on landing. */
  .tools{grid-template-columns:repeat(auto-fit,minmax(100px,1fr));gap:8px}
  .tb-title .stamp{display:none}
  .tb-title h1{font-size:19px}
  .tb-title .sub{font-size:11.5px}
  .ctl select,.ctl input{padding:6px 8px;font-size:12px}
  .seg button{padding:6px 4px;font-size:12px}
  /* Period holds two date fields and an arrow. At a third of a phone screen
     each field clipped its own value ("01-08-202"), so it takes the full row
     here rather than a two-of-three span. */
  .ctl-wide{grid-column:1 / -1}
  /* A bug table cannot usefully shrink to 375px, so it scrolls sideways in its
     own box. -webkit-overflow-scrolling keeps that momentum-scrolled on iOS,
     and the touch hint stops the gesture being invisible. */
  .tbl-wrap{-webkit-overflow-scrolling:touch}
  .tbl{font-size:12px}
  .tbl th,.tbl td{padding:8px 10px}
  /* A tighter gap so today's seven teams fit a 390px screen outright rather
     than relying on the scroll the base rule provides. Columns are flex:1, so
     the space the gaps give up goes straight back into the bars. */
  .cols{gap:8px}

  /* The heatmap is a fixed 7 x 34px grid = 268px; below ~380px of usable width
     it would overflow its card, so the cells give a little.
     `width:100%` is not decoration. .hm carries margin:0 auto to centre it, and
     an auto cross-axis margin cancels its flex parent's stretch — so a grid of
     minmax(0,1fr) tracks has no width to share out and shrink-to-fits to
     min-content instead: 16px cells adrift in a 343px card, too small to read
     and too small to tap. Removing this line brings that straight back. */
  .hm{grid-template-columns:repeat(7,minmax(0,1fr));max-width:100%;width:100%}
  .hm-scale{max-width:100%}
}
