/* Dark is the default, in the CSS rather than in the boot script, so a visitor
   with JavaScript off gets the intended page rather than a light fallback.
   Light is opt-in via [data-theme="light"] on <html>, set by the nav toggle and
   persisted in localStorage. */
/* Three surface steps, darkest first: --ground is the page, --panel is a
   raised card, --bg is the cleanest fill and marks the active/selected thing
   sitting on a panel. */
:root {
  /* `color-scheme` is not decoration: it is what makes the native <select>
     menus, the colour swatches and the scrollbars render dark. Without it they
     stay light and sit on the page like cut-outs. */
  color-scheme: dark;

  --ground: #080b12;
  --bg: #080b12;
  --panel: #0e131d;
  --panel-2: #131a26;
  --line: #1d2532;
  --text: #e9eef8;
  --muted: #8996ad;
  --map-dot: #6092d7;
  --map-dot-hero: #6092d7;
  --accent: #dc2626;

  /* One height for every control, so `align-items: flex-end` does not stagger
     their labels: a segmented control is naturally ~45px and a bare select or
     colour swatch 38px, which pushed those two labels 6px down the row. */
  --control-h: 45px;
  --radius: 14px;
  --max: 1180px;
  --sans: ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", sans-serif;
  --mono: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, monospace;
}

:root[data-theme="light"] {
  color-scheme: light;

  /* Near-neutral grey, not blue-grey: 2-3 levels of blue over red rather than
     12. The dots are neutral, so tinted surfaces under them read as a colour
     cast. Text stays slate — cool type on neutral ground is fine, the reverse
     is what looked off. Light used to run ground and panel 3% apart on pure
     white, which read as one flat sheet; these steps give it the same depth
     dark already had. */
  --ground: #eeeff0;
  --bg: #ffffff;
  --panel: #f7f7f8;
  --panel-2: #e5e6e8;
  --line: #dbdde0;
  --text: #0f172a;
  --muted: #535e6e;
  --map-dot: #6a6b6c;
  /* The hero map has no panel under it — its dots sit straight on --ground,
     which is darker than --panel. The two surfaces can share one value at a
     mid-grey; the variable stays so the hero can diverge again if either
     surface moves. */
  --map-dot-hero: #6a6b6c;
}

* { box-sizing: border-box; }

html { scroll-behavior: smooth; }

body {
  margin: 0;
  background: var(--ground);
  color: var(--text);
  font-family: var(--sans);
  font-size: 16.5px;
  line-height: 1.65;
  -webkit-font-smoothing: antialiased;
}

a { color: inherit; }

h1, h2, h3 { line-height: 1.12; letter-spacing: -0.022em; margin: 0; }

section { padding: 92px 28px; }
section > * { max-width: var(--max); margin-inline: auto; }

.section-head { margin-bottom: 44px; }
.section-head h2 { font-size: clamp(28px, 4vw, 38px); margin-bottom: 12px; }
.section-head p { color: var(--muted); margin: 0; max-width: 640px; }

/* "Try this" list under a section lede. Lives inside .section-head rather than
   as a direct child of <section>, so it inherits the centring from
   `section > *` and sidesteps the margin-shorthand trap entirely. Kept as rules
   rather than panels: the controls directly below are already panelled, and a
   second stack of boxes competes with them. */
.try {
  list-style: none;
  padding: 0;
  margin-top: 20px;
  margin-bottom: 0;
  max-width: 720px;
  display: grid;
  gap: 12px;
}
.try li {
  color: var(--muted);
  font-size: 14px;
  line-height: 1.6;
  border-left: 2px solid var(--line);
  padding-left: 14px;
}
/* The coordinate tip is the one people miss, so it gets the accent rule. */
.try li:first-child { border-left-color: var(--accent); }
.try b { color: var(--text); font-weight: 560; }

/* ---------------------------------------------------------------- nav --- */
.nav {
  position: sticky;
  top: 0;
  z-index: 20;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 20px;
  padding: 14px 28px;
  background: color-mix(in srgb, var(--ground) 86%, transparent);
  backdrop-filter: blur(12px);
  border-bottom: 1px solid var(--line);
}
.wordmark { font-weight: 640; letter-spacing: -0.03em; font-size: 18px; }
.wordmark em {
  font-style: normal;
  color: var(--muted);
  font-weight: 400;
  margin-left: 9px;
  font-size: 15px;
}
.nav nav { display: flex; gap: 6px; align-items: center; }
.nav a {
  text-decoration: none;
  color: var(--muted);
  padding: 7px 12px;
  border-radius: 9px;
  font-size: 14.5px;
}
.nav a:hover { color: var(--text); background: var(--panel-2); }
.nav a.ghost { border: 1px solid var(--line); color: var(--text); }

