/* ============================================================
   base.css — Reset, typography, page-level shell.
   Tokens come from tokens.css. No raw hex values here.
   ============================================================ */

*, *::before, *::after { box-sizing: border-box; }

/* ── overflow-x: clip, NOT hidden ──────────────────────────
   Two jobs in one declaration.

   Ours: the full-bleed drawings and the sticky settings bar are both
   sized against viewport units, and on a platform with a classic
   scrollbar a 100vw box overflows the document by the scrollbar's
   width — a horizontal scrollbar on every page to gain 15px of
   background nobody can see. js/header-offset.js measures that into
   --scrollbar-w for the cases that can do arithmetic; this covers the
   rest.

   The host's: inrix.com's theme declares `overflow-x: hidden` here,
   and `hidden` makes the element a scroll container. A scroll
   container between a sticky element and the viewport is what sticky
   sticks to — so the settings bar stopped sticking, silently, with
   nothing in the styles to point at. `clip` clips without
   scrolling, so it is the same visual result and no scrollport.
   Declaring it ourselves means the bundle wins that one by the
   cascade instead of by the host remembering. Same trap is written
   up over the sticky bar in page-settings.css and in
   js/header-offset.js; see docs/embedding.md. */
html, body {
  background: var(--bg);
  margin: 0;
  padding: 0;
  overflow-x: clip;
}

/* The header is fixed and the settings bar is sticky, so anything the
   browser scrolls to on its own — a fragment link, scrollIntoView, the
   focus ring moving into content below the fold, find-in-page — would
   otherwise land underneath them. One declaration on the scrolling
   element covers all four. The --space-3 slack keeps the 3px focus
   outline clear of the bar rather than tangent to it. */
html {
  scroll-padding-top: calc(var(--chrome-h) + var(--ps-bar-h) + var(--space-3));
}

body {
  color: var(--text);
  font-family: var(--font-body);
  font-feature-settings: "ss01", "cv11";
  line-height: 1.55;
  -webkit-font-smoothing: antialiased;
  text-rendering: optimizeLegibility;
  background: var(--bg);
  /* NAMES COME FROM THE EXPORT, SO THEIR LENGTH IS NOT OURS TO PROMISE.
     Headlines, eyebrows, deks, captions and body prose all interpolate
     a city or neighborhood name, and a name with no break opportunity
     sets a min-content floor its container cannot honour. The city
     page's h1 ran 645px past a 393px viewport, cut mid-word, with the
     eyebrow and dek doing the same underneath it; a highlight card on
     the ranking page ran 524px through the two cards beside it and
     left all three unreadable. The page reported no overflow while
     either happened, because the column never moved — only the glyphs
     did, which is why a page-level probe misses this entirely.

     Declared once here and inherited, rather than at each call site:
     enumerating the classes that print a name was whack-a-mole, and it
     missed two spans the first time. break-word, not break-all or
     anywhere — it breaks a word only when that word has no break
     opportunity AND cannot fit on a line by itself, and it does not
     change intrinsic sizing. Every real name on the site is untouched
     and the auto-layout tables still widen their column rather than
     break; only the pathological name degrades, to a wrap instead of a
     run off the page. */
  overflow-wrap: break-word;
}

/* The type system is Lato throughout (--font-display and --font-body
   both resolve to Lato). `.display` headlines and the big `.figure`
   count-up numbers carry emphasis through weight + tracking, not a
   separate face. Lato ships 400/700/900 only (no 500/800), so
   headings step up to 900 for hierarchy. */
.display {
  font-family: var(--font-display);
  font-weight: 900;
  letter-spacing: -0.02em;
  line-height: 1.05;
  margin: 0;
}

h1, h2, h3 {
  font-family: var(--font-body);
  font-weight: 900;
  letter-spacing: -0.015em;
  line-height: 1.15;
  margin: 0;
  color: var(--text);
}
h1 { font-size: clamp(40px, 6vw, 84px); letter-spacing: -0.02em; line-height: 1.05; }
h2 { font-size: clamp(26px, 3.4vw, 40px); }
h3 { font-size: clamp(18px, 2vw, 24px); }

.eyebrow {
  font-family: var(--font-body);
  font-size: var(--text-xs);
  font-weight: 700;
  letter-spacing: 0.2em;
  text-transform: uppercase;
  color: var(--primary);
}

.mono { font-family: var(--font-mono); font-variant-numeric: tabular-nums; }

