/* ============================================================
   rankings-page.css — Layout for the ranking pages
   (cities / countries / global).
   ============================================================ */

.rankings-page {
  max-width: var(--max-width);
  margin: 0 auto;
  /* Our nav is fixed, so the page has to reserve its height. That was a
     64px literal, which is the header BEFORE the theme toggle mounts and
     26px short once the nav wraps on a phone — the eyebrow slid underneath.
     --header-h is measured at runtime by js/header-offset.js.

     --chrome-overlay-h rather than --header-h, because what has to be
     reserved is the chrome that OVERLAYS content starting at y = 0, not
     all the chrome there is. Vendored into a host the nav is in flow and
     has already taken its own space; reserving it again would open a
     76px hole above the eyebrow on every ranking page. tokens.css. */
  padding: calc(var(--space-7) + var(--chrome-overlay-h)) var(--space-5) var(--space-7);
}

/* ── Hero ───────────────────────────────────────────────────
   The claim — eyebrow, title, dek, optional search, meta — sits in
   normal flow and keeps its own measure, child by child. The
   illustration is a BACKDROP behind it, absolutely positioned across
   the whole section: it fills the side the copy leaves free and
   bleeds under the copy from there, held down to a quarter of its
   strength wherever it does (see hero-figure.css).

   The section itself is full width now. The 900px cap it used to
   carry has moved onto the h1, which was the only child without a
   measure of its own. */
.rankings-hero {
  position: relative;
  margin: 0 0 var(--space-6);
  /* Flex column so the reserve created by min-height below can be
     DISTRIBUTED rather than left trailing. See .rankings-hero .meta. */
  display: flex;
  flex-direction: column;
  align-items: stretch;
}
/* Copy over art, always. Without this the two share a stacking
   context and source order decides, which puts the backdrop on top
   of anything that comes before it in the markup. */
.rankings-hero > * { position: relative; z-index: 1; }

/* ONE HEIGHT FOR ALL THREE HEROES, so moving between the pages does
   not move the fold.

   Measured, not guessed. The copy alone runs, at every desktop width
   from 910 to 1440:

     Cities     343 → 452   (2-line h1 from 1100 up)
     Global     320 → 350
     Countries  260 → 294   (no search box, shortest dek)

   So 452 was the tallest the content ever got, and 470 cleared it with
   18px to spare. It is a MINIMUM, not a height: copy that outgrows it
   still grows rather than being clipped — but it would also break the
   equality, so anything added to a hero gets re-measured against the
   452 above.

   The report CTA is the first thing that has been. It adds one .hero-cta
   row to all three — a 49px pill plus its 24px bottom margin — and the
   copy alone now runs, re-measured the same way:

     Cities     525          (h1 wraps to two lines at every width here)
     Countries  443 → 367
     Global     424

   Cities alone cleared the old 470 floor, which is exactly how the
   equality breaks: it would have stood taller than the two pages beside
   it. 545 clears the new tallest with 20px to spare. Remove the CTA and
   this goes back to 470.

   Only where the art shows. Below that breakpoint the reader is on a
   phone, the figure is gone, and reserved empty space is just
   scrolling to do. */
@media (min-width: 901px) {
  .rankings-hero { min-height: 545px; }
  /* Opt-out for a hero that is only borrowing the class. The floor
     exists to hold three PAGES to one fold; methodology has no art, no
     meta strip and four lines of copy, so it was inheriting a reserve
     of empty space it has nothing to put in — 225px of it once the CTA
     raised the floor. It is not one of the three, so it does not pay. */
  .rankings-hero--plain { min-height: 0; }
}

/* Below this the art has nowhere to go but under the copy, and a
   decoration does not get to compete with the claim for the fold. */
@media (max-width: 900px) {
  .rankings-hero .hero-figure { display: none; }
}
/* The min-height above is a floor, and on the shortest of the three
   heroes the copy does not reach it — Countries runs 367-443 against a
   545 floor, so up to 178px of the reserve had nowhere to go and fell
   below the last line as a trailing void.

   The floor itself stays. It is what holds the filter bar to the same y
   on all three pages, so a reader toggling Cities → Countries → Global
   does not watch the controls jump; that is worth more than the gap
   costs. What changes is where the slack sits: the meta strip is pushed
   to the bottom of the hero, so the reserve becomes space BETWEEN the
   CTA and the counts instead of empty space after them. Nothing is
   invented to fill it, and on Cities — which nearly reaches the floor
   unaided — the strip barely moves. */
.rankings-hero .meta { margin-top: auto; }

.rankings-hero .eyebrow { margin-bottom: var(--space-3); }
.rankings-hero h1.display {
  font-size: clamp(40px, 6vw, 72px);
  /* The cap the section used to carry. On the widest screens the
     column runs past 900px, and a headline set across all of it
     breaks at a measure nothing else on the page uses. */
  max-width: 900px;
  margin-bottom: var(--space-4);
}
/* Measured, not guessed: at 720px this ran to 83 characters a line, well
   past the 45–75 that stays comfortable to read. A ch measure holds the
   line count steady if the type size ever moves. */
.rankings-hero .dek { max-width: var(--measure-text); margin-bottom: var(--space-4); }
.rankings-hero .meta {
  display: flex;
  gap: var(--space-5);
  flex-wrap: wrap;
  color: var(--muted);
  font-size: var(--text-sm);
}
.rankings-hero .meta strong { color: var(--text); font-weight: 700; }
/* The last item in the strip is not a fourth statistic — it names the
   geography or the method the three counts before it were measured
   under. Setting it apart stops it being read as one.

   It is set apart with SPACE, not a glyph. This used to carry a leading
   '·', which worked while the strip held one line and orphaned to the
   start of a line the moment it wrapped — a stray dot followed by a 24px
   hole. Where it wrapped depended on the page and the width (measured on
   global: fine at 480, orphaned at 560, 640 and 700), so no breakpoint
   could predict it. Extra margin reads the same wherever the item lands.

   The other three items each open with a bold figure and this one does
   not, so the separation is reinforcing a distinction the content
   already makes.

   THE SPACE TRAILS THE ITEM BEFORE IT, rather than leading this one.
   Leading margin was the '·' problem again in a quieter register: it
   separates while the strip is one line, and on the line it wraps to it
   is a 12px indent under a flush "121 cities", which reads as a
   misalignment rather than as a note. A trailing margin on the item
   before spends itself at the end of the line above — where there is
   nothing to push out of true — and the note starts its own line flush.
   No breakpoint, for the reason the paragraph above gives: the width
   that wraps it is not knowable from here. */
.rankings-hero .meta:has(.meta-note) > :nth-last-child(2) {
  margin-right: var(--space-3);
}
.rankings-hero .meta .meta-note {
  color: var(--muted);
  font-style: normal;
}

/* ── Note block ─────────────────────────────────────────────
   A caveat that has to be read before the table. Set the way a print
   standfirst is: a heavy kicker rule above, a hairline below, no
   tinted box and no coloured bar down one side.

   This was .gl-scope, styled from an inline <style> in global.html
   and living INSIDE that page's hero. It moved out so all three
   ranking heroes have one structure — eyebrow, h1, dek, optional
   search, meta — and it is a shared class because the next page that
   needs a caveat should not write a second copy.

   No margin of its own: .rankings-hero already carries space below
   and the .rp-section after it carries space above. */
.rp-note {
  margin: 0;
  padding: var(--space-4) 0;
  max-width: var(--reading-width);
  border-top: 2px solid var(--text);
  border-bottom: 1px solid var(--line-strong);
}
.rp-note h2 {
  margin: 0 0 var(--space-2);
  font-size: var(--text-lg);
  font-weight: 900;
  letter-spacing: -0.01em;
}
.rp-note p { margin: 0; color: var(--text-soft); font-size: var(--text-sm); }
.rp-note p + p { margin-top: var(--space-2); }

/* Two orphans from the same inline block, used outside the hero on
   the global page. */
/* Same rule as .rp-tablenote above: a short note under a full-width block
   spans it, rather than stopping short of the edge. */
/* The scarce-end chart and the reason it has no companion, side by side.
   The explanation was a caption under a half-width chart with an empty
   column beside it; it is the answer to the question the missing chart
   raises, so it goes where the missing chart would have been. */
.gl-scarce {
  display: grid;
  /* Seven and five, and the note is centred against the chart.

     Six and six top-aligned a four-line note beside a 456px chart, which
     left 677 x 304px of empty column under it — the largest single void
     on the page. Two changes, neither of which invents content: the chart
     takes the extra column, which gives its ten bars more room to be
     read, and the note centres, so what is left over reads as margin
     around a caption rather than a hole beneath it.

     The note cannot be made to fill 456px honestly. It is four lines
     explaining why a companion chart does not exist, and padding it out
     would be worse than the gap. */
  grid-template-columns: minmax(0, var(--span-7)) minmax(0, 1fr);
  gap: var(--space-5) var(--grid-gutter);
  align-items: center;
}
/* ch, not a column measure: this sits INSIDE a grid cell, so a
   page-level span would divide the wrong container. */
.gl-scarce-aside { max-width: 46ch; }
.gl-scarce-title {
  margin: 0 0 var(--space-3);
  font-size: var(--text-lg);
  font-weight: 900;
  color: var(--text);
}
.gl-saturation {
  margin: 0;
  color: var(--text-soft);
  font-size: var(--text-md);
  line-height: 1.6;
}
@media (max-width: 980px) {
  .gl-scarce { grid-template-columns: 1fr; }
  .gl-scarce-aside { padding-top: 0; }
}
.gl-tag {
  display: inline-block;
  margin-left: var(--space-2);
  padding: 1px var(--space-2);
  border-radius: var(--radius-pill);
  background: var(--primary-soft);
  color: var(--primary-bright);
  font-size: var(--text-xs);
  font-weight: 700;
}

/* ── Section container ──────────────────────────────────────
   A card now means one thing: a bounded data object you can read on
   its own (a chart, the table). Controls and prose sit directly on the
   page — the spacing rhythm already separates them, so a box around
   them was chrome with nothing to hold. */
