/* /maps/ — the city.

   Strictly greyscale. The ONLY colour anywhere on this page is light: the warm
   sodium of the streetlamps and lit windows at night, and the cool white of
   headlights. Everything else — paper, roads, water, buildings, type — is a
   value. One accent in the whole design, and it only exists after dark.

   Day and night are driven by [data-theme] on <html>, set before first paint by
   the inline script in index.html and shared with the rest of the site under
   the `list-theme` key. */

:root {
  --paper:      #f4f3f0;
  --ink:        #14161a;
  --ink-soft:   #4a4f57;
  --ink-faint:  #878d96;
  --panel:      rgba(252, 251, 249, 0.94);
  --panel-line: rgba(20, 22, 26, 0.14);
  --accent:     #ffb84d;      /* lantern amber, per DESIGN.md */
  --shadow:     0 10px 30px rgba(20, 22, 26, 0.13);
  --scrim:      rgba(20, 22, 26, 0.34);
  --pin:        #e0342a;      /* the map pin — see .pin__mark */
  --pin-hole:   #f4f3f0;
  --frost:      rgba(244, 243, 240, 0.55);
  --sat: env(safe-area-inset-top, 0px);
  --sar: env(safe-area-inset-right, 0px);
  --sab: env(safe-area-inset-bottom, 0px);
  --sal: env(safe-area-inset-left, 0px);
  /* HOW MUCH OF THE SCREEN THE BROWSER IS SITTING ON. env() cannot answer
     this: the safe-area insets describe the DEVICE — the notch, the home
     indicator — not the browser's own furniture, and on an iPhone Safari's
     toolbar is neither. app.js writes these from visualViewport, and they
     stay 0 everywhere the browser has no floating chrome (desktop, Android,
     a home-screen app), so every rule below is a no-op until it isn't. */
  --vvt: 0px;
  --vvb: 0px;
}

html[data-theme="dark"] {
  --paper:      #0c0d10;
  --ink:        #eceae4;
  --ink-soft:   #a2a7b0;
  --ink-faint:  #5d636d;
  --panel:      rgba(16, 18, 22, 0.92);
  --panel-line: rgba(236, 234, 228, 0.16);
  --shadow:     0 10px 34px rgba(0, 0, 0, 0.5);
  --scrim:      rgba(0, 0, 0, 0.58);
  /* a shade hotter at night, where it sits on near-black rather than on paper,
     and the hole takes the night paper so it stays a hole */
  --pin:        #ff4438;
  --pin-hole:   #0c0d10;
  --frost:      rgba(12, 13, 16, 0.62);
}

* { box-sizing: border-box; }

/* `hidden` is only `display:none` at the UA level, so any rule of ours that
   sets display beats it. #boot sets display:grid, which is exactly how the boot
   card stayed on screen over a fully drawn city. */
[hidden] { display: none !important; }

/* THE LARGE VIEWPORT, deliberately. `100%` is the viewport as it stands with
   the browser's toolbar showing, which left a band of bare paper under the
   city on an iPhone — the page read as a document in a browser rather than as
   a map. `100lvh` is the screen as if the toolbar were retracted, so the
   drawing runs edge to edge and iOS 26's floating toolbar sits ON the city
   instead of beside it. The `100%` before it is the fallback for engines
   without the unit; the HUD is lifted clear of the toolbar separately, via
   --vvb, so nothing readable is buried by this. */
html, body {
  margin: 0;
  padding: 0;
  height: 100%;
  height: 100lvh;
  overflow: hidden;
  background: var(--paper);
  color: var(--ink);
  font-family: "Atkinson Hyperlegible", system-ui, -apple-system, sans-serif;
  -webkit-font-smoothing: antialiased;
  overscroll-behavior: none;
}

#stage {
  position: fixed;
  inset: 0;
  bottom: auto;          /* height wins: see html,body above */
  height: 100%;
  height: 100lvh;
  display: block;
  width: 100%;
  touch-action: none;
  cursor: grab;
}
#stage:active { cursor: grabbing; }

#veil {
  position: fixed;
  inset: 0;
  bottom: auto;          /* height wins: see html,body above */
  height: 100%;
  height: 100lvh;
  background: var(--paper);
  opacity: 0;
  pointer-events: none;
  transition: opacity 110ms linear;
  z-index: 40;
}
#veil.on { opacity: 1; }