.dek {
  color: var(--muted);
  font-size: clamp(var(--text-md), 1.5vw, var(--text-lg));
  line-height: 1.55;
  max-width: var(--reading-width);
  margin: 0;
}

/* A SECOND DEK PARAGRAPH. Some scenes say two things, and the second
   is a real second paragraph: its own <p>, so it can animate in a beat
   after the first (see renderExplainer). This is the only thing that
   separates it from the first — the gap a paragraph break would leave.
   It replaced `.dek-break`, an empty block inside a single <p> that
   stood in for a paragraph break back when a dek was one string; a
   break cannot arrive later than the sentence above it. */
.dek + .dek-next {
  margin-top: var(--space-3);
}

a { color: var(--primary-bright); text-decoration-color: var(--primary-soft); text-underline-offset: 3px; }
a:hover { color: var(--primary); }

/* A link inside a credit line — the data source in the hero meta and
   in every page footer. It keeps the colour of the line it sits in
   rather than taking the site's link blue: those lines are muted
   attribution, and a bright link in them reads as a call to action.
   The underline is what marks it as a link; the hover is what
   confirms it. */
.source-link {
  color: inherit;
  text-decoration: underline;
  text-decoration-color: currentColor;
  text-decoration-thickness: 1px;
}
.source-link:hover { color: var(--text); }

/* ── Touch target ───────────────────────────────────────────
   The source attribution is the one link in the footer and it is 16px
   tall, which fails even the 24px AA floor. It sits alone on its row
   with 24px of gap around it, so the hit area can reach 14px in every
   direction without meeting anything — 44px, and the footer's own
   layout is untouched because the overlay does not participate in it.

   The max-height arm is not optional: a landscape phone is 852px wide
   and a width-only query leaves it on the 16px target. */
@media (hover: none) and (pointer: coarse), (max-width: 720px), (max-height: 500px) {
  .source-link { position: relative; }
  .source-link::before {
    content: '';
    position: absolute;
    inset: -14px -8px;
  }
}

a:focus-visible,
button:focus-visible,
[role="button"]:focus-visible,
[tabindex]:focus-visible {
  outline: 2px solid var(--primary-bright);
  outline-offset: 3px;
  border-radius: 4px;
}

/* ── Skip link ─────────────────────────────────────────────
   Off-screen until focused, then a real control at the top-left of
   the page. Not .sr-only with a focus override: .sr-only clips to a
   1px box, and a clipped element that becomes visible has to undo
   six properties to do it. Position is simpler and does not fight.

   Above --z-header, because the one thing it must clear is the fixed
   nav it exists to skip past. */
.psc-skip-link {
  position: fixed;
  top: var(--space-3);
  left: var(--space-3);
  z-index: var(--z-popover);
  padding: var(--control-py-md, var(--space-3)) var(--space-4);
  border-radius: var(--radius-pill);
  background: var(--btn-primary-bg);
  color: var(--on-fill);
  font-size: var(--text-sm);
  font-weight: 700;
  text-decoration: none;
  /* translate rather than `left: -9999px`: a transform does not
     affect layout, so nothing below can be dragged by it, and the
     link keeps a real box for the focus ring to draw around. */
  transform: translateY(calc(-100% - var(--space-4)));
  transition: transform var(--dur-fast) var(--ease-out);
}
.psc-skip-link:focus-visible {
  transform: translateY(0);
}
@media (prefers-reduced-motion: reduce) {
  .psc-skip-link { transition: none; }
}

/* Visually hidden but accessible to screen readers. We add `clip-path` so
   that wide descendants (e.g. an accessibility data-table for a chart)
   are truly contained and don't extend the document's horizontal scroll
   area on narrow viewports — the legacy `clip: rect()` only clips paint,
   not layout/scroll contribution. `max-width: 1px` ensures intrinsic-size
   children (tables, pre) can't push the absolute container wider. */
.sr-only {
  position: absolute;
  width: 1px; height: 1px;
  max-width: 1px; max-height: 1px;
  padding: 0; margin: -1px;
  overflow: hidden;
  clip: rect(0,0,0,0);
  clip-path: inset(50%);
  white-space: nowrap;
  border: 0;
}

/* ── Page shell ────────────────────────────────────────── */