.rp-card {
  border: 1px solid var(--line);
  border-radius: var(--radius-lg);
  background: var(--card-grad);
  padding: var(--space-5);
}
.rp-bare { padding: 0; }
/* THREE COLUMNS, NOT ONE.

   "About these metrics" is three peer notes — what availability measures,
   how missing data is handled, how prices are converted — stacked in a
   single 566px column inside a full-width section. That left roughly
   669 x 450px of empty page beside it on every ranking page, and made
   three independent notes read as one wall of prose to be waded through
   in order.

   Four columns each puts them on 24 / 491 / 958 — the same verticals the
   "Where to go next" band below already uses — at 443px, which is about
   59ch at 15px and a better measure than the 566 they had. The width is
   used, the height roughly halves, and the three notes read as three
   things.

   Scoped with :not(.mx-doc) because methodology.html shares this class
   for its document body, and a 560-line reference document set in three
   columns would be unreadable — you would scroll to the bottom of column
   one and back up to the top of column two, for the length of the page. */
.rp-bare.rp-methodology:not(.mx-doc) {
  max-width: none;
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  column-gap: var(--grid-gutter);
  align-items: start;
}
.rp-methodology:not(.mx-doc) > h3 { grid-column: 1 / -1; }
/* The column is the measure now; a second cap inside it would only
   narrow the text and re-open the gap this is closing. */
