  /* === THE [hidden] RULE, and it has cost this file twice ==========================
     ANY element this app toggles via the `hidden` ATTRIBUTE must carry an explicit
     `[hidden]{display:none}` override IF this stylesheet sets a display value on it.

     `hidden` works by a UA stylesheet rule of exactly `[hidden]{display:none}`. That is
     the WEAKEST possible source, so any author `display:` we set -- flex, block, grid --
     beats it, and the element keeps rendering while `el.hidden` reads true.

     What makes it dangerous is the failure signature: the JS is correct, the attribute is
     correct, `aria-expanded` is correct, and reading any of them back CONFIRMS the element
     is hidden. Only computed display disagrees. So it survives code review, it survives a
     test that asserts on the attribute, and it ships a control that reports success and
     does nothing.

     Bitten twice:
       - #feedback-panel / #fb-fields / #fb-thanks (see that block)
       - .rail-section-body, 2026-08-24: a collapsed rail section flipped its caret, set
         aria-expanded=false, and rendered all 17 chips anyway.

     Audited 2026-08-24 across every element static/js toggles with `hidden` -- #map-note,
     #feedback-panel, #fb-fields, #fb-thanks, .caveat-pop, #disabled-reason-pop,
     .rail-section-body -- by setting the attribute and reading COMPUTED DISPLAY on each.
     All seven hide correctly today. The two caveat popovers are created on demand, so
     they were forced into the DOM first rather than skipped.

     If you add a display value to something toggled by `hidden`, add the override in the
     same edit. Do not rely on noticing. ============================================== */

  @import url('https://fonts.googleapis.com/css2?family=Source+Serif+4:opsz,wght@8..60,400;8..60,600;8..60,700&family=IBM+Plex+Mono:wght@400;500&family=Inter:wght@400;500;600&display=swap');

  :root{
    --paper:#F5F1E8;
    --ink:#1D2B26;
    --ink-soft:#4A5A54;
    --line:#C9C0AC;
    --gold:#A8791F;
    --gold-soft:#E4D3A8;
    --up:#8A3B2E;
    --down:#2F5D4F;
    --pending:#B9B2A0;
    /* --pending was doing THREE incompatible jobs, and could only be right for one:
       muted text on cream, muted text on the dark selected cluster tab, and the
       disabled-Send fill. Measured on 2026-08-24 with an in-page WCAG 2.x harness
       (self-tested at 21:1 black-on-white and 1:1 white-on-white):

         .dataset-figure.pending  "NOT COUNTY-LEVEL"   1.87:1 on --paper   FAIL
         .stat-value.pending                            1.87:1 on --paper   FAIL
         .cluster-tab-count, tab UNselected             1.87:1 on --paper   FAIL
         #fb-comment-count      (panel is lighter)      1.97:1              FAIL
         .cluster-tab-count, tab SELECTED               NOT --pending -- see below

       CORRECTION, from a cold review of this same change: the selected-tab count was
       recorded here as 6.97:1 on --ink and used to argue that darkening --pending would
       have traded a failure for a failure. It renders --gold-soft (9.95:1), set by a
       later identical-specificity rule, so it was never --pending and darkening could
       not have touched it. The reasoning was wrong; the four FAILING rows above are
       real, reproduce exactly, and are what this change actually fixes.

       --pending stays exactly as it was. Its only remaining live use is the disabled-Send
       fill -- the "text on --ink" case named here originally turned out to be --gold-soft.
       --pending-ink is the same hue and saturation (H 43.2, S 0.152) taken down in
       lightness only, so it reads as the same muted tan rather than a new colour. */
    --pending-ink:#716953;   /* 4.84:1 on --paper -- clears 4.5:1 with margin */
  }
  *{box-sizing:border-box;}
  body{
    margin:0;
    background:var(--paper);
    color:var(--ink);
    font-family:'Inter',sans-serif;
    min-height:100vh;
  }
  header{
    padding:28px 32px 16px;
    border-bottom:1px solid var(--line);
  }
  .eyebrow{
    font-family:'IBM Plex Mono',monospace;
    font-size:11px;
    letter-spacing:0.14em;
    text-transform:uppercase;
    color:var(--gold);
    margin-bottom:6px;
  }
  h1{
    font-family:'Source Serif 4',serif;
    font-weight:700;
    font-size:28px;
    margin:0 0 6px;
    letter-spacing:-0.01em;
  }
  .sub{
    font-size:13.5px;
    color:var(--ink-soft);
    max-width:640px;
    line-height:1.5;
  }
  .sub p{margin:0 0 8px;}
  .sub p:last-child{margin-bottom:0;}
  /* THE COLOUR-BY RAIL. Measured at 965x910 before this existed: the block sat at
     y=1137 on a 3625px page with the fold at 910, so the product's primary interaction --
     all 17 datasets -- was below the fold and a first-time visitor never saw it.

     WHY A RAIL AND NOT AN ACCORDION, since an accordion was the first proposal. Measured
     with the real labels and real chip styling: the full rail is 600px at 200px wide and
     fits without scrolling at both 900px and 768px viewport heights, so there is no
     height problem to solve. A one-section-open accordion is ~270px and shows FIVE of
     seventeen -- it would have re-created the same discoverability defect above the fold
     instead of below it. Sections are open by default and collapsible by choice.

     WHAT THIS COSTS, accepted deliberately by the developer rather than discovered:
     200 + 360 leaves the map column 405px at a 965px viewport, so the map draws ~385
     instead of today's 520. Above ~1100px nothing changes at all, because #map's own
     max-width:520px caps it long before the column runs out. The shrink applies only
     while a county panel is open -- which is the state where the reader is looking at
     the panel anyway. */
  .layout{
    display:grid;
    grid-template-columns:200px 1fr 360px;
    gap:0;
  }
  /* #stat-rail sits AFTER .map-pane in the DOM so the narrow layout needs no override --
     see the markup comment. `order` moves it to column 1 only where a rail exists. */
  .stat-rail{
    order:-1;
    padding:20px 16px 40px;
    border-right:1px solid var(--line);
  }
  .stat-rail .map-controls{
    display:block; max-width:none; margin:0; width:auto;
  }
  .stat-rail .controls-head{
    display:flex; justify-content:space-between; align-items:baseline;
    gap:8px; margin-bottom:4px;
  }
  .rail-section{border-top:1px solid var(--line); padding-top:2px;}
  .rail-section:first-of-type{border-top:none;}
  /* The whole heading is the hit target, not just the caret -- a 10px caret is a 10px
     tap target, and this row is the only new control on the page. */
  .rail-section-head{
    display:flex; align-items:center; gap:6px; width:100%;
    background:none; border:none; padding:9px 0 5px; cursor:pointer;
    font-family:inherit; text-align:left;
  }
  .rail-caret{color:var(--gold); font-size:9px; line-height:1;}
  .stat-rail .layer-cluster-label{
    flex:1 1 auto; margin:0; color:var(--gold);
  }
  .rail-count{
    font-family:'IBM Plex Mono',monospace; font-size:9px; color:var(--pending-ink);
  }
  .rail-section-body{
    display:flex; flex-direction:column; align-items:stretch; gap:4px;
    padding-bottom:8px;
  }
  /* THE [hidden] RULE (see the top of this file). `display:flex` above beats [hidden]'s
     UA `display:none`, so without this a collapsed section kept rendering all its chips
     while the caret and aria-expanded both said collapsed. Caught by measuring computed
     display, not by reading the attribute back -- which said "collapsed". */
  .rail-section-body[hidden]{display:none;}
  /* Chips fill the rail rather than shrink-wrapping: a ragged right edge across 17 items
     under five headings reads as noise, and the label is the thing being scanned. */
  .stat-rail .layer-btn{
    width:100%; text-align:left; white-space:normal;
  }
  @media (max-width:820px){
    /* No rail below the breakpoint: .layout is one column, so there is no side to put it
       on. #stat-rail loses its order override and renders exactly where #map-controls
       used to -- after the legend block, before the panel -- and controls.js keeps its
       existing "Colour by / Change" collapsed control at the same COLOUR_BY_BREAKPOINT.
       One number, 820, shared by the grid and the script. */
    .layout{grid-template-columns:1fr;}
    .stat-rail{order:0; padding:0; border-right:none;}
    .stat-rail .map-controls{
      display:flex; flex-wrap:wrap; max-width:520px; margin:0 auto 10px; width:100%;
    }
  }
  .map-pane{
    padding:20px 10px 40px;
    display:flex;
    flex-direction:column;
    align-items:center;
  }
  /* Scoped to the map. A bare `svg` selector here also captured the deep-dive charts
     and capped them at 520px inside a 780px column. */
  #map{max-width:520px; width:100%; height:auto;}
  .county-shape{
    cursor:pointer;
    stroke:var(--paper);
    stroke-width:0.6;
    transition:opacity 0.15s ease;
  }
  .county-shape:hover{opacity:0.82;}
  .county-shape.selected{stroke:var(--ink); stroke-width:1.6;}
  .county-shape:focus{outline:none; stroke:var(--ink); stroke-width:1.6;}
  .county-shape:focus-visible{stroke:var(--gold); stroke-width:2.4;}
  /* One card per dataset. The panel renders a LIST, because the API returns a list —
     the previous single-paragraph layout silently dropped everything after the first. */
  .dataset{
    border:1px solid var(--line);
    border-radius:2px;
    padding:14px 15px;
    margin-bottom:12px;
    background:#fff;
  }
  .dataset.not-comparable{background:#FAF7EF; border-style:dashed;}
  /* A dataset GROUP with more than one series (several Garda Divisions for one county,
     several hospitals x list-type for NTPF) collapses behind <details> rather than
     dumping every card open — Dublin alone has 30 NTPF cards, which is unreadable
     rendered flat. Groups of exactly one card render as before, unwrapped: the common
     case (most counties, most datasets) should stay a single click away, not two. */
  details.dataset-group{margin-bottom:12px;}
  details.dataset-group > .dataset{margin-bottom:8px;}
  details.dataset-group > .dataset:last-child{margin-bottom:0;}
  .dataset-group-summary{
    cursor:pointer; list-style:none; user-select:none;
    display:flex; justify-content:space-between; align-items:baseline; gap:10px;
    font-family:'Source Serif 4',serif; font-size:14.5px; font-weight:600;
    padding:10px 14px; border:1px solid var(--line); border-radius:2px; background:#FBF8F0;
  }
  .dataset-group-summary::-webkit-details-marker{display:none;}
  .dataset-group-summary::before{
    content:'▸'; color:var(--gold); margin-right:8px; font-size:11px;
  }
  details.dataset-group[open] > .dataset-group-summary::before{content:'▾';}
  details.dataset-group[open] > .dataset-group-summary{
    margin-bottom:8px; border-bottom-left-radius:0; border-bottom-right-radius:0;
  }
  .dataset-group-count{
    font-family:'IBM Plex Mono',monospace; font-size:10px; letter-spacing:0.06em;
    text-transform:uppercase; color:var(--ink-soft); font-weight:400; white-space:nowrap;
  }
  .dataset-head{
    display:flex; justify-content:space-between; align-items:baseline; gap:10px;
    margin-bottom:6px;
  }
  .dataset-name{
    font-family:'Source Serif 4',serif; font-size:15px; font-weight:600;
    line-height:1.3;
  }
  .dataset-figure{
    font-family:'IBM Plex Mono',monospace; font-size:15px; font-weight:500;
    white-space:nowrap; flex-shrink:0;
  }
  .dataset-figure.up{color:var(--up);}
  .dataset-figure.down{color:var(--down);}
  /* "Not county-level" is prose, not a figure — it must wrap and stay inside the card
     rather than overflowing the way a short percentage never would. */
  /* 11px, not 9.5px. Fixing the contrast to 4.84:1 answered half the review's
     complaint; its actual words were "9.5px at 1.87:1 -- that is not a contrast miss,
     it is invisible ink". This is the site's most important honesty label, and 11px is
     the same floor already set for chart axis labels rather than a new number.
     Developer's call, 2026-08-24. The wider type census (thirteen sizes, seven between
     9 and 12.5px) is deliberately NOT addressed here -- that is its own pass. */
  .dataset-figure.pending{
    color:var(--pending-ink); font-size:11px; letter-spacing:0.06em;
    text-transform:uppercase; white-space:normal; text-align:right;
    max-width:88px; line-height:1.3;
  }
  .dataset-sub{font-size:11.5px; color:var(--ink-soft); margin-bottom:4px;}
  .dataset-headline{font-size:13px; line-height:1.5; margin:8px 0 4px;}
  /* --- Breakdown chart -------------------------------------------------------
     Form: horizontal bars, sorted by magnitude. Horizontal because the category
     names are long ("Attempts/threats to murder, assaults, harassments...").
     ONE sequential hue — the job is comparing magnitude, not telling identities
     apart, so bar LENGTH carries the value and colour stays constant.
     Single series, so no legend: the heading names what is plotted. */
  .breakdown{margin-top:12px;}
  .bd-row{
    display:grid; grid-template-columns:1fr 46px 44px; align-items:center;
    gap:8px; padding:2px 0; cursor:default;
  }
  .bd-label{
    font-size:11px; color:var(--ink-soft); white-space:nowrap;
    overflow:hidden; text-overflow:ellipsis;
  }
  /* display:block matters — these are <span>s, and width/height do not apply to
     inline elements, so the bars render at zero size without it. */
  .bd-track{
    grid-column:1 / -1; display:block; height:9px; margin:-1px 0 3px;
    background:transparent;
  }
  .bd-bar{
    display:block; height:9px;
    /* 4px rounded data-end, square at the baseline */
    border-radius:0 4px 4px 0;
    background:var(--gold);
    min-width:2px;
  }
  .bd-value{
    font-family:'IBM Plex Mono',monospace; font-size:11px; text-align:right;
    font-variant-numeric:tabular-nums; color:var(--ink);
  }
  .bd-delta{
    font-family:'IBM Plex Mono',monospace; font-size:10px; text-align:right;
    font-variant-numeric:tabular-nums; color:var(--ink-soft);
  }
  .bd-delta.alert-up{color:var(--up);}
  .bd-delta.alert-down{color:var(--down);}
  /* Shared by the PSRA county-rank list and the crime division leaderboard: the
     currently-open geography's own row, so a reader can place themselves in the
     ranking rather than reading it as a disconnected top-10. */
  .bd-row-current{ border-left:2px solid var(--gold); padding-left:6px; margin-left:-8px; }
  .bd-row-current .bd-label{ font-weight:700; color:var(--ink); }
  .bd-note{
    font-size:10.5px; color:var(--ink-soft); font-style:italic;
    margin-top:8px; line-height:1.5;
  }
  .summary{
    font-size:12.5px; line-height:1.55; margin:10px 0 4px;
    padding-left:10px; border-left:2px solid var(--gold-soft);
  }
  /* --- Deep-dive overlay: a "newspaper page" over the map ------------------- */
  .overlay{
    position:fixed; inset:0; z-index:50;
    display:none; overflow-y:auto;
    padding:28px 20px 60px;
  }
  /* The blurred backdrop lives on ::before, not on .overlay itself, and this is load-
     bearing, not tidiness: `backdrop-filter` on an element makes IT the containing block
     for its own `position:fixed` descendants (CSS Filter Effects spec). With the filter
     on .overlay, every "fixed" child -- .sheet-close, .overlay-brand below -- was fixed
     relative to .overlay's own box instead of the viewport, so once .overlay's
     `overflow-y:auto` scrolled its content, both scrolled away with it: found while
     verifying the brand mark actually survives scroll (TODO.md's own requirement),
     confirmed via getBoundingClientRect() moving in lockstep with overlay.scrollTop. A
     pseudo-element carries the filter instead, so .overlay itself never gains a
     containing block and its fixed children stay pinned to the true viewport. */
  .overlay::before{
    content:''; position:fixed; inset:0; z-index:-1;
    background:rgba(29,43,38,0.55);
    backdrop-filter:blur(3px);
  }
  .overlay.open{display:block;}
  .sheet{
    max-width:860px; margin:0 auto;
    background:rgba(250,247,239,0.97);
    border:1px solid var(--line);
    box-shadow:0 20px 60px rgba(0,0,0,0.28);
    padding:34px 40px 40px;
  }
  /* Fixed to the viewport, not sticky-floated inside the sheet: a floated sticky
     element sits ON TOP of the text as you scroll, which it did. */
  .sheet-close{
    position:fixed; top:18px; right:calc(50% - 430px + 10px); z-index:60;
    font-family:'IBM Plex Mono',monospace; font-size:11px; letter-spacing:0.08em;
    text-transform:uppercase; background:var(--paper); border:1px solid var(--line);
    padding:6px 12px; cursor:pointer; color:var(--ink-soft);
    box-shadow:0 2px 8px rgba(0,0,0,0.12);
  }
  @media (max-width:920px){ .sheet-close{right:24px;} }
  .sheet-close:hover{background:var(--ink); color:var(--paper); border-color:var(--ink);}
  /* Persistent brand mark, mirroring .sheet-close on the opposite corner -- TODO.md's
     finding: a deep-dive screenshot (exactly the content people share) carries no
     attribution back to databeat anywhere in the overlay. Fixed, not sticky-floated, for
     the same reason as .sheet-close: it must survive scroll and whatever crop keeps the
     top of the overlay. No URL yet -- databeat.ie is purchased but not DNS-live
     (TODO.md, "Domain registration"); add it here once the domain resolves rather than
     ship a dead link. */
  .overlay-brand{
    position:fixed; top:18px; left:calc(50% - 430px + 10px); z-index:60;
    font-family:'IBM Plex Mono',monospace; font-size:11px; letter-spacing:0.08em;
    text-transform:uppercase; background:var(--paper); border:1px solid var(--line);
    padding:6px 12px; color:var(--ink-soft);
    box-shadow:0 2px 8px rgba(0,0,0,0.12);
  }
  @media (max-width:920px){ .overlay-brand{left:24px;} }
  .sheet h1{
    font-family:'Source Serif 4',serif; font-size:31px; font-weight:700;
    margin:4px 0 2px; letter-spacing:-0.01em; line-height:1.15;
  }
  .sheet-standfirst{
    font-size:14px; color:var(--ink-soft); margin-bottom:20px;
    padding-bottom:16px; border-bottom:2px solid var(--ink);
  }
  /* Sibling-measure nav: homelessness's six per-region measures, and the two-way
     cross-links between the treatment pair and the LA-budgets pair. Sits directly under
     the standfirst, deliberately quieter than .expand-link (which is a call to action);
     this is orientation -- "here is the rest of this dataset". Wraps rather than scrolls,
     so six links stay readable on a narrow sheet. */
  .sheet-siblings{
    font-size:12px; color:var(--ink-soft); margin:-12px 0 20px;
    line-height:1.9;
  }
  .sibling-link{
    font-family:'IBM Plex Mono',monospace; font-size:10px; letter-spacing:0.06em;
    text-transform:uppercase; color:var(--gold); cursor:pointer;
    border:none; background:none; padding:0 2px; text-decoration:underline;
  }
  .sibling-link:hover{color:var(--ink);}
  .sibling-sep{color:var(--line); padding:0 2px;}
  .sheet-lede{
    font-family:'Source Serif 4',serif; font-size:16.5px; line-height:1.6;
    column-count:2; column-gap:30px; margin-bottom:26px;
  }
  @media (max-width:700px){ .sheet-lede{column-count:1;} .sheet{padding:24px 20px;} }
  .sheet-section{margin:26px 0 0;}
  .sheet-section > h2{
    font-family:'IBM Plex Mono',monospace; font-size:10.5px; letter-spacing:0.12em;
    text-transform:uppercase; color:var(--gold); margin:0 0 12px;
    padding-bottom:6px; border-bottom:1px solid var(--line);
  }
  .kpi-row{display:flex; flex-wrap:wrap; gap:10px; margin-bottom:8px;}
  .kpi{
    flex:1 1 130px; border:1px solid var(--line); background:#fff; padding:11px 13px;
  }
  .kpi-label{font-size:10.5px; color:var(--ink-soft); margin-bottom:4px;}
  .kpi-value{
    font-family:'IBM Plex Mono',monospace; font-size:21px; font-weight:500;
  }
  /* A KPI whose value is a short SENTENCE rather than a single number ("1,863 families,
     4,102 children" -- two different units glued into one human phrase, which is the
     point). At the standard 21px mono it wraps to four lines and stretches every other
     tile in the flex row to match. Smaller and tighter, so the tile stays the same height
     as its numeric neighbours. */
  .kpi-value.is-phrase{font-size:14.5px; line-height:1.35;}
  .kpi-delta{font-family:'IBM Plex Mono',monospace; font-size:11px; margin-top:2px;}
  .kpi-delta.up{color:var(--up);} .kpi-delta.down{color:var(--down);}

  /* Data table — the accessibility backstop for every chart on the page */
  .dtable{width:100%; border-collapse:collapse; font-size:12px;}
  .dtable th, .dtable td{
    text-align:right; padding:5px 7px; border-bottom:1px solid #EFE9D8;
    font-variant-numeric:tabular-nums;
  }
  .dtable th:first-child, .dtable td:first-child{text-align:left;
    font-variant-numeric:normal;}
  .dtable thead th{
    font-family:'IBM Plex Mono',monospace; font-size:9.5px; letter-spacing:0.07em;
    text-transform:uppercase; color:var(--ink-soft); border-bottom:1px solid var(--line);
  }
  .dtable td.up{color:var(--up);} .dtable td.down{color:var(--down);}

  /* Heatmap: one hue, light->dark. Magnitude only — never a rainbow. */
  .heat{border-collapse:separate; border-spacing:2px; font-size:10px;}
  .heat th{
    font-family:'IBM Plex Mono',monospace; font-size:9px; color:var(--ink-soft);
    font-weight:400; text-align:right; padding-right:6px; white-space:nowrap;
  }
  .heat thead th{text-align:center; padding:0 0 3px;}
  .heat td{
    width:34px; height:20px; text-align:center; color:var(--ink);
    font-variant-numeric:tabular-nums;
  }
  .sheet svg{display:block; width:100%; height:auto;}
  /* Small-multiples sparkline grid — baby names' "name trajectories" and HIPE's 12
     diagnosis chapters (see sparklineGrid() in the deep-dive script). auto-fill so the
     grid reflows from ~5 across on desktop to 2 across at mobile widths without a media
     query, the same responsive technique .cluster-grid already uses below. */
  .spark-grid{
    display:grid; grid-template-columns:repeat(auto-fill, minmax(130px, 1fr));
    gap:10px 14px; margin-top:8px;
  }
  .spark-cell{
    border:1px solid var(--line); background:#fff; padding:8px 10px 6px; border-radius:2px;
  }
  .spark-label{
    font-size:10.5px; color:var(--ink-soft); white-space:nowrap; overflow:hidden;
    text-overflow:ellipsis; margin-bottom:2px;
  }
  .spark-value{
    font-family:'IBM Plex Mono',monospace; font-size:12px; color:var(--ink);
    font-variant-numeric:tabular-nums; margin-top:2px;
  }
  .glossary dt{
    font-family:'Source Serif 4',serif; font-weight:600; font-size:13.5px;
    margin-top:12px;
  }
  .glossary dd{
    margin:3px 0 0; font-size:12.5px; line-height:1.55; color:var(--ink-soft);
  }
  .expand-link{
    display:inline-block; margin-top:10px;
    font-family:'IBM Plex Mono',monospace; font-size:10px; letter-spacing:0.08em;
    text-transform:uppercase; color:var(--gold); cursor:pointer;
    border:none; background:none; padding:0; text-decoration:underline;
  }
  /* Housing cluster: RPPI + PSRA + completions + homelessness, side by side for one
     county. auto-fit so four legs sit in a row on the sheet's ~780px column and stack
     to one column under the same width the sheet-lede already collapses at. */
  .cluster-grid{
    display:grid; grid-template-columns:repeat(auto-fit, minmax(210px, 1fr));
    gap:12px; margin-top:4px;
  }
  .cluster-grid .dataset{margin-bottom:0;}
  .section-head{
    font-family:'IBM Plex Mono',monospace; font-size:10px; letter-spacing:0.1em;
    text-transform:uppercase; color:var(--ink-soft); margin:22px 0 10px;
    padding-top:16px; border-top:1px solid var(--line);
  }
  /* Dataset toggles */
  /* Cluster tabs -- replaced the .toggle pill row (unified-design-and-data-plan.md §8).
     §8.1 found the "black slab" was mostly a STATE-MODEL problem, not a typography one:
     every toggle rendered aria-pressed="true", and the pressed style is a solid --ink
     fill, so the default state painted all 23 pills dark on a cream page. A tab bar has
     exactly one active item, so the same fill now produces one dark chip and the rest
     light -- the visual weight problem is largely solved before any type changes.
     §8.7 then asks for Inter sentence case rather than 10px uppercase mono: this is the
     most conspicuous instance of the mono-uppercase-everywhere problem on the page. */
  .cluster-tabs{
    display:flex; flex-wrap:wrap; gap:6px; margin:14px 0 4px;
  }
  .cluster-tab{
    font-family:'Inter',sans-serif; font-size:12.5px; letter-spacing:0;
    padding:6px 12px; border:1px solid var(--line); border-radius:13px;
    cursor:pointer; user-select:none; color:var(--ink-soft); background:transparent;
    display:inline-flex; align-items:center; gap:6px;
  }
  .cluster-tab[aria-selected="true"]{
    background:var(--ink); color:var(--paper); border-color:var(--ink);
  }
  /* NOTE, added after a cold review disproved the original claim here. This block used
     to carry a `.cluster-tab[aria-selected="true"] .cluster-tab-count{color:var(--pending)}`
     rule, justified as "the selected tab keeps the light value, and that pair is the whole
     reason the token was split rather than darkened".

     BOTH HALVES WERE WRONG. An identical-specificity rule four lines below already sets
     --gold-soft and, coming later, wins -- so the added rule never applied. Verified live
     on a selected cluster tab: computed colour rgb(228,211,168) = --gold-soft, measuring
     9.95:1 on --ink, not the 6.97:1 that was recorded. The selected count was therefore
     never --pending, and darkening --pending could not have broken it.

     The split itself still stands on its own merits -- it fixed four real text failures
     (1.87:1 and 1.97:1) and --pending remains correct for the disabled-Send fill. Only the
     stated REASON for preferring a split over a darken was false, and it is corrected here
     rather than deleted, because the number 2.86:1 is in a commit message on master. */
  .cluster-tab-count{
    font-family:'IBM Plex Mono',monospace; font-size:10px; color:var(--pending-ink);
  }
  .cluster-tab[aria-selected="true"] .cluster-tab-count{ color:var(--gold-soft); }
  /* WCAG 2.5.8 target size -- §8.7 measured the old pills at 23px tall. */
  @media (max-width:820px){
    .cluster-tab{ min-height:40px; padding:6px 14px; font-size:13px; }
  }
  .county-label{
    font-family:'IBM Plex Mono',monospace;
    font-size:8.5px;
    fill:var(--ink-soft);
    pointer-events:none;
  }
  /* --- Alert strip -----------------------------------------------------------
     This product's premise is telling a journalist WHERE TO LOOK. Without this the
     page required them to already know which county to click, which inverts the
     value proposition. */
  .alertbar{
    border-bottom:1px solid var(--line);
    background:#F1ECDF;
    padding:11px 32px;
    display:flex; align-items:baseline; gap:14px;
    overflow-x:auto; white-space:nowrap;
  }
  .alertbar .lbl{
    font-family:'IBM Plex Mono',monospace; font-size:10px; letter-spacing:0.12em;
    text-transform:uppercase; color:var(--gold); flex-shrink:0;
  }
  .alert-item{
    font-size:12.5px; color:var(--ink); flex-shrink:0;
    border:none; background:none; cursor:pointer; padding:0;
    font-family:inherit;
  }
  .alert-item:hover{text-decoration:underline;}
  .alert-item + .alert-item{
    border-left:1px solid var(--line); padding-left:14px;
  }
  /* §4.2: the full display_name now always renders (no more guessing which half of an
     em-dash split is the identifying part) -- truncated visually here so the strip
     stays scannable at a consistent width; the untruncated text is still in `title`. */
  .alert-name{
    max-width:200px; overflow:hidden; text-overflow:ellipsis;
    display:inline-block; vertical-align:bottom; white-space:nowrap;
  }
  .alert-move{font-family:'IBM Plex Mono',monospace; font-weight:500;}
  .alert-move.up{color:var(--up);} .alert-move.down{color:var(--down);}
  .alert-flag{
    font-family:'IBM Plex Mono',monospace; font-size:9px; letter-spacing:0.06em;
    text-transform:uppercase; color:var(--ink-soft); border:1px solid var(--line);
    padding:1px 4px; margin-left:5px; border-radius:2px;
  }
  @media (max-width:820px){ .alertbar{padding:10px 20px;} }

  /* What the map is coloured BY. Without this the legend said "Lower / Mid / Higher
     growth" — growth of what? Unambiguous when property was the only dataset; not
     once crime existed. */
  .map-controls{
    display:flex; flex-wrap:wrap; gap:6px; align-items:center;
    max-width:520px; margin:0 auto 10px; width:100%;
  }
  /* Floating hover label. pointer-events:none matters — without it the label sits under
     the cursor, steals the mouseleave, and the thing flickers as you move across a shape. */
  #map-hover{
    position:fixed; z-index:60; display:none; pointer-events:none;
    background:var(--ink); color:var(--paper);
    padding:5px 9px; border-radius:4px; max-width:240px;
    box-shadow:0 2px 10px rgba(0,0,0,0.22);
  }
  #map-hover .hv-name{
    display:block; font-family:'Source Serif 4',Georgia,serif; font-size:13px;
    line-height:1.25;
  }
  #map-hover .hv-detail{
    display:block; font-family:'IBM Plex Mono',monospace; font-size:10px;
    letter-spacing:0.04em; opacity:0.78; margin-top:1px;
  }
  #boundary-controls{ margin-top:-4px; }
  /* Caveat marker on a county card. The full text lives in a rollover so the panel
     shows figures rather than paragraphs — see caveatChip(). */
  .dataset-foot{
    display:flex; align-items:center; justify-content:space-between;
    gap:10px; flex-wrap:wrap; margin-top:10px;
  }
  .caveat-chip{
    font-family:'IBM Plex Mono',monospace; font-size:10px; letter-spacing:0.04em;
    text-transform:uppercase; cursor:help;
    padding:3px 8px; border-radius:999px; border:1px solid var(--line);
    background:transparent; color:var(--ink-soft);
  }
  .caveat-chip:hover, .caveat-chip:focus{ border-color:var(--ink-soft); outline:none; }
  .caveat-chip-warning{ color:#8A3B2E; border-color:rgba(138,59,46,0.35); }
  .caveat-chip-critical{ color:#fff; background:#8A3B2E; border-color:#8A3B2E; }
  /* Explicit, though it matches the base .caveat-chip look already -- a freshness-only
     chip (no real caveats) should read as low-key/neutral, never a warning. */
  .caveat-chip-info{ color:var(--ink-soft); border-color:var(--line); }
  .caveat-pop{
    position:fixed; z-index:70; max-height:60vh; overflow-y:auto;
    background:var(--paper); border:1px solid var(--line); border-radius:6px;
    padding:10px 12px; box-shadow:0 6px 24px rgba(0,0,0,0.16);
  }
  .caveat-pop .caveats{ margin:0; }
  .srclist{ list-style:none; padding:0; margin:10px 0 0; }
  .srclist li{
    padding:7px 0; border-bottom:1px solid var(--line);
    display:flex; flex-wrap:wrap; gap:4px 10px; align-items:baseline;
  }
  .srclist li:last-child{ border-bottom:none; }
  .srclist a{ font-family:'IBM Plex Mono',monospace; font-size:12px; }
  .src-note{ color:var(--ink-soft); font-size:12px; }
  .map-controls .lbl{
    font-family:'IBM Plex Mono',monospace; font-size:10px; letter-spacing:0.08em;
    text-transform:uppercase; color:var(--ink-soft); margin-right:2px;
  }
  /* Opens each cluster's run of chips. Reuses .lbl's type so it reads as a separator
     rather than another chip.
     flex-basis:100% forces each label onto its OWN line, and that is the whole point
     rather than a spacing preference. Inline, the label is just another flex item, so
     two of the five landed at a line end with their chips on the next line -- and a
     trailing "HEALTH" reads as if it labels the chips BEFORE it, which is the exact
     opposite of what it means. Seen on production, not predicted: the inline version
     shipped in 0c193c4 and misgrouped two runs in five. Costs ~134px of height on
     desktop; narrow viewports never see it, because they collapse to the active stat
     plus a "Change" button well before this row gets long. */
  .map-controls .layer-cluster-label{
    flex-basis:100%; margin:6px 0 0; color:var(--gold);
  }
  /* Reset belongs to the whole control, not to the last cluster -- but once the runs are
     visually delimited it reads as a member of "Economy & national" unless it breaks too.
     The sibling combinator scopes this to the expanded row only: no cluster label precedes
     it in the narrow collapsed branch, so that layout is untouched. */
  /* Shared heading row for BOTH controls -- "Colour by" and "Boundaries" -- so the two read
     as the same kind of thing: a title line, then its choices underneath. flex-basis:100%
     takes the whole line (a full-width WRAPPER, never flex-basis on the button itself:
     that stretches Reset into a ~440px bar, and max-width:max-content stops the line break
     happening at all on a fresh layout pass). space-between parks Reset at the right end
     of the title line rather than crowding the label. */
  .map-controls .controls-head{
    flex-basis:100%; display:flex; align-items:center; justify-content:space-between;
    gap:10px; margin-bottom:2px;
  }
  .map-controls .controls-head .lbl{ margin-right:0; }
  .layer-btn{
    font-family:'IBM Plex Mono',monospace; font-size:10px; letter-spacing:0.05em;
    text-transform:uppercase; padding:4px 10px; border:1px solid var(--line);
    border-radius:11px; cursor:pointer; color:var(--ink-soft); background:transparent;
  }
  .layer-btn[aria-pressed="true"]{
    background:var(--ink); color:var(--paper); border-color:var(--ink);
  }
  /* aria-disabled, not the native disabled attribute: a real disabled <button> dispatches
     no click/focus at all in mainstream browsers, which is exactly what leaves a tapping
     phone user with nothing (§3.1) -- these stay real, focusable, clickable buttons that
     just refuse their normal action and show a popover instead. :disabled kept too in
     case a genuinely inert button ever reuses this class. */
  .layer-btn:disabled, .layer-btn[aria-disabled="true"]{opacity:0.45; cursor:not-allowed;}
  /* Reset needs to read as an ACTION among 17 near-identical selection pills, not just
     another pill distinguishable by its label alone (§3.2). Reuses existing vocabulary:
     var(--line) is the codebase's standard divider colour, &#8634; is the same
     reset/reload glyph already used at the empty-state and Boundaries-fetch-failure
     fallbacks elsewhere in this file. */
  .layer-btn-reset{ border-left:1px solid var(--line); margin-left:4px; padding-left:14px; }
  /* §3.3 narrow-viewport collapse affordance -- same pill shape, gold text marks it as
     the "expand/collapse" control rather than a 18th stat choice. */
  .colour-by-toggle{ color:var(--gold); border-color:var(--gold-soft); }
  /* A fact (how current), not description (what the colour means) -- kept in the
     codebase's own typographic split (monospace for figures, serif/italic for prose)
     rather than folded into the italic caption below it. */
  .legend-freshness{
    max-width:520px; margin:6px auto 0; font-family:'IBM Plex Mono',monospace;
    font-size:10px; letter-spacing:0.04em; color:var(--ink-soft);
  }
  .legend-caption{
    max-width:520px; margin:6px auto 0; font-size:10.5px; line-height:1.5;
    color:var(--ink-soft); font-style:italic;
  }
  /* Fix 2: a stat with a genuinely EMPTY compatible_levels (crime, confirmed by
     stat-region-selector-design.md §3.3 -- Garda Divisions have no 'contains' relation to
     any boundary level, not just this one) can never colour the map, at any zoom. Greying
     every boundary control at once and leaving the map uniformly grey read as the whole
     interface being broken rather than "this one stat has no map view" -- this banner
     says that plainly, in place of the map, instead of a reader having to infer it from
     seven simultaneously-dimmed buttons and an all-grey country. */
  .map-note{
    max-width:520px; margin:18px auto; padding:14px 16px; text-align:center;
    border:1px solid var(--line); background:#FBF8F0; border-radius:2px;
    font-size:12.5px; line-height:1.6; color:var(--ink-soft);
  }
  .map-note strong{ color:var(--ink); font-family:'Source Serif 4',serif; font-size:14px; }
  .legend{
    display:flex;
    gap:18px;
    flex-wrap:wrap;
    font-size:11px;
    font-family:'IBM Plex Mono',monospace;
    color:var(--ink-soft);
    margin-top:14px;
    max-width:520px;
  }
  .legend-item{display:flex; align-items:center; gap:6px;}
  .swatch{width:10px; height:10px; border-radius:50%; display:inline-block;}

  .panel{
    background:#fff;
    border-left:1px solid var(--line);
    padding:28px 26px;
    min-height:100%;
  }
  .panel-eyebrow{
    font-family:'IBM Plex Mono',monospace;
    font-size:10.5px;
    letter-spacing:0.1em;
    text-transform:uppercase;
    color:var(--gold);
    margin-bottom:4px;
  }
  .panel h2{
    font-family:'Source Serif 4',serif;
    font-size:24px;
    margin:0 0 4px;
  }
  .region-tag{
    font-size:12.5px;
    color:var(--ink-soft);
    margin-bottom:18px;
  }
  .headline{
    font-family:'Source Serif 4',serif;
    font-size:16.5px;
    line-height:1.45;
    margin-bottom:20px;
    padding-bottom:18px;
    border-bottom:1px solid var(--line);
  }
  .stat-row{
    display:flex;
    justify-content:space-between;
    align-items:baseline;
    padding:10px 0;
    border-bottom:1px solid #EFE9D8;
  }
  .stat-label{font-size:12.5px; color:var(--ink-soft);}
  .stat-value{
    font-family:'IBM Plex Mono',monospace;
    font-size:14.5px;
    font-weight:500;
  }
  .stat-value.up{color:var(--up);}
  .stat-value.down{color:var(--down);}
  .stat-value.pending{color:var(--pending-ink); font-size:12px;}
  .source-note{
    margin-top:22px;
    font-size:11px;
    color:var(--ink-soft);
    line-height:1.6;
    font-style:italic;
  }
  /* Caveats are data from the API, not decoration. Severity drives the accent so a
     'critical' qualification can't be styled to look like a footnote. */
  .caveats{margin-top:22px; display:flex; flex-direction:column; gap:10px;}
  .caveat{
    font-size:11px;
    line-height:1.55;
    color:var(--ink-soft);
    padding:9px 11px;
    border-left:2px solid var(--line);
    background:#FBF8F0;
  }
  .caveat-info{border-left-color:var(--gold-soft);}
  .caveat-warning{border-left-color:var(--gold);}
  .caveat-critical{border-left-color:var(--up);}
  .caveat-label{
    display:block;
    font-family:'IBM Plex Mono',monospace;
    font-size:9.5px;
    letter-spacing:0.07em;
    text-transform:uppercase;
    color:var(--ink);
    margin-bottom:3px;
  }
  .caveat-body a{color:var(--gold);}
  .empty-state{
    color:var(--ink-soft);
    font-size:13.5px;
    line-height:1.6;
    padding-top:40px;
    text-align:center;
  }
  .empty-state .big{font-size:32px; display:block; margin-bottom:10px;}
  /* The 74px clearance that briefly lived here was for the FIXED feedback pill, which
     no longer exists -- the pill docks in flow now, so nothing overlaps the footer and
     there is nothing to clear. Back to the original padding rather than left at a value
     whose reason has gone. */
  footer{
    padding:16px 32px 28px;
    font-size:11px;
    color:var(--ink-soft);
    border-top:1px solid var(--line);
  }
  /* --- Feedback widget (docs/specs/feedback-widget-frontend-spec.md) --------- */
  /* HISTORY OF THIS ELEMENT'S POSITION -- THREE DECISIONS IN ONE DAY, ALL 2026-08-24.
     Read this before moving it a fourth time; each position was measured, and each was
     defeated by something that did not exist when it was chosen.

     THE Z-INDEX BELOW IS GONE. This block opened with "z-index 55 is chosen, not
     arbitrary: above the deep-dive .overlay (50)..." for several hours after the element
     stopped having a z-index at all. It is recorded here as history, not as a live fact:
     the widget is in normal flow now and sets no z-index.

     1. FIXED BOTTOM-RIGHT, z-index 55, chosen so the tab stayed reachable with a sheet
        open. Defeated: measured 97.8% of the pill over the open sheet, on the chart.
     2. HIDDEN WHILE A SHEET IS OPEN, then moved FIXED BOTTOM-LEFT -- the map pane
        carried no body text, so the left was clear of the reading column. Defeated
        within the hour by the colour-by rail, which moved into that same empty column;
        the pill then covered "Homelessness" 100% and "Babies' names" 96%.
     3. NOT FIXED AT ALL (current). Both remaining regions carry something a reader
        needs, so there was no third anchor -- only a third thing to cover.

     The reachability tradeoff was RE-DECIDED at step 2 and the original wording is kept
     above rather than deleted, so nobody rediscovers the old intent and reverts it.
     Measured at 375px with a deep dive open: 97.8% of the pill sat over the sheet, with
     the trend chart's own SVG directly underneath it. The original choice assumed
     REACHABILITY was the scarce thing. It is not -- a reader with a sheet open is
     reading a chart; feedback is still reachable the moment they close it, while the
     occluded chart is not reachable at all.
     `:has()` keys this off the overlay's own `.open` class so there is no second piece
     of state to keep in sync, and a browser without `:has()` ignores the rule and
     behaves exactly as before. Escape or Close brings the pill straight back (verified). */
  /* NOW DEFENCE IN DEPTH RATHER THAN THE FIX. With the widget in normal flow it sits
     under the overlay's fixed, full-viewport z-index:50 anyway, so this hides nothing
     that was visible. Kept because this element has moved three times in one day: if it
     is ever given a position again, this is the rule that stops the sheet-occlusion
     defect coming back with it. */
  body:has(.overlay.open) #feedback-widget{display:none;}

  /* WHERE THIS PILL SITS HAS NOW BEEN DECIDED THREE TIMES. Keeping the trail, because a
     future session seeing only the final position will re-propose a rejected one:

       1. Originally bottom-RIGHT, fixed, z-index 55 above the overlay -- chosen so the
          tab stayed reachable with a deep dive open.
       2. 2026-08-24: hidden while a sheet is open (the rule above). Reachability is not
          the scarce thing; a reader with a sheet open is reading a chart.
       3. 2026-08-24, same day: moved OFF the reading column entirely, because it also
          occluded the panel AT REST, with no sheet open at all. Measured on production,
          #county:cork, viewport 965x910, pill 90x30 at (842,862):

              glyph runs covered: 2      "262" 70%      "+2%" 100%

          That is the New dwellings row's value and its year-on-year change -- one number
          partly gone, the other entirely. Not prose with a hole in it.

     TRIED AND REJECTED, so they are not re-tried:
       - Clearance PADDING (.panel, then footer). Reverted the same day: could not be
         demonstrated to work, and it can only ever help the final line, never mid-scroll.
       - HIDE-ON-SCROLL. Rejected by the developer on the measurement above: it returns
         the pill to exactly the position that covers "262" and "+2%" the moment
         scrolling stops, and at-rest is the state the defect was measured in.

     .layout is `1fr 360px` above 820px, so anchoring LEFT puts the pill over the map
     pane, which carries no body text, and clear of the 360px reading column.
     Below 820px .layout collapses to ONE column and the panel is full-width -- there is
     no side to move to, and a fixed element at any horizontal position overlaps the
     reading column at some scroll offset. So it stops being fixed there and docks at the
     end of the document instead. That is the only arrangement that reaches zero glyph
     overlap at 375px; it is not a stylistic preference. */
  #feedback-widget{
    /* NOT FIXED, AT ANY WIDTH -- and this is the third position in one day, decided by
       the developer after the second one collided with the rail. See the trail above.

       Fixed positioning was the problem, not any particular anchor. Bottom-right covered
       the reading column; bottom-left covered the rail once the rail existed. Both
       remaining regions carry something a reader needs, so there was no third anchor to
       move to -- only a third thing to cover. Docking in flow makes the overlap ZERO BY
       CONSTRUCTION at every viewport and, more usefully, keeps it zero through layout
       changes nobody has made yet. No future column can collide with an element that
       occupies no fixed space.

       What it costs, accepted deliberately: on desktop the tab is no longer persistently
       visible. A reader reaches it at the end of the page rather than always seeing it.

       column-reverse keeps the spec's markup order (button first, form second) while
       putting the panel ABOVE the tab on screen. */
    display:flex; flex-direction:column-reverse; align-items:flex-end; gap:8px;
    padding:0 20px 24px;
  }
  #feedback-tab{
    font-family:'IBM Plex Mono',monospace; font-size:11px; letter-spacing:0.08em;
    text-transform:uppercase; background:var(--paper); border:1px solid var(--line);
    padding:7px 14px; cursor:pointer; color:var(--ink-soft);
    box-shadow:0 2px 8px rgba(0,0,0,0.12);
  }
  #feedback-tab:hover{background:var(--ink); color:var(--paper); border-color:var(--ink);}
  /* Same paper/border/shadow language as .sheet, at a much smaller footprint -- a
     lightweight complement to the deep-dive, not another deep-dive-sized surface. */
  #feedback-panel{
    width:320px; background:rgba(250,247,239,0.98);
    border:1px solid var(--line); box-shadow:0 20px 60px rgba(0,0,0,0.28);
    padding:16px 18px 18px;
  }
  /* THE [hidden] RULE (see the top of this file) -- this is the first of the two
     occurrences that produced it. A display value set on the element beats [hidden]. */
  #feedback-panel[hidden], #fb-fields[hidden], #fb-thanks[hidden]{display:none;}
  #feedback-panel label{
    display:block; font-family:'IBM Plex Mono',monospace; font-size:10px;
    letter-spacing:0.07em; text-transform:uppercase; color:var(--ink-soft);
    margin-bottom:4px;
  }
  #feedback-panel .fb-count-label{display:flex; justify-content:space-between; align-items:baseline;}
  #fb-comment-count{
    font-family:'IBM Plex Mono',monospace; font-size:9.5px; letter-spacing:0.04em;
    text-transform:none; color:var(--pending-ink);
  }
  #fb-comment-count.fb-out-of-range{color:var(--up);}
  #feedback-panel input[type="email"], #feedback-panel textarea{
    width:100%; font-family:'Inter',sans-serif; font-size:13px; color:var(--ink);
    background:var(--paper); border:1px solid var(--line); padding:7px 9px;
    margin-bottom:10px;
  }
  #feedback-panel textarea{min-height:88px; resize:vertical; line-height:1.5;}
  #feedback-panel input[type="email"]:focus, #feedback-panel textarea:focus{
    outline:none; border-color:var(--gold);
  }
  #fb-status{font-size:11.5px; line-height:1.5; color:var(--ink-soft); margin-bottom:8px;}
  #fb-status:empty{display:none;}
  #fb-status.fb-error{color:var(--up);}
  #fb-submit{
    font-family:'IBM Plex Mono',monospace; font-size:11px; letter-spacing:0.08em;
    text-transform:uppercase; background:var(--ink); color:var(--paper);
    border:1px solid var(--ink); padding:7px 16px; cursor:pointer;
  }
  #fb-submit:hover:not(:disabled){background:var(--ink-soft); border-color:var(--ink-soft);}
  #fb-submit:disabled{background:var(--pending); border-color:var(--pending); cursor:default;}
  #fb-thanks{
    font-family:'Source Serif 4',serif; font-size:14px; line-height:1.55;
    color:var(--ink); margin:0;
  }
  /* Off-screen rather than display:none -- see the honeypot comment in feedback.js. */
  .fb-visually-hidden{position:absolute; left:-9999px; top:-9999px;}
  @media (max-width:400px){ #feedback-panel{width:calc(100vw - 36px);} }