.theme-toggle {
  appearance: none;
  border: 1px solid var(--line);
  background: transparent;
  color: var(--muted);
  width: 36px;
  height: 34px;
  border-radius: 9px;
  cursor: pointer;
  display: grid;
  place-items: center;
  margin-left: 4px;
}
.theme-toggle:hover { color: var(--text); background: var(--panel-2); }
.theme-toggle svg { width: 17px; height: 17px; }
.theme-toggle .moon { display: none; }
.theme-toggle .sun { display: block; }
:root[data-theme="light"] .theme-toggle .moon { display: block; }
:root[data-theme="light"] .theme-toggle .sun { display: none; }

/* --------------------------------------------------------------- hero --- */
.hero {
  display: grid;
  grid-template-columns: minmax(0, 0.92fr) minmax(0, 1.08fr);
  gap: 56px;
  align-items: center;
  padding-top: 72px;
  padding-bottom: 64px;
  /* Intentionally full-bleed — no max-width. The strip below is full-bleed
     too, so the two read as one full-width top zone, and the narrowing at the
     first heading becomes a shift into reading measure. */
}
.hero h1 { font-size: clamp(38px, 5.6vw, 62px); font-weight: 660; }
.hero h1 .accent { color: var(--accent); }
.lede { color: var(--muted); font-size: 18.5px; margin: 22px 0 30px; max-width: 46ch; }

.cta { display: flex; gap: 12px; align-items: center; flex-wrap: wrap; }
.install {
  font-family: var(--mono);
  font-size: 14.5px;
  background: var(--panel);
  border: 1px solid var(--line);
  padding: 11px 16px;
  border-radius: 10px;
}
.install::before { content: "$ "; color: var(--muted); }
.btn {
  text-decoration: none;
  background: var(--accent);
  color: #fff;
  font-weight: 560;
  font-size: 14.5px;
  padding: 12px 20px;
  border-radius: 10px;
}
.btn:hover { filter: brightness(1.1); }

.boundary {
  margin: 36px 0 0;
  padding: 16px 18px;
  border-left: 2px solid var(--accent);
  background: var(--panel);
  border-radius: 0 10px 10px 0;
  color: var(--muted);
  font-size: 15px;
}
.boundary strong { color: var(--text); display: block; margin-bottom: 4px; }

.hero-map { margin: 0; }
.hero-map svg { width: 100%; height: auto; display: block; }

/* -------------------------------------------------------------- strip --- */
.strip {
  display: grid;
  grid-template-columns: repeat(5, 1fr);
  gap: 1px;
  background: var(--line);
  border-block: 1px solid var(--line);
  padding: 0;
  max-width: none;
}
/* Cancel the section-level centring, which would give each cell its own
   max-width and auto margins and break the even grid. */
.strip > div {
  max-width: none;
  margin-inline: 0;
  background: var(--bg);
  padding: 26px 20px;
  text-align: center;
}
.strip b { display: block; font-size: 24px; letter-spacing: -0.02em; }
.strip span { color: var(--muted); font-size: 13.5px; }

/* --------------------------------------------------------------- demo --- */
/* Map left, settings right, once there is room for both.

   The whole demo stacked is 1319px tall, so with the controls at the top of a
   1440x790 laptop only 49% of the map was on screen — 36% at 1280x700, and even
   1920x1080 reached 92%. Changing a control and seeing what it did meant
   scrolling, every time.

   1120px is where the split starts paying: below it the sidebar squeezes the
   segmented controls onto second rows faster than the map gains from the width
   it gives back. Between 1000 and 1120 the page is one column with the groups
   open, which is what it did before this rule existed. */
.demo-body { display: grid; }
@media (min-width: 1120px) {
  .demo-body {
    /* minmax(0, 1fr) is a guard, not a fix for anything observed: swapped for a
       plain 1fr the columns measure identically at 1120/1200/1280/1440/1920,
       because nothing in this column has a large intrinsic minimum — the SVG is
       width:100%, the readout's tracks are already minmax(132px, 1fr) and the
       hint is text. It stays because the automatic-minimum trap is real the
       moment something wide lands here, and .code-card is what it looks like. */
    grid-template-columns: minmax(0, 1fr) 372px;
    gap: 34px;
    /* Start, not stretch, and this one *is* load-bearing — measured: as shipped
       the map column pins at top 81px and is still at 81px after a further
       200px of scroll. Switch to stretch and the item grows from 711px to 943px
       to fill the row, has nowhere left to travel, and scrolls away to top
       -123px: the sticky becomes a silent no-op. */
    align-items: start;
  }
  /* Placed rather than reordered: the controls stay first in the DOM so the
     phone layout is untouched, and the grid puts them in column two here. The
     sidebar is a single column of groups — at 372px the wrap has nothing to
     wrap onto, and letting them try just staggers the headings. */
  .controls {
    grid-column: 2;
    grid-row: 1;
    flex-direction: column;
    align-items: stretch;
    gap: 26px;
  }
  .control-group { flex: 0 0 auto; }
  .demo-main {
    grid-column: 1;
    grid-row: 1;
    position: sticky;
    /* The sticky nav measures 69px, so this is it plus 12px of air.
       The column itself measures 711px at 1280 and up, 718px at 1120, so with
       the offset it does not fit a 700px viewport — and does not need to.
       Measured while stepping through all four control groups, the map is 100%
       visible at every one of them from 1120x700 up; the only place it is
       clipped is the instant you land on the section top, before the pin
       engages (54% at 1120x700, 46% at 1280x700, 66% at 1440x790, 100% at
       1920x1080). */
    top: 81px;
  }
}