.rp-methodology:not(.mx-doc) > p { max-width: none; }
@media (max-width: 980px) {
  .rp-bare.rp-methodology:not(.mx-doc) { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}
@media (max-width: 620px) {
  .rp-bare.rp-methodology:not(.mx-doc) { grid-template-columns: minmax(0, 1fr); }
}
/* The methodology page keeps its single reading column. */
.rp-bare.rp-methodology.mx-doc { max-width: var(--measure-long); }
/* ── Section rhythm ─────────────────────────────────────────
   Sections used to sit a uniform 36px apart, which gave the page no
   phrasing: summary, charts and raw data all read as one list. Three
   steps now, so space itself signals where one movement ends and the
   next begins.

     --tight   closely related, read as a pair (two chart cards)
     (default) a new idea, same movement
     --major   a change of register (summary → the data → the caveats) */
.rp-section { margin-top: var(--space-6); }        /* 36px */
.rp-section--tight { margin-top: var(--space-4); }
.rp-section--major { margin-top: var(--space-8); } /* 88px */

/* ── OVER THE HERO ART, NOT UNDER IT ─────────────────────
   .hero-figure is a BACKDROP that deliberately bleeds below the hero
   it belongs to — 300px on global, 200 on cities — so it reaches over
   whatever the hero is followed by. It does that as an absolutely
   positioned element at z-index: 0, and .rankings-hero is
   position: relative with z-index: auto, so it never became a stacking
   context and the art competes in the ROOT one. There a positioned
   z-index: 0 element paints above the text of an in-flow static
   sibling no matter how far down the source that sibling sits.

   The result: on global the settings bar's readout ("All hours · Every
   observed hour") and the whole AVG ON-STREET column of the lead
   figures were drawn UNDER the globe — grey-on-grey where the art is
   strongest, which is exactly the corner it fills.

   The hero solves this for its own children with `.rankings-hero > *
   { z-index: 1 }`. This is the same move for its siblings, and it is
   the whole page's rule rather than a patch on whichever section is
   currently unlucky.

   NOT z-index: -1 on the figure, which is the other obvious fix and is
   wrong: the art escapes to the root stacking context, <body> carries
   an opaque --bg, and the drawing disappears completely. Verified.

   .ps-bar is excluded by being position: sticky at --z-sticky already,
   and every existing z-index inside a section (map overlays at 2–4,
   the sticky table head at 1) is internal to that section and
   unaffected by the new context. */
.rankings-page > .rp-section { position: relative; z-index: 1; }

/* ── Lead statistic ─────────────────────────────────────────
   Availability is the subject of this scorecard; six equally weighted
   tiles said it was one of six equal facts. It now leads at roughly 3×
   the supporting figures, and the counts that were tiles (cities,
   countries, mapped curb) have moved into the hero meta line as running
   text, where trivia belongs. */
.stat-lead {
  display: grid;
  /* SEVEN COLUMNS AND FIVE, not 1.6fr and 1fr. A ratio is not a grid
     position: 1.6:1 resolves to wherever the container happens to be, so
     the same component started at x=894 on cities.html and x=885 on
     countries.html — one component, two positions, neither of them on a
     column edge. 7/5 puts the support figures at column 8 (x=850) on
     every page that uses it.

     The gutter is --grid-gutter for the same reason: two blocks align
     only if they share a container AND a gutter, and --space-6 (36px)
     is not the grid's 24. */
  /* SIX AND SIX, not seven and five.

     7/5 put the support column on column 8, which is a grid line, so it
     looked right in isolation. It is not right as a SYSTEM: the support
     column is itself a grid, and thirds of five columns land nowhere —
     its three figures came out at 841 / 1036 / 1230, only the first of
     which is a column start. A container on the grid whose contents are
     not is half a grid.

     Six divides cleanly both ways this component needs:
       3 figures (cities, countries) -> 2 cols each -> 724 / 958 / 1192
       2x2      (global, four)       -> 3 cols each -> 724 / 1075
     Every figure on every page now starts on a column line, and the
     primary column keeps 676px, which is more than its headline and
     its --measure-note prose ask for. */
  grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
  gap: var(--space-5) var(--grid-gutter);
  /* SIX AND SIX IS THE THREE-FIGURE SHAPE. Two figures want four and
     eight — see the :has() rule below the placement block. */
  /* Was `end`, which bottom-aligned the support figures to the primary
     column. That was fine while every page's primary column was a label,
     a figure and one note; once countries.html led with a range plus
     three paragraphs, `end` pushed the support row 300px down the page
     and left the top-right of the section empty. Top-aligned, the
     support figures sit beside the headline they support. */
  align-items: start;
}
/* Explicit placement, because the notes row sits BEFORE .stat-lead-support
   in source order — the notes belong to the headline they qualify, and a
   screen reader should meet them there rather than after the price
   figures. Grid lets the visual order differ from the reading order, and
   this is the case it is for.

   THE DEFAULT IS ONE COLUMN OF PROSE BESIDE ONE COLUMN OF FIGURES: the
   headline and its notes stack down column 1, and the support figures
   span both rows so column 2 sets its own height. Row one is then as
   tall as the HEADLINE rather than as tall as whichever side happens to
   have more in it, which is the only arrangement that cannot open a hole
   under the lead figure. See the countries exception below for the one
   shape that wants something else. */
.stat-lead-primary { min-width: 0; grid-column: 1; grid-row: 1; }
.stat-lead-notes   { grid-column: 1; grid-row: 2; }
.stat-lead-support { grid-column: 2; grid-row: 1 / span 2; }
/* A highlights row placed INSIDE the lead block closes it — see the
   comment on #highlights in countries.html for why it lives there.
   Row 3 is free in both arrangements this block has: the default spans
   the support column across rows 1-2, and the countries exception below
   pulls it back to row 1. */
.stat-lead > .ci-highlights { grid-column: 1 / -1; grid-row: 3; }

/* TWO SUPPORT FIGURES ARE NOT THREE, AND THE SPLIT HAS TO FOLLOW TOO.

   Six and six was sized for three support figures: two grid columns
   each, filling the right half edge to edge. The 1.1 review dropped the
   price ratio and left two, and nothing downstream noticed — the right
   half still reserved three columns, so the last one stood empty and
   the pair sat marooned 377px from the headline they support. Three
   figures spread across 1377px read as a row; two read as three things
   flung apart.

   The primary column asks for 330px — its longest hard-broken label
   line is 324 and its --measure-note prose 326 — so six columns of it
   were half padding. Four and eight pulls the support figures back to
   x=491 and x=802, both column lines, and x=491 is where the eye
   expects the next figure after a headline that ends at 348.

   The support grid itself is left alone deliberately: thirds of eight
   columns keep the two prices EQUAL in width, which is the one thing
   the comment on .stat-lead-support insists on — peers must not render
   at two sizes. Sizing those tracks to their content would close
   another 39px and buy it by making one price wider than the other.

   Count-following, like every other grid in this file: a page that
   regains a third figure gets six and six back without an edit. */
.stat-lead:has(.stat-lead-support .kpi:nth-child(2):last-child) {
  grid-template-columns: minmax(0, 1fr) minmax(0, 2fr);
}

/* The notes get columns of their own, count-following like every other
   grid here — but the count that works depends on how wide the block is,
   so it is set with the placement rather than beside it. Two notes in a
   half-width column are 279px each and read fine; three would be 178px
   ribbons, so three stack. */
.stat-lead-notes {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: var(--space-3) var(--grid-gutter);
  align-items: start;
}
.stat-lead-notes:has(> .stat-lead-note:nth-child(3)) {
  grid-template-columns: minmax(0, 1fr);
}

/* ── THE ONE SHAPE THAT WANTS A FULL-WIDTH NOTES ROW ──────
   countries.html: three notes AND only three support figures.

   Its headline is a range — figure, track and two names, 88px — and its
   support column is a single row of three, 113px. The two sides of row
   one are within 25px of each other, so there is no hole to avoid, and
   the notes are better off spread across three columns of the full width
   than stacked down a half-width one.

   The gate used to be the note count alone, and that was the wrong
   variable. global.html also carries three notes, but it carries FOUR
   support figures, which the rule below arranges 2x2 — 257px of column
   two against a 60px fraction in column one. Row one took the taller of
   the two and left 197px of nothing under "527 of 1,054". Nothing about
   the note count said so; the figure count did.

   So the exception now asks for both: three notes, and a support column
   that is still one row. Add a fourth figure to countries and it goes
   back to the default of its own accord.

   INSIDE A min-width QUERY, not left to the cascade. These selectors are
   two and three classes deep; the single-column stack at the bottom of
   this block is written at `.stat-lead-support`, one class, and would
   lose to them. Countries proved it — support took grid-row: 1 on a
   phone and rendered ABOVE the headline it supports, with its notes
   still in three 229px columns. A rule about the two-column arrangement
   has no business existing at a width that has one column, so it does
   not. */
@media (min-width: 861px) {
  .stat-lead:has(.stat-lead-notes > .stat-lead-note:nth-child(3)):not(:has(.stat-lead-support .kpi:nth-child(4))) .stat-lead-notes {
    grid-column: 1 / -1;
    grid-template-columns: repeat(3, minmax(0, 1fr));
  }
  .stat-lead:has(.stat-lead-notes > .stat-lead-note:nth-child(3)):not(:has(.stat-lead-support .kpi:nth-child(4))) .stat-lead-support {
    grid-row: 1;
  }
}
/* The column is the measure now — --measure-note would re-narrow it to
   330px and put the gap straight back. */
.stat-lead-notes .stat-lead-note { max-width: none; margin-top: 0; }
@media (max-width: 860px) {
  .stat-lead-primary, .stat-lead-support, .stat-lead-notes,
  .stat-lead > .ci-highlights {
    grid-column: 1 / -1; grid-row: auto;
  }
  .stat-lead-notes,
  .stat-lead-notes:has(> .stat-lead-note:nth-child(3)) {
    grid-template-columns: minmax(0, 1fr);
  }
}
.stat-lead-primary .label {
  color: var(--muted);
  font-size: var(--text-xs);
  font-weight: 700;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  margin-bottom: var(--space-2);
}
/* Sized by --figure-size, shared with the support figures beside it.
   This used to run to 68px, which made it the loudest thing under the
   h1 — but it sat in a row with 24px support values under identical
   12px caps labels, and the row read as inconsistent rather than
   ranked. It still leads: it is first, it is in the display face, and
   its label names the reading. It is no longer louder.

   THE FACE IS THE MONO FACE, not --font-display at 900. The token said
   32px on both sides of the row and the two figures still did not match:
   Lato Black's digits are taller and far heavier than JetBrains Mono's
   at the same font-size, so "48.1%" out-drew "$2.94/hr" beside it even
   though neither rule had changed. The system fact is that figures are
   set in mono with tabular-nums; this was the one figure that was not,
   and the mismatch was the whole of the difference. */
.stat-lead-figure {
  font-family: var(--font-mono);
  font-weight: 700;
  font-size: var(--figure-size);
  line-height: var(--figure-leading);
  letter-spacing: -0.02em;
  color: var(--text);
  font-variant-numeric: tabular-nums;
}
.stat-lead-figure .unit {
  font-family: var(--font-body);
  font-weight: 700;
  font-size: var(--figure-unit);
  color: var(--muted);
  letter-spacing: 0.02em;
  /* em, not --space-2: 8px read as a considered gap beside a 68px
     figure and as a detached glyph beside a 32px one. */
  margin-left: 0.1em;
  /* BASELINE, not super. A raised glyph reads as a taller figure: the
     "%" sat above the digits' cap line while "/hr" beside it sat on the
     baseline, so the two figures looked like different sizes even once
     they shared a face and a font-size. The unit is already subordinate
     at 0.45em and --muted; it does not also need lifting. */
  vertical-align: baseline;
}
/* global.html's headline is a fraction, not a percentage — deliberately
   a different shape from the two pages that do lead with a percentage,
   so no reader mistakes 527-of-1,054 for a reading they can set beside
   61.1%. The denominator sits on the baseline at a third the size; the
   .unit treatment (superscript) is for a unit, and "of 1,054" is not
   one. */
.stat-lead-figure--fraction { white-space: nowrap; }
.stat-lead-figure--fraction .denom {
  font-family: var(--font-body);
  font-weight: 700;
  /* Not --figure-unit: "of 1,054" is a phrase, not a unit glyph, and
     0.45em would put it at 14.4px. 0.5em holds it at 16px, still
     clearly subordinate to the numerator. */
  font-size: 0.5em;
  letter-spacing: 0;
  color: var(--muted);
  margin-left: var(--space-3);
}

.stat-lead-note {
  margin: var(--space-3) 0 0;
  max-width: var(--measure-note);
  color: var(--muted);
  font-size: var(--text-sm);
}
/* ── Range headline (countries.html) ─────────────────────
   A country rollup is a spread, not a point, so its headline is drawn
   as one. Two named ends with a ramp between them, running scarce →
   plenty in the same direction as every availability legend here.

   The ends now take --figure-size, the same as every other figure on
   the page. They used to sit at 48px against 24px support values —
   exactly 2x, which reads as one element arbitrarily scaled rather
   than two ranks. Two ends at a shared size no longer out-shout the
   h1 either, so the reason they were held down is gone.

   The track carries the continuous scale that the removed twin bar
   charts used to imply with two flat blocks of green and red. */
.stat-lead-range {
  display: grid;
  grid-template-columns: auto minmax(40px, 1fr) auto;
  align-items: center;
  gap: var(--space-4);
  max-width: 34rem;
}
.range-end { min-width: 0; }
.range-end--high { text-align: right; }
/* Below its figure now, so the 2px that separated it from the number
   underneath became 2px of nothing above one. */
.range-name {
  color: var(--text-soft);
  font-size: var(--text-sm);
  font-weight: 700;
  margin-top: var(--space-2);
  white-space: nowrap;
}
.range-figure {
  font-family: var(--font-display);
  font-weight: 900;
  font-size: var(--figure-size);
  line-height: var(--figure-leading);
  letter-spacing: -0.03em;
  color: var(--text);
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
}
.range-figure .unit {
  font-family: var(--font-body);
  font-weight: 700;
  font-size: var(--figure-unit);
  color: var(--muted);
  margin-left: 0.1em;
  vertical-align: super;
}
/* Painted from AVAIL_STOPS at runtime so it cannot drift from the ramp
   the map, the bars and the table cells all use. Unbordered, like every
   other ramp on the site — see .dist-swatch--ramp in chart.css. */
.range-track {
  height: 8px;
  border-radius: var(--radius-pill);
}
@media (max-width: 560px) {
  .stat-lead-range { grid-template-columns: 1fr; gap: var(--space-3); }
  .range-end--high { text-align: left; }
  .range-track { width: 100%; }
}

/* Supporting figures: tight pair, no card chrome — they are context for
   the lead, not four more things to weigh against it. */
/* A GRID OF THREE, not a flex row that wraps.

   flex-wrap sized each figure by its own content and then stretched
   whatever fell to the second row: cities came out 120 / 120 / 373 as
   2+1, and global 203 / 120 / 120 / 381 as 3+1 — unequal even within
   the first row. Three or four peer figures rendered at four different
   widths read as four different levels of importance, and the wrapped
   one, being widest, read as the most important.

   Equal columns say what is true: these are peers, and the reader can
   scan down them. */
.stat-lead-support {
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: var(--space-5) var(--grid-gutter);
  padding-bottom: var(--space-2);
  align-content: start;
}
/* FOUR FIGURES BALANCE AS 2x2, NOT 3+1.

   cities and countries carry three support figures; global carries four
   (it gained the citywide average when the ceiling fraction took over
   the headline). A fixed three-column grid fixes cities and hands
   global a new raggedness — three across and one below with two empty
   cells — which is the same fault in a different arrangement.

   :has() lets the column count follow the item count, so a page that
   later gains or loses a figure re-balances itself instead of waiting
   for someone to notice. */
.stat-lead-support:has(.kpi:nth-child(4)) {
  grid-template-columns: repeat(2, minmax(0, 1fr));
}

.stat-lead-support .kpi {
  padding: 0;
  border: 0;
  background: none;
  gap: var(--space-2);
  /* min-width: 120px went with the flex row it was there to serve. In a
     grid it would fight minmax(0, 1fr) and force an overflow at narrow
     widths instead of letting the column shrink. */
  min-width: 0;
}
/* No size override here any more — the support values take --figure-size
   from .kpi, the same as the headline beside them. */

@media (max-width: 860px) {
  /* The :has() four-and-eight rule above outranks a bare .stat-lead, so
     the single-column collapse has to name it too or two figures stay
     side by side on a phone. */
  .stat-lead,
  .stat-lead:has(.stat-lead-support .kpi:nth-child(2):last-child) {
    grid-template-columns: 1fr;
  }
  .stat-lead-support { padding-bottom: 0; }
}
/* Two across on a phone. A fixed three-column grid held at 390px — it
   fits, at 98px a column — but "AVG CITYWIDE AVAILABILITY" over a mono
   figure does not read at that width. The flex row this replaced used
   to stack implicitly here; a grid has to be told. */
@media (max-width: 620px) {
  .stat-lead-support,
  .stat-lead-support:has(.kpi:nth-child(4)) {
    grid-template-columns: repeat(2, minmax(0, 1fr));
  }
}

/* ── Loading state ──────────────────────────────────────────
   These figures used to render "—" until the fetch resolved, which
   is the exact glyph this site defines as "we could not measure
   this". On a slow connection the scorecard looked like it had
   measured nothing. A shimmer says "not yet" instead of "not ever".

   The placeholder is sized in `ch` off the figure's own font, so it
   reserves the right space and the real number drops in without
   shifting the page. Matches .nav-search.is-skeleton. */
[data-loading='true'] .stat-lead-figure,
[data-loading='true'] .sp-hero-figure-value,
[data-loading='true'] .kpi .value,
[data-loading='true'].meta strong {
  color: transparent;
  position: relative;
}
[data-loading='true'] .stat-lead-figure::after,
[data-loading='true'] .sp-hero-figure-value::after,
[data-loading='true'] .kpi .value::after,
[data-loading='true'].meta strong::after {
  content: '';
  position: absolute;
  inset: 0.12em 0 0.18em 0;
  width: 3.2ch;
  border-radius: var(--radius-sm);
  background: linear-gradient(90deg,
    var(--surface-1) 0%, var(--shimmer) 50%, var(--surface-1) 100%);
  background-size: 200% 100%;
}
/* The hero counts sit in running text, so their placeholder has to hold
   its own width rather than collapse and drag the words after it. */
[data-loading='true'].meta strong {
  display: inline-block;
  min-width: 3.2ch;
}
[data-loading='true'] .stat-lead-figure .unit,
[data-loading='true'] .sp-hero-figure-value .sp-unit,
[data-loading='true'] .kpi .value .unit { visibility: hidden; }
/* The coverage note is empty until the data says how many places were
   measured. Two lines is what it runs to at every breakpoint we ship, so
   reserving them keeps the supporting figures from jumping. */
[data-loading='true'] .stat-lead-note {
  visibility: hidden;
  min-height: 3.1em;
}

@media (prefers-reduced-motion: no-preference) {
  [data-loading='true'] .stat-lead-figure::after,
  [data-loading='true'] .sp-hero-figure-value::after,
  [data-loading='true'] .kpi .value::after,
  [data-loading='true'].meta strong::after {
    animation: figure-shimmer 1.4s ease-in-out infinite;
  }
}
@keyframes figure-shimmer {
  from { background-position: 200% 0; }
  to   { background-position: -200% 0; }
}

/* A CAPTION IS A SUBTITLE, SO IT SITS UNDER ITS HEADING.

   This was `display: flex; justify-content: space-between`, which put
   the caption hard against the right edge and made the gap between a
   heading and its own explanation a function of how long the two
   strings happened to be:

     Time period                     1121px
     City leaderboard                 805px
     The two ends                     691px
     Neighborhood leaderboard         392px
     How evenly is parking spread?    212px

   Five headings, five different relationships to their own subtitle,
   none of them chosen. At 1121px the caption is on the far side of the
   viewport from the thing it describes, so the reader has to hold the
   heading in memory while crossing the page to find out what it means —
   and at 200% zoom the two are never on screen together at all.

   Stacked, every caption starts on the same left edge as its heading
   and is capped at the standard text measure, so the pair reads as one
   unit on every section of every page.

   The one exception is .ps-lede below, and it is not an exception to
   the rule so much as a different case: that row is a control LABEL
   beside a live READOUT, not a heading beside its subtitle. */
.rp-section-title {
  display: block;
  margin: 0 0 var(--space-4);
}
.rp-section-title h2 { margin: 0; }
.rp-section-title h2 {
  font-family: var(--font-body);
  font-weight: 900;
  font-size: clamp(20px, 2vw, 28px);
  letter-spacing: -0.01em;
}
.rp-section-title .caption {
  display: block;
  margin-top: var(--space-2);
  max-width: var(--measure-text);
  color: var(--muted);
  font-size: var(--text-sm);
  line-height: 1.5;
}

/* A LONG DEK FILLS THE CARD, in one column.

   Most section deks are a line or two and sit fine in the 5-column
   measure above. A few have grown past three lines — the city map's,
   which now names the neighborhood coverage floor and the coverage area
   as well as the ramp, and the curb composition's — and at that length a
   566px block under a 1392px card stops reading as a comfortable
   measure and starts reading as a layout fault: three quarters of a
   wide card left empty by a paragraph that visibly could have filled it.

   This was briefly two columns of --measure-text, which held the line
   length the page uses everywhere else. It was worse. A dek is one short
   run of prose introducing the thing below it, and splitting two lines
   into two balanced columns asks the reader to finish at the bottom of
   the left column and jump back up to the right for a sentence and a
   half — newspaper machinery for a paragraph far too small to earn it.
   Reading straight across beats a shorter line here.

   So: no cap, the same call .rp-tablenote makes further down this file
   and for the same reason — these are captions on the content below
   them, not body copy, and they are kept short enough that the card's
   width is fine. The constraint moves from the CSS to the copy: if one
   of these grows past about three lines at desktop width, cut it rather
   than reaching for a measure again.

   It does NOT reintroduce the side-by-side heading this file retired
   above. The dek still starts on its heading's left edge, directly
   under it, and is still read as one unit with it. */
.rp-section-title .caption--wide { max-width: none; }

/* The settings-bar row: a small-caps control label on the left, the live
   period readout on the right. Both are peers on a control strip, so
   here the split IS the design rather than a side effect of string
   length — and the readout has its own class so it cannot inherit the
   subtitle treatment above. */
.ps-lede .rp-section-title {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  flex-wrap: wrap;
  gap: var(--space-3);
}
.ps-readout {
  color: var(--muted);
  font-size: var(--text-sm);
  text-align: right;
}

/* ── Sortable table ─────────────────────────────────────── */
/* A ranking section used to be .rp-card (border, radius, gradient,
   padding) wrapping .rp-table-wrap (border, radius, scroll) wrapping the
   table. Two rounded boxes, one inside the other, four pixels apart, for
   one table — and the outer one's padding narrowed the columns of the
   widest component on the site.

   .rp-card--table drops the outer chrome and keeps the heading. The
   scroller is the surface; the card was decoration around it. */
.rp-card--table {
  border: 0;
  border-radius: 0;
  background: none;
  padding: 0;
}

.rp-table-wrap {
  max-height: 640px;
  overflow: auto;
  border: 1px solid var(--line);
  border-radius: var(--radius-md);
}
.rp-table {
  width: 100%;
  border-collapse: collapse;
  font-size: var(--text-sm);
}
.rp-table th,
.rp-table td {
  padding: var(--space-2) var(--space-3);
  border-bottom: 1px solid var(--line);
  text-align: right;
  white-space: nowrap;
  /* ── DECLARED BECAUSE A HOST THEME DECLARES IT ─────────────
     The size above is on the TABLE, and cells took it by
     inheritance — which is not a declaration, and so loses to any
     stylesheet that names the cell. inrix.com's theme ships
     `table td { font-size: .75em }`, and that one rule set the body
     copy of both ranking pages to 9.75px on staging: not a
     preference, a WCAG failure, and on the page that carries 1,418
     rows of the thing this site exists to publish.

     `inherit` rather than a repeat of --text-sm, so the variants
     below that change the table's size keep working. */
  font-size: inherit;
}
/* The same theme draws its own hairline on `tbody tr`. Ours is on the
   cell, one rule up, so the two do not overwrite each other — they
   stack, and a light-theme grey lands under every row of the dark
   one. Declaring the row's border empty is what removes it. */
.rp-table tbody tr { border-bottom: 0; }
.rp-table th:nth-child(1),
.rp-table td:nth-child(1),
.rp-table th:nth-child(2),
.rp-table td:nth-child(2) { text-align: left; }
.rp-table thead th {
  position: sticky;
  top: 0;
  z-index: var(--z-raised);
  background: var(--surface-1);
  color: var(--muted);
  font-family: var(--font-body);
  font-weight: 700;
  font-size: var(--text-xs);
  letter-spacing: 0.1em;
  text-transform: uppercase;
  cursor: pointer;
  user-select: none;
}
/* The row counter's header is the one cell in a thead that does not
   sort, so it does not offer to: no pointer, no hover colour, nothing
   that reads as a control. wireTableSort() skips it because it carries
   no data-sort, which is also what keeps a sort arrow off it. */
.rp-table thead th.rownum {
  cursor: default;
  user-select: auto;
}
/* The sort control inside the header cell — built by wireTableSort()
   in js/leaderboard.js, so it exists on every sortable column and
   nowhere else. Transparent and inheriting: the cell keeps its own
   typography, the button supplies the tab stop, the Enter/Space
   handling and the focus ring the bare <th> could never have. */
.rp-sort-btn {
  font: inherit;
  letter-spacing: inherit;
  text-transform: inherit;
  color: inherit;
  background: none;
  border: 0;
  padding: 0;
  margin: 0;
  cursor: pointer;
  /* Inherit the cell's alignment: most columns are right-aligned
     figures, the first two are left. */
  text-align: inherit;
  width: 100%;
}
.rp-sort-btn:focus-visible {
  outline: 2px solid var(--primary);
  outline-offset: 2px;
  border-radius: var(--radius-sm);
}
.rp-table thead th:hover,
.rp-sort-btn:hover { color: var(--primary-bright); }
.rp-table thead th.rownum:hover { color: var(--muted); }
/* The arrow marks the sorted column and sits AFTER the label, on the
   same line. It hangs off the button rather than off the <th> because
   the button is `width: 100%`: a ::after on the cell lands past the
   button's full-width box and drops to a second line under the header,
   which reads as a mark on the column below it rather than on the
   label. Inside the button it trails the words and follows the cell's
   own alignment — left of the gap on name columns, hard right on the
   figures. */
.rp-table thead th[aria-sort="ascending"] .rp-sort-btn::after  { content: " ▲"; color: var(--primary); }
.rp-table thead th[aria-sort="descending"] .rp-sort-btn::after { content: " ▼"; color: var(--primary); }
/* The row counter. Recessed like the rank column it replaced: it is
   where a row sits in the table as sorted, not a measurement, and it
   must never compete with the reading two columns over. Wide enough for
   four digits, because the neighborhood pool runs to 2,073 rows. */
.rp-table td.rownum,
.rp-table th.rownum { color: var(--muted); width: 52px; }
.rp-table td.city-name { color: var(--text); font-weight: 700; }
/* The state abbreviation after a US or Canadian city. Quiet and
   non-bold: it disambiguates the name, it is not part of it. */
.rp-table td.city-name .city-admin1 {
  margin-left: 0.4em;
  color: var(--muted);
  font-weight: 500;
  font-size: var(--text-xs);
  letter-spacing: 0.04em;
}
/* ── Every link in a ranking table, one treatment ─────────────
   The name column used to inherit --text with no underline and declare
   itself only on hover, which left a whole column of bold black names
   that read as labels. A reader scanning the table had no way to know
   the rows led anywhere without sweeping the pointer across them, and a
   touch reader never gets a hover at all.
   So it now carries the site's own link signal at rest — the --primary
   blue and the 3px-offset underline from the `a` rule in base.css —
   rather than a bespoke one. Bold weight keeps the name the row's
   subject; the colour and underline say it goes somewhere. Hover and
   focus then intensify what is already there (--primary-bright, a
   thicker rule) instead of introducing it.

   SCOPED TO .city-link, NOT TO THE NAME CELL. It used to be written as
   `td.city-name .city-link`, so the other links a row carries — the
   country beside the city, a country's top city — fell through to the
   generic `a` in base.css: a brighter blue underlined in
   --primary-soft, which is a 12% background tint and renders as nothing
   at this size. That put two different link treatments in one row, one
   of them not reading as a link at all. One rule, every cell. */
.rp-table .city-link {
  color: var(--primary);
  text-decoration: underline;
  /* base.css underlines links in --primary-soft, but that token is a 12%
     background tint: under a name in a dense table it renders as nothing.
     A fraction of the link's own colour is the same idea at a strength
     that actually reads, and it tracks the hover colour for free. */
  text-decoration-color: color-mix(in srgb, currentColor 45%, transparent);
  text-decoration-thickness: 1px;
  text-underline-offset: 3px;
  /* A slightly taller pointer/tap target than the text's own line box,
     pulled back out with a negative margin so row height is unchanged. */
  padding-block: 3px;
  margin-block: -3px;
  transition: color .15s ease, text-decoration-color .15s ease,
              text-decoration-thickness .15s ease;
}
/* The name cell alone lays its link out as a flex line: it is the one
   that carries a trailing chip beside the name. */
.rp-table td.city-name .city-link {
  display: inline-flex;
  align-items: baseline;
  gap: var(--space-2);
}
.rp-table .city-link:hover,
.rp-table .city-link:focus-visible {
  color: var(--primary-bright);
  text-decoration-color: currentColor;
  text-decoration-thickness: 2px;
  outline: none;
}
.rp-table .city-link:focus-visible {
  outline: 2px solid var(--primary);
  outline-offset: 3px;
  border-radius: 2px;
}
@media (prefers-reduced-motion: reduce) {
  .rp-table .city-link { transition: none; }
}
.rp-table td .muted { color: var(--muted); }
/* The row tint is a scanning aid, not a claim that the row is clickable —
   only the name is. It stays subtle for that reason, and the link's own
   hover is what confirms the target. */
.rp-table tbody tr:hover { background: var(--primary-soft); }
/* Hovering anywhere on the row promotes the name, so the reader who
   swept a finger down the rank column still learns where the link is. */
.rp-table tbody tr:hover .city-link { text-decoration-color: currentColor; }

/* Long country / region names would otherwise force horizontal scroll at the
   page's max width. Let the name columns wrap; keep numeric and badge columns
   single-line so figures stay legible. */
.rp-table--wrap { table-layout: auto; }
.rp-table--wrap th,
.rp-table--wrap td { padding: var(--space-2); }
.rp-table--wrap th:nth-child(2),
.rp-table--wrap td:nth-child(2),
.rp-table--wrap th:nth-child(3),
.rp-table--wrap td:nth-child(3) {
  white-space: normal;
  word-break: break-word;
  hyphens: auto;
}
.rp-table--wrap td:nth-child(2) { min-width: 220px; }
.rp-table--wrap td.city-name .city-link { display: inline; }

/* ── Unmeasured rows ────────────────────────────────────────
   A city with no measurement for the selected period. Recessed, but
   still present and still readable — it is a real city we could not
   measure, not an error and not a zero. */
.rp-table tbody tr.is-unmeasured td { color: var(--muted); }
.rp-table tbody tr.is-unmeasured td.city-name { font-weight: 400; }
/* The measurement is missing, not the page behind the link — so the row
   recedes but the link keeps its full colour and underline. */
.rp-table tbody tr.is-unmeasured .city-link { color: var(--primary); }

/* ── The reader's own city ──────────────────────────────────
   Set on the overview and carried here by js/saved-city.js. The row
   keeps its true rank; this marks it where it sits.

   THE TINT IS NOT THE MESSAGE. Every marked row also carries the
   words "Your city" (.your-city-tag below), because a reader who
   cannot separate this wash from the row above it still has to be
   able to find it — and because the availability ramp already spends
   this table's colour budget. The wash is a find-me cue, not an
   encoding.

   Painted with box-shadow rather than a border: a border on the first
   cell would widen the # column by 3px and shunt every other column
   along, and that reflow lands on a table whose rows stream in. It is
   inset on the cells rather than the row because a <tr> cannot carry
   a background in every engine once the cells have their own.

   The state combines with .is-unmeasured on purpose. The reader's
   city may well be one with no reading, and that row has to say both
   things at once rather than pick one. */
.rp-table tbody tr.is-your-city > td {
  background: color-mix(in srgb, var(--gold) 12%, transparent);
}
.rp-table tbody tr.is-your-city > td:first-child {
  box-shadow: inset 3px 0 0 0 var(--gold);
}
/* The row takes programmatic focus when the jump control fires, so it
   has to show it landing. -2px keeps the ring inside the row rather
   than under the one above. */
.rp-table tbody tr.is-your-city:focus-visible {
  outline: 2px solid var(--primary-bright);
  outline-offset: -2px;
}

/* The words that do the work the tint cannot. Gold draws the frame;
   the text is --text, because --gold-text is the brand gold on both
   themes and reads 1.63:1 on this page (see tokens.css) — fine for a
   1px frame, nowhere near AA for a word. */
.your-city-tag {
  display: inline-block;
  margin-left: 0.5em;
  padding: 0 var(--space-2);
  font-size: var(--text-xs);
  font-weight: 700;
  letter-spacing: 0.04em;
  white-space: nowrap;
  vertical-align: middle;
  color: var(--text);
  background: color-mix(in srgb, var(--gold) 16%, transparent);
  border: 1px solid var(--gold);
  border-radius: var(--radius-pill);
}

/* The controls that sit under a filter box: "Jump to Boston" over the
   city table, "Show only Boston" over the pool. Full-width inside the
   flex filter row so they start a line of their own rather than
   crowding the count. */
.rp-filter-row .rp-yourcity {
  flex: 1 0 100%;
  display: flex;
  align-items: baseline;
  flex-wrap: wrap;
  gap: var(--space-1);
  margin: 0;
  font-size: var(--text-sm);
}
/* `display` on the class beats the UA rule for the attribute — same
   trap as .rp-filter-row above. Both controls spend most of their life
   hidden, so this is the common case, not the edge one. */
.rp-filter-row .rp-yourcity[hidden] { display: none; }

/* ── Neighborhood leaderboard ─────────────────────
   The neighborhood is the row's subject and the city is its address, so
   the name carries the weight and the city link stays secondary. This
   inverts the city table, where the city name IS the link. */
.rp-table td.hood-name { color: var(--text); font-weight: 700; }
.rp-table tbody tr.is-unmeasured td.hood-name { font-weight: 400; }

/* Twelve columns — more than any other table here — so the widths come from
   a <colgroup> rather than from content. Two reasons, and only the second
   is about speed: under auto layout the City column collapsed to 33px and
   broke "Los Angeles" one letter per line, and the rows stream in over
   several frames, which under auto layout resizes every column as they
   arrive. */
.rp-table--hoods {
  table-layout: fixed;
  /* Twelve columns will not fit a phone, and letting fixed layout divide a
     360px viewport by twelve gives 26px columns that clip every figure.
     Below this the wrapper scrolls sideways instead — which it is already
     set up to do, and which keeps the numbers readable. */
  min-width: 1300px;
}
/* The column widths the <colgroup> in cities.html used to carry inline.
   They live here, and they are keyed on POSITION rather than on
   column, because js/leaderboard.js reorders those <col> elements on
   a phone: a width written onto the element travels with its column,
   a width written here stays with its place in the layout, which is
   what the narrow block at the foot of this file needs to override. */
.rp-table--hoods col:nth-child(1) { width: 4rem; }
.rp-table--hoods col:nth-child(2) { width: 14%; }
.rp-table--hoods col:nth-child(3) { width: 11%; }
.rp-table--hoods col:nth-child(4) { width: 10%; }
.rp-table--hoods col:nth-child(5) { width: 6.5%; }
.rp-table--hoods col:nth-child(6) { width: 10.5%; }
.rp-table--hoods col:nth-child(7) { width: 8.5%; }
.rp-table--hoods col:nth-child(8) { width: 8.5%; }
/* On-street : off-street. Wider than either rate beside it because the
   cell prints both halves of the quotient — "$1 : $7.75" — not one
   figure. */
.rp-table--hoods col:nth-child(9) { width: 10%; }
.rp-table--hoods col:nth-child(10) { width: 8.5%; }
.rp-table--hoods col:nth-child(11) { width: 6.5%; }
/* Coverage, the trailing column. The twelve add to 100 so nothing is
   handed back; when js/cities.js removes an optional trailing column it
   removes its <col> with it, and the rest are laid out at their own
   shares of a narrower table. */
.rp-table--hoods col:nth-child(12) { width: 6%; }
.rp-table--hoods th,
.rp-table--hoods td { padding: var(--space-2); }
.rp-table--hoods th:nth-child(2),
.rp-table--hoods td:nth-child(2),
.rp-table--hoods th:nth-child(3),
.rp-table--hoods td:nth-child(3),
.rp-table--hoods th:nth-child(4),
.rp-table--hoods td:nth-child(4),
.rp-table--hoods th:nth-child(5),
.rp-table--hoods td:nth-child(5) {
  text-align: left;
  white-space: normal;
  overflow-wrap: anywhere;
}
.rp-table--hoods td.city-name .city-link { display: inline; }
/* The header labels are long and the columns are narrow; let them wrap
   rather than clip the sort arrow off the end. */
.rp-table--hoods thead th { white-space: normal; vertical-align: bottom; }
/* The "nothing matched" row. Scoped to .rp-table, not to one table:
   both leaderboards on this page filter, and both land here. */
.rp-table td.rp-table-empty {
  text-align: center;
  color: var(--muted);
  padding: var(--space-6) var(--space-4);
  white-space: normal;
}

/* A muted paragraph INSIDE a .rp-card, between the heading and the table.
   Deliberately not .rp-note — that is the hero's bordered caveat block
   and its rules would draw two rules across the middle of a card. The
   other near-misses are also scoped elsewhere: .caption to
   .rp-section-title / .rp-controls, .meta-note to .rankings-hero. */
.rp-tablenote {
  color: var(--muted);
  font-size: var(--text-sm);
  line-height: 1.55;
  /* No max-width. These notes are captions on the table below them, not
     body copy, and they are kept to a couple of lines — at that length the
     60-80 character measure buys nothing and costs alignment: a paragraph
     stopping two thirds of the way across a full-width card reads as a
     layout fault rather than a comfortable measure. Keep them short and
     they stay readable at the card's width; if one grows past three lines,
     cut it rather than reintroducing a cap here. */
  margin: 0 0 var(--space-3);
}
.rp-tablenote strong { color: var(--text); font-weight: 700; }
.rp-tablenote[hidden] { display: none; }
/* Reuses the figure-shimmer placeholder the KPIs use while the edition
   loads. The neighborhood index arrives after first paint, so this line
   is genuinely pending rather than missing, and reserving its height
   stops the table below it from jumping when it fills. */
[data-loading='true'].rp-tablenote {
  color: transparent;
  position: relative;
  max-width: 46ch;
}
[data-loading='true'].rp-tablenote::after {
  content: '';
  position: absolute;
  inset: 0.12em 0 0.18em 0;
  border-radius: var(--radius-sm);
  background: linear-gradient(90deg,
    var(--surface-1) 0%, var(--shimmer) 50%, var(--surface-1) 100%);
  background-size: 200% 100%;
}
@media (prefers-reduced-motion: no-preference) {
  [data-loading='true'].rp-tablenote::after {
    animation: figure-shimmer 1.4s ease-in-out infinite;
  }
}

/* ── Table filter ────────────────────────────────
   A distinct class rather than `.rp-filter input`: that selector was
   removed once for matching nothing, and .rp-filter is a label+pill
   pattern, not a text field. */
.rp-filter-row {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-3);
  margin-bottom: var(--space-3);
}
/* `display` on the class beats the UA rule for the attribute, so a
   component that sets one has to say what [hidden] means. The failure
   path hides this row, and without this it would stay on screen. */