/* ── WHY THE psc- PREFIX ───────────────────────────────────
   This was .site-header and .site-footer, which are the two most
   generic names in front-end work — every WordPress theme ships
   both. The bundle is vendored into inrix.com's theme, where its
   .site-header is the INRIX masthead and ours is a section nav
   inside a page; two different elements answering to one selector,
   in two stylesheets, one of which we do not own.

   Until now the theme resolved that by rewriting our source on the
   way in. That is a rename applied by somebody else's script on
   every re-vendor, which means our shipped names and the names the
   integration CSS targets are only ever equal by that script being
   run and being right. Owning the name upstream deletes the step.

   psc-: Parking ScoreCard. Only the two chrome elements carry it,
   because only those two collide; the rest of the bundle's class
   names are already distinct from the theme's. docs/embedding.md.

   Three children in one centered row: brand, nav, and the control
   cluster the toggles mount into. Everything sits on the same optical
   center line because every item — the brand, each link, each button —
   is a 44px-tall flex box, so nothing depends on where a text baseline
   happens to fall. */
.psc-section-nav {
  position: fixed;
  inset: 0 0 auto 0;
  z-index: var(--z-header);
  display: flex;
  align-items: center;
  gap: var(--space-5);
  padding: var(--space-4) var(--space-6);
  /* A solid band that follows the theme — INRIX navy on dark, white on
     light. See the masthead block in tokens.css for both palettes and
     for why the header stopped being navy on both.

     This was --header-fade, a gradient to transparent, with a --bg swap
     once the page had scrolled. The scroll-state swap and its transition
     are gone with the fade — there is nothing left to swap between, and
     watchScrolled() in js/header-offset.js still sets data-scrolled for
     anything else that wants it.

     The border is the theme's, not the header's: --header-edge is
     transparent on dark, where the step from navy to the page draws the
     boundary on its own and the sticky settings bar below supplies a
     hairline anyway. On light the two surfaces are within a few percent
     of each other and the hairline is the only thing separating them. */
  background: var(--header-bg);
  border-bottom: 1px solid var(--header-edge);
  pointer-events: none;
}
.psc-section-nav > * { pointer-events: auto; }
/* Not a flex box: the brand needs an ellipsis on narrow phones, and a
   flex container can't ellipsize an anonymous text run. Centering comes
   from the header, and line-height 1.1 keeps its half-leading small
   enough that the cap-height sits on the same line as the 44px controls. */
.psc-section-nav .brand {
  flex: 0 1 auto;
  min-width: 0;
  white-space: nowrap;
  /* overflow: hidden; */
  text-overflow: ellipsis;
  font-family: var(--font-body);
  font-weight: 900;
  font-size: var(--text-md);
  line-height: 1.1;
  letter-spacing: -0.005em;
  color: var(--header-ink);
}
.psc-section-nav .brand .edition { color: var(--header-accent); margin-left: var(--space-2); font-size: var(--text-xs); letter-spacing: 0.16em; text-transform: uppercase; vertical-align: middle; }

/* The nav takes the slack so the control cluster is pinned right.
   overflow-x on it means an unexpectedly wide nav (long brand, large
   text setting) scrolls instead of wrapping into the page. The negative
   block margin cancels the padding that keeps the 3px focus outline
   from being clipped by that scroll box — visual height is unchanged. */
.psc-section-nav nav {
  display: flex;
  align-items: center;
  flex: 1 1 auto;
  min-width: 0;
  gap: var(--space-5);
  flex-wrap: nowrap;
  justify-content: flex-end;
  overflow-x: auto;
  overscroll-behavior-x: contain;
  scrollbar-width: none;
  padding-block: 6px;
  margin-block: -6px;
}
.psc-section-nav nav::-webkit-scrollbar { display: none; }
.psc-section-nav nav a {
  display: inline-flex;
  align-items: center;
  min-height: 44px;
  flex: 0 0 auto;
  font-size: var(--text-sm);
  color: var(--header-ink-muted);
  text-decoration: none;
  white-space: nowrap;
}
.psc-section-nav nav a:hover { color: var(--header-accent-bright); }
.psc-section-nav nav a[aria-current="page"] { color: var(--header-ink); }

/* ── The reader's city, in the masthead ────────────────────
   Written into the nav by js/saved-city.js on all four pages, and
   present only for a reader who set one on the overview. It is the
   one nav item that is not a section of the report, so it is set
   apart by a rule rather than left to read as a fourth destination.

   `display` on the class would beat the UA rule for [hidden] — the
   link spends most of its life hidden, so it has to say what hidden
   means before it says anything else. */