.controls {
  /* Groups size to their contents and wrap, rather than sitting in fixed
     columns: a column narrow enough to fit four groups across forces the wide
     controls inside them to stack, which costs more height than the grouping
     saves. Measured, fixed columns ran 526px against 380px this way. */
  display: flex;
  flex-wrap: wrap;
  gap: 4px 34px;
  align-items: flex-start;
  margin-bottom: 22px;
}
.control-group { flex: 0 1 auto; min-width: 0; }

/* A group is a headed section on a wide screen and an accordion on a narrow
   one, from the same markup: <details> already carries the open/closed state,
   the keyboard handling and the accessible name that a div and a button would
   have to reinvent between them. */
.control-group > summary {
  display: flex;
  align-items: baseline;
  gap: 10px;
  cursor: pointer;
  padding-bottom: 7px;
  margin-bottom: 13px;
  border-bottom: 1px solid var(--line);
  list-style: none;
}
.control-group > summary::-webkit-details-marker { display: none; }
.control-group > summary:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 3px;
  border-radius: 4px;
}
.group-name {
  font-size: 12px;
  text-transform: uppercase;
  letter-spacing: 0.09em;
  color: var(--text);
}
.group-blurb { font-size: 12px; color: var(--muted); }
.group-body {
  display: flex;
  flex-wrap: wrap;
  gap: 18px 20px;
  align-items: flex-end;
}

/* Narrow: one column of accordions, closed by default. The map is what the
   page is for, and it should not sit below a screen and a half of settings. */
/* Below this the groups stop fitting side by side, and open groups cost more
   height than the flat strip ever did — 576px at 900px wide against 351px. So
   the accordion takes over here rather than at phone width: collapsed it is
   221px at every size, and the map comes up the screen instead of down it. */
@media (max-width: 1000px) {
  /* `.controls` is a flex row, so the single column comes from the direction,
     not from a grid template — and `align-items` has to be reset with it, or
     the base rule's `flex-start` shrinks each accordion to the width of its
     own heading instead of the screen. */
  .controls { flex-direction: column; align-items: stretch; gap: 10px; }
  .control-group {
    border: 1px solid var(--line);
    border-radius: 10px;
    background: var(--panel);
  }
  .control-group > summary {
    padding: 13px 14px;
    margin-bottom: 0;
    border-bottom: 0;
  }
  .control-group[open] > summary { border-bottom: 1px solid var(--line); }
  /* A chevron, so a closed group reads as something that opens. */
  .control-group > summary::after {
    content: "";
    margin-left: auto;
    width: 7px;
    height: 7px;
    border-right: 1.5px solid var(--muted);
    border-bottom: 1.5px solid var(--muted);
    transform: translateY(-2px) rotate(45deg);
  }
  .control-group[open] > summary::after { transform: translateY(2px) rotate(-135deg); }
  .group-blurb { display: none; }
  .group-body { padding: 15px 14px 16px; }
}
/* Lets a control narrower than its content shrink onto the line rather than
   overflowing it — a flex item's automatic minimum is its content's width. */
.control { max-width: 100%; min-width: 0; }
.control label {
  display: block;
  font-size: 12px;
  text-transform: uppercase;
  letter-spacing: 0.09em;
  color: var(--muted);
  margin-bottom: 8px;
}
/* A label with something beside it. The gap between the two carries the
   spacing, so the label's own bottom margin moves up to the row. */
.control-head {
  display: flex;
  align-items: baseline;
  gap: 8px;
  margin-bottom: 8px;
}
.control-head label { margin-bottom: 0; }

/* Undoes a choice, so it reads as a link rather than competing with the
   controls it labels. */
.linkish {
  appearance: none;
  border: 0;
  background: none;
  padding: 0;
  font: inherit;
  font-size: 11px;
  text-transform: uppercase;
  letter-spacing: 0.07em;
  color: var(--muted);
  text-decoration: underline;
  text-underline-offset: 2px;
  cursor: pointer;
}
.linkish:hover { color: var(--text); }
/* visibility, not display: this button appears the moment a colour is picked,
   and a control that changes width can re-wrap the row and push the map down
   under the pointer. Reserve the space always. */