.rp-filter-row[hidden] { display: none; }
.rp-filter-input {
  flex: 1 1 260px;
  max-width: 360px;
  padding: var(--space-2) var(--space-3);
  font: inherit;
  font-size: var(--text-sm);
  color: var(--text);
  background: var(--surface-1);
  border: 1px solid var(--line);
  border-radius: var(--radius-md);
  transition: border-color .15s ease;
}
.rp-filter-input::placeholder { color: var(--muted); }
.rp-filter-input::-webkit-search-cancel-button { -webkit-appearance: none; appearance: none; }
.rp-filter-input:hover { border-color: var(--primary); }
.rp-filter-input:focus-visible {
  outline: 2px solid var(--primary-bright);
  outline-offset: 3px;
  border-color: var(--primary);
}
.rp-filter-row .caption { color: var(--muted); font-size: var(--text-sm); }

/* ── Filter row ─────────────────────────────────────────── */
.rp-controls {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-3) var(--space-5);
  margin-top: var(--space-4);
  padding-top: var(--space-4);
  border-top: 1px solid var(--line);
}
/* The .rp-filter block (label + <select>) lived here. It is gone with
   the last <select> on the site — global.html's Scope control, now the
   same pill group as the region toggle beside it. See .ps-group in
   page-settings.css for what replaced its label.

   Left in place it would be a styled, documented, working component for
   a widget this design system no longer uses, which is how a second
   control vocabulary gets reintroduced. */