/* ------------------------------------------------------------------ boot */

#boot {
  position: fixed;
  inset: 0;
  bottom: auto;          /* height wins: see html,body above */
  height: 100%;
  height: 100lvh;
  display: grid;
  place-content: center;
  justify-items: center;
  gap: 10px;
  background: var(--paper);
  z-index: 60;
}
.boot__mark {
  font-family: "Bricolage Grotesque", system-ui, sans-serif;
  font-weight: 600;
  font-size: 15px;
  letter-spacing: 0.24em;
  text-transform: uppercase;
  color: var(--ink);
}
.boot__msg { font-size: 13px; color: var(--ink-faint); }
.boot__out {
  font: inherit;
  background: none;
  cursor: pointer;
  margin-top: 14px;
  font-size: 14px;
  color: var(--ink);
  text-decoration: none;
  border: 0;
  border-bottom: 1px solid var(--panel-line);
  padding-bottom: 2px;
}
.boot__out--quiet { margin-top: 8px; font-size: 12px; color: var(--ink-faint); }

/* ------------------------------------------------------------------- hud */

.hud {
  position: fixed;
  z-index: 30;
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  pointer-events: none;
}
.hud--top {
  top: calc(14px + max(var(--sat), var(--vvt)));
  left: calc(16px + var(--sal));
  right: calc(16px + var(--sar));
  gap: 16px;
}
.hud--bottom {
  /* max(), not sum: the two insets describe the same edge from different
     authorities — the device's home indicator and the browser's toolbar — and
     whichever reaches further into the screen is the one to clear. Adding them
     would float the credit halfway up the map on an iPhone. This rail carries
     the OpenStreetMap credit, which is a licence condition, so it is the one
     rail that must never end up under anything. */
  bottom: calc(12px + max(var(--sab), var(--vvb)));
  right: calc(16px + var(--sar));
  justify-content: flex-end;
}
.hud a, .hud button { pointer-events: auto; }

.wordmark {
  display: grid;
  gap: 1px;
  text-decoration: none;
  color: inherit;
  max-width: 62vw;
}
.wordmark__title {
  font-family: "Bricolage Grotesque", system-ui, sans-serif;
  font-weight: 600;
  font-size: 18px;
  letter-spacing: 0.2em;
  line-height: 1.15;
  text-transform: uppercase;
}
.wordmark__site { font-size: 12px; color: var(--ink-faint); letter-spacing: 0.08em; line-height: 1.3; }
.wordmark__tag {
  font-size: 12px;
  color: var(--ink-faint);
  max-width: 32ch;
  line-height: 1.35;
  margin-top: 3px;
}

.hud__actions { display: flex; align-items: center; gap: 22px; flex-wrap: wrap; justify-content: flex-end; }

/* THE TEXT IS THE BUTTON. No pill, no border, no panel behind it — a row of
   words in the corner of a map, which is what the rest of the page already
   looks like. What a pill was really providing was legibility over a busy
   drawing, and a paper-coloured halo does that without putting a piece of UI
   chrome on top of the cartography. Bigger, too: these were 12px inside a
   border, and the border was doing the work of looking like a target. */
.iconbtn, .textbtn {
  font-family: "Bricolage Grotesque", system-ui, sans-serif;
  font-size: 18px;
  font-weight: 600;
  letter-spacing: 0.01em;
  color: var(--ink);
  background: none;
  border: 0;
  border-radius: 0;
  padding: 2px 0;
  cursor: pointer;
  text-decoration: none;
  white-space: nowrap;
  /* Six stops, not three. Bare words over a street map are only as legible as
     the paper they can put behind themselves, and the halo is the whole of it
     now that the pills are gone — at three stops the thin strokes of the map
     still came through the counters of the letters. */
  text-shadow:
    0 0 4px var(--paper), 0 0 8px var(--paper), 0 0 14px var(--paper),
    0 0 22px var(--paper), 0 0 30px var(--paper), 0 0 44px var(--paper);
}
.iconbtn:hover, .textbtn:hover { text-decoration: underline; text-underline-offset: 5px; }
.iconbtn { font-size: 24px; padding: 0; line-height: 1; }
.iconbtn__moon { display: none; }
html[data-theme="dark"] .iconbtn__sun { display: none; }
html[data-theme="dark"] .iconbtn__moon { display: inline; }