.linkish.is-hidden { visibility: hidden; }

/* The districts view paints every dot from regionColor, so the base dot colour
   has nothing left to show. Dimming says so; a live control that did nothing
   would just look broken. */
.control.is-off { opacity: 0.45; }
/* `flex-wrap` is a safety valve, not a layout: it only engages where the
   control genuinely cannot fit, which today is the four-button View control
   (306px) at widths under ~330px (280px of content). Without it the control
   pushed the whole page sideways instead. `max-width` is what lets the flex
   item shrink to the line in the first place. */
.segmented {
  display: inline-flex;
  flex-wrap: wrap;
  max-width: 100%;
  min-height: var(--control-h);
  background: var(--panel);
  border: 1px solid var(--line);
  border-radius: 10px;
  padding: 3px;
  gap: 2px;
}
.segmented button {
  appearance: none;
  border: 0;
  background: transparent;
  color: var(--muted);
  font: inherit;
  font-size: 14px;
  padding: 7px 13px;
  border-radius: 7px;
  cursor: pointer;
}
.segmented button:hover:not(:disabled) { color: var(--text); }
.segmented button:disabled { cursor: not-allowed; }
.segmented button.on {
  background: var(--bg);
  color: var(--text);
  box-shadow: inset 0 0 0 1px var(--line);
}

/* A select rather than a fifth segmented control: five placement rules spelled
   out as buttons would be wider than the View, Interaction and Labels controls
   put together and wrap the row on any laptop. */
select {
  appearance: none;
  display: block;
  font: inherit;
  font-size: 14px;
  color: var(--text);
  background: var(--panel);
  border: 1px solid var(--line);
  border-radius: 10px;
  padding: 8px 30px 8px 12px;
  height: var(--control-h);
  cursor: pointer;
  background-image: linear-gradient(45deg, transparent 50%, var(--muted) 50%),
    linear-gradient(135deg, var(--muted) 50%, transparent 50%);
  background-position: right 14px center, right 9px center;
  background-size: 5px 5px, 5px 5px;
  background-repeat: no-repeat;
}
select:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }
select:disabled { cursor: not-allowed; }

input[type="color"] {
  appearance: none;
  /* block, not the default inline-block: an inline swatch sits on a text
     baseline and the line box reserves descender space under it, making this
     control ~3px taller than its neighbours and staggering the row's labels. */
  display: block;
  width: 52px;
  height: var(--control-h);
  padding: 3px;
  background: var(--panel);
  border: 1px solid var(--line);
  border-radius: 10px;
  cursor: pointer;
}
input[type="color"]::-webkit-color-swatch-wrapper { padding: 0; }
input[type="color"]::-webkit-color-swatch { border: 0; border-radius: 6px; }
input[type="color"]:disabled { cursor: not-allowed; }

/* `.demo-body` is now the only `section > *` in the demo — measured, the
   section's children are `.section-head` and `.demo-body`, and `.demo-body` is
   the element carrying `max-width: 1180px` and the auto margins. Everything
   under it, this stage included, simply fills its column: the `auto` here
   computes to 0px today. It is kept because it costs nothing and is correct the
   moment anything caps the stage's own width — but it is no longer what centres
   the map, so do not read it as load-bearing. The shorthand rule now belongs to
   `.demo-body`: give *that* a `margin` and the whole demo goes flush left. */
.stage {
  margin: 0 auto;
  border: 1px solid var(--line);
  border-radius: var(--radius);
  overflow: hidden;
  background: var(--panel);
}
.stage svg { width: 100%; height: auto; display: block; }

/* The wrapper exists only to give the minimap something to anchor to — the
   stage itself is rewritten wholesale on every render. */
.stage-wrap { position: relative; }
.minimap {
  position: absolute;
  width: 150px;
  padding: 8px;
  border: 1px solid var(--line);
  border-radius: 10px;
  background: var(--panel);
  /* Opaque, and shadowed rather than faded: this floats over a field of dots,
     and anything translucent lets them show through the country it is drawing.
     A locator you have to read twice is not doing its job. */
  box-shadow: 0 6px 20px rgb(0 0 0 / 0.35);
}
/* Placement is four lines of CSS and nothing more — which is the point the
   demo is making. The library renders an SVG and has no say in where it goes. */
.minimap.at-top-right { top: 12px; right: 12px; }
.minimap.at-top-left { top: 12px; left: 12px; }
.minimap.at-bottom-right { bottom: 12px; right: 12px; }
.minimap.at-bottom-left { bottom: 12px; left: 12px; }