.rp-controls .caption { margin: 0; flex: 1 1 320px; }

/* ── Highlights ─────────────────────────────────────────────
   Was four identical cards in a 4-up grid. The two that matter are the
   poles of the ranking, so they now sit adjacent as a genuine
   comparison, at full weight. Value and affordability are real but
   secondary, so they drop to compact rows with no card chrome. */
/* Column count follows the card count, the same way .stat-lead-support
   does. cities.html shows three superlatives; countries.html shows only
   affordability, because its two poles are already named in the range
   headline at the top of the page. Two columns for a pair, three for a
   trio, and a lone card spans the row rather than sitting in column one
   with an empty cell beside it. */
.ci-highlights {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: var(--space-3) var(--grid-gutter);
  align-items: stretch;
}
.ci-highlights:has(.ci-highlight:nth-child(3)) {
  grid-template-columns: repeat(3, minmax(0, 1fr));
}
.ci-highlights:not(:has(.ci-highlight:nth-child(2))) .ci-highlight { grid-column: 1 / -1; }
.ci-highlight {
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
  padding: var(--space-5);
  border: 1px solid var(--line);
  border-radius: var(--radius-md);
  color: var(--text);
  text-decoration: none;
  transition: border-color var(--dur-fast) var(--ease-out),
              transform var(--dur-fast) var(--ease-out);
}
.ci-highlight:hover { border-color: var(--primary); transform: translateY(-2px); }
/* The lift GOES, rather than being made instant. Unlike the button's
   press, this is not feedback that something registered — it is
   decoration on a hover that already says everything it needs to say
   with border-color. An instant 2px jump under the cursor would be a
   worse artefact than either the animation or its absence. Same shape
   as .ci-highlight--static below, for the same reason. */