:where(a, button):focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
}

/* The other door. Bottom right, in the HUD's own voice — a word with a paper
   halo rather than a button, like everything else on this page that is not the
   map. It sits ABOVE the address line rather than beside it, because the credit
   under it is a licence condition and must not have to compete for the corner. */
.hud--bottom { flex-direction: column; align-items: flex-end; gap: 8px; }
.worldbtn {
  font-family: "Bricolage Grotesque", system-ui, sans-serif;
  font-size: 18px;   /* the pair with .textbtn is deliberate — keep them equal */
  font-weight: 600;
  letter-spacing: 0.01em;
  color: var(--ink);
  text-decoration: none;
  white-space: nowrap;
  text-shadow:
    0 0 4px var(--paper), 0 0 8px var(--paper), 0 0 14px var(--paper),
    0 0 22px var(--paper), 0 0 30px var(--paper), 0 0 44px var(--paper);
}
.worldbtn:hover { text-decoration: underline; text-underline-offset: 5px; }

.footer__line { font-size: 11px; color: var(--ink-faint); letter-spacing: 0.05em; }

/* Whose streets these are. Quieter than the address, but never hidden: two of
   the five crops are OpenStreetMap and that credit is a licence condition. */
.footer__credit {
  font-size: 10px;
  color: var(--ink-faint);
  opacity: 0.72;
  letter-spacing: 0.03em;
  margin-left: 10px;
  padding-left: 10px;
  border-left: 1px solid currentColor;
}

/* ----------------------------------------------------------------- intro */

#intro {
  position: fixed;
  inset: 0;
  bottom: auto;          /* height wins: see html,body above */
  height: 100%;
  height: 100lvh;
  display: grid;
  place-content: center;
  z-index: 25;
  pointer-events: none;
}
/* ONLY THE TAIL IS IN FLOW HERE. The stack is therefore as tall as that one
   line, and #intro centres it — so the sentence sits dead centre of the screen,
   the words are centred over it, and the site line hangs below without moving
   either. Everything else in this block exists to keep those three facts. */
.intro__stack {
  display: grid;
  justify-items: center;
  gap: 0;
  text-align: center;
  position: relative;
  /* Not a flat 5rem side pad. On a 390px phone that left 230px of line, which
     wrapped the tail onto two lines — and the tail is what the site line was
     positioned under. Room first, then the rule below stops caring how many
     lines it takes. */
  padding: 3rem clamp(1rem, 5vw, 5rem);
}
/* The words sit over a busy street map, so they get a breath of paper behind
   them. A soft radial rather than a panel: a box would read as UI, and this has
   to read as a title over a picture. */
.intro__stack::before {
  content: "";
  position: absolute;
  inset: -16%;
  background: radial-gradient(closest-side, var(--paper) 34%, transparent 100%);
  opacity: 0.82;
  z-index: -1;
}
/* Why none of these is simply a grid row.
   The site line was once placed by a fixed translateY(2.6em) measured against a
   ONE-LINE tail — and on a phone the tail wrapped, so "teacheng.work" printed
   straight through its second line. Rows fixed that, and broke something else:
   a row is as tall as its tallest occupant, so the tail sharing the words' row
   sat a 16px line inside a 46px box, and all that empty box printed as a gulf
   above the site line. Giving the tail its own row closed the gulf but pushed
   the whole pair BELOW the middle of the screen, because the words' row was
   still reserving its height above them — the sentence no longer arrived where
   the word it replaced had been.
   So: the tail alone is in flow and defines the stack, the words overlay it
   (absolutely, hence centred on exactly the tail's centre), and the site hangs
   off the bottom with its own height cancelled so it cannot push the tail up.
   Each of the three lands where it should and none of them moves another. */