.minimap-map {
  cursor: grab;
  /* Without this a touch drag scrolls the page instead of moving the viewport:
     the browser claims the gesture before any listener sees a move. */
  touch-action: none;
}
.minimap-map.is-dragging { cursor: grabbing; }
.minimap svg { width: 100%; height: auto; display: block; }
.minimap-zoom {
  display: flex;
  gap: 3px;
  margin-top: 7px;
}
.minimap-zoom button {
  appearance: none;
  flex: 1;
  border: 1px solid var(--line);
  background: var(--bg);
  color: var(--muted);
  font: inherit;
  font-size: 12px;
  line-height: 1;
  padding: 5px 0;
  border-radius: 6px;
  cursor: pointer;
}
.minimap-zoom button:hover { color: var(--text); }
@media (max-width: 560px) {
  /* 150px of overview across a phone-width map covers the country it is
     describing. The viewport readout carries the same information in words. */
  .minimap { display: none; }
}

/* The cursor is the library's to set: attachInteractions raises it only over
   something that actually has a click handler, so "Off" doesn't lie. */
.stage svg:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: -2px;
}

/* A grid, not wrapping flex: in the map column the nine tiles no longer fit on
   one line, and wrapped flex rows size independently — five tiles above four
   put every rule in a different place on each row. Grid tracks line up. */
.readout {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(132px, 1fr));
  margin-top: 18px;
  border: 1px solid var(--line);
  border-radius: 10px;
  overflow: hidden;
  background: var(--panel);
}
/* Rules as outset shadows rather than borders: a border sits *inside* the
   border box, so the tile ending a full row drew its rule 1px in from the
   container edge and read as a doubled line. An outset shadow lands on that
   edge instead and `overflow: hidden` takes it.

   That only covers the tile that ends a *full* row. The last row is normally
   short — the readout is 8 or 9 tiles and `auto-fit` gives 5 columns in the
   774px map column, 4 at 658px, 2 at 390px — so the final tile has an empty
   cell beside it and its right shadow hangs in the middle of the row with
   nothing to clip it. Measured at 1440 the gap is 154px, at 600 it is 419px,
   and it is visible in both. `:last-child` is the one thing CSS does know here,
   and it is exactly the tile that needs the rule dropped. */
.readout div {
  padding: 13px 16px;
  box-shadow: 1px 0 0 var(--line), 0 1px 0 var(--line);
}
.readout div:last-child { box-shadow: 0 1px 0 var(--line); }
.readout k {
  display: block;
  font-size: 11.5px;
  text-transform: uppercase;
  letter-spacing: 0.08em;
  color: var(--muted);
}
.readout v { font-family: var(--mono); font-size: 16px; }

/* Sits above the map, so its height is *reserved* rather than natural. The text
   changes on every hover, and a line count that grows would push the map down
   under the pointer — moving the dot being pointed at, changing the text again,
   and oscillating. The reserve is measured, not guessed: --hint-lines is the
   worst case over every view x interaction x language x hover state at that
   width, plus one line of headroom. Re-measure before editing the copy. */
.hint {
  --hint-lines: 3;
  color: var(--muted);
  font-size: 14.5px;
  line-height: 1.5;
  /* The hint sits inside `.demo-main` now, not directly under <section>, so
     `section > *` no longer reaches it and the inline `auto` computes to 0px.
     Harmless and kept for the same reason as `.stage`; the trap it used to
     guard has moved up to `.demo-body`. */
  margin: 0 auto 14px;
  min-height: calc(var(--hint-lines) * 1.5em);
}
.hint b { color: var(--text); font-weight: 560; }
/* Devanagari counterparts and the "how to drive this" aside, one step back. */
.hint .alt-script { opacity: 0.7; }
.hint code {
  font-family: var(--mono);
  font-size: 13px;
  background: var(--panel-2);
  padding: 1px 5px;
  border-radius: 5px;
}

/* ------------------------------------------------------------- finder --- */
/* Width is capped on the input, not on .finder. The original reason was
   `section > *` centring every direct child; .finder is inside `.demo-main`
   now, so that no longer applies — but the cap stays on the input because a
   narrowed .finder would still drag its absolutely positioned results list in
   with it, and the list should stay as wide as the field it belongs to. */
.finder { position: relative; margin-bottom: 18px; }
.finder label {
  display: block;
  font-size: 12px;
  text-transform: uppercase;
  letter-spacing: 0.09em;
  color: var(--muted);
  margin-bottom: 8px;
}
.finder input {
  width: 100%;
  max-width: 480px;
  font: inherit;
  font-size: 15px;
  color: var(--text);
  background: var(--panel);
  border: 1px solid var(--line);
  border-radius: 10px;
  padding: 10px 14px;
}
.finder input::placeholder { color: var(--muted); opacity: 0.75; }
.finder input:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 1px;
  background: var(--bg);
}

