/* ============================================================
   page-settings.css — The sticky settings bar.

   Everything these controls change — the lead figure, four charts,
   a 125-row table — sits below the fold. Leaving them at the top
   meant scrolling up to click and back down to look, so a reader
   never actually SAW the period change. The bar follows them down
   instead.

   THE ANTI-JUMP INVARIANT: the bar's height is identical in flow
   and when stuck. The heading and caption live in .ps-lede, which
   is NOT sticky and simply scrolls away; the sticky element holds
   only the controls, on one row, at all times. `data-stuck`
   therefore drives paint only — background, hairline, shadow.

   Never put height, padding, display, flex-direction or font-size
   behind [data-stuck]. The moment one of those changes on stick,
   the content below jumps and this whole design is pointless.

   WARNING: position: sticky dies silently if any ancestor gets
   `overflow` other than visible. `overflow-x: hidden` on <main> is
   the usual culprit — it is the standard fix for a horizontal
   scrollbar and it would break this with no error anywhere.
   ============================================================ */

/* Zero-height probe. It scrolls under the header a moment before the
   bar reaches it, which is what flips data-stuck. Kept OUTSIDE the
   sticky element so its own position is never affected by sticking. */
.ps-sentinel { height: 0; }

/* The hero art bleeds below the section it belongs to, so the page
   needs more room here than the standard section rhythm gives — and
   the fold reads better for it either way: the claim and the
   controls are two different registers. */
.rp-section.ps-lede { margin-top: var(--space-8); }
.ps-lede { margin-bottom: var(--space-3); }
/* This block's readout used to render BEHIND the hero art — see the
   "under the hero art" rule in rankings-page.css, which lifts every
   section including this one. Nothing to do here. */

.ps-bar {
  position: sticky;
  /* -1px closes the sub-pixel hairline that otherwise lets a strip of
     scrolling content show between the header's bottom edge and this
     bar's top on fractional-DPR displays. */
  top: calc(var(--chrome-h) - 1px);
  z-index: var(--z-sticky);

  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-3) var(--space-5);

  /* min-height, not height: at an awkward intermediate width the row
     wraps and the bar gets taller, which is fine. What must not happen
     is content being clipped, or the height differing between the flow
     and stuck states. */
  min-height: 56px;

  /* FULL BLEED TO THE VIEWPORT, with the controls left where they were.

     It used to bleed by the page gutter alone — margin-inline: -24px,
     padding-inline: 24px — which is exactly right up to --max-width and
     wrong above it. .rankings-page caps at 1440 and centres, so on a
     1920 display the stuck bar's background stopped 240px short on each
     side and the table scrolled through the gap beside it: a bar that
     is holding the page still, with the page moving past both of its
     ends.

     --ps-bleed is the negative margin that takes the border box from
     the container's content edge out to the viewport edge. 50% resolves
     against the containing block, so at any width at or below the cap
     it evaluates to the old -24px and nothing changes; above the cap it
     grows by half the leftover. --scrollbar-w (js/header-offset.js)
     removes the classic scrollbar's gutter, which vw counts and the
     document does not — without it this overflows by ~15px on Windows.

     The padding is the exact negative of the margin, so the pills land
     on the container's content edge either way. Written as one variable
     used twice for that reason: the two cannot drift. */
  --ps-bleed: calc(50% - 50vw + (var(--scrollbar-w, 0px) / 2));
  margin-inline: var(--ps-bleed);
  padding-inline: calc(-1 * var(--ps-bleed));

  /* Vertical padding matched to the row gap above, so a bar that wraps
     is inset by the same 12px at its top, between its rows, and at its
     bottom — one rhythm rather than two.

     It used to have none. While the bar held one row min-height hid
     that, but on the global page it wraps to two and the content came
     to 74px inside a 75px bar: the pills sat hard against the top edge
     and the select against the bottom border, with the table scrolling
     directly beneath it. A single row still measures 54px here, so
     min-height keeps those pages at exactly 56 and this costs them
     nothing. */
  padding-block: var(--space-3);

  background: transparent;
  border-bottom: 1px solid transparent;
}