.intro__word, .intro__tail, .intro__site {
  font-family: "Bricolage Grotesque", system-ui, sans-serif;
  color: var(--ink);
  opacity: 0;
}
.intro__words {
  position: absolute;
  inset: 0;
  display: grid;
  place-items: center;
}
.intro__word { grid-area: 1 / 1; }
.intro__site {
  /* 0.9em down, and its own line box (~1.2em) plus that margin taken back out
     of the layout, so the stack stays exactly one tail tall */
  margin-top: 0.9em;
  margin-bottom: -2.1em;
}
.intro__word {
  font-weight: 500;
  font-size: clamp(2.4rem, 9vw, 6rem);
  letter-spacing: -0.03em;
}
.intro__tail {
  font-weight: 400;
  font-size: clamp(1rem, 3.2vw, 1.9rem);
  letter-spacing: -0.01em;
}
.intro__site {
  font-weight: 600;
  font-size: clamp(0.7rem, 1.7vw, 0.95rem);
  letter-spacing: 0.26em;
  text-transform: uppercase;
  color: var(--ink-faint);
}

/* --------------------------------------------------------- cards and pins */

/* Read at arm's length from a laptop, not squinted at. The hover card carries
   the only description of a project the map ever shows, so it is sized like
   something you are meant to read rather than like a tooltip. */
#card {
  position: fixed;
  z-index: 35;
  max-width: min(27rem, 82vw);
  padding: 15px 18px 16px;
  background: var(--panel);
  border: 1px solid var(--panel-line);
  border-radius: 16px;
  box-shadow: var(--shadow);
  pointer-events: none;
  transform: translate(-50%, -100%);
  /* FROSTED, BUT LEGIBILITY WINS THE TIE. A solid card dropped on a map reads
     as a lid on it, so the city keeps going underneath — but this holds two
     sentences of body copy, not a label, so it sits at ~0.77 alpha rather than
     the index list's 0.55. The blur is what does the real work: streets
     passing under it are smeared to a flat tone, which is what stops the
     counters of the letters filling up with cartography. Measured over the
     drawn city, the blurb reads at 7.7:1 in daylight and 7.8:1 at night —
     past AAA for body text, with the map still visible through it. */
  background: color-mix(in srgb, var(--panel) 82%, transparent);
  backdrop-filter: blur(18px) saturate(1.25);
  -webkit-backdrop-filter: blur(18px) saturate(1.25);
}
.card__title {
  margin: 0 0 5px;
  font-family: "Bricolage Grotesque", system-ui, sans-serif;
  font-size: 18px;
  font-weight: 600;
  letter-spacing: -0.01em;
}
.card__blurb { margin: 0; font-size: 14.5px; line-height: 1.45; color: var(--ink-soft); }
/* The category is the answer to "what IS this quarter", and it was set as a
   footnote: 12px of --ink-faint under a 14.5px paragraph. It now carries a
   rule above it and the page's own ink, so the card reads title / blurb /
   CATEGORY rather than trailing off. */
.card__count {
  margin: 11px 0 0;
  padding-top: 9px;
  border-top: 1px solid var(--panel-line);
  font-size: 12.5px;
  font-weight: 600;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--ink-soft);
}
.card__count[hidden] { display: none; }

/* The Open row: touch only, and the reason the card takes taps at all. It is
   drawn as a full-width action strip — rule above, weight up, arrow pushed to
   the far edge — because on a phone this row IS the second tap; a caption-sized
   line would be a control pretending to be a footnote. The card's own bottom
   padding joins its 10px to land the strip comfortably past 44px of thumb. */
.card__open {
  display: flex;
  align-items: center;
  margin-top: 11px;
  padding: 10px 0 2px;
  border-top: 1px solid var(--panel-line);
  font-family: "Bricolage Grotesque", system-ui, sans-serif;
  font-size: 15px;
  font-weight: 600;
  color: var(--ink);
  text-decoration: none;
  min-height: 32px;
}
.card__open[hidden] { display: none; }
.card__open-arrow { margin-left: auto; }
/* THE LINK COVERS THE CARD. #card is already position:fixed, so it is the
   positioned ancestor and this pseudo-element reaches every corner of it —
   which makes a tap anywhere on the card a real click on the anchor rather
   than a handler forwarding one. That distinction is the whole point: the
   link opens a new tab now, and a synthetic .click() on a target=_blank
   anchor is what a popup blocker is looking for. Only while armed, because
   only then is there a project for the card to be about. */
#card.card--live .card__open::after {
  content: '';
  position: absolute;
  inset: 0;
}

/* Taps reach the card only while a pin is ARMED — a state only touch enters.
   On a mouse the card must stay untouchable: it floats above the pin, and a
   card that can be hovered steals the pin's pointerleave and flickers. */