.find-results {
  position: absolute;
  z-index: 20;
  left: 0;
  width: 100%;
  max-width: 480px;
  margin: 6px 0 0;
  padding: 4px;
  list-style: none;
  background: var(--bg);
  border: 1px solid var(--line);
  border-radius: 10px;
  box-shadow: 0 12px 32px rgb(0 0 0 / 0.13);
  max-height: 306px;
  overflow-y: auto;
}
.find-results li {
  display: flex;
  align-items: baseline;
  gap: 8px;
  padding: 8px 10px;
  border-radius: 7px;
  cursor: pointer;
  font-size: 14.5px;
}
.find-results li.active { background: var(--panel-2); }
.find-results li.empty { color: var(--muted); cursor: default; }
.find-results .nm { color: var(--text); font-weight: 560; }
.find-results .alt { color: var(--muted); font-size: 13.5px; }
.find-results .ctx {
  margin-left: auto;
  color: var(--muted);
  font-size: 12.5px;
  white-space: nowrap;
}
.find-results .tag {
  font-family: var(--mono);
  font-size: 10.5px;
  text-transform: uppercase;
  letter-spacing: 0.06em;
  color: var(--muted);
  border: 1px solid var(--line);
  border-radius: 5px;
  padding: 1px 5px;
}

/* ---------------------------------------------------------------- how --- */
.cards { display: grid; grid-template-columns: repeat(2, 1fr); gap: 18px; }
.cards article {
  background: var(--panel);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  padding: 26px 26px 22px;
}
.cards h3 { font-size: 19px; margin-bottom: 10px; }
.cards p { color: var(--muted); font-size: 15px; margin: 0 0 12px; }
.cards em { color: var(--text); font-style: normal; }
.fig { font-family: var(--mono); font-size: 13px; color: var(--muted) !important; }
.fig b { color: var(--accent); font-weight: 500; }

/* ---------------------------------------------------------------- use --- */
.code-grid { display: grid; grid-template-columns: repeat(2, 1fr); gap: 18px; }
/* Cards stretch and the <pre> fills the rest, so two samples of different
   lengths still bottom out on the same line rather than leaving one panel
   floating short of its neighbour.

   `min-width: 0` is the load-bearing part. A grid item's automatic minimum
   size is its *content's*, and the pre's overflow only makes the pre itself
   shrinkable — the card wrapping it stays content-sized. So the 1fr track grew
   to the longest code line and `overflow-x: auto` never engaged: at 390px the
   card measured 591px and the whole page scrolled sideways instead of the
   sample. Zero the minimum and the scrolling goes back where it belongs. */
.code-card { display: flex; flex-direction: column; min-width: 0; }
.code-card pre { flex: 1; }
.code-grid h3 { font-size: 15px; color: var(--muted); font-weight: 520; margin-bottom: 10px; }
/* The <code> scrolls, not the <pre>. An absolutely positioned child of a
   scroll container scrolls *with* the content — the copy button drifted left
   across the sample as soon as one was scrolled sideways, which is exactly
   what happens on every phone. Demoting the pre to a plain clipping box keeps
   the button anchored, and costs only moving the inline padding inward: the
   scroller has to span the pre's full width, or the code would vanish 22px
   before the border instead of running to it. */
pre {
  margin: 0;
  background: var(--panel);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  padding: 22px 0;
  overflow: hidden;
  /* The scroller has to fill the pre, not just its own text. `.code-card pre`
     is `flex: 1`, so a short sample's pre still stretches to whatever the
     tallest card in that grid row needs — but the <code> inside it stopped at
     its content, leaving its horizontal scrollbar stranded mid-panel. On
     desktop the CommonJS pre is 531px against 245px of code: the bar sat 241px
     above the bottom edge. */
  display: flex;
  flex-direction: column;
}
code { font-family: var(--mono); font-size: 13.5px; line-height: 1.7; }
pre > code {
  display: block;
  /* `1 0 auto`, not `1`: basis stays the content height, so a pre that is not
     being stretched still sizes to its text. Grow fills a stretched one; no
     shrink, so the code is never compressed into its own scrollbar. */
  flex: 1 0 auto;
  overflow-x: auto;
  padding-inline: 22px;
  /* `code`'s own 1.7 was never what shipped. As an inline box its line boxes
     were sized by the <pre>'s strut — monospace at 16.5px times the body's
     1.65, or 27.225px — which is taller, so the strut won and 1.7 was dead.
     Promoting the code to a block finally lets 1.7 apply, which would tighten
     every sample from 27.22px per line to 22.94px: a 16% restyle smuggled in
     under a layout fix. Restate the strut's value instead. */
  line-height: 27.225px;
}

/* Copy buttons are injected by main.ts, so these rules only ever apply where
   the clipboard is actually available. `.has-copy` is set on the host at the
   same time, so a page without JS gets no reserved gutter either. */