@media (prefers-reduced-motion: reduce) {
  .ci-highlight { transition: border-color var(--dur-fast) var(--ease-out); }
  .ci-highlight:hover { transform: none; }
}
.ci-highlight:focus-visible { outline: 2px solid var(--primary-bright); outline-offset: 3px; }
/* The card as a PLAIN BLOCK, used on countries.html since the
   per-country page it linked to was removed.

   Only the hover is withdrawn, and that is the whole point of the
   modifier: lift-and-light under the cursor is a promise of a click,
   and a <div> cannot keep it. Everything else the card is — the border,
   the padding, the type ladder — belongs to the card rather than to its
   interactivity, so none of it is restated here.

   No :focus-visible rule to remove either. A <div> is not focusable, so
   the rule above simply never matches it. */
.ci-highlight--static:hover { border-color: var(--line); transform: none; }
.ci-highlight-title {
  color: var(--muted);
  font-size: var(--text-xs);
  font-weight: 700;
  letter-spacing: 0.12em;
  text-transform: uppercase;
}
/* Takes --figure-size so a highlight card's city name cannot outrank the
   headline reading. At 34px against a 68px headline it was clearly
   subordinate; against 32px it became the largest thing in the content
   column, which inverted the page. Matching is better than 34-vs-32,
   which reads as a mistake rather than a rank. */
.ci-highlight-city {
  font-family: var(--font-display);
  font-weight: 900;
  font-size: var(--figure-size);
  line-height: 1.02;
  letter-spacing: -0.02em;
}
.ci-highlight-meta { color: var(--muted); font-size: var(--text-sm); }

/* Secondary pair: same information architecture, a third of the weight. */
/* FULL WIDTH ON ITS OWN ROW. The --sub treatment is deliberate — the
   two poles of the ranking are cards, and affordability is a different
   axis, so it gets a ruled line rather than a third card. What was not
   deliberate was where it landed: as the third item in a two-column
   grid it took one cell and left a 677px hole beside it, so a
   considered piece of hierarchy read as a card that failed to load.

   Spanning both columns makes the subordination legible — its top rule
   now runs the full width of the section and reads as a divider under
   the pair rather than a half-width stub. It also fixes countries.html,
   where this is the ONLY item left after the pole cards were removed
   and it was sitting in column one with column two empty. */
.ci-highlight--sub {
  grid-column: 1 / -1;
  flex-direction: row;
  align-items: baseline;
  flex-wrap: wrap;
  gap: var(--space-2) var(--space-3);
  padding: var(--space-3) 0 0;
  border: 0;
  border-top: 1px solid var(--line);
  border-radius: 0;
}
.ci-highlight--sub:hover { border-color: var(--line); transform: none; }
.ci-highlight--sub:hover .ci-highlight-city { color: var(--primary); }
.ci-highlight--sub .ci-highlight-city {
  font-size: var(--text-lg);
  font-weight: 700;
  font-family: var(--font-body);
  letter-spacing: 0;
}
.ci-highlight--sub .ci-highlight-title { flex: 0 0 100%; }

/* The :has() rule above outranks a bare .ci-highlights here, so the
   breakpoint has to repeat the selector — three 98px cards on a phone
   otherwise. Same trap as #cp-charts-grid: a higher-specificity rule
   does not inherit the base component's responsive behaviour. */
@media (max-width: 900px) {
  .ci-highlights,
  .ci-highlights:has(.ci-highlight:nth-child(3)) {
    grid-template-columns: repeat(2, minmax(0, 1fr));
  }
}
@media (max-width: 620px) {
  .ci-highlights,
  .ci-highlights:has(.ci-highlight:nth-child(3)) {
    grid-template-columns: minmax(0, 1fr);
  }
}

/* Search landed on a row that has no detail page to navigate to. */
@keyframes rp-flash {
  from { background: var(--gold-soft); }
  to { background: transparent; }
}
.rp-table tbody tr.is-flash { animation: rp-flash 1.6s var(--ease-out) both; }
@media (prefers-reduced-motion: reduce) {
  .rp-table tbody tr.is-flash { animation: none; background: var(--gold-soft); }
}

/* ── Chart hosts ────────────────────────────────────────── */
.rp-grid-2 {
  display: grid;
  grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
  gap: var(--grid-gutter);
}
.rp-card--full { grid-column: 1 / -1; }
@media (max-width: 980px) {
  .rp-grid-2 { grid-template-columns: 1fr; }
}
.rp-plot { width: 100%; height: 480px; }
.rp-plot--tall { width: 100%; height: 880px; }

/* ── Methodology block ──────────────────────────────────── */
/* "Missing data is a finding, shown at full volume" is principle 2, and
   this block is where the site says what the numbers do and do not mean
   — that availability is not the share of the curb that is empty, that
   cores and whole cities are not comparable, that a "—" is a thing we
   did not measure. It was set at 13px in --muted, the smallest and
   palest type on the page, at the very bottom. The caveats were being
   published at a volume that invited skipping them.

   15px in --text-soft is the same size as the page's other running
   prose. It is still quieter than a finding, which is right: it is not
   a finding. It is no longer a footnote. */
.rp-methodology { color: var(--text-soft); font-size: var(--text-md); line-height: 1.65; }
.rp-methodology p { margin: 0 0 var(--space-4); max-width: var(--measure-text); }
.rp-methodology p strong { color: var(--text); font-weight: 700; }
/* Was var(--text-base) — a token that does not exist, so this resolved
   to nothing and the heading inherited the paragraph's size. "About
   these metrics" and the prose under it were typographically identical. */