#card.card--live { pointer-events: auto; cursor: pointer; }

/* #hint lives at the bottom edge too, and on a narrow laptop the docked card
   reaches far enough across to sit under it. While a card is up the hint has
   nothing left to say anyway — "hover a district" to someone plainly hovering
   one — so it yields. Done with :has() rather than a class the JS has to
   remember to toggle: the card's own `hidden` attribute is already the source
   of truth, and this keeps the change out of hud.js, which every branch commit
   touches. An engine without :has() just ignores the rule and shows the hint. */
html:has(#card:not([hidden])) #hint { opacity: 0; }

/* ON A PHONE THE DISTRICT CARD STOPS CHASING AND DOCKS to the bottom edge.
   Hanging it off the pointer is a mouse idea: a finger IS the pointer, so a
   card beside it sits under the hand that summoned it, and one wide enough to
   read leaves nowhere on a 390px screen to put it anyway. Docked, the map
   stays visible above it and the text always lands in the same place.

   The pin card is exempt, as it is on the desktop: a pin is one mark on the
   map and its description belongs on it. Docked at the bottom, the pin and
   the sentence about it would sit at opposite ends of the screen. */
@media (max-width: 760px) {
  #card:not(.card--pin) {
    left: 8px;
    right: 8px;
    bottom: calc(8px + max(var(--sab), var(--vvb)));
    top: auto;
    max-width: none;
    transform: none !important;   /* placeCard writes a transform on every move */
  }
  #card.card--pin { max-width: min(27rem, 86vw); }
}

#pins, #districtButtons { position: fixed; inset: 0; z-index: 28; pointer-events: none; }
#pins a { pointer-events: auto; }
/* The district buttons stay INVISIBLE TO THE POINTER, deliberately.
   They exist for the keyboard and the screen reader — tab to a quarter, hear
   its name and its count, press to enter — and focus, blur and click all work
   without ever being pointed at. Given pointer-events they did active harm:
   132x44 of transparent button sat over each district's NAME, which is the one
   spot a visitor aims for, so the canvas underneath never got the pointermove
   and the quarter you were plainly pointing at did not light up. Five dead
   rectangles on the map, in the five worst possible places. The mouse is
   already served by the canvas, which hit-tests the whole quarter. */
#districtButtons button { pointer-events: none; }
/* A pin whose description is showing but which has not been opened yet — the
   touch equivalent of hover, so it wears the hover treatment: the plate goes
   solid and the mark grows the way it does under a mouse. */
#pins a.pin--armed .pin__label {
  background: var(--ink);
  color: var(--paper);
  border-color: var(--ink);
}
#pins a.pin--armed .pin__mark { transform: scale(1.12); }
/* just-mounted pins ignore the ghost click that follows the entering tap */
#pins.pins--deaf a { pointer-events: none; }

/* Project landmarks. Real <a href> elements, moved with transform only — never
   left/top, which would relayout thirty of them per frame under a canvas that
   is trying to hold sixty. */
.pin {
  position: absolute;
  top: 0; left: 0;
  display: grid;
  justify-items: center;
  gap: 3px;
  text-decoration: none;
  color: var(--ink);
  will-change: transform;
  /* HOLDING A PIN IS A GESTURE HERE, and iOS spends a long press on a link
     putting up its own preview-and-share sheet instead. The description is
     what a hold is meant to produce, so the callout is turned off and the
     text under the finger is not selectable either — a selection handle
     appearing mid-hold is the same interruption by another route. */
  -webkit-touch-callout: none;
  -webkit-user-select: none;
  user-select: none;
}
/* THE ONE PIECE OF PIGMENT ON THE PAGE.
   Everything else here is a value — paper, roads, water, buildings, type — and
   the only other colour is light after dark. This is the deliberate exception,
   and it earns it by being the one thing a visitor is looking for: a project.
   Red because that is the colour every map anyone has used marks a place with,
   and the point of a convention is that it needs no explaining. */