/* ── Focus must not scroll the page ─────────────────────────
   Clicking the region select while the bar was stuck threw the reader
   back up to where the bar sits in normal flow — several screens, in
   the middle of choosing a filter.

   Nothing in this codebase asked for that scroll. It is the browser
   scrolling a newly focused element into view, and the reason a STUCK
   control is treated as out of view is html { scroll-padding-top }
   (base.css), which reserves the header AND this bar's own height. A
   control inside the bar therefore sits inside the reserved strip by
   definition, and the only scroll position that satisfies the
   algorithm is the bar's unstuck one.

   scroll-margin shrinks the box that has to be made visible, so a
   negative top of the bar's own height cancels exactly the part of
   the reservation the bar itself is responsible for. Written against
   the same variables as the padding, so the two cannot drift.

   Not JS. A focusin handler that restores window.scrollY works, but it
   fights a scroll the browser has already performed, and every reader
   sees the flash. */
.ps-bar :is(select, button, input, a) {
  scroll-margin-top: calc(-1 * (var(--ps-bar-h, 56px) + var(--space-3)));
}

/* Stuck: the page, held still. Not a floating card — --bg rather than
   a surface, with a hairline and a light shadow doing the separating.
   No backdrop-filter: 56px of full-width blur repaints every scroll
   frame while Plotly and a Mapbox canvas compete for the compositor. */
.ps-bar[data-stuck='true'] {
  background: var(--bg);
  border-bottom-color: var(--line);
  box-shadow: var(--shadow-sticky);
}

/* The filter row's own separator belongs to the two-row layout it used
   to sit in. Inside the bar it is one half of a single row. Scoped, so
   .rp-controls keeps its border everywhere else. */
.ps-filters {
  margin-top: 0;
  padding-top: 0;
  border-top: 0;
}

@media (prefers-reduced-motion: no-preference) {
  .ps-bar {
    transition: background-color var(--dur-fast) var(--ease-out),
                border-color     var(--dur-fast) var(--ease-out),
                box-shadow       var(--dur-fast) var(--ease-out);
  }
}

/* ── Narrow viewports ───────────────────────────────────────
   Five period pills plus one or two selects wrap to three rows on a
   phone, which would put a third of the viewport under the bar
   permanently. The pills become one horizontally scrollable row
   instead, and js/sticky-controls.js keeps the selected one in view. */
/* The bar in a cramped viewport. Narrow, or short, or both: a 393px
   portrait phone and an 852x393 landscape one are the same problem seen
   on two different axes, and most of the answer — scroll the pills
   rather than wrap them, and give them a real touch target — is the
   same for both.

   The height arm is why it is not a width query alone. A phone on its
   side is 852px WIDE, so every width query on the site reads it as a
   laptop; it was handed the desktop bar and 197px of a 393px viewport
   went to chrome, leaving four table rows. See the nav's matching note
   in base.css and --header-h in tokens.css — the three move together.

   THE ONE THING THAT STAYS WIDTH-ONLY IS THE BAR'S OWN AXIS, in the two
   blocks directly below. The compact SHAPES are a cure for having no
   horizontal room, and a landscape phone has plenty — it is short, not
   narrow, and its bar already fits one row. Short-and-wide gets the
   density change instead and keeps its row. */