.nav-your-city[hidden] { display: none; }
/* Selector matched to `.psc-section-nav nav a` above, which sets
   align-items on every nav link. A bare `.nav-your-city` loses that
   one declaration and only that one, which stacks the two lines and
   then centres them against a rule they should sit flush to. */
.psc-section-nav nav a.nav-your-city {
  flex-direction: column;
  align-items: flex-start;
  justify-content: center;
  gap: 0;
  line-height: 1.2;
  padding-left: var(--space-3);
  border-left: 1px solid var(--line);
}
.nav-your-city .nav-your-city__label {
  font-size: var(--text-xs);
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--header-ink-muted);
}
.nav-your-city .nav-your-city__name {
  font-weight: 700;
  color: var(--header-ink);
  max-width: 14ch;
  overflow: hidden;
  text-overflow: ellipsis;
}
.nav-your-city:hover .nav-your-city__name { color: var(--header-accent-bright); }

/* ── A button that reads as a link ─────────────────────────
   Used by the controls that change state rather than navigate: "Not
   Boston?", "Jump to Boston", "Show only Boston". They must be
   <button>, because a link that goes nowhere is a lie to a screen
   reader — and they must look like the links they sit among, because
   a reader does not care which element we picked.

   This is the general case. map.css carries its own copy for the
   personalized map card, whose colours answer to the glass panel it
   sits on rather than to the page. */
.link-button {
  background: none;
  border: 0;
  padding: 0;
  margin: 0;
  font: inherit;
  color: var(--primary);
  cursor: pointer;
  text-decoration: underline;
  text-underline-offset: 3px;
}
.link-button:hover { color: var(--primary-bright); }
.link-button:focus-visible {
  outline: 2px solid var(--primary-bright);
  outline-offset: 3px;
  border-radius: var(--radius-sm);
}

/* ── Touch target ───────────────────────────────────────────
   These sit inside a sentence — "Your city · Not Boston?" — so the
   text box IS the control, and that box is 20px tall. The control is
   also the only way back out of a personalization the reader did not
   ask for, which makes it exactly the wrong one to have to aim at.

   The hit area grows and the layout does not: a ::before pinned to the
   button's own box with a NEGATIVE inset reaches past it on all four
   sides without taking part in line layout, so the sentence around it
   does not move by a pixel. 12px top and bottom takes 20px to 44px.
   The horizontal reach is smaller because these do sometimes sit in a
   row with a sibling, and a target that overlaps its neighbour is a
   worse bug than a narrow one.

   Short height counts as touch here for the same reason it does in the
   footer block above: a phone on its side clears every width arm. */
@media (hover: none) and (pointer: coarse), (max-width: 720px), (max-height: 500px) {
  .link-button { position: relative; }
  .link-button::before {
    content: '';
    position: absolute;
    inset: -12px -6px;
  }
}

/* Theme and locale toggles land here (js/theme.js, js/locale.js).
   Empty until they mount; the 44px nav links hold the header's height
   so nothing shifts when they arrive. */
.psc-section-nav .header-actions {
  display: flex;
  align-items: center;
  flex: 0 0 auto;
  gap: var(--space-2);
}

/* The report CTA, first in the cluster so it reads as the continuation
   of the nav row rather than as a third toggle. Authored into the markup
   rather than mounted the way the toggles are: a primary-coloured pill
   appearing a frame after first paint is far more noticeable than a 44px
   icon doing the same. It opens the dialog in js/report-dialog.js.

   The extra right margin is the seam between two kinds of control — one
   labelled action, then the icon toggles. */
.psc-section-nav .header-cta {
  flex: 0 0 auto;
  margin-right: var(--space-2);
  white-space: nowrap;
}

/* Phones drop it. Below 600px the top row is brand + controls, and a
   217px pill there leaves the brand a stub — measured at 375px the brand
   had literally zero visible width. Nothing is lost: every page still
   carries the CTA in its hero, which on a phone is a swipe away rather
   than a scroll away. Kept as display:none rather than visibility so it
   costs no layout, and no width so the two toggles keep the row. */
@media (max-width: 599px) {
  .psc-section-nav .header-cta { display: none; }
}