.pin__mark {
  order: 2;
  display: block;
  width: 24px; height: 34px;
  /* the tip, not the middle, is what stands on the roof */
  margin-bottom: -3px;
  filter: drop-shadow(0 3px 5px rgba(20, 22, 26, 0.35));
  transition: transform 120ms cubic-bezier(0.16, 1, 0.3, 1);
  transform-origin: 50% 100%;
}
.pin__mark path { fill: var(--pin); }
.pin__mark circle { fill: var(--pin-hole); }
.pin:hover .pin__mark, .pin:focus-visible .pin__mark { transform: scale(1.18); }
.pin__label {
  order: 1;
  font-family: "Bricolage Grotesque", system-ui, sans-serif;
  /* A name plate on a landmark, not a tooltip: readable from across the
     quarter, which is the zoom the district fly-in actually lands at. */
  font-size: 17px;
  font-weight: 700;
  letter-spacing: 0.02em;
  padding: 7px 13px;
  background: var(--panel);
  border: 1px solid var(--panel-line);
  border-radius: 9px;
  white-space: nowrap;
  box-shadow: var(--shadow);
}
.pin:hover .pin__label, .pin:focus-visible .pin__label {
  background: var(--ink);
  color: var(--paper);
  border-color: var(--ink);
}

/* The five district buttons are invisible but real: focus reaches them, Enter
   flies in, and their position comes from the projection each frame so they
   cannot drift away from the thing they name. */
.districtbtn {
  position: absolute;
  top: 0; left: 0;
  width: 132px; height: 44px;
  padding: 0;
  border: 0;
  background: transparent;
  color: transparent;
  font: inherit;
  cursor: pointer;
  overflow: hidden;
  will-change: transform;
}
.districtbtn:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 0;
  border-radius: 8px;
}
html.in-district #districtButtons { display: none; }

#hint {
  position: fixed;
  left: 50%;
  bottom: calc(26px + max(var(--sab), var(--vvb)));
  transform: translateX(-50%);
  margin: 0;
  font-size: 12px;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--ink-faint);
  opacity: 0;
  transition: opacity 400ms ease;
  z-index: 30;
  pointer-events: none;
}
#hint.on { opacity: 1; }

/* ----------------------------------------------------------- the DOM index */

.sr-only, #index:not(.index--open) {
  position: absolute;
  width: 1px; height: 1px;
  margin: -1px; padding: 0;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
}
/* still focusable when collapsed — tabbing must reach every project even with
   the sheet shut */
#index:not(.index--open):focus-within {
  position: fixed;
  width: auto; height: auto;
  margin: 0;
  clip-path: none;
  white-space: normal;
}
#index {
  z-index: 50;
}
/* THE LIST, FULL BLEED AND ON ONE PAGE.
   It was a bottom sheet of 210px columns in 13.5px type — a filing cabinet
   drawer. It is the whole screen now, frosted over the city so the map is
   still faintly there, and it holds every project at once with no scrolling:
   one column, numerical, no categories. The categories were a property of a
   project, not a way anyone looks for one; the numbers are how the owner
   refers to these things and they are already in the names.

   The rows SHARE the height of the screen — flex-grow across a fixed-height
   column — so pointing at one takes height from the far ones and gives it to
   that one. That is the Dock, and it is also the only way a fisheye can
   magnify without ever pushing anything off the bottom. hud.js writes the
   grow factor and the size; everything here is the resting state. */
#index.index--open, #index:not(.index--open):focus-within {
  position: fixed;
  inset: 0;
  max-height: none;
  overflow: hidden;
  overscroll-behavior: contain;
  padding: calc(clamp(52px, 8vh, 84px) + var(--sat)) calc(clamp(20px, 4vw, 60px) + var(--sar))
           calc(clamp(24px, 4vh, 48px) + var(--sab)) calc(clamp(20px, 4vw, 60px) + var(--sal));
  /* Translucent, not opaque. The owner asked to be able to make the city out
     behind it. A heavy blur is the wrong lever: at 18px the streets stopped
     being streets and the whole thing was just a pale fog, which is not
     "vaguely making out the city", it is a wash. So the blur comes down to
     where a road is still recognisably a road, and the veil does the
     legibility work instead — 0.55 of paper is plenty of contrast for type
     this size, and the map stays a ghost of itself rather than a smear. */
  background: var(--frost);
  backdrop-filter: blur(7px) saturate(118%);
  -webkit-backdrop-filter: blur(7px) saturate(118%);
  display: block;
}
.index__grid {
  height: 100%;
  display: grid;
  grid-template-columns: minmax(190px, 27%) minmax(0, 1fr);
  gap: clamp(24px, 4vw, 72px);
  align-items: stretch;
}