@media (max-width: 720px) {
  /* ONE ROW. Not two, and certainly not the three it started as — a
     stretched column, one row per cluster, which on cities.html is the
     period strip, the continent trigger and the locale pills: 165px of
     permanently sticky chrome under a 101px header on a 393px-tall
     phone.

     The clusters did not shrink; the controls did. Below
     COMPACT_CONTROLS_QUERY (js/config.js — the same 720px, named once
     so the two cannot drift) js/period.js swaps its five pills for the
     same menu button the continent filter is, and js/locale.js swaps
     each two-option pill pair for a single square printing the setting
     in force. That took city.html to one row on its own.

     NOWRAP IS WHAT TAKES cities.html THERE TOO, and it is a deliberate
     reversal — this read `flex-wrap: wrap` and let the clusters find
     their own rows. The arithmetic is why. At 393px the bar has 345px
     inside its gutter; the three clusters want 147 + 136 + 92 plus two
     gaps, which is 391 in the worst case and over the line in every
     case but one. Wrapping obeys the hypothetical sizes and happens
     BEFORE flex-shrink is consulted, so with `wrap` the row never
     shrinks to fit — it breaks, and the locale pair drops to a second
     44px row to carry two glyphs.

     Nowrap spends the overflow on the one label that can afford it
     instead. The period trigger takes `flex-shrink: 0` at the foot of
     this file and the continent trigger takes the whole reduction, so
     "North America" — the only option whose name is twice the length
     of the others — ellipses on a narrow phone and every other state
     prints in full. The full string stays in the DOM, so the
     accessible name is untouched and WCAG 2.5.3 holds; the menu prints
     it in full besides.

     The per-cluster bases are at the foot of this file, because
     .ps-period and .ps-scope are authored further down and would
     otherwise win on order.

     min-height is the row floor, and now that the bar cannot wrap it
     is also its ceiling: what it stops is the bar changing height
     between its flow and stuck states on a page whose controls mount
     late. */
  .ps-bar {
    flex-wrap: nowrap;
    align-items: center;
    /* A tighter column gap than the bar pays anywhere else, and the
       last 8px the row needed. Every cluster here is a bordered pill
       carrying 14px of its own inset, so the space a reader sees
       between two of them is 4 + 14 + 14, and at 360px those 8px are
       the difference between the continent trigger printing "All" and
       printing "A…". The row gap is moot — the bar cannot wrap. */
    gap: var(--space-1);
    min-height: 60px;
    padding-block: var(--space-2);
  }
}

/* THE ONE PLACE WHERE ONE ROW IS NOT POSSIBLE. The row needs the
   period trigger whole (147px), a continent trigger wide enough to
   print "All" (63px), two 44px squares, two gaps and the page's own
   24px gutter on each side: 310px of content, which it has at 360px —
   the narrowest phone still sold — and not a pixel below. With nowrap
   the shortfall lands on the continent trigger, and because it shrinks
   without a floor (see the foot of this file) it arrives at 341px as a
   caret with no label beside it.

   So below 360 the bar wraps, as it did at every width before this
   review, and prints both triggers whole on one row with the locale
   pair beneath. Two rows that read beat one row that does not. This is
   a fourth width in a file that had three, and it is written as its
   own query rather than folded into the block above because it undoes
   exactly one declaration of it. */
@media (max-width: 359px) {
  .ps-bar { flex-wrap: wrap; }
}

/* Short but not narrow. Keeps the base wrapping row — the clusters sit
   side by side and the bar is two rows, which at this width is what the
   content actually asks for — and spends less on rhythm: --space-3 paid
   above, between and below is 36px of a 393px viewport. Density only;
   no control and no target changes size. Still a media query, so the
   flow/stuck invariant at the top of this file holds — both states
   match the same query and the height cannot differ between them. */
@media (min-width: 721px) and (max-height: 500px) {
  .ps-bar {
    gap: var(--space-2) var(--space-5);
    padding-block: var(--space-2);
    /* ONE ROW, because the period strip can pay for it. The bar wrapped
       here for the usual reason — its two clusters are wider than the
       viewport — but the left one is a horizontally scrolling strip, so
       it does not need its natural width; it needs whatever is left
       after the locale pills, and it scrolls through the rest. Letting
       it wrap instead bought nothing and cost a whole 44px row, which
       on a 393px-tall screen is a row of the table.

       Only safe BECAUSE the strip scrolls. Do not copy this to the
       narrow block: there the locale cluster alone fills the width and
       nowrap would push the bar off-screen rather than scroll it. */
    flex-wrap: nowrap;
  }
  /* min-width:0 is the whole mechanism — without it the strip's
     min-content width is its five pills and it refuses to shrink. */
  .ps-period { flex: 1 1 auto; min-width: 0; }
  .ps-filters { flex: 0 0 auto; }
}