.has-copy { position: relative; }
.code-card pre.has-copy { padding-top: 44px; }
.copy-btn {
  position: absolute;
  top: 8px;
  right: 8px;
  font: 500 12px/1 var(--sans);
  color: var(--muted);
  background: var(--panel-2);
  border: 1px solid var(--line);
  border-radius: 7px;
  padding: 6px 10px;
  cursor: pointer;
  /* Hidden until the card is approached, but never to keyboard users. */
  opacity: 0;
  transition: opacity 120ms ease, color 120ms ease, border-color 120ms ease;
}
.code-card:hover .copy-btn,
.copy-btn:focus-visible,
.copy-btn.is-done { opacity: 1; }
.copy-btn:hover { color: var(--text); border-color: var(--muted); }
.copy-btn.is-done { color: var(--accent); border-color: var(--accent); }
/* The install chip is small and inline, so its button sits alongside rather
   than over the text — there is no room to overlap. */
.install.has-copy { padding-right: 62px; }
.install .copy-btn { top: 50%; transform: translateY(-50%); right: 6px; opacity: 1; }
/* Coarse pointers have no hover, so the button must always be visible. */
@media (hover: none) {
  .copy-btn { opacity: 1; }
}
p code { background: var(--panel-2); padding: 1px 5px; border-radius: 5px; font-size: 0.9em; }
/* Longhand margins throughout: `section > *` centres direct children with
   `margin-inline: auto`, and the shorthand would reset that to 0 — leaving
   these flush against the section padding while the code grid stays centred. */
.support-head {
  font-size: 15px;
  color: var(--muted);
  font-weight: 520;
  margin-top: 44px;
  margin-bottom: 12px;
}
.support {
  margin-top: 0;
  margin-bottom: 0;
  background: var(--panel);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  overflow: hidden;
}
.support > div {
  display: grid;
  grid-template-columns: 150px 1fr;
  gap: 18px;
  padding: 14px 22px;
  border-top: 1px solid var(--line);
}
.support > div:first-child { border-top: 0; }
.support dt { color: var(--muted); font-size: 14px; }
.support dd { margin: 0; font-size: 15px; }
.support code {
  font-family: var(--mono);
  background: var(--panel-2);
  padding: 1px 5px;
  border-radius: 5px;
  font-size: 0.86em;
}

.footnote { color: var(--muted); font-size: 15px; margin-top: 22px; max-width: 66ch; }

footer {
  border-top: 1px solid var(--line);
  padding: 40px 28px 60px;
  color: var(--muted);
  font-size: 14px;
}
footer p { max-width: var(--max); margin: 0 auto 6px; }

/* ------------------------------------------------------------- consent --- */
/* Fixed, not in the flow. The banner is revealed after the bundle loads, and a
   block appearing *in* the flow at that moment pushes everything below it down
   under the pointer — the same feedback loop that put #hint above the stage
   rather than below it. Fixed, it overlays and nothing reflows.

   z-index 30 clears both the sticky nav and the finder's dropdown, which share
   20. Anything less and the nav paints over a banner pinned to a short page. */
.consent {
  position: fixed;
  z-index: 30;
  bottom: 18px;
  left: 50%;
  transform: translateX(-50%);
  width: calc(100% - 36px);
  max-width: 580px;
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 14px 18px;
  padding: 16px 18px;
  background: var(--panel);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  /* The page's own surfaces are flat, so the only thing separating a --panel
     card from the --panel cards behind it is this shadow. */
  box-shadow: 0 16px 40px rgb(0 0 0 / 0.38);
  color: var(--muted);
  font-size: 14px;
  line-height: 1.5;
}
/* `hidden` needs restating here, and this is not belt-and-braces. The attribute
   is `display: none` in the UA stylesheet, which *any* author `display` beats —
   so `.consent { display: flex }` above silently un-hides the banner and the
   markup's `hidden` attribute does nothing at all. Without this rule the banner
   ships visible to every visitor in every region and stays up after a choice is
   made, because the only thing initConsent ever does to dismiss it is set that
   same attribute. Caught by a screenshot pass, not by reading the code. */
.consent[hidden] { display: none; }

:root[data-theme="light"] .consent { box-shadow: 0 16px 40px rgb(15 23 42 / 0.16); }

/* Basis 240px, so the text keeps the buttons on its own row while there is room
   for both and drops them beneath once there is not. */
.consent p { margin: 0; flex: 1 1 240px; }
.consent-actions { display: flex; gap: 8px; flex: 0 0 auto; margin-left: auto; }