.index__names {
  margin: 0; padding: 0; list-style: none;
  height: 100%;
  display: flex;
  flex-direction: column;
  align-items: stretch;
  /* The list is exactly one screen tall and never scrolls, so a vertical drag
     across it has no other job — claim it, or the browser reads the gesture as
     a pan, cancels the pointer stream and the fisheye dies halfway down. */
  touch-action: none;
}
.index__names li {
  flex: 1 1 0;
  min-height: 0;
  display: flex;
  align-items: center;
  /* both animated, because both are what the magnification changes — the row
     takes more of the column AND the word inside it gets bigger */
  transition: flex-grow 140ms cubic-bezier(0.16, 1, 0.3, 1),
              font-size 140ms cubic-bezier(0.16, 1, 0.3, 1);
}
.index__names a {
  display: block;
  width: 100%;
  color: var(--ink);
  text-decoration: none;
  font-family: "Bricolage Grotesque", system-ui, sans-serif;
  font-size: inherit;
  font-weight: 600;
  letter-spacing: -0.025em;
  line-height: 1;
  white-space: nowrap;
  transition: color 120ms linear;
}
.index__names li { font-size: clamp(15px, 1.35vw, 20px); }
.index__names li.is-on a { color: var(--pin); }
.index__names a:focus-visible { outline-offset: 3px; }

/* What the thing does, on the right, following the pointer down the list. */
/* Bigger, and tighter with it. This is the only place the map ever says what a
   project DOES, so it is set like something meant to be read across a desk —
   and the air between its four lines comes down as the type goes up, or a
   larger panel just reads as a more spread-out one. */
.index__detail {
  align-self: center;
  max-width: 46ch;
  display: grid;
  align-content: center;
  gap: 4px;
}
.index__kicker {
  margin: 0;
  min-height: 1em;
  font-size: clamp(10px, 0.95vw, 13px);
  font-weight: 600;
  letter-spacing: 0.22em;
  text-transform: uppercase;
  color: var(--ink-faint);
}
.index__title {
  margin: 2px 0 6px;
  font-family: "Bricolage Grotesque", system-ui, sans-serif;
  font-size: clamp(2.3rem, 5.2vw, 4.6rem);
  font-weight: 600;
  letter-spacing: -0.03em;
  line-height: 0.98;
}
.index__blurb {
  margin: 0;
  font-size: clamp(16px, 1.6vw, 23px);
  line-height: 1.42;
  color: var(--ink-soft);
}
.index__open {
  justify-self: start;
  white-space: nowrap;
  margin-top: 12px;
  display: inline-flex;
  align-items: center;
  gap: 8px;
  font-size: 16px;
  color: var(--paper);
  background: var(--ink);
  border-radius: 999px;
  padding: 12px 22px;
  text-decoration: none;
}
.index__open:hover { opacity: 0.86; }

/* The phone keeps BOTH columns.
   It used to drop the detail panel on the grounds that a finger has no hover
   to fill it with — but a finger drags now, and dragging fills it, so the right
   half of the screen was simply empty while the description it should have been
   showing had nowhere to go. Half and half: the names never need more width
   than the longest one at its magnified size, and hud.js magMax() derives that
   size from this column rather than from a constant, so the split can move
   here and the type follows it. */
@media (max-width: 860px) {
  .index__grid { grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); gap: 16px; }
  .index__names li { font-size: clamp(13px, 3.7vw, 18px); }
  .index__detail { max-width: none; gap: 7px; }
  .index__kicker { font-size: 9.5px; letter-spacing: 0.14em; }
  .index__title { font-size: clamp(18px, 5.6vw, 27px); }
  .index__blurb { font-size: 12.5px; line-height: 1.42; }
  .index__open { font-size: 13px; padding: 8px 14px; gap: 6px; }
  /* "Open 5.TRADEOFFS ↗" is two lines in half a phone. The button keeps the
     verb; the aria-label keeps the noun. */
  #indexOpenLabel { display: none; }
}

/* Closing it. The list covers the whole screen now, so the toggle that opened
   it is underneath — there has to be a way out from inside. */