@media (max-width: 720px), (max-height: 500px) {
  /* Selector carries BOTH classes deliberately. chart-toggle.css loads
     after this file and gives .chart-toggle-group `flex-wrap: wrap` for
     the desktop row; at equal specificity the later file wins and this
     strip wrapped to three rows, taking the bar to 349px. Matching both
     classes outranks it regardless of load order. Every .ps-pills in the
     site is also a .chart-toggle-group. */
  .ps-pills.chart-toggle-group {
    display: flex;
    /* Must be explicit: this row is a single scrolling strip, never a
       wrapping one. Same trap as .ps-bar above — a wrap declared for one
       breakpoint leaking into another. */
    flex-wrap: nowrap;
    gap: var(--space-2);
    overflow-x: auto;
    /* Don't chain to the page or trigger the browser's back-swipe when
       the row runs out of scroll. */
    overscroll-behavior-x: contain;
    scroll-snap-type: x proximity;
    /* Bleed so the first and last pill can reach the gutter. */
    margin-inline: calc(var(--space-5) * -1);
    padding-inline: var(--space-5);
    /* A scrollbar under a 30px row is more noise than affordance; the
       fade at the right edge says "there is more" instead. */
    scrollbar-width: none;
    -webkit-mask-image: var(--mask-fade-x);
            mask-image: var(--mask-fade-x);
  }
  .ps-pills::-webkit-scrollbar { display: none; }

  /* The rule that stood here stacked .ps-group's label above its
     control at this breakpoint. It was there because the group held a
     .ps-pills strip, which bleeds into the gutter with a negative inline
     margin so its first pill can reach the screen edge — beside an
     inline label that bleed pulled the strip back OVER it, and "SCOPE"
     rendered underneath the first pill.

     The group holds a menu button now (js/ps-select.js). A trigger
     has no bleed, so the label sits beside it here exactly as it does on
     desktop, and the bar keeps a row. */

  /* 44px, the site's own convention and the platform floor, rather than
     the 29.6px the compact ladder gives.

     It stood at 40 and said so: "well under this site's own 44px
     convention." That was the honest price of a strip that wrapped to
     three rows here — 44 × 3 is 132px of pills alone, and the bar was
     already taking 35% of a portrait phone. The strip is one scrolling
     row again (see the min-width: 721px note below the narrow block),
     so the last 4px costs a single row 4px instead of three rows 12px,
     and there is no longer any reason to sit under the floor. */
  .ps-bar .chart-toggle {
    flex: 0 0 auto;
    scroll-snap-align: start;
    min-height: 44px;
    padding: var(--control-py-sm) var(--control-px-pill);
  }

  .ps-filters { display: flex; gap: var(--space-3); flex-wrap: wrap; }
  /* The two .rp-filter rules that stood here sized a <select> that no
     longer exists. The scope control is pills now and needs no help. */
}

/* ── The settings bar's own label ────────────────────────
   "Time period" sits above the period pills on four pages, and it was
   set as .rp-section-title h2 — 28px at weight 900, identical to "City
   leaderboard" and "The two ends". A filter's label was shouting as
   loudly as the content sections it filters, which put a control at the
   top of the page's visual hierarchy.

   It stays an <h2>: a reader navigating by heading should be able to
   jump to the controls, and removing the heading to fix its size would
   cost that. Only the type changes — to the same small-caps label the
   SCOPE group below uses, so the two controls on global.html are
   labelled in one voice instead of two. */
.ps-lede .rp-section-title h2 {
  font-family: var(--font-body);
  font-size: var(--text-xs);
  font-weight: 700;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--muted);
}