/* Narrow viewports — brand and controls hold the top row, the nav drops
   to a full-width row below it and scrolls horizontally rather than
   wrapping. It bleeds to both screen edges so the row reads as
   swipeable, and the trailing fade says there's more to the right.

   Grid, not wrapped flex: a flex line breaks before it shrinks, so on a
   320px phone the controls would drop to a third row and leave the
   brand stranded above them. minmax(0, 1fr) makes the brand the only
   thing that gives, and it ellipsizes instead.

   1120px, not 640px: one row needs the brand, five links, two 44px
   toggles AND the 217px report CTA. Measured, that is ~1,100px — below
   it the brand is the thing that gives, and "INRIX Parking Sco…" is a
   worse outcome than moving the nav to its own row. The number is a
   content budget, not a device class, which is why adding the CTA moved
   it from 820: tokens.css keys --header-h off the same number.

   Letting the nav scroll instead was the alternative and is worse — it
   would hide Countries, Global and Methodology behind a swipe on a
   1024px laptop. Two rows shows everything. */
@media (max-width: 1120px) {
  .psc-section-nav {
    display: grid;
    grid-template-columns: minmax(0, 1fr) auto;
    grid-template-areas:
      "brand actions"
      "nav   nav";
    align-items: center;
    column-gap: var(--space-3);
    row-gap: var(--space-1);
    padding: var(--space-1) var(--space-4);
  }
  .psc-section-nav .brand {
    grid-area: brand;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
    font-size: var(--text-sm);
  }
  .psc-section-nav .brand .edition { font-size: 10px; letter-spacing: 0.14em; }
  .psc-section-nav .header-actions { grid-area: actions; justify-self: end; }
  .psc-section-nav nav {
    grid-area: nav;
    /* Overflows its own column pair by the header's side padding, so the
       scrolling row starts and ends on the viewport edges. */
    width: calc(100% + var(--space-4) * 2);
    justify-content: flex-start;
    gap: var(--space-4);
    margin-inline: calc(-1 * var(--space-4));
    padding-inline: var(--space-4);
    -webkit-mask-image: var(--mask-fade-x);
            mask-image: var(--mask-fade-x);
  }
}

/* ── Landscape phone: put the nav back on one row ──────────
   The block above trades height for completeness — two rows so that
   no link hides behind a swipe on a 1024px laptop. That trade is the
   right one when the screen is 768px tall and the wrong one when it
   is 393px, which is what a phone on its side gives us. There, 101px
   of header is a quarter of the viewport and the thing being spent is
   the only axis that is short.

   So this reverses exactly that one decision, and nothing else: the
   nav returns to the base flex row and scrolls horizontally, which it
   is already built to do — overflow-x, overscroll-behavior and the
   hidden scrollbar are all on the base rule, and the fade mask set
   above is kept precisely because the row now scrolls again.

   Height alone is the condition, and it is sufficient: no portrait
   phone is 500px tall, so a narrow screen can never enter this block
   and keeps the two rows that are right for it. The viewport here is
   852px wide — wider than the tablets this block's sibling is written
   for — which is exactly why width cannot express the rule. Paired
   with --header-h in tokens.css: change one, re-measure the other. */
@media (max-height: 500px) {
  .psc-section-nav {
    display: flex;
    grid-template-columns: none;
    grid-template-areas: none;
    gap: var(--space-4);
    padding: var(--space-2) var(--space-4);
  }
  .psc-section-nav nav {
    /* Undo the full-bleed overhang from the two-row block: on one row
       the nav is a middle column, not a strip spanning the header. */
    width: auto;
    margin-inline: 0;
    padding-inline: 0;
    justify-content: flex-end;
    gap: var(--space-4);
  }
}

/* ── Portrait tablet: put the nav back on one row ──────────
   The two-row block reaches up to 1120px, so every portrait tablet
   falls inside it — a 768px iPad and a 1024px iPad Pro both spend
   101px on a header whose contents have long since stopped needing
   it. The reason given for 1120px was five links plus a 217px report
   CTA, measured at about 1,100px. There are three links now, the
   Countries and Global pages having gone, and the CTA measures 182px:
   one row needs 679px, which every tablet in portrait clears with at
   least 89px to spare.

   So this reverses the one decision, exactly as the landscape block
   above does, and for the same reason — the row that was traded for
   completeness no longer buys any.

   WIDTH ALONE CANNOT SAY THIS, and neither can height. A landscape
   phone is 852px wide and a portrait phone is 393px, so a width query
   that admits a 768px tablet either admits the landscape phone too
   (already handled above, harmlessly) or excludes the tablet. The
   pairing that is true of a portrait tablet and nothing else is a
   lower bound on width AND portrait orientation: no phone is both
   721px wide and taller than it is wide.

   Paired with --header-h in tokens.css: change one, re-measure the
   other. */