.index__x {
  position: fixed;
  top: calc(14px + var(--sat));
  right: calc(16px + var(--sar));
  z-index: 2;
  width: 44px; height: 44px;
  display: grid;
  place-items: center;
  font: inherit;
  font-size: 16px;
  line-height: 1;
  color: var(--ink);
  background: var(--panel);
  border: 1px solid var(--panel-line);
  border-radius: 50%;
  cursor: pointer;
}
.index__x:hover { background: var(--ink); color: var(--paper); border-color: var(--ink); }
#index:not(.index--open) .index__x { display: none; }
/* ...and no way out of the no-canvas fallback, where the list IS the page.
   The detail panel goes too: nothing is wired to fill it there — the app that
   would do the filling is the thing that failed to start. */
#index.index--only .index__x,
#index.index--only .index__detail { display: none; }
#index.index--only .index__grid { grid-template-columns: 1fr; }

.noscript { position: fixed; inset: 0; display: grid; place-content: center; text-align: center; padding: 24px; }

/* ?bare=1 — the map alone, for judging the cartography. Everything DOM goes;
   app.js separately skips the canvas-drawn district names. */
.bare .hud,
.bare #pins,
.bare #districtButtons,
.bare #card,
.bare #hint,
.bare #intro { display: none !important; }

/* ------------------------------------------------------------- the phone */

@media (max-width: 560px) {
  /* "THE REPOSITORY" at 15px with 0.2em of tracking wraps onto two lines in
     62vw of a 390px screen, and the two quiet lines under it then sit right on
     its descenders. Smaller keeps it on one line, which is what the corner of a
     map wants anyway. */
  /* Two actions left, and they belong together — both are ways out of where
     you are — so they go on ONE line, side by side, instead of stacking with a
     row of air between them. Getting them there is a width problem and both
     halves of it are paid here: the button says "Back" rather than "Back to
     the city" (the aria-label still says all of it), and the tagline in the
     corner stands down while you are inside a quarter, where a way back
     matters more than a description of the site. Together that is 206px of
     actions in 237px of room; either one alone leaves it 11px short. */
  .hud__actions { gap: 10px; }
  .hud--top { gap: 8px; flex-wrap: wrap; }
  /* THE ACTIONS DO NOT MOVE. The theme toggle and the list button live at the
     top right at the overview, and entering a quarter must not relocate them —
     an earlier version dropped the whole row to a second line the moment Back
     joined it, and the two controls a visitor had already found jumped
     somewhere else. Instead the ROW is made to fit: the tagline stands down
     (a way back matters more, right then, than a description of the site) and
     every button loses its long tail — Back to the city becomes Back, Browse
     as list becomes Browse; each aria-label still says all of it. That is
     enough for the trio to sit beside the wordmark on one line, and on a
     screen where it still is not, flex-wrap above gives back the second line
     rather than pushing anything off the edge. */
  html.in-district .wordmark__tag { display: none; }
  html.in-district .textbtn__more { display: none; }
  .iconbtn, .textbtn { font-size: 14px; }
  .iconbtn { font-size: 19px; }
  .worldbtn { font-size: 14px; }
  /* The bottom corner is two lines deep now — the way into /world/ above the
     address — and the hint runs the width of the screen down there, so it has
     to clear the whole block rather than just the address it used to. */
  #hint { bottom: calc(72px + max(var(--sab), var(--vvb))); }
  .wordmark { max-width: 46vw; gap: 2px; }
  /* nowrap, and not only smaller. The wordmark is a grid, so its column is as
     wide as the widest thing in it — the tag, at 28ch — and "THE REPOSITORY"
     landed a hair over that and broke after "THE". Forbidding the break makes
     the title itself size the column, which is the right way round: the name is
     the thing that must not be mangled. */
  .wordmark__title { font-size: 13px; letter-spacing: 0.16em; white-space: nowrap; }
  .wordmark__tag { font-size: 10.5px; max-width: 30ch; }

  .pin__label { font-size: 15px; padding: 6px 11px; }
  #card { padding: 13px 15px 14px; }
  .card__title { font-size: 16.5px; }
  .card__blurb { font-size: 13.5px; }
}

@media (prefers-reduced-motion: reduce) {
  * { transition-duration: 1ms !important; animation-duration: 1ms !important; }
}