/* ── The label row above the bar ─────────────────────────
   ONE ROW OF CONTROLS IS THE POINT. The bar held its own captions —
   "SCOPE" beside the scope pills, "SHOW IN" beside the currency and
   unit pills, and the period readout beside the period pills — and on
   global.html that came to 1,444px of content in a 1,217px bar at
   1280px wide. It wrapped to two rows and 96px of permanently sticky
   chrome, which is a lot of page to hold still.

   The captions were never the controls. They are labels, and this page
   already had a place for a label: the non-sticky lede that says
   "TIME PERIOD" above the bar. The rest moved there, so what sticks is
   pills and nothing else.

   Left cluster mirrors .ps-period, right cluster mirrors .ps-filters,
   and .ps-lede .rp-section-title (rankings-page.css) is already
   space-between with wrap — the same geometry as the bar, so each label
   lands over the cluster it names. The gap below matches the bar's own
   column-gap for the same reason: the labels have to track the controls
   they sit above, and two different rhythms would drift them apart. */
.ps-lede .rp-section-title {
  position: relative;
  /* The floor the parked labels stand on. --ps-label-h is published by
     js/sticky-controls.js from the labels' own measured height and only
     while they are parked; unset — no JS, a wrapped bar, an unrendered
     control — the row is sized by the labels themselves, as authored. */
  min-height: var(--ps-label-h, auto);
}
.ps-lede-period,
.ps-lede-scope,
.ps-lede-filters {
  display: flex;
  align-items: baseline;
  min-width: 0;
}
.ps-lede-period { gap: var(--space-3); }
.ps-lede-scope { gap: var(--space-3); }
.ps-lede-filters { gap: var(--space-5); }

/* Parked: js/sticky-controls.js has measured the bar and placed each
   label over the control it names. Authored as a plain flex row above,
   so a page with no JS, a wrapped bar, or a control that has not
   rendered yet still gets the labels in the right order at the right
   end of the row — just not pixel-aligned. See the note in that file
   for when it gives up.

   BOTH clusters, because both can hold a parked label and the one that
   does differs by page. Scoped to the labels themselves rather than
   `> *` so a cluster that also carries something in flow — the city
   page's "Time period" <h2>, which has no data-ps-label-for and is
   never parked — keeps it. */
.ps-lede-period.is-parked > [data-ps-label-for],
.ps-lede-scope.is-parked > [data-ps-label-for],
.ps-lede-filters.is-parked > [data-ps-label-for] { position: absolute; top: 0; }
/* Left-aligned, because it is now a caption ON the "Time period" label
   rather than the right-hand end of a two-item row. */
.ps-lede-period .ps-readout { text-align: left; }

/* ── A named control group in the settings bar ───────────
   The scope pills on global.html. Region pills need no caption — the
   names ARE the axis — but "All cities / Also in the city ranking / Not
   in the city ranking" does not announce what it is filtering on, and
   two unlabelled pill groups in one bar read as a single control with
   nine states. The caption is what keeps them two controls.

   It replaces the <label><span>Scope</span><select> this control used to
   be: same label, same axis, now with its state visible instead of
   folded into a closed menu. */
.ps-group {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  min-width: 0;
}
.ps-group-label {
  flex: 0 0 auto;
  color: var(--muted);
  font-size: var(--text-xs);
  font-weight: 700;
  letter-spacing: 0.12em;
  text-transform: uppercase;
}

/* ── The continent filter ────────────────────────────────
   A menu button (js/ps-select.js), mounted into the middle of the
   bar's three clusters: the period on the left, prices and units on
   the right, and the one control that changes WHICH ROWS are ranked
   between them. The trigger is a .ps-select and brings its own
   display, position and narrow-viewport behaviour from
   chart-toggle.css; this only says how the cluster sits in the row.

   flex: 0 0 auto, like .ps-filters and for the same reason as the
   note on .ps-period: a greedy middle child would push the locale
   cluster onto a second row and buy 40px of permanent sticky chrome
   with it. */