@media (min-width: 721px) and (orientation: portrait) {
  .psc-section-nav {
    display: flex;
    grid-template-columns: none;
    grid-template-areas: none;
    gap: var(--space-4);
    padding: var(--space-2) var(--space-4);
  }
  .psc-section-nav .brand { font-size: var(--text-md); }
  .psc-section-nav nav {
    width: auto;
    margin-inline: 0;
    padding-inline: 0;
    justify-content: flex-end;
    gap: var(--space-4);
  }
}

/* The report CTA is the one item the row cannot always hold. With a
   saved city in the nav the header needs 807px, which a 768px or
   820px tablet does not have, and the overflow would put "Overview"
   behind a swipe — the precise outcome the two-row block existed to
   avoid. Dropping the CTA costs 182px and brings the worst case to
   613px, inside every tablet in portrait. The same trade phones make
   at 599px, for the same reason and with the same consolation: every
   page still carries the CTA in its hero. Above 900px it fits with
   the saved city and stays. */
@media (min-width: 721px) and (max-width: 899px) and (orientation: portrait) {
  .psc-section-nav .header-cta { display: none; }
}

.psc-section-footer {
  border-top: 1px solid var(--line);
  padding: var(--space-7) var(--space-6) var(--space-6);
  color: var(--muted);
  font-size: var(--text-sm);
  max-width: var(--max-width);
  margin: 0 auto;
  display: flex;
  justify-content: space-between;
  gap: var(--space-5);
  flex-wrap: wrap;
}
.psc-section-footer .meta { display: flex; gap: var(--space-5); flex-wrap: wrap; }

/* ── Loading state ─────────────────────────────────────── */
#app:empty::before {
  content: "";
  display: block;
  height: 100vh;
}

/* ── Transitions off when a repaint is not a change ────────
   .btn is the only thing on these pages that transitions its colours,
   so it is the only thing that FADES when a restyle sweeps the
   document — everything else snaps, and the buttons trail 180ms
   behind it. Two moments sweep like that: the first frames of a load
   (header-offset.js sets data-scrolled on :root, which re-resolves
   every var()-dependent colour beneath it) and a theme switch. In
   neither case did the reader change the button, so in neither case
   should the button animate.

   Armed before first paint by the inline script in each page's
   <head>, cleared a frame after load, and re-armed by theme.js around
   a flip. TRANSITIONS ONLY: a keyframe animation is deliberate
   choreography — the hero figure draws itself with one — and none of
   this touches those. */
:root[data-no-transitions] *,
:root[data-no-transitions] *::before,
:root[data-no-transitions] *::after {
  transition: none !important;
}

/* ── Theme toggle (injected into the header nav by theme.js) ─ */
.theme-toggle {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  flex: 0 0 auto;
  width: 44px;
  height: 44px;
  padding: 0;
  /* Masthead ink: this control only ever mounts inside .psc-section-nav, so
     it takes the masthead's palette rather than the page's — and that
     palette follows the theme, which is the whole point of a toggle
     that has to look like it belongs to the mode it is showing. */
  border: 1px solid var(--header-line);
  border-radius: var(--radius-pill);
  background: transparent;
  color: var(--header-ink-muted);
  cursor: pointer;
  transition: color var(--dur-fast) var(--ease-out),
              border-color var(--dur-fast) var(--ease-out),
              background-color var(--dur-fast) var(--ease-out);
}
.theme-toggle:hover { color: var(--header-ink); border-color: var(--header-accent-bright); }
.theme-toggle svg { width: 18px; height: 18px; display: block; }
/* Show the moon in dark mode (tap to go light), sun in light mode. */
.theme-toggle .icon-sun { display: none; }
.theme-toggle .icon-moon { display: block; }
:root[data-theme="light"] .theme-toggle .icon-sun { display: block; }
:root[data-theme="light"] .theme-toggle .icon-moon { display: none; }

/* The locale flag button that used to live here is gone. It was one
   control switching currency and units together, mounted beside the
   theme toggle; the 2026 UAT found it undiscoverable there and asked
   for the two settings to separate. They are now a pair of pill groups
   in the sticky page-settings bar (and in the hero on the landing
   page) — see mountLocaleControls() in js/locale.js, which reuses
   .chart-toggle rather than bringing styles of its own. */