.rp-methodology h3 {
  font-family: var(--font-body);
  font-size: var(--text-lg);
  font-weight: 900;
  color: var(--text);
  margin: var(--space-5) 0 var(--space-3);
  max-width: var(--measure-text);
}
.rp-methodology h3:first-of-type { margin-top: 0; }
.rp-methodology ul {
  margin: 0 0 var(--space-4);
  padding-left: 1.25em;
  max-width: var(--measure-text);
}
.rp-methodology li { margin: 0 0 var(--space-1); }
.rp-methodology li strong { color: var(--text); font-weight: 700; }


/* ── Where to go next ────────────────────────────────────
   Every page on this site used to end the same way: a block of fine
   print about how the metrics are defined, then the footer. That is a
   correct place to stop reading and a bad place to leave a reader — the
   last thing a journalist saw was a caveat, with no route to the page
   that answers the question the caveat raises.

   Deliberately not cards. Three cards would be the fourth grid of
   equal boxes on the page, and these are links, not findings. A ruled
   band with a heading and a short list reads as what it is: the end of
   this page, and the doors out of it. */
.rp-next {
  border-top: 1px solid var(--line);
  padding-top: var(--space-5);
}
.rp-next-label {
  color: var(--muted);
  font-size: var(--text-xs);
  font-weight: 700;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  margin: 0 0 var(--space-4);
}
.rp-next-list {
  list-style: none;
  margin: 0;
  padding: 0;
  display: grid;
  /* Three equal columns on the grid rather than auto-fit at a 90ch cap.
     auto-fit sized the band by its own content and stopped at x=859,
     which matched nothing; thirds of twelve are four columns each and
     land on 24 / 491 / 958 — the same verticals the leaderboard headers
     above already use. */
  grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: var(--space-4) var(--grid-gutter);
}
@media (max-width: 860px) {
  .rp-next-list { grid-template-columns: minmax(0, 1fr); }
}
.rp-next-list a {
  display: inline-block;
  font-size: var(--text-lg);
  font-weight: 700;
  color: var(--primary);
  text-decoration: none;
}
.rp-next-list a:hover,
.rp-next-list a:focus-visible { text-decoration: underline; }
.rp-next-list p {
  margin: 4px 0 0;
  color: var(--muted);
  font-size: var(--text-sm);
  line-height: 1.5;
}

/* ── The search in the third column ─────────────────────────
   Both pages close with it (nav-search.css owns the input and its
   popup; this only fits the one into the other). It was a panel of its
   own above this band on the city page until the 2026-10-05 review —
   reading width, its own eyebrow, its own h2, its own dek — which
   printed a control at the size of a section and said the page had one
   more thing to say when what it had was one more way out. The grid
   was already three columns carrying two links, so the search took the
   empty slot and shrank to a title, a sentence and the input, at the
   size of its two neighbours.

   It lives here rather than in state-page.css because it is now the
   city page's and the ranking page's alike, and this file is the one
   that owns the grid both of them put it in — state-page.css is not
   loaded on cities.html at all.

   The column is otherwise NOT re-styled: the rules above already set
   the title and dek type, and the input is the ranking hero's own
   560px-capped control, which simply fits a narrower column. */
.rp-next-list .rp-next-find-title {
  /* Typed as its sibling links, coloured as text: it heads a control,
     and a reader should not have to click it to find out it is not a
     link. */
  margin: 0;
  font-size: var(--text-lg);
  font-weight: 700;
  color: var(--text);
}
/* The component's own label would print a second title two lines under
   the first. Hidden to the eye only — it is still the input's
   accessible name, and the skeleton's stand-in goes with it. */
.rp-next-find .nav-search__label,
.rp-next-find .nav-search__skeleton-label {
  position: absolute;
  width: 1px;
  height: 1px;
  margin: -1px;
  padding: 0;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
}
/* The default top margin is tuned to a hero, not to a short column. */
.rp-next-find .nav-search,
.rp-next-find .nav-search.is-skeleton { margin: var(--space-3) 0 0; }

/* ── Bounded chart ───────────────────────────────────────
   REMOVED 2026-09-23 (UAT) with the spread chart it bounded. It capped
   the chart at 700px with its own scroll because at 121 rows the plot
   ran ~1,960px and was 30% of the whole page — a secondary analysis
   outweighing the ranking it existed to support. Nothing on this page
   uses .rp-chart-wrap now; the page has no chart at all. Kept as a
   note because the same cap is the right answer if a tall chart ever
   returns: bound it like the leaderboards, do not hide rows. */

/* ── Row cap notice ──────────────────────────────────────
   Partner to the `cap` option in renderRowsChunked. Only ever visible
   on phones, where the table loses its own scroll box (see the media
   block below) and an uncapped 1,808-row pool would lay out ~71,000px
   of page. role="status" so the count is announced rather than found. */
.rp-row-cap[hidden] { display: none; }
.rp-row-cap {
  margin: var(--space-3) 0 0;
  color: var(--muted);
  font-size: var(--text-sm);
}
.rp-row-cap strong { color: var(--text); font-variant-numeric: tabular-nums; }
.rp-row-cap-btn {
  font: inherit;
  font-weight: 700;
  color: var(--primary);
  background: none;
  border: 0;
  padding: var(--space-2) 0;
  margin-left: var(--space-2);
  min-height: 44px;
  cursor: pointer;
  text-decoration: underline;
}
.rp-row-cap-btn:hover { color: var(--primary-bright); }

/* ── Phones: one scroll axis per surface ─────────────────
   At 390px this page carried FOUR nested scrollers: two pill groups
   scrolling sideways, and two tables scrolling in BOTH axes inside the
   page's own vertical scroll. A swipe that landed on a table scrolled
   the table — 4,089px of it on the city leaderboard, 71,016px on the
   neighborhood pool — so the page below was effectively unreachable by
   thumb, and none of the four had any visible cue that it scrolled.

   THE TABLES SCROLL IN BOTH AXES AGAIN, and the three things that made
   that unusable are each answered rather than argued with:

     The nested count. Two of the four scrollers are gone. The settings
     bar's pill strips became a menu button and two switches at this
     same width (see COMPACT_CONTROLS_QUERY in js/config.js), so the
     page's only other horizontal scrollers went with them. What is
     left is the page, and one table at a time inside it.

     Reaching the page below. The box is capped WELL short of the
     viewport, not at it. --rp-table-vh leaves the sticky chrome plus
     --space-8 of page, so there is always a strip of page above and
     below the table for a thumb to land on, and the reader can see
     that the section ends. A box sized to the full screen would be the
     old trap with a nicer number.

     Running out of rows. overscroll-behavior-y is AUTO, deliberately:
     a swipe that reaches the last row carries on down the page, which
     is the behaviour that makes 1,421 rows passable in one gesture
     rather than a scroll trap. Only the horizontal axis is contained,
     to keep a sideways overscroll off the browser's back gesture.

   What the cap buys is the thing the page is for: ten or so rows on
   screen at once with the header pinned above them, instead of a
   1,421-row column the reader scrolls through to reach anything else. */