.ps-scope {
  display: flex;
  align-items: center;
  flex: 0 0 auto;
  min-width: 0;
}
/* WHAT THE THIRD CLUSTER COSTS, measured on cities.html at the widths
   that matter, bar height with the control against without:

     ≥1200   56 / 56   one row, labels parked
     1180    96 / 56   the period strip wraps a row earlier
     1080    96 / 96   no cost — it had already wrapped
     820–721 134 / 134 no cost
     720–360  61 / 61  no cost — compact controls, all three on one row
     ≤359    109 / 61  the bar wraps; nothing fits on 320px but two rows

   The only regression above the compact breakpoint is the 1081–1180
   band, where 90px of trigger plus a 24px gap pushes the period strip
   into a second line about 100px sooner than it went on its own. That
   is the price of a third control in a row, not a bug in this one:
   nothing overflows, the degradation is the same wrap the bar already
   performs below 1081, and the labels unpark through
   js/sticky-controls.js exactly as they do there. Shrinking the
   trigger to claw it back would cost the option counts, which are what
   make the filter legible before it is opened.

   BELOW THE COMPACT BREAKPOINT IT NOW COSTS NOTHING AT ALL, down to
   360px. It cost a second 44px row until the 2026-10-05 review, which
   found the last 46px in four places — nowrap, a 4px column gap, 44px
   locale squares instead of 48, and the count chip off the trigger —
   and spent the remainder of the shortfall on the continent label
   rather than on the reader's row. Re-measure this table if a fourth
   cluster is ever added; there is no slack left to absorb one. */

/* ── Prices and units ────────────────────────────────────
   Two .ps-group children, mounted by js/locale.js. They are separate
   settings and separate controls; this only keeps them adjacent, so a
   reader who finds one has found the other.

   THE TIGHTER GAP IS THE POINT. The bar's own column-gap is --space-5,
   which is the distance between UNRELATED controls — period, region,
   scope. These two are related without being one thing, and at 24px
   apart they read as two more items in that list rather than as a pair.
   --space-4 is the closest the bar's rhythm gets to "these belong
   together" without merging their labels. */
.ps-locale {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: var(--space-2) var(--space-4);
  min-width: 0;
}
/* One caption over both pairs. It sits outside them rather than inside
   either, which is what lets it read as covering the two. */
.ps-locale-caption { flex: 0 0 auto; }
/* Inside the pair the groups are unlabelled, so their own gap is what
   separates them — tighter than the caption's. */
.ps-locale .ps-group { gap: 0; }

/* The period pills and the readout that says what they mean. One item
   in the bar's flex row, so the readout travels with the control
   instead of being left behind in the non-sticky lede above — which is
   where it used to be, and where a reader could not see it once the bar
   stuck. */
.ps-period {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  min-width: 0;
  /* NOT flex: 1 1 auto. Greedy, this claims the free space and pushes
     the filter cluster onto a second row even at 2560px — 40px of
     permanent sticky chrome bought for nothing. The bar is
     space-between; both children sizing to content is what puts them at
     opposite ends of one row. */
  flex: 0 1 auto;
}
/* The readout that stood here moved up into .ps-lede — see the label-row
   note above. .ps-period now wraps the period pills alone, and keeps
   doing so for its flex-basis comment below rather than for its
   children. */

/* ── The period control ─────────────────────────────────
   FIVE pills, not two, and that is the whole sizing problem. The locale
   groups are "$ / €" and "mi / km" — four characters. "Weekend Evening"
   is fifteen, five times over, and at 1280px the row cannot hold that
   plus the filter cluster.

   So the group is allowed to wrap INTERNALLY rather than pushing the
   filters onto a second row: the pills reflow within the left cluster
   and the bar stays one row for as long as it can. The readout is the
   first thing to give, because it is the only part whose information is
   also available elsewhere — the selected pill still names the window,
   and the methodology page defines all five. */