.consent-btn {
  font: 500 13.5px var(--sans);
  color: var(--muted);
  background: var(--panel-2);
  border: 1px solid var(--line);
  border-radius: 8px;
  padding: 9px 16px;
  cursor: pointer;
  transition: color 120ms ease, border-color 120ms ease, filter 120ms ease;
}
.consent-btn:hover { color: var(--text); border-color: var(--muted); }
.consent-btn:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
.consent-btn.is-primary {
  background: var(--accent);
  border-color: var(--accent);
  color: #fff;
}
.consent-btn.is-primary:hover { color: #fff; filter: brightness(1.1); }

@media (max-width: 900px) {
  .hero { grid-template-columns: 1fr; gap: 36px; }
  .cards, .code-grid { grid-template-columns: 1fr; }
  .strip { grid-template-columns: repeat(2, 1fr); }
  section { padding: 64px 20px; }
  /* The nav and the footer set their own padding rather than inheriting the
     section rule, so without this the wordmark and the footer text sit 8px
     inside every other left edge on the page. */
  .nav { padding-inline: 20px; }
  footer { padding-inline: 20px; }
}

@media (max-width: 560px) {
  /* The 150px label column leaves too little for values like the TypeScript
     row, which then wraps to one word per line. Stack instead. */
  .support > div { grid-template-columns: 1fr; gap: 2px; }

  /* The wordmark and five nav items measure 421px against 334px of usable
     width at 390px, so the row overflowed the screen and carried the theme
     toggle — the last item in it — clean off the right edge, with no way to
     reach it. The three section anchors are the part that is pure convenience
     on a page you scroll through anyway; the GitHub link and the toggle are
     the two things a visitor cannot get to any other way, so they are what
     stays. The row breaks below 477px measured; 560 is the nearest breakpoint
     already in this file and leaves headroom for a wider wordmark. */
  .nav nav a:not(.ghost) { display: none; }

  /* Samples run to 67 characters and no phone fits that, so the sideways
     scroll is a given — but a step down in type and padding is worth 4.6 more
     characters before it starts: 37.4 per line to 42.0 at 390px, measured off
     the live font rather than an assumed advance width. */
  pre { padding: 18px 0; }
  pre > code { padding-inline: 16px; }
  .code-card pre.has-copy { padding-top: 40px; }
  .code-card code { font-size: 12.5px; }
}

/* Reserves carry one line of headroom over the measured worst case at each
   width. Below 500px the type also drops a step: the hint wraps by character
   count, so a smaller face fits more per line and costs fewer reserved lines
   on exactly the screens where vertical space is scarcest.

   The ladder is measured and the breakpoints are the crossings themselves, not
   round numbers. Harvested by driving the page: every distinct `hint.innerHTML`
   the demo can emit across mode x language x interaction x district context x
   selection, mouse and touch, sweeping every cell of the viewBox plus every pin
   and route element — 33,349 strings, of which 32,932 quote a per-dot
   coordinate and 2,260 carry click mode's copy button. Replayed at every pixel
   from 320 to 1920, worst case:

       <= 360   6 lines      624..876    3 lines
       361..438 5 lines      877..1119   2 lines
       439..623 4 lines      >= 1120     3 lines  (two columns)

   Everything below 1120 is set by a single line: find mode on Rukum East, whose
   HQ has no shipped coordinate, so the hint explains the 2015 split instead.
   Miss that one string and every crossing lands early — which is why the sample
   has to be collected by driving the real page, and why the size of it is worth
   stating. A first pass here fired its events at the <svg> instead of at
   `elementFromPoint`, so `closest()` never matched a pin or a route, and it
   scanned click mode for hover feedback that mode does not give: 19,278
   strings, none of them the long ones, and a 3-to-2 crossing at 816.

   Re-derive this, don't nudge it. Every round set tried here has sat *below* its
   crossing — 750/560/400 first, then 909/563/408/342 — and a breakpoint below
   its crossing reserves one line too few in the band above it, which is the
   feedback loop this reserve exists to prevent, live again.

   The jump back at 1120 is the two-column layout: the hint rides in the map
   column, 658-774px wide rather than the full 1063, so it wraps like a tablet
   even on a 1920 screen. Change that grid and re-measure this. */
@media (max-width: 876px) { .hint { --hint-lines: 4; } }
@media (max-width: 623px) { .hint { --hint-lines: 5; } }
@media (max-width: 500px) { .hint { font-size: 13.5px; } }
@media (max-width: 438px) { .hint { --hint-lines: 6; } }
@media (max-width: 360px) { .hint { --hint-lines: 7; } }
@media (min-width: 1120px) { .hint { --hint-lines: 4; } }

/* The reserve exists to stop the map moving under a *hovering* cursor. With no
   hover there is no cursor to protect: a tap changes the hint after the finger
   has already lifted, so a reflow costs nothing — while the reserved space
   costs real estate on exactly the screens with least of it. */
@media (hover: none) {
  .hint { min-height: 0; }
}