@media (max-width: 720px) {
  /* The usable strip between the two pieces of chrome that never
     scroll away. --chrome-h is the host's (tokens.css) and --ps-bar-h
     is the settings bar's own measured height, published live by
     js/sticky-controls.js and 0px on a page without one. --space-8 is
     the page we deliberately do not fill, half of it above the box and
     half below.

     max() rather than a bare calc: a short, narrow viewport — a small
     phone with the keyboard up, a 700px-wide window two rows tall —
     would otherwise resolve this to a box of a few rows or none. 320px
     is about seven rows, which is still a table. */
  .rp-table-wrap {
    --rp-table-vh: calc(100svh - var(--chrome-h) - var(--ps-bar-h) - var(--space-8));
    max-height: max(320px, var(--rp-table-vh));
    overflow: auto;
    overscroll-behavior-x: contain;
    overscroll-behavior-y: auto;
    /* The affordance. Fades the right edge only while there is more to
       reach: 100% background-attachment on the right shadow means it
       disappears when the box is scrolled to its end. */
    background:
      linear-gradient(to right, var(--surface-1) 30%, transparent) left / 24px 100% no-repeat local,
      linear-gradient(to left, var(--surface-1) 30%, transparent) right / 24px 100% no-repeat local,
      radial-gradient(farthest-side at 0 50%, var(--line-strong), transparent) left / 12px 100% no-repeat scroll,
      radial-gradient(farthest-side at 100% 50%, var(--line-strong), transparent) right / 12px 100% no-repeat scroll;
  }
  /* The header pins to the BOX, which is the whole point of giving the
     box back its vertical axis: ten rows deep into a sort the reader
     can still see which column they sorted on. It was static here for
     exactly as long as the box did not scroll — sticky inside a box
     with no vertical axis pins to the PAGE instead, and slid the
     header under the masthead and the settings bar. */
  .rp-table thead th {
    position: sticky;
    top: 0;
  }

  /* Tighter cells, and the availability cell STACKS rather than
     squeezing. Number beside bar makes that column ~111px wide, which
     put its right edge one pixel past a 390px viewport — the bar, the
     one visual encoding in the whole table, was clipped in the default
     scroll position. Stacked, the column is the width of the number
     (~50px) and the bar gets a full-width run under it, so it is both
     on screen and easier to read than it was squeezed. */
  .rp-table th,
  .rp-table td { padding: var(--space-2) var(--space-2); }
  /* Header labels wrap here, and only here. At full width a wrapped
     heading is just a ragged table; on a phone it is width back —
     under auto layout a nowrap heading sets its column's floor, and
     "On-Street Availability" held the city page's reading column at
     204px, which on its own put the pair over the edge of the box.
     Wrapped it is the width of its longest word. Two lines of heading
     cost one row of height, once, at the top of the table.
     The tracking goes with it. 0.1em is an airy-layout refinement; on
     a 12px cap-height label it is 1.2px a letter, which spends 14px of
     the 93px "AVAILABILITY" needs — and that word is one the browser
     cannot break, so those 14px were the difference between a heading
     that fits its column and one clipped at the edge of the box. */
  .rp-table thead th { white-space: normal; vertical-align: bottom; }
  .rp-table thead th,
  .rp-table .rp-sort-btn { letter-spacing: 0.03em; }
  /* Left-aligned, unlike every other measurement cell. The table
     right-aligns figures so digits line up in a column, which is right
     when the cell IS the figure. Here the cell is a figure stacked on a
     bar: right-aligned, the number hugged the column's right edge and
     the last digit and the "%" fell outside the scroll window, so the
     bar was visible and the reading it encodes was not. */
  .rp-table .avail {
    flex-direction: column;
    align-items: stretch;
    text-align: left;
    gap: 3px;
  }
  .rp-table .avail-bar {
    flex: 0 0 auto;
    min-width: 0;
    max-width: none;
    width: 100%;
    height: 5px;
  }

  /* THE NAME AND THE READING TRAVEL TOGETHER, by order rather than by
     stickiness. js/leaderboard.js moves the Availability column up
     beside the name below this breakpoint; the note over
     applyColumnOrder() there carries the reasoning, and the short
     version is that the figure the table is sorted on used to sit off
     the right edge while the name sat on screen, so no scroll position
     showed both.

     A sticky first column was tried here first and removed. It works,
     but only by fighting the layout engine: `table-layout` is auto, so
     the column is sized by its content's max-content width and neither
     a max-width on the <td> nor one on its children reins it in — the
     name column took 220px, 56% of a 390px viewport, permanently
     occupied by one column. Forcing it needs table-layout: fixed and an
     explicit width for all nine to eleven columns. Wrapping the cell to
     make it narrower drove rows from 44px to 98px, which at 1,054 rows
     is a page in six figures. Reordering costs none of that: the
     three leading columns land at 299px inside a 330px scroll box on
     their natural widths, and every other column keeps the axis it
     had. At 320px — the narrowest phone still worth the name — the
     ranking's 299px overhangs a 257px box by the tail of the bar,
     with the figure itself still on screen; the pool below is sized
     to fit outright.

     So: one horizontal axis, no sticky column, single-line rows, and
     the two columns worth reading together at the head of the axis.
     The edge fade above says the rest of it is there. */

  /* The reading lands at position 3 on both tables, where the base
     rules right-align figures. Left is correct here for the same
     reason .avail is: the cell is a number stacked on a bar, and
     right-aligned the "%" fell outside the scroll window. */
  .rp-table th:nth-child(3),
  .rp-table td:nth-child(3) { text-align: left; }

  /* The pool is table-layout: fixed, so its three leading columns are
     whatever the <colgroup> widths above resolve to — and on the
     authored percentages the reading's right edge lands at 390px,
     sixty past the edge of a 330px scroll box, which is the whole
     problem again one column along.

     THE TABLE'S WIDTH IS WHAT MOVES, not the columns'. Pixel widths
     on the <col> elements do not hold: fixed layout hands any width
     the specified columns leave over back to all of them, so a column
     asked for 104px is laid out at 128px as soon as the three of them
     stop adding up to the table. Shares do hold, exactly, whatever
     the table is — so the three below claim 31% of it and the table
     itself tracks the viewport, which is the thing that actually
     differs between a 393px phone (330px of scroll box) and a 320px
     one (257px). The min() caps it at the authored 1,180px, so
     anything from ~426px up is laid out as before and only the
     genuinely narrow screens pay for it.

     The shares are what the three columns hold: four digits, a name
     that wraps rather than truncates, and "100.0%". Measured over the
     pool's first 100 rows they also come out SHORTER in total than
     the authored widths — the rows that run to two lines do it in the
     City and Country columns, not the name. */
  .rp-table--hoods { min-width: min(calc(322vw - 212px), 1300px); }
  .rp-table--hoods col:nth-child(1) { width: 5.5%; }
  .rp-table--hoods col:nth-child(2) { width: 14%; }
  .rp-table--hoods col:nth-child(3) { width: 9%; }
  /* Positions 4-6 are City, Country and Region once the reading has
     moved past them; left on the authored percentages they would take
     the shares written for Region and the reading, which gives a
     country name half the room of a two-letter state code. Positions
     7-12 are unmoved and keep theirs. These nine share what the
     three above leave, and the twelve add to 100 so nothing is handed
     back and every column is laid out at the share it asked for. */
  .rp-table--hoods col:nth-child(4) { width: 11.5%; }
  .rp-table--hoods col:nth-child(5) { width: 10.5%; }
  .rp-table--hoods col:nth-child(6) { width: 5.5%; }
  .rp-table--hoods col:nth-child(7) { width: 8.5%; }
  .rp-table--hoods col:nth-child(8) { width: 8.5%; }
  .rp-table--hoods col:nth-child(9) { width: 9.5%; }
  .rp-table--hoods col:nth-child(10) { width: 8%; }
  .rp-table--hoods col:nth-child(11) { width: 5%; }
  .rp-table--hoods col:nth-child(12) { width: 4.5%; }
  /* Region fell out of the nth-child(2..5) run the base rules use to
     left-align the place names, for the same reason. */
  .rp-table--hoods th:nth-child(6),
  .rp-table--hoods td:nth-child(6) { text-align: left; }
  /* Position 3 is the reading now, not a place name. It inherits the
     wrap rules written for the name columns it displaced, and
     `overflow-wrap: anywhere` would break a percentage across two
     lines the moment the column is tight. */
  .rp-table--hoods th:nth-child(3),
  .rp-table--hoods td:nth-child(3) { overflow-wrap: normal; }

  /* global.html's table carries .rp-table--wrap, which lets the name and
     country columns wrap and pins the name column to min-width 220px.
     Both are right at the page's 1,440px max width, where wrapping is
     what stops long country names forcing a scrollbar. Both are wrong at
     390px: 220px is 56% of the viewport, and wrapping inside it drove
     rows from 44px to 98px — at 1,054 rows, a page measured in six
     figures. Here the table scrolls sideways by design, so the columns
     may take the width they need on one line. */
  .rp-table--wrap th:nth-child(2),
  .rp-table--wrap td:nth-child(2),
  .rp-table--wrap th:nth-child(3),
  .rp-table--wrap td:nth-child(3) {
    white-space: nowrap;
    word-break: normal;
    hyphens: none;
  }
  .rp-table--wrap td:nth-child(2) { min-width: 0; }

  /* The "core ranked" marker sits inline after the city name, which
     on this table costs ~120px of a 390px viewport and pushes the
     availability column — the measure the page ranks on — off the first
     screen entirely. On its own line it costs the row 13px instead, and
     the name column halves. It is a marker, not a second name, so a
     quieter line of its own is where it belonged anyway. */
  .gl-tag {
    display: block;
    margin: 1px 0 0;
    padding: 0;
    background: none;
    font-size: 10px;
    letter-spacing: 0.02em;
  }

  /* Region is the widest remaining column — "Latin America & Caribbean"
     runs 168px on one line — and it sits between the reader and the
     availability figure the table is sorted on. Allowed to wrap here,
     where the rows are already two lines tall. */
  .rp-table--wrap th:nth-child(4),
  .rp-table--wrap td:nth-child(4) {
    white-space: normal;
    max-width: 88px;
  }

}

/* ── Touch targets ──────────────────────────────────────────
   A FINGER IS NOT A CURSOR, AND THE TABLE WAS BUILT FOR A CURSOR.

   Two controls carried the whole page and neither could be hit
   reliably: the sort button was 19px tall under a header cell 72px
   tall, and the city link 26px inside a row 45px tall. Both numbers
   are the text's own line box, because both elements are inline and
   neither was ever given a size — on a pointer that is correct and
   invisible, and on a phone it is a 19px target under an instruction
   that reads "Click any column header to sort."

   THE FIX IS AN OVERLAY, NOT A SIZE. Growing the control is the
   obvious move and the wrong one here: the arrow is a ::after INSIDE
   the button, so making it flex puts the mark on its own line, and
   adding block padding moves the label off the baseline its
   neighbours share (the cells are vertical-align: bottom, and the
   header row is three lines tall on the widest label). A transparent
   ::before pinned to the positioned cell takes the hit area to the
   FULL CELL while leaving every pixel of layout and paint untouched —
   72px and 45px respectively, both past the 44px floor.

   The cell is also the honest target: the whole header cell sorts,
   which is what the instruction already promised, and the whole name
   cell opens the city, which is what a reader aiming at a two-word
   link is trying to do anyway.

   Gated on coarse pointer OR narrow width OR short height, never width
   alone: a touch laptop at 1280px has the same finger, a desktop window
   dragged narrow loses nothing by it, and — the arm that was missing on
   the first pass — a phone on its side is 852px WIDE, so a width-only
   query hands a landscape handset the pointer-sized targets. Every one
   of these fell back to 19px and 26px there. `:has()` is the guard that
   keeps the overlay off cells that carry no control — notably the `#`
   counter, which has no button and must not look like one. */
@media (hover: none) and (pointer: coarse), (max-width: 720px), (max-height: 500px) {
  /* height on a cell is a MINIMUM, not a fixed size — the row still
     grows for a name that wraps to two lines. It is here because the
     overlay can only ever be as tall as the box it pins to: in portrait
     the cells come out 72px and 45px on their own, but give the table
     more width and the header collapses to one line and the cell to
     35px, so the full-cell target quietly dropped back under the floor
     on exactly the viewport this block was extended to cover. */
  .rp-table thead th:has(.rp-sort-btn),
  .rp-table tbody td:has(> .city-link) { height: 44px; }
  /* The cell is the overlay's containing block, and `relative` is the
     cheapest way to say so — but a header cell is already positioned,
     by `sticky`, and relative would quietly un-stick it. That cost the
     whole pinned header the first time these two rules met: the cell
     scrolled away with the rows and nothing said why. Only the body
     cell needs the declaration. */
  .rp-table tbody td:has(> .city-link) { position: relative; }

  .rp-table thead th .rp-sort-btn::before,
  .rp-table tbody td > .city-link::before {
    content: '';
    position: absolute;
    inset: 0;
  }

  /* The filter field, on both counts a phone cares about: 38px is under
     the floor, and a font under 16px makes iOS Safari zoom the page on
     focus — which on this page means landing in a zoomed viewport with
     the table pushed off-screen, and no way back but pinching. 16px is
     the threshold, so it is the value. */
  .rp-filter-input {
    min-height: 44px;
    font-size: 16px;
  }
}