.ps-period-control { min-width: 0; }
/* ABOVE THE BREAKPOINT ONLY, and the media query is the whole point.

   This rule is (0,2,0) and sits after the narrow block above, so
   unqualified it beat that block's `.ps-pills.chart-toggle-group {
   flex-wrap: nowrap }` on specificity-tie-plus-order — for the period
   pills alone, and at every width. That is the exact trap the comment
   on the narrow rule warns about, sprung by a rule written later and
   further down: the five pills wrapped to three rows on a phone, the
   scrolling strip had nothing to overflow, and `reveal()` in
   js/sticky-controls.js returned early on `scrollWidth <= clientWidth`
   every time. The bar stood 201px tall under a 101px header — 35% of a
   portrait phone and 50% of a landscape one, permanently.

   Internal wrapping is a DESKTOP accommodation: it keeps the filter
   cluster on row one at 1280px. A phone wants the opposite, and the
   narrow block already says so.

   Both arms are needed. max-height excludes the landscape phone, which
   clears 721px easily — without it a rule written for a 1280px desktop
   reaches an 852x393 handset and wraps the strip there too. */
@media (min-width: 721px) and (min-height: 501px) {
  .ps-period-control .ps-pills { flex-wrap: wrap; }
}

/* The hours drop and the pills carry the window alone. Not display:none
   on a live region — it is static text and nothing announces it — so
   this is a layout decision, not an accessibility one. Same pair of axes
   as the bar's own block: the readout is as unaffordable on a short
   screen as on a narrow one. */
@media (max-width: 720px), (max-height: 500px) {
  .ps-period .ps-readout { display: none; }
}

/* The landing page has no settings bar, so its hero carries the pair
   directly. Same component, same spacing; only the placement differs. */
.scene--hero .hero-settings {
  position: relative;
  z-index: 2;
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: var(--space-2) var(--space-4);
  margin-top: 0;
  padding-top: 0;
  border-top: 0;
}

/* ── THE COMPACT BAR ─────────────────────────────────────
   The foot of the file, deliberately. .ps-period, .ps-scope and the
   locale cluster are all authored above with their desktop flex
   values, and a narrow override written beside the .ps-bar block would
   tie with them on specificity and lose on order. This is the same
   breakpoint as that block and as COMPACT_CONTROLS_QUERY in
   js/config.js; the three move together.

   THE WHOLE BAR IS ONE ROW AND EVERY VALUE HERE IS SPENT BUYING IT.
   It was three rows in March, two after the locale pills became
   squares (2026-10-05), and one after this. The row is 345px wide
   inside a 393px phone and the clusters want 391 of it in their
   longest state, so three things give and one does not:

     the count chip   hidden on the trigger, kept in the menu — 22px
     the squares      44px on the touch floor, not 48 and not a pill
     the continent    shrinks, and ellipses in the one state that
                      cannot fit
     the period       does not give. It names the window every figure
                      on the page is read in, and a reader who cannot
                      tell "Weekday Midday" from "Weekend Midday" has
                      lost the one thing the bar is for.

   Nothing here scrolls horizontally, so js/sticky-controls.js has
   nothing to reveal and quietly does nothing — see reveal() there. */
@media (max-width: 720px) {
  /* The period is the fixed point of the row: natural width, no growth,
     no shrink. It does not grow because a pill stretched to twice its
     own label is the wasted space this row was rebuilt to remove, and
     because every pixel it took was a pixel off the continent label
     beside it. It does not shrink at all — not even by the half-pixel
     a proportional distribution would hand it, which is enough to put
     an ellipsis on "Weekday Midday" with 19px of slack still in the
     row. It names the window every figure on the page is read in. */
  .ps-bar .ps-period { flex: 0 0 auto; min-width: 0; }
  /* And the continent is the one that gives, all of it. Content-sized,
     shrinking without a floor, with .ps-select-value ellipsing the
     remainder (chart-toggle.css) — no floor, because a floor is what
     pushes the overflow back onto the period or out through the bar's
     gutter, and this is the label that can best afford to lose its
     tail.      The widths it lands on:

       430px and up   nothing clipped in any state
       360–430        "All" and "Europe" whole, "North America" clipped
       below 360      the bar wraps instead; see the query above it

     The string stays whole in the DOM, so the accessible name is
     untouched, WCAG 2.5.3 holds, and the open menu prints it in full
     beside its count.

     .ps-bar-scoped, because the trigger carries .ps-scope AND
     .ps-select on the one element, and chart-toggle.css gives
     .ps-select `flex: 1 1 auto` at this very width. Two single-class
     selectors tie, and which file loads last is not a thing this rule
     should depend on. */
  .ps-bar .ps-scope { flex: 0 1 auto; min-width: 0; }
  /* Content-sized and never stretched: two squares are the whole
     cluster, and a cluster that grows would pull them apart. */
  .ps-bar .ps-filters { flex: 0 0 auto; margin-left: auto; }

  /* The chain the menu trigger stretches along. .ps-select already
     takes `flex: 1 1 auto` at this width (chart-toggle.css); these are
     the two wrappers between it and the bar, which are display:flex
     and display:block respectively and would otherwise size to
     content. */
  .ps-period-control { display: flex; min-width: 0; }
  .ps-period-menu { flex: 1 1 auto; min-width: 0; }

  /* THE COUNT COMES OFF THE TRIGGER, not out of the control. It is a
     footnote to a name — "All 121", "North America 46" — and a
     footnote is the first thing to go when the row is 46px short. It
     is still printed beside every option in the open menu, which is
     where a reader comparing the three wants it, and the page prints
     the filtered count in the scorecard heading underneath. */
  .ps-bar .ps-select-trigger .chart-toggle-count { display: none; }

  .ps-bar .ps-locale { flex: 0 0 auto; gap: var(--space-1); }
  /* Both squares, sized by the touch floor in chart-toggle.css and by
     nothing else. Two class selectors, matching `.ps-bar
     .chart-toggle` above, which pins every pill in the bar to
     `flex: 0 0 auto` for the desktop strip — a tie on specificity,
     and this file's later rule wins. */
  .ps-bar .ps-switch-group { flex: 0 0 auto; gap: var(--space-1); }
  .ps-bar .ps-switch { flex: 0 0 auto; }
}

/* ── AND THE LABEL ROW GOES WITH THEM ────────────────────
   The non-sticky captions above the bar — "Time period", "Continent",
   "Show in" — are parked over the control they name by
   js/sticky-controls.js, and parking only runs while the bar is ONE
   row. Compact, the bar is two, so the labels fall back to the
   space-between flow they are authored in: "SHOW IN" sits above the
   continent trigger, naming the wrong control, and the section costs a
   third row to do it.

   VISUALLY HIDDEN, NOT REMOVED. The same declarations as .sr-only in
   base.css, written here rather than toggled as a class because the
   markup is static and a media query is the honest home for a
   viewport decision. The row stays in the accessibility tree, so the
   city page's "Time period" <h2> still takes its place in the heading
   outline and a reader navigating by heading loses nothing.

   What a sighted reader loses is a caption on each control, and each
   control answers for itself at this width: the period trigger prints
   the window, the switches print both of their states, and the
   continent trigger prints the option and its count. All four carry
   their accessible name on the control, where it was already needed
   for the cases this row never covered. */
@media (max-width: 720px) {
  .ps-lede .rp-section-title {
    position: absolute;
    width: 1px; height: 1px;
    padding: 0; margin: -1px;
    overflow: hidden;
    clip-path: inset(50%);
    white-space: nowrap;
    border: 0;
  }
  /* The lede is now an empty box whose only job is the gap between the
     hero and the bar, so it pays the section rhythm rather than the
     hero's own bleed allowance. */
  .ps-lede { margin-bottom: 0; }
  .rp-section.ps-lede { margin-top: var(--space-5); }
}
