/* ==========================================================================
   Tallinn Tastebuds — tokens
   --------------------------------------------------------------------------
   ink      near-black, cast towards the style's own hue
   muted    secondary text, hairline labels
   paper    the floating cards — the surface that carries the colour
   wash     page ground, button hovers, the ground behind a photo
   hairline the only border weight on the site
   accent   links and pins; Baltic blue here, from the blue of the flag

   Every one of them is restated by each of the seven styles below. Nothing
   on the site names a colour of its own — a component that did would be the
   one thing that failed to change when you press a swatch.
   ========================================================================== */

/* These are Red's, to the value, because Red is the style the page opens on
   and this block is what it wears for the instant before the script sets
   data-style. They used to be a palette of their own, which meant every cold
   load flashed a colour world that is not one of the two. */
:root {
  --ink:        #27141a;
  --muted:      #7d5754;
  --paper:      #fff0ea;
  --wash:       #f7ddd4;
  --hairline:   #f0cec3;
  --accent:     #a81e28;
  --accent-lit: #c9323d;
  --here:       #0b62c4;

  --shadow: 0 1px 2px rgba(74, 26, 22, .06), 0 8px 28px rgba(74, 26, 22, .14);
  --attrib-bg: rgba(255, 240, 234, .88);
  --map-tint: rgba(168, 30, 40, .36);
  --swatch-edge: rgba(0, 0, 0, .14);

  /* Three tokens for chrome that stands on the map with no card under it.
     --lift is a short shadow for a picture at logo size: --shadow is a card's
     shadow, 28px of blur meant to lift a 288px panel, and under a 40px circle
     it reads as a smudge rather than as a lift. --glass is the tint the
     button bar is washed with when the map shows through it. And --glow is
     what holds a letter apart from the map when there is nothing behind it at
     all: paper pressed tight around the glyph on all four sides and a wider
     haze past that, so the ground reads as light around the word rather than
     as a box with an edge. The brand's name, its sentence and its handle all
     wear that one, in both layouts — and it is built out of var(--paper), so
     it turns dark with the rest of the palette without a second line
     anywhere. */
  --lift:  0 1px 6px rgba(74, 26, 22, .28);
  --glass: rgba(255, 240, 234, .74);
  --glow:
     0   1px 2px var(--paper), 0  -1px 2px var(--paper),
     1px 0   2px var(--paper), -1px 0   2px var(--paper),
     0   0   8px var(--paper);

  /* The ring round the mark when a story is up, and what is left of it once
     you have watched them all. A gradient rather than a colour because that
     is the shape the gesture has everywhere else: a ring that is clearly not
     a border is the one thing on a profile picture that reads as "there is
     something new here" without a word of explanation. Ours turns, which is
     the part Instagram's does not do, and it is brick and ember rather than
     their purple — the site has one hue and the ring keeps it. */
  --story-ring: conic-gradient(from 210deg, #a81e28, #d84a2f, #f2a63c, #e1615a, #a81e28);
  --story-ring-seen: linear-gradient(#f0cec3, #f0cec3);

  --display: "Familjen Grotesk", "Helvetica Neue", Helvetica, Arial, sans-serif;
  --body: "Literata", Georgia, "Times New Roman", serif;
  --mono: "IBM Plex Mono", ui-monospace, "SF Mono", Menlo, Consolas, monospace;

  /* Two radii, because the page has two kinds of edge. --radius is the one a
     picture, a tooltip or a sheet gets: nearly square, so a photograph still
     reads as a photograph. --radius-soft is for the chrome floating on the
     map, where a 4px corner is small enough to read as a box someone forgot
     to round rather than as a deliberate shape. */
  --radius: 4px;
  --radius-soft: 12px;
  --gap: 16px;
  /* The measure the brand is set to, and the number the chip row starts
     after: two rules that have to agree, so they read the same one. It is
     the width the sentence had inside the card that used to be drawn here —
     the card is gone, the line breaks are not supposed to move with it. */
  --brand-w: 250px;
  --panel-w: 420px;
  --ease: cubic-bezier(.22, .61, .36, 1);

  --safe-t: env(safe-area-inset-top, 0px);
  --safe-b: env(safe-area-inset-bottom, 0px);
  --safe-l: env(safe-area-inset-left, 0px);
  --safe-r: env(safe-area-inset-right, 0px);
}

/* ---------- reset ---------- */

*, *::before, *::after { box-sizing: border-box; }

html, body {
  height: 100%;
  margin: 0;
  /* iOS inflates text on its own when it decides a page was written for a
     desktop and is being read on a phone. This one was written for the phone,
     and every size in here is a chosen one. */
  -webkit-text-size-adjust: 100%;
  text-size-adjust: 100%;
}

body {
  background: var(--wash);
  color: var(--ink);
  font-family: var(--body);
  font-size: 15px;
  line-height: 1.6;
  -webkit-font-smoothing: antialiased;
  overflow: hidden;
  /* --------------------------------------------- the browser's own zoom
   * This page is a map, not a document: it fills the window, nothing behind
   * it scrolls, and every gesture over it is meant for Leaflet. But the
   * chrome standing on the map — the chip row, the rail, the brand card, the
   * corner buttons — is ordinary DOM, and the browser claims gestures that
   * land on it. On a phone that went wrong two ways.
   *
   * Double tap: a chip is a toggle, so tapping one twice quickly is a normal
   * thing to do, and to Safari it is the gesture that zooms the page in on
   * whatever was tapped. `manipulation` is `auto` minus that one gesture, so
   * taps, drags and pinches all still work and the double tap stops being a
   * zoom. It sits on the body, where every control inherits it: the permitted
   * gestures are the intersection down the tree, so the map keeps the `none`
   * Leaflet sets on its own container and goes on handling pinch itself.
   *
   * The other way is pinch, and it is handled per element below.
   */
  touch-action: manipulation;
}

/* A pinch aimed at the map that starts a finger on the chrome is a pinch the
   browser zooms the *page* with — and on a phone the chip row spans the top
   and the rail runs down the left, so that is easy to do by accident. The
   result is the whole interface suddenly twice the size, panned to wherever
   the fingers were, with the header off the top of the screen; and since
   nothing here scrolls, there is no obvious way back out of it. So the
   chrome does not take pinches: nothing inside these scrolls, so `none`
   costs them nothing.
   Two deliberate exceptions. The chip row sets its own `pan-x` further down,
   which already excludes pinch — and it has to be set on the scroller itself,
   because a `none` above it would forbid the sideways drag as well. And the
   sheet is left alone: it is the one part of this that is text to sit and
   read, so pinching it still zooms, which is the accessible answer. */
.brand,
.controls,
.rail { touch-action: none; }

/* The bar is a full-width strip and the chips only fill part of it, so the
   rest of it was a piece of dead map that swallowed pinches and drags. The
   bar stops taking touches at all; the scroller inside it takes its own. */
.filter-bar { pointer-events: none; }
.filters { pointer-events: auto; }

h1, h2, h3, p, figure, ul, ol { margin: 0; }
ul, ol { padding: 0; list-style: none; }
img { display: block; max-width: 100%; }

button {
  font: inherit;
  color: inherit;
  background: none;
  border: 0;
  margin: 0;
  padding: 0;
  cursor: pointer;
}

a { color: var(--accent); text-underline-offset: 2px; }
a:hover { color: var(--accent-lit); }

:focus-visible {
  outline: 2px solid var(--accent-lit);
  outline-offset: 2px;
  border-radius: 2px;
}

/* ---------- shared bits ---------- */

.card {
  background: var(--paper);
  border: 1px solid var(--hairline);
  border-radius: var(--radius);
  box-shadow: var(--shadow);
}

.eyebrow {
  font-family: var(--mono);
  font-size: 10px;
  font-weight: 500;
  letter-spacing: .16em;
  text-transform: uppercase;
  color: var(--muted);
}

.mono { font-family: var(--mono); }

/* ---------- the controls, and there are four ----------
   Every pressable thing on this site is one of four shapes, and they are
   drawn here once rather than per page. See "The design rules" in the README.

     .go     the filled action. One per surface, never two.
     .alt    the quiet one beside it: a way out, a switch, a second thought.
     .chip   a toggle in the filter row — further down, with the row.
     .menu   a list of places to go, drawn as rows rather than as links.

   The map and the lists page used to keep their own copies of the first two,
   which had drifted a whole typeface apart: one page's primary action was a
   mono pill and the other's was set in the display face. Two pages, one site,
   one button. */

.go {
  display: inline-block;
  padding: 11px 18px;
  background: var(--accent);
  border: 1px solid var(--accent);
  border-radius: 100px;
  color: var(--paper);
  font-family: var(--mono);
  font-size: 12px;
  font-weight: 500;
  letter-spacing: .06em;
  text-align: center;
  text-decoration: none;
  cursor: pointer;
}
.go:hover { background: var(--accent-lit); border-color: var(--accent-lit); }
.go[disabled] { opacity: .6; cursor: default; }

.alt {
  display: inline-block;
  padding: 9px 12px;
  background: none;
  border: 0;
  color: var(--muted);
  font-family: var(--mono);
  font-size: 12px;
  letter-spacing: .02em;
  text-decoration: underline;
  text-underline-offset: 3px;
  cursor: pointer;
}
.alt:hover { color: var(--ink); }
.alt.is-danger:hover { color: var(--accent); }
.alt[disabled] { opacity: .5; cursor: default; }
.alt[disabled]:hover { color: var(--muted); }

/* A list of places to go. Rows, hairline-ruled, the full width of the card:
   what tells one from another is the line under the name saying what it does,
   and the target is the whole row rather than the length of a word. */
.menu { display: flex; flex-direction: column; }

.menu-row {
  display: flex;
  align-items: center;
  gap: 12px;
  width: 100%;
  padding: 13px 2px;
  border: 0;
  border-top: 1px solid var(--hairline);
  background: none;
  color: inherit;
  text-align: left;
  text-decoration: none;
  cursor: pointer;
}
.menu-item:last-child .menu-row { border-bottom: 1px solid var(--hairline); }

.menu-say { display: flex; flex-direction: column; gap: 3px; min-width: 0; }
.menu-name {
  font-family: var(--display);
  font-size: 15.5px;
  font-weight: 600;
  letter-spacing: -.01em;
}
.menu-why {
  font-family: var(--mono);
  font-size: 11px;
  line-height: 1.4;
  color: var(--muted);
}

.menu-go {
  flex: 0 0 auto;
  margin-left: auto;
  color: var(--muted);
  transition: transform .15s var(--ease), color .15s var(--ease);
}
.menu-go svg {
  display: block;
  width: 17px;
  height: 17px;
  fill: none;
  stroke: currentColor;
  stroke-width: 1.7;
  stroke-linecap: round;
  stroke-linejoin: round;
}

.menu-row:hover .menu-name,
.menu-row:hover .menu-go { color: var(--accent); }
.menu-row:hover .menu-go { transform: translateX(2px); }


@media (prefers-reduced-motion: reduce) {
  .menu-go { transition: none; }
  .menu-row:hover .menu-go { transform: none; }
}

.skip-link {
  position: absolute;
  left: -9999px;
  top: 0;
  z-index: 3000;
  padding: 10px 14px;
  background: var(--paper);
  border: 1px solid var(--ink);
  font-family: var(--mono);
  font-size: 12px;
}
.skip-link:focus { left: var(--gap); top: var(--gap); }

/* ---------- map ---------- */

#map {
  position: fixed;
  inset: 0;
  background: var(--wash);
  z-index: 0;
}

.leaflet-container {
  font-family: var(--mono);
  background: var(--wash);
}

/* The tiles carry the style's hue. Only the tile pane is filtered, so pins,
   tooltips and every control keep their own exact token colours. */
/* A luminance-preserving wash over the tiles. mix-blend-mode: color takes hue
   and saturation from this layer but lightness from the map underneath, so
   roads and labels keep exactly the contrast CARTO drew them with. A filter()
   chain cannot do this: sepia() flattens Positron's light greys into one tone
   and the streets vanish. Measured, the tint costs 0-4% of label contrast;
   the sepia chain it replaces cost 30-52%. */
#map::after {
  content: "";
  position: absolute;
  inset: 0;
  z-index: 250;              /* over tilePane (200), under overlayPane (400) */
  pointer-events: none;
  background: var(--map-tint, transparent);
  mix-blend-mode: color;
}

/* One dot standing in for the pins too close together to tell apart. It is
   a count and not a place, so it is a flat disc with a numeral on it and no
   picture underneath: the mark is what a pin wears, and a cluster wearing it
   too was a dozen mouths across a phone at city zoom, each nearly twice the
   size of a pin, with the numbers cased in paper to survive the picture. The
   places it stands for are still places — pressing it is how you get to
   them — but the thing on the screen is the arithmetic, and a numeral on a
   disc is how a count reads.

   It still never reads as a place: a pin is the painting in a collar, a
   cluster is a disc in the accent with the numeral in paper, and it is bigger
   than any pin at every count.

   --cluster-scale is what a phone draws it at: the box Leaflet sizes is the
   same, the disc inside it is four-fifths, and the numeral shrinks with it.
   The dot is centred in its box by the grid on .cluster-pin, which is what
   lets the box keep its anchor while the disc changes size. */
.cluster-pin { display: grid; place-items: center; background: none; border: 0; }
.cluster-dot {
  display: grid;
  place-items: center;
  width: calc(100% * var(--cluster-scale, 1));
  height: calc(100% * var(--cluster-scale, 1));
  border-radius: 100px;
  background: var(--accent);
  box-shadow: 0 0 0 2.5px var(--paper), 0 1px 3px rgba(20, 26, 31, .3);
  color: var(--paper);
  font-family: var(--mono);
  font-weight: 500;
  line-height: 1;
  transition: transform .15s var(--ease), box-shadow .15s var(--ease), background-color .15s var(--ease);
}

/* Two places under one dot is barely a crowd — it is the pair of doors you
   could not tell apart at this zoom — and it is the quiet one: paper, with
   the number and the rim in the accent. Past two the disc fills with the
   style's own colour, the same brick or mint, so the dots worth pressing are
   the ones the map says something with. It never reads as a pin even wearing
   the accent: a pin is 22px, carries the painting and no number, and the
   smallest of these is a third wider than one. */
.cluster-dot:not(.is-many) {
  background: var(--paper);
  color: var(--accent);
  box-shadow: 0 0 0 2px var(--accent), 0 1px 3px rgba(20, 26, 31, .3);
}
.cluster-pin:hover .cluster-dot.is-many,
.cluster-pin:focus-visible .cluster-dot.is-many {
  background: var(--accent-lit);
}
.cluster-pin:hover .cluster-dot:not(.is-many),
.cluster-pin:focus-visible .cluster-dot:not(.is-many) {
  box-shadow: 0 0 0 2px var(--accent-lit), 0 1px 3px rgba(20, 26, 31, .3);
}

/* The count grows with the dot it sits in — a third of the width, so ten
   places carry a 14px number where two carry a 12px one. A fixed 12px was
   the whole reason the digits were hard to find: it read as a caption rather
   than as the thing the disc is there to count. Nothing is cased any more:
   paper on the accent is the contrast the rest of the site's buttons read
   at, and the casing went with the picture it was there to survive. */
.cluster-count {
  font-size: calc(var(--cluster-d, 40px) * .36 * var(--cluster-scale, 1));
  transition: color .15s var(--ease);
}
.cluster-pin:hover .cluster-dot,
.cluster-pin:focus-visible .cluster-dot { transform: scale(1.08); }
.cluster-pin:hover .cluster-count,
.cluster-pin:focus-visible .cluster-count { color: var(--accent-lit); }
.cluster-pin:focus-visible { outline: none; }
.cluster-pin:focus-visible .cluster-dot {
  box-shadow: 0 0 0 2.5px var(--paper), 0 0 0 4.5px var(--accent-lit);
}
@media (prefers-reduced-motion: reduce) {
  .cluster-pin:hover .cluster-dot,
  .cluster-pin:focus-visible .cluster-dot { transform: none; }
}

/* On a phone the pins draw at four-fifths and the clusters likewise. Seventy
   places at city zoom on a 390px screen is a dozen clusters and a scatter of
   pins, and at desk size they were the whole screen: 22px pins and 40px
   discs on a map a hand wide. The script still hands every pin its size in
   --pin-d — 22, 17, 34 for the chosen one — and the stylesheet scales what it
   was handed, so the three readings and the chosen one keep their
   proportions and nothing in assets/app.js knows the screen is narrow. */
@media (max-width: 480px) {
  .leaflet-container { --pin-scale: .82; --cluster-scale: .8; }
}

/* Where you are, which is not one of the places on the map and should never
   look like one. The dot carries a hue that no style's accent uses, plus the
   ring of real accuracy around it, which no pin ever has. */
.here-accuracy {
  animation: herePulse 2.6s var(--ease) infinite;
}
@keyframes herePulse {
  0%, 100% { fill-opacity: .12; }
  50%      { fill-opacity: .22; }
}

.leaflet-control-attribution {
  font-family: var(--mono) !important;
  font-size: 10px !important;
  line-height: 1.5;
  padding: 3px 8px !important;
  background: var(--attrib-bg) !important;
  border: 1px solid var(--hairline);
  border-right: 0;
  border-bottom: 0;
  color: var(--muted) !important;
}
.leaflet-control-attribution a { color: var(--muted) !important; }
.leaflet-control-attribution a:hover { color: var(--accent) !important; }

/* pin tooltip */
.leaflet-tooltip.pin-tip {
  font-family: var(--display);
  font-size: 13px;
  font-weight: 600;
  letter-spacing: -.005em;
  color: var(--ink);
  background: var(--paper);
  border: 1px solid var(--hairline);
  border-radius: var(--radius);
  box-shadow: var(--shadow);
  padding: 3px 9px;
}
.leaflet-tooltip.pin-tip::before { display: none; }

/* An open name is a target, not a caption. Leaflet turns the pointer events
   on; this says so on the way in. */
.leaflet-tooltip.pin-tip.leaflet-interactive {
  cursor: pointer;
  transition: border-color .15s var(--ease), color .15s var(--ease);
}
.leaflet-tooltip.pin-tip-quiet.leaflet-interactive:hover {
  color: var(--ink);
  border-color: var(--muted);
}

/* Names that stand open past zoom 14 sit quieter than the chosen one: paper,
   not accent, and behind it in the stack, so the place you picked still reads
   first on a map where a dozen names are showing. */
.leaflet-tooltip.pin-tip-quiet {
  color: var(--muted);
  font-weight: 500;
  z-index: 600;
}

/* The chosen place: its name label stays open rather than waiting for a hover
   that never comes on a phone, and a ring is drawn around the dot. */
.leaflet-tooltip.pin-tip-on {
  background: var(--accent);
  border-color: var(--accent);
  color: var(--paper);
  font-weight: 600;
  z-index: 650;
}

/* The place you last had open, with the panel now shut. It keeps the open
   label rather than dropping back into the grey ones — the map has to go on
   saying which place you were reading, or closing a write-up loses the place
   it was about — but it is drawn in outline where the open one is filled, so
   the two are never the same state said twice. */
.leaflet-tooltip.pin-tip-kept {
  background: var(--paper);
  border-color: var(--accent);
  color: var(--accent);
  font-weight: 600;
  z-index: 640;
}

.pin-halo {
  pointer-events: none;
  animation: halo-breathe 2.6s ease-in-out infinite;
}
/* Steadier than the open one: still there, no longer asking to be looked at. */
.pin-halo-kept { animation-duration: 4.2s; }
@keyframes halo-breathe {
  0%, 100% { opacity: .9; }
  50%      { opacity: .3; }
}
@media (prefers-reduced-motion: reduce) {
  .pin-halo { animation: none; opacity: .85; }
  .pin-halo-kept { opacity: .6; }
}
/* The broken circle round a closed place. Inert, like the halo: the dot
   inside it is the thing you click, and a ring that took the pointer would
   swallow the taps aimed at the edge of its own pin. */
.pin-shut { pointer-events: none; }

/* Where you are. Still a plain circle, and now the only one on the map — a
   place wears the mark, so nothing else can be mistaken for you. */
.pin-here { pointer-events: none; }

/* Every place on the map is the mark — the painting itself, cropped round,
   not a drawing of it. A plain circle is left to the one dot out there that
   is not a place: the one that says where you are.

   The icon Leaflet anchors is a fixed square, so a pin can grow or shrink
   without the anchor moving under it. The square takes no pointer and the
   mark inside it takes all of them, so a tap only ever lands on the picture
   you can see rather than on the empty corners around it. */
.pin-mark {
  display: grid;
  place-items: center;
  background: none;
  border: 0;
}
/* Leaflet claims the whole icon box for the pointer at .leaflet-marker-icon
   .leaflet-interactive, which outranks a single class — so the hand-back has
   to be said at the same weight. */
.pin-mark.leaflet-interactive { pointer-events: none; }
.pin-mark:focus { outline: none; }

.pin-face {
  width: calc(var(--pin-d, 22px) * var(--pin-scale, 1));
  height: calc(var(--pin-d, 22px) * var(--pin-scale, 1));
  border-radius: 50%;
  background: var(--paper) url(logo/mark-round.webp) center / cover no-repeat;
  pointer-events: auto;
  cursor: pointer;
  transition: width .15s var(--ease), height .15s var(--ease),
              box-shadow .15s var(--ease), transform .15s var(--ease);
}
.pin-mark:hover .pin-face { transform: scale(1.12); }

/* One picture, three readings of it, for the three amounts of place behind
   it. The mark cannot change — that is the whole point of putting it on every
   pin — so the collar drawn round it carries what the fill of a dot used to:
   filmed is a solid collar, photographed is a paper gap and then a hairline,
   which is a different silhouette rather than a paler version of the same
   one, and the write-up on its own is smaller and quieter. Told apart at a
   glance, at the size the map actually draws them. */
.is-reel .pin-face {
  box-shadow: 0 0 0 2.5px var(--pin-tone, var(--accent)), 0 1px 3px rgba(20, 26, 31, .32);
}
.is-photos .pin-face {
  box-shadow: 0 0 0 2.5px var(--paper), 0 0 0 4.2px var(--pin-tone, var(--accent)),
              0 1px 3px rgba(20, 26, 31, .28);
}
.is-words .pin-face {
  box-shadow: 0 0 0 2px var(--paper), 0 0 0 3px var(--pin-tone, var(--accent)),
              0 1px 2px rgba(20, 26, 31, .24);
  opacity: .8;
}

/* The one you are looking at keeps whichever of the three it is — it is
   bigger, brighter-collared and haloed, which is already three ways of
   saying it — but it is never the faded one. */
.is-chosen .pin-face,
.is-kept .pin-face { opacity: 1; }

/* Shut for good: the paint drains out of the mark. The broken circle drawn
   round it says the same thing a second time, in the shape the list uses. */
.is-shut .pin-face { filter: grayscale(1) contrast(.92); opacity: .86; }

.pin-mark:focus-visible .pin-face {
  box-shadow: 0 0 0 2.5px var(--paper), 0 0 0 5px var(--accent-lit);
  opacity: 1;
}

@media (prefers-reduced-motion: reduce) {
  .pin-face { transition: none; }
  .pin-mark:hover .pin-face { transform: none; }
}

/* ---------- brand ---------- */

/* No card, at either width. There was one here for the tagline's sake — a
   line of serif prose over a map full of street names looked like the one
   thing a glow behind the letters could not rescue — and what it actually
   put in the corner was a filled box with an edge, sitting on the map the
   site is entirely about. The glow does hold the sentence: --glow is paper
   packed tight around each glyph, so the letters carry their own ground with
   them and the streets fade out behind the words instead of stopping at a
   border. The phone layout had been proving that for the handle for as long
   as it has existed; this is the desktop taking the same deal.

   The measure stays what it was inside the card — --brand-w is the 288px box
   less the padding it used to hold — because that is what breaks the name
   after Tallinn and the sentence into two lines, and it is the number
   .filter-bar starts the chip row after, so the two columns still clear each
   other.

   And with nothing drawn here, nothing here takes a press either. A card was
   honest about swallowing the drag underneath it; a transparent box the same
   size is not — the words are lying on the city and the city has to come away
   under them. The two things that are actually pressed — the mark when a
   story is up, and the handle — take their pointer events back one at a time.
   Above the chip row nothing drags the map in any case, guard or no guard
   (see wireTopGuard in assets/app.js), so what this frees in practice is the
   sentence and the handle, which hang below that line. */
.brand {
  position: fixed;
  z-index: 1001;
  top: calc(var(--gap) + var(--safe-t));
  left: calc(var(--gap) + var(--safe-l));
  width: var(--brand-w);
  pointer-events: none;
}
.brand-ring,
.brand-home,
.brand .ig-link { pointer-events: auto; }

/* The name, which is the way back to the whole map. It takes the ink and the
   size of the heading it is inside — a logo that goes home is not a link to
   be picked out of a sentence — and answers on hover the way the same name
   does at the top of the lists page. */
.brand-home {
  color: inherit;
  text-decoration: none;
}
.brand-home:hover,
.brand-home:focus-visible { color: var(--accent); }

/* The mark and the name, locked together. Stacked above it the mark was a
   picture parked in the corner of the card with half a line of nothing beside
   it, which read as something left there rather than as the logo. Beside the
   name it has a job: the wordmark breaks after Tallinn, and two lines of it
   stand as tall as the picture does.
   The break is the width doing it, not a <br> — the wordmark is set from
   ui.json by textContent, which would eat one. Every language ships the same
   two words, so every language breaks in the same place. */
.brand-head {
  display: flex;
  align-items: center;
  gap: 13px;
  margin: 0 0 10px;
}

/* Round, and the square crop rather than the wide one. The site frames its
   pictures at 2px and no border, and the mark was framed the same way — but
   this one is not a picture on a page, it is the account's face at the top
   left of a map, in the position and at the size every profile picture on a
   phone occupies. Round is what that position means, and the mouth fills a
   circle where the wide crop would have had to be cut again to fit one.
   66px, not the 82 the wide crop ran at: a circle at 82 stands taller than
   the two lines of the wordmark beside it and starts leading the card. */
.brand-mark {
  width: 66px;
  height: 66px;
  border-radius: 100px;
  object-fit: cover;
  flex: 0 0 auto;
  /* A paper ring rather than a border: it holds the circle apart from
     whatever the map puts behind it without drawing a second edge, and it is
     the one the pins already wear. The short shadow under it is the same one
     the pins take, for the same reason — a photograph lying straight on the
     city needs a millimetre of air under it to read as a thing on top. */
  box-shadow: 0 0 0 2px var(--paper), var(--lift);
}

/* ---------- the story ring ---------- */

/* When something is up, the mark stops being a picture and becomes a button.
   The ring is drawn outside the box the picture occupies — inset is negative,
   so nothing in the card moves when a story goes up or runs out — and the
   paper gap between ring and photograph is a box-shadow on the mark rather
   than a border, which is what the pins already wear.

   Two things move, and they are asking for two different things.

   The ring turns, which says the thing behind it is live. A turning ring is
   only legible if there is something on it to watch go past, so the gradient
   is not an even wheel of colour: it has one bright arc in it and the rest is
   the site's own brick, and that arc is what the eye follows round.

   And every few seconds a second ring leaves it and opens outwards, the way
   a dropped stone leaves a ripple. That is the part that catches somebody who
   is not looking at the corner of the screen. It runs for a fifth of its
   cycle and is gone for the rest, because a thing that pulses without pause
   is not asking any more, it is nagging — and this is a map somebody is
   trying to read.

   Both stop the moment the last story has been watched. */
.brand-ring {
  position: relative;
  display: block;
  flex: 0 0 auto;
  line-height: 0;
  border-radius: 100px;
  cursor: pointer;
}
.brand-ring::before {
  content: "";
  position: absolute;
  inset: -5px;
  border-radius: 100px;
  background: var(--story-ring);
  animation: story-spin 3.4s linear infinite;
}
/* The ripple. Drawn as a border with nothing inside it, so what expands over
   the map is a hoop rather than a disc, and it is behind nothing: the mark
   sits above it and the photograph is never dimmed by its own halo. */
.brand-ring::after {
  content: "";
  position: absolute;
  inset: -5px;
  border-radius: 100px;
  border: 2px solid var(--accent-lit);
  opacity: 0;
  pointer-events: none;
  animation: story-ping 5s var(--ease) infinite;
}
.brand-ring .brand-mark {
  position: relative;
  box-shadow: 0 0 0 2.5px var(--paper), var(--lift);
  transition: transform .18s var(--ease);
}
/* A pointer gets the last nudge: the mark leans up a step under it, which is
   the plainest way a picture says it is a button. */
@media (hover: hover) and (pointer: fine) {
  .brand-ring:hover .brand-mark { transform: scale(1.06); }
}
/* Watched, and still up. Instagram greys the ring out at this point and so
   does this, down to the hairline the rest of the site draws its edges in —
   the story has not gone anywhere, it is just no longer asking. Nothing
   turns, nothing ripples: it has had its go. */
.brand-ring.is-seen::before {
  background: var(--story-ring-seen);
  animation: none;
}
.brand-ring.is-seen::after { animation: none; }
.brand-ring:focus-visible { outline-offset: 6px; }

@keyframes story-spin { to { transform: rotate(1turn); } }

/* Two ripples, close together, and then three seconds of nothing: a double
   blink reads as something signalling, where one on its own can be missed and
   a steady beat is a fire alarm.

   They go wide. A hoop that reaches half again the size of the mark crosses
   twenty-odd pixels of map, which is a movement you catch with your eye on
   the other side of the screen — the point of the thing. It passes over the
   first letter of the wordmark on the way out and that is fine: by the time
   it is out that far it is nearly transparent, and it is gone a quarter of a
   second later. Both ripples snap back to the ring's own size while they are
   invisible, which is the whole trick: the eye only ever sees outward. */
@keyframes story-ping {
  0%   { opacity: .6;  transform: scale(1); }
  16%  { opacity: 0;   transform: scale(1.6); }
  17%  { opacity: .5;  transform: scale(1); }
  33%  { opacity: 0;   transform: scale(1.6); }
  100% { opacity: 0;   transform: scale(1.6); }
}

/* All three lines wear the glow, because all three of them are lying on the
   map. The sentence takes the ink rather than the muted grey it wore inside
   the card as well: on paper a second colour reads as a caption under a
   name, and out here it reads as text that has not quite landed. */
.wordmark {
  font-family: var(--display);
  font-size: 25px;
  font-weight: 600;
  letter-spacing: -.025em;
  line-height: 1.1;
  margin: 0;
  text-shadow: var(--glow);
}

.tagline {
  font-size: 13.5px;
  line-height: 1.55;
  color: var(--ink);
  margin-bottom: 11px;
  text-shadow: var(--glow);
}

/* The underline went with the card: a hairline under text lying on a map is
   a stray line, one more thing in the corner with an edge. It comes back on
   hover and on focus, where it is answering a question rather than
   decorating — and the border stays in the box either way, so nothing shifts
   by a pixel when it does. */
.ig-link {
  display: inline-block;
  font-family: var(--mono);
  font-size: 11.5px;
  letter-spacing: .01em;
  text-decoration: none;
  border-bottom: 1px solid transparent;
  padding-bottom: 1px;
  text-shadow: var(--glow);
}
.ig-link:hover,
.ig-link:focus-visible { border-bottom-color: currentColor; }

/* ---------- top-right controls ---------- */

/* Two things left once the zoom buttons went and the locate button moved to
   the far corner, so they sit on one line instead of a column: it frees the
   whole band under them for the chips, and the top of the map becomes a
   single strip of chrome. row-reverse keeps the language switch in the
   corner, where its menu drops from. Above the chip row in the stack, because
   the chip scroller claims invisible padding around itself and a button must
   always win the tap. */
.controls {
  position: fixed;
  z-index: 1001;
  top: calc(var(--gap) + var(--safe-t));
  right: calc(var(--gap) + var(--safe-r));
  display: flex;
  flex-direction: row-reverse;
  align-items: flex-start;
  gap: 8px;
  transition: right .34s var(--ease);
}

.seg {
  display: flex;
  overflow: hidden;
}

/* The pair in the top-right corner are the only chrome on the map that is
   pressed rather than read, so they keep a surface when the brand loses its
   own — but it is a pane rather than a card: the map goes on underneath,
   blurred, and the hairline border comes off. What was three filled boxes
   across the top of the screen is now one photograph, one line of text and
   two panes you can see the city through.
   The rail down the left keeps its solid pills on purpose. Those are round
   and they sit in the middle of the map; frosting them as well would leave
   nothing on the page with an edge you can trust. */
.controls .seg {
  border-radius: var(--radius-soft);
  background: var(--glass);
  -webkit-backdrop-filter: blur(10px) saturate(1.3);
  backdrop-filter: blur(10px) saturate(1.3);
  box-shadow: var(--lift);
}

/* Firefox before 103 and anything older ignore the blur and would be left
   with a bar you can read the street names through. They get the paper. */
@supports not ((backdrop-filter: blur(2px)) or (-webkit-backdrop-filter: blur(2px))) {
  .controls .seg {
    background: var(--paper);
    border: 1px solid var(--hairline);
  }
}

.btn {
  display: grid;
  place-items: center;
  min-width: 38px;
  height: 38px;
  color: var(--muted);
  transition: background-color .15s var(--ease), color .15s var(--ease);
}
.btn + .btn { border-left: 1px solid var(--hairline); }
.btn:hover { background: var(--wash); color: var(--ink); }
.btn[aria-pressed="true"], .btn.is-on { color: var(--paper); background: var(--accent); }

/* The language switch has two shapes. Wide enough, it is the row of codes it
   always was. On a phone it is the current code with a menu under it, because
   ten codes side by side reach the handle in the opposite corner. */
.btn-lang-now { display: none; }
.lang-list { display: flex; }
.lang-name { display: none; }

/* The pin on the Places button. It used to carry a magnifier and say List,
   which named the shape of the panel rather than what is in it; the word is
   the places now and the glyph is the same pin the map is covered in, so the
   button and the thing it opens are drawn with the same mark. */
.btn-wide {
  display: flex;
  align-items: center;
  gap: 6px;
  padding: 0 14px;
  font-family: var(--mono);
  font-size: 11px;
  font-weight: 500;
  letter-spacing: .13em;
  text-transform: uppercase;
}
.btn-ico {
  flex: 0 0 auto;
  width: 14px;
  height: 14px;
  fill: none;
  stroke: currentColor;
  stroke-width: 2;
  stroke-linecap: round;
}

.btn-lang {
  padding: 0 11px;
  font-family: var(--mono);
  font-size: 11px;
  font-weight: 500;
  letter-spacing: .1em;
  min-width: 0;
}

/* ==========================================================================
   The two styles — brick by day, forest by night.

   Each is a colour world rather than an accent swap: card, ground, ink,
   hairline and shadow all move to the hue, and the map takes the same hue
   with them. Everything is redefined as tokens only, never as component
   rules, so the site follows.

   - The card is the surface you actually look at, so it carries the hue.
     An earlier version kept the light styles' paper within a point of white,
     which measured out at a colour difference of 2-5 dE between any two of
     them, i.e. the styles were the pins and nothing else. Red's paper sits
     at L* 96 with real chroma, so switching styles changes the page rather
     than the map key.
   - The colour lives in the chrome and the water, not the land. An earlier
     version tinted the tiles with sepia() + saturate() + hue-rotate() and it
     made the map unreadable: sepia flattens Positron's light greys into a
     single tone, so road-against-land contrast fell from 1.30 to about 1.03
     and labels lost 30-52% of theirs. --map-tint replaces it with
     mix-blend-mode: color, which takes hue from the tint and lightness from
     the tiles, so contrast is preserved by construction. Modelled on
     Positron's own tones through the compositing spec's ClipColor, every
     pair that matters — road/land, land/label, road/label, land/water —
     holds at 94-104% of untinted all the way to alpha .45. So .36, not the
     .22 it used to run at: Positron's land is too light to hold saturation
     either way, but the bay is not, and in a coastal city the bay is a
     third of the screen.
   - Both accents clear 4.5:1 on their own paper and their own wash.
   - --here paints the "you are here" dot, and is deliberately a hue neither
     accent uses: a blue dot next to brick pins, a warm one against green.
     Otherwise you cannot tell yourself from a restaurant.
   - Green is the dark one and swaps to CARTO Dark Matter.
   ========================================================================== */

/* Brick and ceramic — the daylight one, and what the page opens on. Its
   accent is darker than its own hue reads: oxblood, not pillarbox. */
[data-style="red"] {
  --ink: #27141a; --muted: #7d5754; --paper: #fff0ea; --wash: #f7ddd4;
  --hairline: #f0cec3; --accent: #a81e28; --accent-lit: #c9323d;
  --map-tint: rgba(168, 30, 40, .36);
  --shadow: 0 1px 2px rgba(74, 26, 22, .06), 0 8px 28px rgba(74, 26, 22, .14);
  --lift: 0 1px 6px rgba(74, 26, 22, .28);
  --glass: rgba(255, 240, 234, .74);
  --attrib-bg: rgba(255, 240, 234, .88);
  --swatch-edge: rgba(0, 0, 0, .14);
  --here: #0b62c4;
  --story-ring: conic-gradient(from 210deg, #a81e28, #d84a2f, #f2a63c, #e1615a, #a81e28);
  --story-ring-seen: linear-gradient(#f0cec3, #f0cec3);
}

/* Forest — the dark one. */
[data-style="green"] {
  --ink: #e7f1ea; --muted: #a2b7a9; --paper: #1d2a23; --wash: #101a15;
  --hairline: #354740; --accent: #6fd39a; --accent-lit: #93e7b7;
  --map-tint: transparent;
  --shadow: 0 1px 2px rgba(0, 0, 0, .34), 0 8px 28px rgba(0, 0, 0, .46);
  --lift: 0 1px 6px rgba(0, 0, 0, .5);
  /* Denser than the light style's. Dark Matter under it is nearly black even
     after the brightness lift, so a bar this side of opaque loses its own
     edge against the map instead of sitting on it. */
  --glass: rgba(29, 42, 35, .82);
  --attrib-bg: rgba(16, 26, 21, .86);
  --swatch-edge: rgba(255, 255, 255, .22);
  --here: #e0873a;
  /* The same ring in the dark style's own hue, with the ember arc left in it:
     a red ring on a green page would be the one thing on the site that did
     not follow the swatch you pressed. */
  --story-ring: conic-gradient(from 210deg, #6fd39a, #b7e8a4, #f2a63c, #93e7b7, #6fd39a);
  --story-ring-seen: linear-gradient(#354740, #354740);
}

/* Dark Matter is drawn almost black, which reads as a hole in the screen next
   to the panel. The tiles get a brightness lift, then a screen pass in the
   style's own hue.
   The screen is the half doing the real work. A multiplier cannot lift a black
   off zero, so brightness alone leaves the land where it started; screen adds,
   and adds most to the darkest tones. Measured on Dark Matter's own tones, the
   pair takes the land from #1a1c1e to around #44474f and the water from
   #0e1013 to #353843, while label contrast reads 5.7-5.8 against the 5.0 it
   has untouched. Lighter and still more legible than raw, which is the trade
   worth making; past this the labels start giving that back. */
[data-style="green"] .leaflet-tile-pane { filter: brightness(1.4); }
[data-style="green"] #map::after {
  mix-blend-mode: screen;
  background: rgba(176, 226, 197, .18);
}

/* ------- the left rail: who you are, the two picks, the two settings -------

   Top to bottom: the account, Surprise me, the radio, the colour swatch,
   where am I. The account leads because it is the only one whose answer
   outlasts the visit, and because it wears the name of whoever is signed in.
   Then the two that change your evening, then the two that change the map: a
   rail that opens with a colour picker reads as a settings strip rather than
   as the shortcut it is. */

.rail {
  position: fixed;
  z-index: 1000;
  left: calc(var(--gap) + var(--safe-l));
  top: 50%;
  transform: translateY(-50%);
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: 10px;
}

/* One pill, written once, worn by every button on it. The account, Surprise me and the
   radio had it and the two below them did not: a colour swatch in a round
   case of its own and a crosshair in a second one, both of them mute, under
   three buttons that say what they are. Down the left edge of the map that
   read as two things somebody had forgotten to finish rather than as the
   quieter half of one rail — the eye counts labels before it counts jobs. So
   the swatch and the crosshair are pills too, wearing the words they were
   already carrying for the phone's sake, and the rail is one column of one
   shape.

   display on a class beats the UA sheet's [hidden] rule, so every button in
   here that can be taken away has to be told to go. Two are: the radio, when
   data/radio.json has no station for the language, and the account, until
   /api/account says the database behind it is actually bound. Without this
   both draw as an empty pill instead of not drawing at all — the same trap
   .search-clear and .panel-save fall into further down. */
.rail-btn[hidden] { display: none; }

.rail-btn {
  flex: 0 0 auto;
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 8px 14px 8px 9px;
  background: var(--paper);
  border: 1px solid var(--hairline);
  border-radius: 100px;
  box-shadow: var(--shadow);
  color: var(--ink);
  font-family: var(--mono);
  font-size: 10px;
  font-weight: 500;
  letter-spacing: .13em;
  text-transform: uppercase;
  white-space: nowrap;
  transition: border-color .15s var(--ease), color .15s var(--ease);
}
.rail-btn:hover { border-color: var(--accent); color: var(--accent); }
.rail-btn svg {
  width: 21.6px;
  height: 21.6px;
  fill: none;
  stroke: var(--accent);
  stroke-width: 1.5;
  transition: transform .25s var(--ease);
}
.rail-btn svg .pip { fill: var(--accent); stroke: none; }
.rail-btn:hover svg { transform: rotate(-20deg); }

/* ---------- the colour swatch ---------- */

/* A group with nothing drawn on it. The button inside is the pill, the way it
   is everywhere else on the rail; the group is only what assets/app.js
   renders the switch into, and what names the pair of them to a screen
   reader. */
.styles { display: flex; }

/* One button, not two. There are two styles and one of them is always the one
   you are already looking at, so the second swatch was a control that did
   nothing. What is left shows the side you are not on: press the dark swatch
   to go dark, press the light one to come back — and the label beside it
   names that side, so the pill reads as the thing you are about to get.

   The colour is a dot inside the button rather than the button itself, which
   is what lets the switch wear the same pill as the rest of the rail. It is
   the size of the icons above it and it leans in under a pointer the way
   they turn, so the one button here with no drawing in it still answers. */
.sw-dot {
  width: 21.6px;
  height: 21.6px;
  flex: 0 0 auto;
  border-radius: 100px;
  border: 1px solid var(--swatch-edge);
  transition: transform .15s var(--ease);
}
.rail-btn:hover .sw-dot { transform: scale(1.15); }

/* The light style is its accent; the dark one is its card with the accent as
   a ring, so the rail says which of the two you are about to get before you
   press it. */
.sw-red    { background: #a81e28; }
.sw-green  { background: #1d2a23; box-shadow: inset 0 0 0 4.3px #6fd39a; }

/* ---------- the radio ---------- */

/* The radio wears the same pill as Surprise me: it is the same kind of thing,
   a switch for the mood of the place rather than part of the map. */
.radio svg { fill: var(--accent); stroke: none; }
.radio .ico-stop { display: none; }
.radio[aria-pressed="true"] .ico-play { display: none; }
.radio[aria-pressed="true"] .ico-stop { display: inline; }
.radio[aria-pressed="true"] {
  border-color: var(--accent);
  color: var(--accent);
}
.radio:hover svg { transform: none; }
/* Wide enough for the longest station actually on the dial — "Raadio Tallinn"
   needs 103px and 15ch only bought 90, so every name arrived clipped to
   "RAADIO TALL…". The label is uppercased and letter-spaced, which buys fewer
   characters per ch than the unit suggests. 18ch clears it and still leaves
   the button inside the rail; the ellipsis stays for anything longer. */
.radio-name { max-width: 18ch; overflow: hidden; text-overflow: ellipsis; }

/* ---------- the locate button: the foot of the rail ---------- */

/* Where you are is a question about the map, not about the chrome, so the
   button answers it from inside the map rather than from the strip of
   controls at the top. It used to answer from the far bottom-left corner,
   which is the free one but also as far from every other map control as the
   screen allows — a thumb that had just pressed Surprise me had the length of
   the page to travel. It is the foot of the rail now, and one of the rail's
   own buttons rather than a card parked beneath them — which is also what
   turns the crosshair from the muted grey of the chrome at the top into the
   accent the rest of the rail's icons are drawn in. The dot in the middle of
   it is filled with the accent for the same reason, rather than with the ink
   the label beside it is set in. */
#btn-locate svg circle:first-child { fill: var(--accent); stroke: none; }

/* ---------- filters ---------- */

/* The bar is fixed; only the chips inside it scroll, so the random-pick
   button stays reachable however far along the vocabulary you are. */
/* The chips sit under the top strip of chrome. On a desktop they start where
   the brand's column ends, so the name and its sentence keep that column and
   the row runs from there to the edge; on a phone the brand is one short line
   and the row spans the full width beneath it. --chrome-h is the height of
   that strip. */
.filter-bar {
  position: fixed;
  z-index: 1000;
  --chrome-h: 40px;
  left: calc(var(--gap) + var(--safe-l) + var(--brand-w) + 16px);
  right: calc(var(--gap) + var(--safe-r));
  top: calc(var(--gap) + var(--safe-t) + var(--chrome-h) + 12px);
  display: flex;
  align-items: center;
  gap: 8px;
  transition: right .3s var(--ease), opacity .2s var(--ease),
              transform .38s var(--ease);
}

/* Under 860px the row is a drawer, and everything about it lives in the one
   media query below. A phone has about 300px of width for a vocabulary of
   fourteen words: laid out flat it is the map's loudest thing and the one
   nobody has asked a question of yet, a row of words in front of Tallinn
   before the visitor has looked at it, and thirteen of them off the right
   edge besides. A desktop has the width, so a desktop keeps the flat row —
   there is nothing to fold away and folding it would only hide the answer
   behind a press.

   That is why the drawer is not just a narrow skin on the same behaviour: on
   a phone, shut is All, and a desktop has no shut. See the media query. */

/* The row is 38px of chip standing on live map, and a finger aiming at it
   lands a few pixels off often enough that those pixels used to drag Tallinn
   sideways. So the scroller claims a strip around itself: a little above the
   chips, where the chrome buttons are and they win anyway, and a good deal
   below, which is the side a thumb reaching up overshoots on. The padding is
   invisible and belongs to the scroller itself, so a drag starting in it
   scrolls the chips rather than doing nothing, and the negative margins keep
   the chips drawn on exactly the pixels they were before. touch-action pins
   the gesture to the one axis the row has. */
.filters {
  flex: 0 1 auto;
  min-width: 0;
  display: flex;
  gap: 6px;
  overflow-x: auto;
  overflow-y: hidden;
  padding: 10px 2px 24px;
  margin: -10px 0 -24px;
  touch-action: pan-x;
  scrollbar-width: none;
}
.filters::-webkit-scrollbar { display: none; }

/* Above 860px there is no drawer and no button: the row is what it always
   was, every chip flat on the map and none of them a press away. The button
   is in the markup at every width — it is what the drawer is opened by, and
   markup that appears and disappears with the viewport is markup the script
   has to keep checking for — so this is where it is put away. */
.filter-toggle { display: none; }

@keyframes chip-in {
  from { opacity: 0; transform: translateX(-16px); }
}

/* ---------- under 860px: the chips fold into one button ---------- */

@media (max-width: 860px) {
  /* The button the row lives in. It is a chip like the ones it opens, which
     is the point: shut, the bar reads as one chip that happens to say
     Filters. */
  .filter-toggle {
    display: inline-flex;
    align-items: center;
    gap: 7px;
    pointer-events: auto;
  }
  .filter-toggle .filter-ico {
    width: 13px;
    height: 13px;
    fill: none;
    stroke: currentColor;
    stroke-width: 1.9;
    stroke-linecap: round;
    transition: transform .3s var(--ease);
  }
  .filter-bar.is-open .filter-toggle {
    color: var(--ink);
    border-color: var(--muted);
  }
  .filter-bar.is-open .filter-toggle .filter-ico { transform: rotate(90deg); }

  /* Shut. max-width rather than width because the row still wants to size
     itself to its chips once it is open, and 100% of the bar is the ceiling
     it already had. visibility is what takes the chips out of the tab order
     while they are rolled up — it is delayed by the length of the slide so
     they stay drawn all the way shut, and dropped instantly on the way
     open. */
  .filters {
    max-width: 0;
    /* The 2px the open row keeps for its chips' shadows is padding, and
       padding is floor to a border-box max-width: left in, a shut row is
       still a 4px sliver widening the bar. */
    padding-left: 0;
    padding-right: 0;
    opacity: 0;
    visibility: hidden;
    transition: max-width .4s var(--ease), opacity .18s var(--ease),
                visibility 0s linear .4s;
  }
  .filter-bar.is-open .filters {
    max-width: 100%;
    padding-left: 2px;
    padding-right: 2px;
    opacity: 1;
    visibility: visible;
    transition: max-width .4s var(--ease), opacity .22s var(--ease) .04s,
                visibility 0s;
  }

  /* Each chip arrives a beat after the one before it, so the row reads as
     unrolling rather than appearing. The stagger is capped in the script at
     eight steps: past that the last chip of a long vocabulary would still be
     sliding in a half second after the row had finished opening.

     An animation rather than a transition, and hung on a class the script
     only holds for the length of the slide, because pressing a chip rebuilds
     the whole row: on a transition every press would have replayed the
     entrance, and the row you were filtering with would flash out and back
     in under your finger. */
  .filter-bar.is-opening .filters .chip {
    animation: chip-in .34s var(--ease) both;
    animation-delay: calc(var(--i, 0) * 30ms + 60ms);
  }

  /* The drawer still opens and shuts; it just stops travelling to get there. */
  @media (prefers-reduced-motion: reduce) {
    .filters,
    .filter-bar.is-open .filters,
    .filter-toggle .filter-ico { transition: none; }
    .filter-bar.is-opening .filters .chip { animation: none; }
  }
}

/* With a large type vocabulary the row runs past the viewport. Fade whichever
   edge still has chips behind it, so it reads as scrollable rather than as a
   list that happens to end at "Estonian". */
.filters.can-left  { mask-image: linear-gradient(to right, transparent 0, #000 34px); }
.filters.can-right { mask-image: linear-gradient(to left,  transparent 0, #000 34px); }
.filters.can-left.can-right {
  mask-image: linear-gradient(to right, transparent 0, #000 34px, #000 calc(100% - 34px), transparent 100%);
}

.chip {
  flex: 0 0 auto;
  padding: 7px 13px;
  background: var(--paper);
  border: 1px solid var(--hairline);
  border-radius: 100px;
  box-shadow: var(--shadow);
  font-family: var(--mono);
  font-size: 11px;
  font-weight: 500;
  letter-spacing: .06em;
  color: var(--muted);
  white-space: nowrap;
  transition: color .15s var(--ease), border-color .15s var(--ease), background-color .15s var(--ease);
}
.chip:hover { color: var(--ink); border-color: var(--muted); }
.chip[aria-pressed="true"] {
  background: var(--accent);
  border-color: var(--accent);
  color: var(--paper);
}

/* ==========================================================================
   The signature: the price gauge.
   Four slots, always four. Filled slots in accent, the rest ghosted in
   hairline. It is the only meter anywhere on this site, and it measures
   money, not merit — which is the whole argument of the place.
   ========================================================================== */

.price {
  display: inline-flex;
  gap: .06em;
  font-family: var(--mono);
  font-size: 13px;
  font-weight: 500;
  letter-spacing: .02em;
  white-space: nowrap;
}
.price i { font-style: normal; color: var(--hairline); }
.price i.on { color: var(--accent); }
.is-closed .price i.on { color: var(--muted); }

/* A band can sit on a half step, so a slot can be half lit: the ghosted sign
   underneath, and a second copy of the same glyph laid exactly over it in the
   accent and clipped to its left half. Same glyph, same metrics, so the row of
   four keeps its width whatever the band is. */
.price i.half { position: relative; }
.price i.half b {
  position: absolute;
  top: 0;
  left: 0;
  bottom: 0;
  width: 50%;
  overflow: hidden;
  font-weight: inherit;
  color: var(--accent);
  pointer-events: none;
}
.is-closed .price i.half b { color: var(--muted); }

/* ---------- panel ---------- */

.panel {
  position: fixed;
  z-index: 1200;
  top: calc(var(--gap) + var(--safe-t));
  right: calc(var(--gap) + var(--safe-r));
  bottom: calc(30px + var(--gap) + var(--safe-b));
  width: var(--panel-w);
  max-width: calc(100vw - var(--gap) * 2);
  display: flex;
  flex-direction: column;
  transform: translateX(calc(100% + var(--gap) + 20px));
  visibility: hidden;
  --panel-pad: 26px;
  --panel-pad-t: 44px;
  /* What the group headings stick under. It is the search field plus the
     8px the sticky search box carries below it, and it is written as that
     sum rather than as a number because it had drifted to 60px against a
     real 46 — a 14px slot under the search where rows scrolled past in the
     open, above the heading that was supposed to be covering them. */
  --search-field-h: 38px;
  --search-h: calc(var(--search-field-h) + 8px);
  transition: transform .34s var(--ease), visibility 0s linear .34s,
              background-color .25s var(--ease);
}
.panel.is-open {
  transform: none;
  visibility: visible;
  transition: transform .34s var(--ease), visibility 0s,
              background-color .25s var(--ease);
}

/* A place gets a card of its own colour. The list stays the neutral one, and
   opening a place tints its card with six percent of the accent: enough that
   it reads as picked rather than as the same panel with different words in it,
   not enough to turn a write-up into a coloured box. Measured across all seven
   palettes, text lands between 10.7 and 15.9 to one and the muted line no
   lower than 4.96, so the tint costs nothing anyone has to squint through.
   A browser too old for color-mix simply keeps the plain card. */
body.panel-detail .panel {
  background: color-mix(in srgb, var(--accent) 6%, var(--paper));
}

/* The grab bar on the bottom sheet. It exists only on a phone, where the
   sheet has a height worth changing; on a desktop the panel is a column and
   display:none also keeps the bar out of the tab order. */
.sheet-grip { display: none; }

.panel-scroll {
  overflow-y: auto;
  overscroll-behavior: contain;
  padding: var(--panel-pad-t) var(--panel-pad) 30px;
  -webkit-overflow-scrolling: touch;
}

/* A band of the card's own colour across the top of the panel. Chromium parks
   a sticky element below the scroller's top padding, not at its edge, so
   without this the rows would show through that strip above the stuck section
   label. It doubles as a clean edge for anything scrolling past: rows stop at
   a line rather than being cut through the middle of a letter by the card's
   rounded corner. `inherit` so it follows the tint on an open place. */
.panel::before {
  content: "";
  position: absolute;
  z-index: 1;
  top: 0;
  left: 0;
  right: 0;
  height: var(--panel-pad-t);
  background: inherit;
  border-radius: inherit;
  border-bottom-left-radius: 0;
  border-bottom-right-radius: 0;
}

/* The close button rides in the band at the top of the panel rather than
   floating over the words. It used to be a disc standing on the text: fine
   while the panel was at the top of its scroll, and a hole punched through a
   sentence as soon as it was not. The band is opaque and the content scrolls
   under it, so the button always has its own ground to stand on. */
.panel-close {
  position: absolute;
  z-index: 3;
  top: 5px;
  right: 8px;
  display: grid;
  place-items: center;
  width: 36px;
  height: 36px;
  border: 1px solid transparent;
  border-radius: 100px;
  background: var(--paper);
  color: var(--muted);
}
.panel-close:hover { background: var(--wash); color: var(--ink); }
.panel-close svg { width: 17px; height: 17px; fill: none; stroke: currentColor; stroke-width: 1.7; stroke-linecap: round; }

/* The mark, one step to the left of the close. Same paper, same height, same
   line of the panel's chrome — two things you do TO the panel rather than
   read inside it, so they are drawn as a pair. Both sit in the scroller's top
   padding band, above where the write-up begins, which is why a second
   control up here costs the name underneath no width.

   A disc while it is only a mark, a pill once it carries a number: the width
   is left to the content so a count of 3 and a count of 148 both sit properly
   inside it, and `right` is measured from the close rather than from the edge
   so growing leftwards never walks into it. */
.panel-save {
  position: absolute;
  z-index: 3;
  top: 5px;
  right: 52px;             /* the close, its 36px, and the same 8px again */
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 5px;
  min-width: 36px;
  height: 36px;
  padding: 0 10px;
  border: 1px solid transparent;
  border-radius: 100px;
  background: var(--paper);
  color: var(--muted);
}
/* display on a class beats the UA sheet's [hidden] rule, so the list view's
   hidden mark has to be told to go — the same trap .search-clear falls into
   further down. */
.panel-save[hidden] { display: none; }
.panel-save:hover { background: var(--wash); color: var(--accent); }
.panel-save svg {
  width: 18px;
  height: 18px;
  flex: 0 0 auto;
  fill: none;
  stroke: currentColor;
  stroke-width: 1.7;
  stroke-linecap: round;
  stroke-linejoin: round;
  transition: fill .18s var(--ease);
}
/* Saved is the outline filling in, and nothing else: the same button, in the
   same place, saying the same word. An empty mark and a full one is the
   entire vocabulary here, and everybody already reads it. */
.panel-save[aria-pressed="true"] { color: var(--accent); }
.panel-save[aria-pressed="true"] svg { fill: currentColor; }

/* The count. Tabular figures so the pill does not jitter as it goes from 9 to
   10, and no slot at all at zero — a "0" under a mark reads as a verdict on
   the restaurant rather than as nobody having pressed it yet. */
.save-n {
  font-family: var(--mono);
  font-size: 12px;
  font-weight: 500;
  font-variant-numeric: tabular-nums;
  line-height: 1;
}
.panel-save:not(.has-n) { padding: 0; width: 36px; }
.panel-save:not(.has-n) .save-n { display: none; }

/* One beat, on the press. Short enough to be over before a toast underneath
   has finished arriving, and the global reduced-motion rule at the foot of
   this file takes it out for anyone who has asked for that. */
.panel-save.is-beating svg { animation: save-beat .42s var(--ease); }
@keyframes save-beat {
  30%  { transform: scale(1.26); }
  58%  { transform: scale(.95); }
  100% { transform: scale(1); }
}

/* The count on a list row. Quieter than the discount badge beside it — a
   discount is an offer you act on, this is a fact you glance at — so it
   carries no pill, just the mark and the number in the row's own grey. */
.save-mark {
  display: inline-flex;
  align-items: center;
  gap: 3px;
  flex: 0 0 auto;
  font-family: var(--mono);
  font-size: 11px;
  font-weight: 500;
  font-variant-numeric: tabular-nums;
  color: var(--muted);
  white-space: nowrap;
}
.save-mark-ico {
  display: inline-flex;
  width: 11px;
  height: 11px;
}
/* Filled rather than outlined at this size: an 11px bookmark drawn in a
   1.7px stroke closes up into a smudge, and the outline/filled distinction is
   the panel button's vocabulary, not the row's — a row is not a thing you
   press to save. */
.save-mark-ico svg { width: 11px; height: 11px; fill: currentColor; }

/* ==========================================================================
   The account sheet.
   One card over a dimmed map. The scrim under it is shared with the place
   picker on the lists page, and both of them are modals that take typing, so
   they are centred rather than dropped from an edge and they sit above the
   --kbd variable each page's keyboard handler maintains — so an on-screen
   keyboard pushes them up instead of covering the field being filled in.
   ========================================================================== */

.scrim {
  position: fixed;
  inset: 0;
  z-index: 1200;
  display: grid;
  place-items: center;
  padding: 20px;
  padding-bottom: calc(20px + var(--kbd, 0px));
  background: rgba(39, 20, 26, .42);
  backdrop-filter: blur(2px);
  -webkit-backdrop-filter: blur(2px);
  overscroll-behavior: contain;
}
.scrim[hidden] { display: none; }

.account {
  position: relative;
  width: min(380px, 100%);
  max-height: 100%;
  overflow-y: auto;
  padding: 22px 22px 22px;
}
.ac-close { top: 8px; right: 8px; }

.ac-form { display: flex; flex-direction: column; gap: 14px; }

/* The three lines every view opens with sit tighter than the gap between the
   parts of the sheet: the word, the name of the step and the line explaining
   it are one block, not three. Both clear the close button's corner. */
.ac-eyebrow { margin: 0 34px 0 0; }
.ac-title {
  font-family: var(--display);
  font-size: 25px;
  font-weight: 600;
  letter-spacing: -.02em;
  line-height: 1.15;
  margin: -8px 34px 0 0;
  overflow-wrap: anywhere;
}
.ac-why { margin: -4px 0 0; color: var(--muted); font-size: 13px; line-height: 1.45; }

/* Whether a forgotten password could be got back, in one line under the
   name. The dot carries the answer at a glance and the words carry it for
   anyone the colour is no use to. */
.ac-state {
  display: inline-flex;
  align-self: flex-start;
  align-items: center;
  gap: 7px;
  margin: -6px 0 0;
  font-size: 11.5px;
  letter-spacing: .04em;
  color: var(--muted);
}
.ac-state::before {
  content: "";
  box-sizing: border-box;
  width: 8px;
  height: 8px;
  border: 1px solid var(--muted);
  border-radius: 50%;
}
.ac-state.is-on::before { background: var(--accent); border-color: var(--accent); }

/* A quiet action in a stacked form is not a block: it is a word, and it lines
   up with the words above it rather than with the padding round it. Two of
   them in a row are one pair of afterthoughts, so they close up: the form's
   own gap plus the padding either side had them reading as two unrelated
   lines with nothing between them. */
.ac-form .alt { align-self: flex-start; margin-left: -12px; }
.ac-form .alt + .alt { margin-top: -16px; }

/* The way back up out of a step, at the top of it. */
.ac-back {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  margin: -2px 0 -4px -12px;
  text-decoration: none;
}
.ac-back:hover { text-decoration: underline; }
.ac-back-ico svg {
  display: block;
  width: 13px;
  height: 13px;
  fill: none;
  stroke: currentColor;
  stroke-width: 2;
  stroke-linecap: round;
  stroke-linejoin: round;
}

/* The menu is the sheet's whole body when it is showing an account, so it
   takes the card's own width back off the form's padding. */
.ac-form .menu { margin: 2px -2px 0; }

.ac-field { display: flex; flex-direction: column; gap: 5px; }
.ac-label {
  font-family: var(--mono);
  font-size: 11px;
  font-weight: 500;
  letter-spacing: .06em;
  text-transform: uppercase;
  color: var(--muted);
}
/* 16px is not a taste decision: anything smaller makes iOS zoom the page when
   a field takes focus, and it never zooms back out. */
.ac-field input {
  font: inherit;
  font-size: 16px;
  padding: 10px 12px;
  border: 1px solid var(--hairline);
  border-radius: var(--radius-soft);
  background: var(--paper);
  color: var(--ink);
}
.ac-field input:focus-visible {
  outline: 2px solid var(--accent-lit);
  outline-offset: 1px;
  border-color: transparent;
}
/* The rule a username has to keep, under the field that asks for it. Muted
   and small because it is a thing to glance at rather than to read; the same
   sentence said louder, after a refusal, is .ac-err above the form. */
.ac-hint { color: var(--muted); font-size: 12px; line-height: 1.4; }

/* The two things on this sheet that cannot be put right by pressing the same
   button again — a forgotten password with no address on the account, and
   turning every other device out — are given the weight of a notice rather
   than of a footnote: the accent colour, a rule down its edge, and the same
   size as the words they are warning about. */
.ac-warn {
  display: flex;
  gap: 9px;
  align-items: flex-start;
  margin: -4px 0 0;
  padding: 10px 12px 10px 11px;
  border-left: 3px solid var(--accent);
  border-radius: var(--radius) var(--radius-soft) var(--radius-soft) var(--radius);
  background: var(--wash);
  color: var(--ink);
  font-size: 13px;
  line-height: 1.42;
}
.ac-warn-ico { flex: 0 0 auto; display: block; margin-top: 1px; }
.ac-warn-ico svg {
  width: 15px;
  height: 15px;
  fill: none;
  stroke: var(--accent);
  stroke-width: 1.8;
  stroke-linejoin: round;
  stroke-linecap: round;
}
.ac-warn-ico circle { fill: var(--accent); stroke: none; }

.ac-err, .ac-note {
  margin: 0;
  padding: 9px 12px;
  border-radius: var(--radius-soft);
  font-size: 13px;
  line-height: 1.4;
}
.ac-err { background: var(--wash); color: var(--accent); }
.ac-note { background: var(--wash); color: var(--ink); }

/* The rail button wears the name of whoever is signed in, so it can run
   longer than the fixed-width buttons around it. */
#btn-account.is-on { color: var(--accent); }
#btn-account .rail-label { max-width: 12ch; overflow: hidden; text-overflow: ellipsis; }

/* The offer of an account, after a save made signed out. It stands where the
   toast stands and looks like a quieter relative of it: same corner of the
   screen, same distance up from the bottom, but a card rather than a pill
   because it has two buttons and a sentence rather than a phrase. */
.nudge {
  position: fixed;
  z-index: 2400;
  left: 50%;
  bottom: calc(84px + var(--safe-b));
  transform: translateX(-50%);
  width: min(320px, calc(100vw - 32px));
  padding: 14px 16px;
  background: var(--ink);
  color: var(--paper);
  border-radius: 16px;
  box-shadow: var(--shadow);
}
.nudge[hidden] { display: none; }

.nudge-say {
  margin: 0 0 11px;
  font-size: 13px;
  line-height: 1.4;
}
.nudge-row { display: flex; gap: 8px; align-items: center; }

.nudge-go, .nudge-no {
  font-family: var(--mono);
  font-size: 11px;
  font-weight: 500;
  letter-spacing: .06em;
  border-radius: 100px;
  padding: 7px 13px;
}
.nudge-go {
  background: var(--paper);
  border: 1px solid var(--paper);
  color: var(--ink);
}
.nudge-no {
  background: none;
  border: 1px solid transparent;
  color: var(--paper);
  opacity: .72;
}
.nudge-no:hover { opacity: 1; }

/* Turnstile in invisible mode still wants somewhere to render. It draws
   nothing a visitor can see, but left in the flow it is a 300px hole at the
   foot of the page. */
.turnstile-box {
  position: absolute;
  width: 0;
  height: 0;
  overflow: hidden;
  visibility: hidden;
}

/* detail */

.place-head { margin-bottom: 18px; padding-right: 30px; }

.place-name {
  font-family: var(--display);
  font-size: 30px;
  font-weight: 600;
  letter-spacing: -.028em;
  line-height: 1.08;
  margin: 6px 0 10px;
}
.place-name:focus { outline: none; }
.place-name:focus-visible { outline: 2px solid var(--accent-lit); outline-offset: 3px; }

.head-meta {
  display: flex;
  align-items: baseline;
  flex-wrap: wrap;
  gap: 6px 14px;
}
/* The Call button stands in the link row at the foot of the panel, next to
   the directions. inline-flex keeps the handset glued to the word at any
   text size. */
.call-btn {
  display: inline-flex;
  align-items: center;
  gap: 7px;
}
.call-icon { display: flex; }
.call-icon svg { width: 12px; height: 12px; fill: currentColor; }
/* A closed place keeps its links for the record, but nothing there is worth
   ringing, so the handset greys out with the words. */
.is-closed .call-btn { color: var(--muted); }
.is-closed .call-btn:hover { border-color: var(--hairline); color: var(--muted); }

/* The map's broken circle, at badge size. Stroked rather than filled, unlike
   the depth glyphs, because the gaps in the ring are the whole mark — filled
   in, it is a dot with a bite out of it. */
.shut-glyph { display: flex; }
.shut-glyph svg {
  display: block;
  width: 9px;
  height: 9px;
  fill: none;
  stroke: currentColor;
  stroke-width: 1.5;
}

/* The flag over a closed place's name. It was the word alone; the ring in
   front of it is what ties it to the pin the visitor just pressed. */
.closed-flag {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  margin-bottom: 12px;
  padding: 3px 9px 3px 8px;
  border: 1px solid var(--muted);
  border-radius: 100px;
  font-family: var(--mono);
  font-size: 10px;
  font-weight: 500;
  letter-spacing: .16em;
  text-transform: uppercase;
  color: var(--muted);
}
.closed-flag .shut-glyph svg { width: 10px; height: 10px; }

/* The note that follows it says what is left, not only what is gone, so it
   is worth a hair more room than a stray grey line. */
.is-shut-note { margin-top: 2px; line-height: 1.55; }

.section { border-top: 1px solid var(--hairline); padding-top: 16px; margin-top: 20px; }
.section > .eyebrow { margin-bottom: 8px; }

.blurb { font-size: 15px; line-height: 1.65; }
/* The write-up opens the panel, and the head above it carries the gap. The
   one thing that can come between them is the closed note, which is set
   tight against the flag it explains — so the paragraph brings its own room
   in that case rather than running straight on from a grey line. */
.is-shut-note + .blurb { margin-top: 14px; }
.blurb.is-todo { color: var(--muted); }

.tag-row { display: flex; flex-wrap: wrap; gap: 6px; }
.tag {
  padding: 3px 10px;
  border: 1px solid var(--hairline);
  border-radius: 100px;
  font-family: var(--mono);
  font-size: 10.5px;
  letter-spacing: .06em;
  color: var(--muted);
}

.dish-list li {
  position: relative;
  padding-left: 18px;
  font-size: 14.5px;
  line-height: 1.75;
}
.dish-list li::before {
  content: "";
  position: absolute;
  left: 2px;
  top: .78em;
  width: 6px;
  height: 1px;
  background: var(--accent);
}

.facts { display: grid; grid-template-columns: auto 1fr; gap: 9px 18px; align-items: baseline; }
.facts dt {
  font-family: var(--mono);
  font-size: 10px;
  font-weight: 500;
  letter-spacing: .16em;
  text-transform: uppercase;
  color: var(--muted);
}
.facts dd { margin: 0; font-size: 14px; }
/* The phone number is the one fact you can act on, so it stays tappable in
   the row as well as in the button below. Ink rather than accent: a list of
   facts should read as facts, with the hover doing the telling. */
.facts a { color: inherit; text-decoration-color: var(--hairline); }
.facts a:hover { color: var(--accent); text-decoration-color: currentColor; }

.link-row { display: flex; flex-wrap: wrap; gap: 8px; margin-top: 16px; }
.link-btn {
  padding: 9px 15px;
  border: 1px solid var(--hairline);
  border-radius: var(--radius);
  font-family: var(--mono);
  font-size: 11px;
  font-weight: 500;
  letter-spacing: .12em;
  text-transform: uppercase;
  text-decoration: none;
  color: var(--ink);
  transition: border-color .15s var(--ease), color .15s var(--ease);
}
.link-btn:hover { border-color: var(--accent); color: var(--accent); }
.link-btn.is-primary { background: var(--accent); border-color: var(--accent); color: var(--paper); }
.link-btn.is-primary:hover { background: var(--accent-lit); border-color: var(--accent-lit); color: var(--paper); }

/* discount
   Only ever drawn for a place that has a live deal in data/deals.json. */

/* The number, wherever it is shown: beside the price in a list row and
   beside the price in the panel head. Both lines are otherwise muted grey,
   so the accent is enough to lift the badge out without a fill, which would
   have put a second solid pill in a row that already ends in one. */
.deal-mark {
  display: inline-flex;
  align-items: center;
  flex: none;
  padding: 2px 7px;
  border: 1px solid var(--accent);
  border-radius: 100px;
  font-family: var(--mono);
  font-size: 10px;
  font-weight: 600;
  letter-spacing: .04em;
  line-height: 1.4;
  color: var(--accent);
  white-space: nowrap;
}
/* One step up in the panel head, where it stands beside a 13px price rather
   than an 11px one. */
.head-meta .deal-mark { padding: 3px 9px; font-size: 11px; }

.deal-block {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: 10px;
}

.deal-offer { font-size: 15px; }

/* reel */

/* The player is the reason a profile gets opened, so it is given every pixel
   the panel has: the frame bleeds out through the panel's padding to the card
   edges, the way the search box and the group headings do. That is 52px more
   picture on a desktop and the full width of the screen on a phone, and since
   the frame keeps its ratio, a wider frame is a taller one too — the reel
   gains in both directions rather than sitting in a letterbox.

   Height comes from the ratio, never from the iframe: a cross-origin frame
   cannot be asked how tall it is, and a frame left to size itself collapses
   to 150px. Instagram opens at the shape of a reel and is then corrected to
   the exact one by the measurement it posts out (see wireReelMeasure); TikTok
   publishes at a fixed shape and needs no correcting. */
.reel-frame {
  margin-inline: calc(var(--panel-pad) * -1);
  aspect-ratio: var(--reel-ratio, 9 / 19);
  border-block: 1px solid var(--hairline);
  overflow: hidden;
  background: var(--wash);
}
.reel-frame iframe { display: block; width: 100%; height: 100%; border: 0; }

/* 325x739 is the ratio TikTok's own embed code uses. */
.reel-frame.is-tiktok { --reel-ratio: 325 / 739; }
/* 9:16 of video, and about 120px of Instagram's own chrome above and below it
   at the width the panel gives — the shape a reel embed lands on. It stands
   until Instagram posts the real number, and a frame slightly too tall shows
   a band of card at the bottom, where slightly too short would cut the video. */
.reel-frame.is-instagram { --reel-ratio: 9 / 19; }

.reel-fallback { margin-top: 10px; font-size: 12.5px; color: var(--muted); }

.muted-note { font-size: 13.5px; color: var(--muted); }

/* What Google says a place is, on everything that has no write-up to link to:
   whose description it is, then the score, then the band, then the kinds. One
   rule for the four surfaces it appears on — the card the map draws for a
   place off the export, the row in the panel behind it, a row on a list's own
   page and a row in the picker — because it is one line saying one thing, and
   four copies of it would be four copies to keep in step. It lives here
   rather than in lists.css for that reason: the map draws it too now.

   It leads with the attribution rather than trailing it, so the accent on the
   gauge is read as Google's claim and not as mine.

   The gauge is set a point and a half above the words beside it. At the same
   size the four signs read as four more characters of the sentence — legible,
   but flat — and the ghosted pair all but vanishes; the step up is what makes
   the row read as a gauge with two of its four lit. */
.place-source {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: 2px 9px;
  margin-top: 3px;
  color: var(--muted);
  font-size: 10.5px;
  letter-spacing: .03em;
  overflow-wrap: anywhere;
}
.place-source .price { font-size: 12px; }
/* The one number on the line that is a measurement rather than a label, so it
   is set in the ink the card reads in while the words around it stay quiet. */
.place-source .score { color: var(--ink); }

/* On the card it is a line of its own under the owner's sentence rather than
   a caption inside a row, so it gets the room and the size of one. */
.is-from-list .place-source {
  margin: 14px 0 0;
  font-size: 11.5px;
  gap: 3px 12px;
}
.is-from-list .place-source .price { font-size: 13px; }

/* Google's week, in the same two columns the address and the phone are set
   in. Today is the row somebody is actually asking about, so it is the one in
   ink; the other six are there to be scanned past. */
.facts.is-hours dd { font-variant-numeric: tabular-nums; color: var(--muted); }
.facts.is-hours .is-today { color: var(--ink); }
.facts.is-hours dt.is-today { color: var(--accent); }

/* Places visited but not filmed. It has a heading of its own now, in the slot
   the reel and the photos take, so the line under it is a sentence rather than
   another label: the rule and the mono caps belonged to a stray line and would
   read as a second heading here. Quiet enough not to look like an error. */
.not-filmed {
  font-size: 13.5px;
  line-height: 1.55;
  color: var(--muted);
}

/* photos */

.photo-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 6px; }
.photo-grid button {
  aspect-ratio: 1;
  overflow: hidden;
  border-radius: 2px;
  background: var(--wash);
}
.photo-grid img { width: 100%; height: 100%; object-fit: cover; transition: transform .3s var(--ease); }
.photo-grid button:hover img { transform: scale(1.04); }

/* list */

/* Just the eyebrow now — whose map this is. The heading that used to sit
   under it moved into the list, where each group says its own name. */
.list-head { margin-bottom: 4px; padding-right: 30px; }

/* The chat: a title, the thread, and the field under it all.
 *
 * It is laid out as a chat is: the conversation reads down from the top, and
 * the field is the last thing in the panel and held to its bottom edge, so
 * a thread shorter than the panel still puts the field where a thumb
 * expects it and a longer one scrolls up behind it. The scroller grows to
 * the panel and becomes a column only while the chat is showing —
 * body.panel-ask, set by renderPanel() in assets/app.js — because the list
 * and a place lay themselves out as blocks and their margins collapse on
 * that, and the scroller is otherwise only as tall as what is in it.
 *
 * The field is a question you finish writing and send, so it has a button on
 * it and the accent on its edge — it is the one field on this site that does
 * not narrow as you type. */
body.panel-ask .panel-scroll { display: flex; flex: 1 1 auto; flex-direction: column; }
#panel-ask { display: flex; flex-direction: column; flex: 1 0 auto; }
#panel-ask[hidden] { display: none; }
.ask-title {
  margin: 10px 0 12px;
  font-family: var(--display);
  font-size: 21px;
  font-weight: 600;
  letter-spacing: -.02em;
  line-height: 1.2;
}
.ask-title:focus { outline: none; }
.ask {
  position: sticky;
  bottom: 0;
  z-index: 2;
  margin: auto calc(var(--panel-pad) * -1) 0;
  padding: 12px var(--panel-pad) 10px;
  background: inherit;
}
.ask-field { position: relative; }
.ask-input {
  width: 100%;
  height: var(--search-field-h);
  padding: 0 40px 0 32px;
  background: var(--paper);
  border: 1px solid var(--accent);
  border-radius: 100px;
  font-family: var(--body);
  font-size: 14px;
  color: var(--ink);
  -webkit-appearance: none;
  appearance: none;
  transition: border-color .15s var(--ease), opacity .15s var(--ease);
}
.ask-input::placeholder { color: var(--muted); opacity: 1; }
.ask-input:focus {
  outline: none;
  border-color: var(--accent);
  box-shadow: 0 0 0 3px color-mix(in srgb, var(--accent) 14%, transparent);
}
/* Until a roll is chosen there is nothing to ask it of: the field and its
   arrow fade together, and the two buttons above are the only reply. */
.ask-input:disabled { opacity: .6; }
.ask-icon {
  position: absolute;
  left: 11px;
  top: 50%;
  width: 15px;
  height: 15px;
  transform: translateY(-50%);
  fill: none;
  stroke: var(--accent);
  stroke-width: 1.7;
  stroke-linejoin: round;
  pointer-events: none;
}
/* Inside the field's right edge, filled rather than outlined: it is the one
   control in this panel that sends something, and the only one that has to
   read as pressable without being hovered first. */
.ask-go {
  position: absolute;
  right: 4px;
  top: 50%;
  display: grid;
  place-items: center;
  width: 30px;
  height: 30px;
  transform: translateY(-50%);
  border-radius: 100px;
  background: var(--accent);
  color: var(--paper);
  transition: opacity .15s var(--ease);
}
.ask-go:disabled { opacity: .45; }
.ask-go svg { width: 15px; height: 15px; fill: none; stroke: currentColor; stroke-width: 1.9; stroke-linecap: round; stroke-linejoin: round; }
/* A question in the air: the arrow pulses, and that is the whole of the
   waiting state. The field stays typeable, and the answer that is already
   under the question — the local reader's — stays readable; a spinner over
   either would hide the thing somebody is looking at. */
.ask.is-asking .ask-go { animation: ask-wait 1s var(--ease) infinite; }
@keyframes ask-wait { 0%, 100% { opacity: .45; } 50% { opacity: 1; } }
@media (prefers-reduced-motion: reduce) {
  .ask.is-asking .ask-go { animation: none; }
}

/* The two rolls, offered under the site's first line as the reply to it.
   The same two-button segment the top of the map uses for the language, at
   the panel's own scale: a hairline pill rather than a glass pane, and the
   mono the rest of the panel's small words are set in. The chosen half
   wears the accent the way a pressed chip does; before a choice neither
   does. */
.ask-scope {
  width: max-content;
  max-width: 100%;
  margin: 10px 0 0;
  border: 1px solid var(--hairline);
  border-radius: 100px;
  background: var(--paper);
}
.ask-scope .btn {
  min-width: 0;
  height: 28px;
  padding: 0 12px;
  font-family: var(--mono);
  font-size: 11px;
  letter-spacing: .06em;
  border-radius: 100px;
}
.ask-scope .btn + .btn { border-left: 0; }

/* The thread. Each exchange is the question as it was typed, set apart on
   the right the way a message of your own is in every chat anybody has used,
   and the answer under it in the reading face with the rows beneath. The
   bubble is the only one on this site and it is here because it is the one
   place the reader's own words are printed back at them: it has to be
   unmistakable which line is theirs and which is the site's. */
.ask-thread { padding-top: 6px; }
.ask-turn + .ask-turn {
  margin-top: 22px;
  padding-top: 18px;
  border-top: 1px solid var(--hairline);
}
.ask-you {
  width: max-content;
  max-width: 88%;
  margin: 0 0 0 auto;
  padding: 8px 14px;
  border-radius: 18px 18px 4px 18px;
  background: var(--wash);
  color: var(--ink);
  font-size: 14px;
  line-height: 1.45;
  overflow-wrap: anywhere;
}
.ask-say {
  margin: 14px 0 2px;
  font-size: 15px;
  line-height: 1.5;
  color: var(--ink);
}
/* The site's opening line has nothing of yours above it to sit under. */
.ask-turn > .ask-say:first-child { margin-top: 0; }
/* The sentence over an answer still on its way is the site thinking out
   loud, and it fades the way the arrow does so the two read as one state. */
.ask-turn.is-pending .ask-say { color: var(--muted); }
/* Where "near" was measured from — "Distances are from Tallinna bussijaam,
   Kesklinn" — under the sentence, in the muted small type the rows use for
   their own notes. It is the visitor's check on what the site understood,
   and it earns a line only when there was a place to measure from. */
.ask-from {
  margin: 0 0 6px;
  font-size: 12.5px;
  line-height: 1.4;
  color: var(--muted);
}
/* Under the sentence rather than under a group heading: the sentence is the
   heading. */
.ask-thread .place-list { margin-top: 0; }

/* The search field. A hairline box rather than a filled one: it is a tool
   the list offers, not the thing the panel is about. The magnifier and the
   clear button are laid over the input rather than beside it, so the text
   starts and ends where the row names do. */
.search {
  position: sticky;
  top: 0;
  z-index: 2;
  margin: 14px calc(var(--panel-pad) * -1) 0;
  padding: 0 var(--panel-pad) 8px;
  background: inherit;
}
/* The clear button and the magnifier are positioned against this, not the
   sticky box, so the bleed padding does not push them to the panel edge. */
.search-field { position: relative; }
.search-input {
  width: 100%;
  height: var(--search-field-h);
  padding: 0 34px 0 32px;
  background: var(--paper);
  border: 1px solid var(--hairline);
  border-radius: 100px;
  font-family: var(--body);
  font-size: 14px;
  color: var(--ink);
  -webkit-appearance: none;
  appearance: none;
  transition: border-color .15s var(--ease);
}
.search-input::placeholder { color: var(--muted); opacity: 1; }
.search-input:hover { border-color: var(--muted); }
.search-input:focus {
  outline: none;
  border-color: var(--accent);
  box-shadow: 0 0 0 3px color-mix(in srgb, var(--accent) 14%, transparent);
}
/* Chromium and Safari draw their own clear button inside a search input,
   which would sit next to ours. */
.search-input::-webkit-search-cancel-button,
.search-input::-webkit-search-decoration { display: none; }

.search-icon {
  position: absolute;
  left: 11px;
  top: 50%;
  width: 15px;
  height: 15px;
  transform: translateY(-50%);
  fill: none;
  stroke: var(--muted);
  stroke-width: 1.7;
  stroke-linecap: round;
  pointer-events: none;
}
.search-clear {
  position: absolute;
  right: 4px;
  top: 50%;
  display: grid;
  place-items: center;
  width: 30px;
  height: 30px;
  transform: translateY(-50%);
  border-radius: 100px;
  color: var(--muted);
}
/* display:grid on a class beats the UA sheet's [hidden] rule, so the
   attribute has to be honoured explicitly. */
.search-clear[hidden] { display: none; }
.search-clear:hover { color: var(--ink); background: var(--wash); }
.search-clear svg { width: 13px; height: 13px; fill: none; stroke: currentColor; stroke-width: 1.8; stroke-linecap: round; }

/* The group headings. They are the panel's largest type and its only
   headings — quiet signposts underneath a bigger title was the arrangement
   that made the panel read wrong. Each holds its group's count on the right,
   and sticks to the top of the panel on the way past, so a list sixty rows
   long never leaves you wondering which group you are in. The right inset is
   the close button's, which floats above all of this.

   21px rather than the 24 the old title ran at: this band sticks, so it is on
   screen for most of the scroll rather than only at the top of it, and 24
   sitting over the 17px rows all the way down was a heavier line than the
   rows underneath could carry. Not much less, though — a group name and a
   place name are both display 600, so the size is the whole of what separates
   them, and at 19 they read as the same rank. */
.list-label {
  position: sticky;
  top: var(--search-h);
  z-index: 1;
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 12px;
  margin: 30px calc(var(--panel-pad) * -1) 0;
  padding: 9px var(--panel-pad) 8px;
  padding-right: calc(var(--panel-pad) + 30px);
  background: var(--paper);
  border-bottom: 1px solid var(--hairline);
}
.list-label:first-of-type { margin-top: 10px; }
.list-label:focus { outline: none; }
.list-label:focus-visible { outline: 2px solid var(--accent-lit); outline-offset: -2px; }
.list-group {
  font-family: var(--display);
  font-size: 21px;
  font-weight: 600;
  letter-spacing: -.02em;
  line-height: 1.2;
}
/* The count keeps the eyebrow's mono, so the pair reads as a name and a
   measurement rather than as two competing headings. */
.list-label-n { flex: 0 0 auto; letter-spacing: .04em; opacity: .75; }

.place-list li + li { border-top: 1px solid var(--hairline); }

/* Two columns: everything the row says on the left, what there is to see on
   the right. The mark holds the same edge down the whole list, which is what
   makes it scannable — three words in one place beat three words wherever the
   name happens to end. */
.list-row {
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto;
  grid-template-areas:
    "name mark"
    "sub  mark";
  align-items: center;
  column-gap: 10px;
  width: 100%;
  padding: 14px 2px;
  text-align: left;
}
.list-name { grid-area: name; }
.list-sub { grid-area: sub; }
.list-row > .depth-mark { grid-area: mark; }
.list-types { min-width: 0; }
.list-row:hover .list-name { color: var(--accent); }
.list-name {
  font-family: var(--display);
  font-size: 17px;
  font-weight: 600;
  letter-spacing: -.015em;
  line-height: 1.25;
  transition: color .15s var(--ease);
}
.list-sub {
  display: flex;
  align-items: baseline;
  gap: 12px;
  margin-top: 5px;
  font-family: var(--mono);
  font-size: 11px;
  color: var(--muted);
  letter-spacing: .02em;
}
/* How far a place in an answer is from where the visitor said they are —
   "450 m", "1,2 km". A measurement, so it keeps the row's mono, and in the
   ink rather than the row's grey because it is the one fact in the line the
   visitor asked for. It sits with the badges rather than after the types,
   which can run to two lines, so it holds the same place on every row. */
.list-far {
  flex: 0 0 auto;
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
  color: var(--ink);
}
.list-row.is-closed .list-name { color: var(--muted); }

/* ---------- somebody else's list, on the map ---------- */

/* What the list's owner said about this place. It spans both columns and sits
   under everything else, because it is a sentence somebody wrote and not a
   piece of metadata — so it takes the reading face rather than the mono the
   rest of the row uses, and the full width rather than the left column.

   Quoted with a rule down the inside edge instead of quotation marks: the
   marks would have to be chosen per language, and the rule says the same
   thing in all ten. */
.list-said {
  grid-area: said;
  margin-top: 8px;
  padding-left: 10px;
  border-left: 2px solid var(--hairline);
  font-family: var(--body);
  font-size: 13.5px;
  line-height: 1.45;
  color: var(--ink);
  overflow-wrap: anywhere;
}
/* The row grows a third band for it. Only when there is one — a place with no
   sentence under it keeps the two-line row every other list on the site
   draws. */
.list-row:has(> .list-said) {
  grid-template-areas:
    "name mark"
    "sub  mark"
    "said said";
  align-items: start;
}
/* :has() is recent enough to be worth a floor under it. Without it the
   sentence lands in the implicit row grid puts it in, which is under the name
   and beside the mark — narrower than it should be, and still readable. */
@supports not selector(:has(*)) {
  .list-said { grid-column: 1 / -1; }
}

/* A place added to a list by hand, off nobody's export: a name and an
   address and nothing else is known, and the row says so by being plainer
   rather than by carrying a badge that explains its own absence. The name
   stays the name; what is missing is the mark on the right, so the row is
   one column wide. A place off the Google export is not this — it takes the
   ordinary row's shape with a Google mark in the right-hand slot, see
   listOnlyRow() in assets/app.js. */
.list-row.is-from-list {
  grid-template-columns: minmax(0, 1fr);
  grid-template-areas:
    "name"
    "sub";
}
.list-row.is-from-list:has(> .list-said) {
  grid-template-areas:
    "name"
    "sub"
    "said";
}

/* The heading over a list: its title, its owner's name, the line they wrote
   under it, and the way through to the list's own page. It replaces the
   ordinary group heading in this one state, so it carries the same sticky
   band and the same type. */
.list-credit .list-label.is-credit { margin-top: 10px; }
/* The byline. The name in it is a link since profiles arrived — it leads to
   /u/<name>, the rest of what this list's owner has published — and only the
   name, the way every byline on the lists page is drawn: the words around it
   are about the list, not the door. The link keeps the muted colour of the
   line rather than taking the accent a bare link wears: on a card that is
   somebody else's writing, the byline is not the loudest thing on it. */
.list-credit-by {
  display: inline-block;
  margin: 10px 0 0;
  color: var(--muted);
}
.list-credit-by a { color: inherit; text-underline-offset: 3px; }
.list-credit-by a:hover { color: var(--accent); }
.list-credit-intro {
  margin: 8px 0 0;
  font-size: 14px;
  line-height: 1.5;
  color: var(--ink);
}
/* The way through to the list's own page and the way to send it on. Both were
   or would have been eleven pixels of underlined mono below the keep, which
   reads as a footnote rather than as the other things you can do here, so
   they wear the same pill as the keep and stand on the same line as it.

   Not the accent, though: on this block the accent behind a pill already
   means "you have kept this", and another that always wore it would take that
   reading away. */
.list-credit-act {
  display: inline-block;
  padding: 7px 13px;
  border: 1px solid var(--hairline);
  border-radius: 100px;
  background: var(--paper);
  color: var(--muted);
  font-family: var(--mono);
  font-size: 11px;
  font-weight: 500;
  letter-spacing: .06em;
  text-decoration: none;
  white-space: nowrap;
  /* One of the two is a button and one is a link, so the line box is set here
     rather than left to each element's default: side by side they have to be
     the same height. */
  line-height: 1.35;
  cursor: pointer;
  transition: color .15s var(--ease), border-color .15s var(--ease);
}
.list-credit-act:hover { color: var(--ink); border-color: var(--muted); }

/* The way out, under the three things you do with the list. An .alt rather
   than a fourth pill, because it is not one of them: it hands the whole map
   back. The negative margin pulls the words onto the same left edge as the
   pills above, against .alt's own padding. */
.list-credit-out { margin: 2px 0 0 -12px; }

/* The three controls, under the list's own words: keeping it, opening it in
   full, and sending it on. They are given a line of their own rather than set
   beside the byline, because nothing here should read as though the site is
   speaking in the owner's voice. */
.list-credit-acts {
  display: flex;
  align-items: center;
  gap: 10px;
  flex-wrap: wrap;
  margin: 14px 0 2px;
}
/* The keep and its count travel together: a narrow panel breaks the line
   between the two controls rather than between the button and the number
   belonging to it. */
.list-credit-keep {
  display: inline-flex;
  align-items: center;
  gap: 10px;
}
.list-keep {
  display: inline-flex;
  align-items: center;
  gap: 7px;
  flex: none;
  padding: 7px 13px 7px 10px;
  border: 1px solid var(--hairline);
  border-radius: 100px;
  background: var(--paper);
  color: var(--muted);
  font-family: var(--mono);
  font-size: 11px;
  font-weight: 500;
  letter-spacing: .06em;
  white-space: nowrap;
  cursor: pointer;
  transition: color .15s var(--ease), border-color .15s var(--ease),
              background-color .15s var(--ease);
}
.list-keep svg {
  width: 15px;
  height: 15px;
  flex: none;
  /* Hollow until it is kept, filled after — the same before-and-after the
     save mark on a place uses, so the two marks are read as one gesture
     rather than as two controls that happen to share an outline. */
  fill: none;
  stroke: currentColor;
  stroke-width: 1.6;
  stroke-linejoin: round;
}
.list-keep:hover { color: var(--ink); border-color: var(--muted); }
/* Kept is the filled state, and it is filled the way a pressed chip is: the
   accent behind it rather than in front of it, so "I have this" is legible
   across the panel at a glance and not a colour change you have to look for. */
.list-keep.is-kept {
  background: var(--accent);
  border-color: var(--accent);
  color: var(--paper);
}
.list-keep.is-kept:hover { color: var(--paper); border-color: var(--accent); }
.list-keep.is-kept svg { fill: currentColor; }
.list-keeps {
  color: var(--muted);
  /* No wrapping mid-phrase in a row that is already allowed to wrap: the
     count goes under the button whole or sits beside it whole. */
  white-space: nowrap;
}

/* The row for the place the map is still holding lit. A rule down the inside
   edge and the name in the accent, which is the hover state made permanent —
   the list already says that colour means this place, so coming back to the
   list from a place you have just read lands you looking at it. */
.list-row.is-kept {
  box-shadow: inset 2px 0 0 var(--accent);
  padding-left: 12px;
}
.list-row.is-kept .list-name { color: var(--accent); }
.list-row.is-kept.is-closed .list-name { color: var(--muted); }

/* Sits with the discount badge rather than at the end of the type list: an
   offer and a shut door are the two things in a row you decide on rather than
   read about. Grey where that one takes the accent, so the pair are never
   read as the same kind of note, and nothing here brightens on hover — there
   is nothing to go to. */
.shut-mark {
  display: inline-flex;
  align-items: center;
  flex: none;
  gap: 5px;
  padding: 2px 8px 2px 7px;
  border: 1px solid var(--muted);
  border-radius: 100px;
  font-family: var(--mono);
  font-size: 9.5px;
  font-weight: 500;
  letter-spacing: .11em;
  line-height: 1.4;
  text-transform: uppercase;
  color: var(--muted);
  white-space: nowrap;
}

.list-row:hover .deal-mark { border-color: var(--accent-lit); color: var(--accent-lit); }
.empty-note {
  margin-top: 20px;
  font-family: var(--body);
  font-size: 14px;
  font-weight: 400;
  color: var(--muted);
}
.empty-note:focus { outline: none; }

/* depth mark
 *
 * The same three-way the pins draw, in a badge: a filmed place is solid ink,
 * a photographed one is the outline of the same badge, and a place with only
 * the write-up is a hairline the eye slides over. Reading down the list the
 * three are told apart by weight before the word is read at all, which is the
 * whole point — the word is there for when you do stop on one.
 */
.depth-mark {
  display: inline-flex;
  align-items: center;
  gap: 5px;
  padding: 3px 8px;
  border: 1px solid transparent;
  border-radius: 100px;
  font-family: var(--mono);
  font-size: 9.5px;
  font-weight: 500;
  letter-spacing: .11em;
  line-height: 1.4;
  text-transform: uppercase;
  white-space: nowrap;
}
.depth-mark svg { display: block; width: 9px; height: 9px; fill: currentColor; }

.depth-mark.is-reel {
  background: var(--accent);
  border-color: var(--accent);
  color: var(--paper);
}
.depth-mark.is-photos {
  border-color: var(--accent);
  color: var(--accent);
}
.depth-mark.is-words {
  border-color: var(--hairline);
  color: var(--muted);
}
.depth-mark.is-words svg { opacity: .5; }

/* Whose description a row is. The pill a row of mine wears for its depth, in
   the quiet reading — hairline and muted, the same as "words" — because it is
   an attribution and not a verdict, and the score beside the word sits in the
   ink so the number can be read without the pill shouting it. */
.depth-mark.is-google {
  border-color: var(--hairline);
  color: var(--muted);
  text-transform: none;
  letter-spacing: .06em;
}
.depth-mark.is-google .google-score { color: var(--ink); font-weight: 500; }

/* The depth mark keeps its accent on a closed row, which it used to lose. It
   is the one thing in the row that did not close: the door is shut and the
   reel is still there to watch, and greying the badge said the opposite of
   the reason the entry is still on the map at all. The name greys, the price
   greys, the Call button greys — everything that was about going. What is
   left to look at stays lit, and the CLOSED mark beside it carries the news
   on its own. */

.list-row:hover .depth-mark.is-photos { border-color: var(--accent-lit); color: var(--accent-lit); }
.list-row:hover .depth-mark.is-reel { background: var(--accent-lit); border-color: var(--accent-lit); }
.empty-note:focus-visible { outline: 2px solid var(--accent-lit); outline-offset: 3px; }

/* ---------- lightbox ---------- */

.lightbox {
  position: fixed;
  inset: 0;
  z-index: 2000;
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 8px;
  padding: max(var(--gap), var(--safe-t)) var(--gap) max(var(--gap), var(--safe-b));
  background: rgba(20, 26, 31, .92);
}
.lightbox[hidden] { display: none; }

.lb-figure {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 10px;
  max-width: min(1100px, 100%);
  min-width: 0;
}
.lb-figure img {
  max-width: 100%;
  max-height: calc(100vh - 110px);
  object-fit: contain;
  border-radius: 2px;
  background: var(--ink);
}
#lb-caption { font-size: 10px; letter-spacing: .16em; text-transform: uppercase; color: rgba(255, 255, 255, .6); }

.lb-btn {
  flex: 0 0 auto;
  display: grid;
  place-items: center;
  width: 42px;
  height: 42px;
  border-radius: 100px;
  color: rgba(255, 255, 255, .75);
  transition: color .15s var(--ease), background-color .15s var(--ease);
}
.lb-btn:hover { color: #fff; background: rgba(255, 255, 255, .1); }
.lb-btn svg { width: 20px; height: 20px; fill: none; stroke: currentColor; stroke-width: 1.6; stroke-linecap: round; stroke-linejoin: round; }
.lb-close { position: absolute; top: max(var(--gap), var(--safe-t)); right: var(--gap); }
.lb-btn:focus-visible { outline-color: #fff; }

/* ---------- stories ----------
   One video, full height, with the chrome laid over it: a bar per story along
   the top, who it is from and how long it has left under that, the caption and
   the link along the bottom. Everything is absolute inside .story-stage rather
   than stacked around it, because the stage is the video — a column layout
   would have the picture shrink every time a caption ran to two lines.

   The stage is sized off the height and not the width. A story is a tall
   picture, so the height of the window is what there is to spend, and the
   width follows from it at 9:16. On a phone it fills the screen instead, which
   is the only place a story has ever been watched anyway.

   The height it has to spend is the window less --browser-b, which is what
   Safari's toolbar is standing over: the scrim still covers the whole window,
   because the strip behind a translucent bar is part of what you can see, but
   the stage sits inside what is left, so the caption and the button to the
   place land above the bar rather than under it. It is 0 everywhere the
   browser does not overlap the page, which is everywhere but iOS.
   --------------------------------------------------------------------- */

.stories {
  --story-h: min(calc(var(--vph, 100dvh) - var(--browser-b, 0px) - 2 * var(--gap)), 900px);
  position: fixed;
  inset: 0;
  z-index: 2600;
  display: grid;
  place-items: center;
  padding: var(--gap) var(--gap) calc(var(--gap) + var(--browser-b, 0px));
  background: rgba(14, 9, 10, .95);
}
.stories[hidden] { display: none; }

.story-stage {
  position: relative;
  height: var(--story-h);
  width: min(100%, calc(var(--story-h) * 9 / 16));
  overflow: hidden;
  border-radius: var(--radius-soft);
  background: #000;
  touch-action: pan-y;
}

.story-video,
.story-photo {
  width: 100%;
  height: 100%;
  object-fit: contain;
  background: #000;
}
.story-video[hidden], .story-photo[hidden] { display: none; }

/* Two scrims in one pass: white text has to hold over whatever the video is
   doing at the top and the bottom of the frame, and a video is the one
   surface on the site whose colours nobody controls. */
.story-stage::before {
  content: "";
  position: absolute;
  inset: 0;
  z-index: 1;
  pointer-events: none;
  background: linear-gradient(
    rgba(0, 0, 0, .55), rgba(0, 0, 0, 0) 24%,
    rgba(0, 0, 0, 0) 58%, rgba(0, 0, 0, .68));
}

/* The left third goes back, the rest goes on. Buttons rather than bare
   divs, so the two moves a thumb has are the two moves a keyboard has. */
.story-half {
  position: absolute;
  top: 0;
  bottom: 0;
  z-index: 1;
  background: none;
  border: 0;
  cursor: pointer;
}
.story-half-back { left: 0; width: 32%; }
.story-half-fwd { right: 0; width: 68%; }
.story-half:focus-visible { outline-offset: -6px; }

/* On a phone the two halves are invisible and everybody knows they are there.
   On a desktop nobody does — a mouse expects something to aim at — so the
   halves grow a chevron that fades in under the pointer. It is the same
   button either way; this only gives it a face where a face is wanted. */
.story-chevron {
  position: absolute;
  top: 50%;
  display: grid;
  place-items: center;
  width: 34px;
  height: 34px;
  border-radius: 100px;
  background: rgba(0, 0, 0, .32);
  color: #fff;
  opacity: 0;
  transform: translateY(-50%);
  transition: opacity .18s var(--ease);
}
.story-half-back .story-chevron { left: 10px; }
.story-half-fwd .story-chevron { right: 10px; }
.story-chevron svg {
  width: 18px;
  height: 18px;
  fill: none;
  stroke: currentColor;
  stroke-width: 1.8;
  stroke-linecap: round;
  stroke-linejoin: round;
}
@media (hover: hover) and (pointer: fine) {
  .story-half:hover .story-chevron,
  .story-half:focus-visible .story-chevron { opacity: 1; }
}
/* A held story hides its chrome, and the chevrons are chrome. */
.story-stage.is-held .story-chevron { opacity: 0; }

.story-bars {
  position: absolute;
  top: 0;
  left: 0;
  right: 0;
  z-index: 2;
  display: flex;
  gap: 4px;
  padding: calc(9px + var(--safe-t)) 10px 0;
}
.story-bar {
  flex: 1 1 0;
  height: 2.5px;
  border-radius: 2px;
  background: rgba(255, 255, 255, .34);
  overflow: hidden;
}
.story-bar span {
  display: block;
  width: 0;
  height: 100%;
  border-radius: 2px;
  background: #fff;
}

.story-head {
  position: absolute;
  top: calc(22px + var(--safe-t));
  left: 0;
  right: 0;
  z-index: 2;
  display: flex;
  align-items: center;
  gap: 9px;
  padding: 0 8px 0 10px;
}
.story-mark {
  width: 30px;
  height: 30px;
  border-radius: 100px;
  object-fit: cover;
  flex: 0 0 auto;
}
.story-who {
  flex: 1 1 auto;
  min-width: 0;
  display: flex;
  align-items: baseline;
  gap: 8px;
  margin: 0;
  color: #fff;
  text-shadow: 0 1px 3px rgba(0, 0, 0, .55);
}
.story-name {
  font-family: var(--display);
  font-size: 14px;
  font-weight: 600;
  letter-spacing: -.015em;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
/* How long it has left, in the face the site sets facts in. It is the whole
   point of the top line: a story is a thing that goes away, and saying so is
   what makes anybody open it now rather than later. */
.story-left {
  flex: 0 0 auto;
  font-size: 10.5px;
  letter-spacing: .1em;
  text-transform: uppercase;
  color: rgba(255, 255, 255, .78);
}

.story-btn {
  flex: 0 0 auto;
  display: grid;
  place-items: center;
  width: 34px;
  height: 34px;
  border-radius: 100px;
  color: rgba(255, 255, 255, .86);
  transition: color .15s var(--ease), background-color .15s var(--ease);
}
.story-btn:hover { color: #fff; background: rgba(255, 255, 255, .14); }
.story-btn:focus-visible { outline-color: #fff; }
.story-btn svg {
  width: 20px;
  height: 20px;
  fill: none;
  stroke: currentColor;
  stroke-width: 1.7;
  stroke-linecap: round;
  stroke-linejoin: round;
}
#story-sound[hidden] { display: none; }
#story-sound svg .ico-hush { display: none; }
#story-sound.is-muted svg .ico-loud { display: none; }
#story-sound.is-muted svg .ico-hush { display: inline; }

.story-foot {
  position: absolute;
  left: 0;
  right: 0;
  bottom: 0;
  z-index: 2;
  display: grid;
  justify-items: center;
  gap: 12px;
  padding: 0 16px calc(20px + var(--safe-b));
  text-align: center;
}
.story-caption {
  max-width: 34ch;
  margin: 0;
  color: #fff;
  font-size: 14.5px;
  line-height: 1.5;
  text-shadow: 0 1px 4px rgba(0, 0, 0, .6);
}
.story-caption[hidden] { display: none; }

/* The link, as a pill rather than as a line of text: on a phone this is the
   thing being aimed at with a thumb over a moving picture. The arrow points
   up because that is the gesture this button replaces. */
.story-cta {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  padding: 11px 20px;
  border: 1px solid rgba(255, 255, 255, .34);
  border-radius: 100px;
  background: rgba(255, 255, 255, .16);
  backdrop-filter: blur(8px);
  -webkit-backdrop-filter: blur(8px);
  color: #fff;
  font-family: var(--display);
  font-size: 14.5px;
  font-weight: 600;
  letter-spacing: -.01em;
  text-decoration: none;
  cursor: pointer;
}
.story-cta[hidden] { display: none; }
.story-cta:hover { background: rgba(255, 255, 255, .28); color: #fff; }
.story-cta:focus-visible { outline-color: #fff; }
.story-cta svg {
  width: 15px;
  height: 15px;
  fill: none;
  stroke: currentColor;
  stroke-width: 2;
  stroke-linecap: round;
  stroke-linejoin: round;
  animation: story-nudge 2.4s var(--ease) infinite;
}
@keyframes story-nudge {
  0%, 62%, 100% { transform: translateY(0); }
  76% { transform: translateY(-3px); }
}

/* Held down. Everything laid over the picture gets out of the way, which is
   what holding a story is for. */
.story-stage.is-held .story-bars,
.story-stage.is-held .story-head,
.story-stage.is-held .story-foot { opacity: 0; }
.story-bars, .story-head, .story-foot { transition: opacity .2s var(--ease); }

@media (max-width: 860px) {
  /* Full bleed. A story on a phone is the screen — as much of it as the
     browser has left — and the rounded card with a strip of page either side
     of it is a desktop idea. */
  .stories {
    --story-h: calc(var(--vph, 100dvh) - var(--browser-b, 0px));
    padding: 0 0 var(--browser-b, 0px);
  }
  .story-stage {
    width: 100%;
    border-radius: 0;
  }
}

/* ---------- how this works: the tour ---------- */

/* Three things on one layer over the whole page, each put where it goes by
   placeTourStep() in assets/app.js and left to the stylesheet to get there:
   a ring round the thing being talked about, a cursor whose tip lands on it,
   and the bubble the mouth talks from. All three are positioned with a
   transform so the move between two steps is one eased slide rather than a
   layout, and so a change of step animates the same way a hand would move —
   the cursor most slowly, because it is the thing the eye follows.

   Over the nudge and the account sheet, under the toast: a toast saying the
   location could not be found is worth more than the step that pointed at
   the button. */
.tour {
  position: fixed;
  inset: 0;
  z-index: 2450;
  overscroll-behavior: contain;
}
.tour[hidden] { display: none; }
/* The first placement of each open is a jump, not a slide from wherever
   the pieces were left last time. */
.tour.no-move * { transition: none !important; }

/* The ring is the accent on a paper halo, the same two rings a pin wears,
   and a second ring breathes out from it so the eye is drawn there even
   while the cursor is still on its way. */
.tour-ring {
  position: absolute;
  left: 0;
  top: 0;
  box-sizing: border-box;
  border: 2px solid var(--accent);
  border-radius: 100px;
  box-shadow: 0 0 0 3px var(--paper);
  pointer-events: none;
  transition: transform .6s var(--ease), width .6s var(--ease), height .6s var(--ease);
}
.tour-ring[hidden] { display: none; }
.tour-ring::after {
  content: "";
  position: absolute;
  inset: -3px;
  border: 2px solid var(--accent);
  border-radius: inherit;
  animation: tour-breathe 1.6s ease-out infinite;
}
@keyframes tour-breathe {
  from { transform: scale(1); opacity: .7; }
  to   { transform: scale(1.22); opacity: 0; }
}

/* A cursor the size of a thumb: paper with an ink edge, so it reads on both
   the light map and the dark one, and a shadow under it so it is over the
   page rather than drawn on it. Its tip is at 8,3 of a 32 box, which is
   what placeTourStep() offsets for when it sends the tip somewhere. It
   presses when it arrives — a short scale about the tip. */
.tour-cursor {
  position: absolute;
  left: 0;
  top: 0;
  width: 64px;
  height: 64px;
  pointer-events: none;
  filter: drop-shadow(0 6px 10px rgba(0, 0, 0, .3));
  transition: transform .8s cubic-bezier(.35, .9, .25, 1);
}
.tour-cursor svg {
  width: 100%;
  height: 100%;
  fill: var(--paper);
  stroke: var(--ink);
  stroke-width: 1.4;
  stroke-linejoin: round;
  transform-origin: 16px 6px;
}
.tour-cursor.is-click svg { animation: tour-click .4s var(--ease); }
@keyframes tour-click { 45% { transform: scale(.8); } }

/* The bubble is the card the rest of the site uses, with the mark sitting
   on its top edge like a face over a fence — that is who is talking. The
   mark rocks gently while it talks. Every step is a line of reading text:
   the walk once opened with the site's one sentence set large in the
   display face, and that step went with its styling. */
.tour-bubble {
  position: absolute;
  left: 0;
  top: 0;
  width: min(340px, calc(100vw - 24px));
  padding: 16px 16px 14px;
  transition: transform .6s var(--ease);
}
.tour-face {
  position: absolute;
  top: -24px;
  left: 14px;
  width: 48px;
  height: 48px;
  border-radius: 100px;
  object-fit: cover;
  box-shadow: 0 0 0 3px var(--paper), var(--shadow);
  animation: tour-talk 2.6s ease-in-out infinite;
}
@keyframes tour-talk {
  0%, 100% { transform: rotate(-7deg); }
  50%      { transform: rotate(7deg) translateY(-2px); }
}
.tour-say {
  margin: 16px 0 0;
  font-size: 15px;
  line-height: 1.45;
}
.tour-row {
  display: flex;
  align-items: center;
  gap: 4px;
  margin-top: 12px;
}
/* .alt is display: inline-block, which beats the UA sheet's [hidden] — the
   same trap .rail-btn notes. Skip is hidden on the last step, where Got it
   is the only way out and a second way out beside it would be a choice
   between two things that do the same. */
#tour-skip[hidden] { display: none; }
.tour-dots {
  display: flex;
  gap: 5px;
  margin-left: auto;
}
.tour-dots i {
  width: 6px;
  height: 6px;
  border-radius: 100px;
  background: var(--hairline);
}
.tour-dots i.is-on { background: var(--accent); }

@media (prefers-reduced-motion: reduce) {
  .tour-ring, .tour-cursor, .tour-bubble { transition: none; }
  .tour-ring::after, .tour-face, .tour-cursor.is-click svg { animation: none; }
}

/* ---------- toast ---------- */

.toast {
  position: fixed;
  z-index: 2500;
  left: 50%;
  bottom: calc(84px + var(--safe-b));
  transform: translateX(-50%);
  padding: 9px 16px;
  background: var(--ink);
  color: var(--paper);
  border-radius: 100px;
  font-family: var(--mono);
  font-size: 11.5px;
  letter-spacing: .02em;
  max-width: calc(100vw - 32px);
  text-align: center;
}
.toast[hidden] { display: none; }

/* ---------- noscript ---------- */

.noscript {
  position: fixed;
  z-index: 1500;
  left: 50%;
  top: 50%;
  transform: translate(-50%, -50%);
  width: min(420px, calc(100vw - 32px));
  padding: 24px;
}
.noscript h2 { font-family: var(--display); font-size: 24px; font-weight: 600; letter-spacing: -.02em; margin: 5px 0 10px; }

/* ---------- desktop: keep the chips clear of the panel ---------- */

@media (min-width: 861px) {
  body.panel-open .filter-bar,
  body.panel-open .controls {
    right: calc(var(--panel-w) + var(--gap) * 2 + var(--safe-r));
  }
}

/* ---------- under 860px: bottom sheet ---------- */

@media (max-width: 860px) {
  /* vh is the *large* viewport on iOS — the one with the browser chrome
     collapsed — so a sheet sized in vh and anchored to the bottom can start
     above the top of what you can actually see, taking its close button and
     its grip with it. That is how a place could open with no way out of it.
     dvh follows the viewport you have right now, and --vph is the same number
     measured in JS and written back, which covers a browser that does not know
     dvh or gets it wrong; the plain vh line stays in front for one that has
     neither. The last term is the floor: whatever else happens the sheet
     leaves the chrome strip and the chip row showing, which is a way back out
     and the filters besides. */
  body { --sheet-h: min(82vh, 720px); }
  body { --sheet-h: min(calc(var(--vph, 100dvh) * .82), 720px, calc(var(--vph, 100dvh) - 110px - var(--safe-t))); }

  /* On a phone there is no room for the tagline, and with the tagline gone
     there is nothing left in the card that needed a card. What stands on the
     map instead is the account itself: the round mark, and beside it the name
     over the handle — the same three things in the same arrangement and the
     same corner as a profile page opens with.

     A grid rather than the flex row this was, because the mark has to stand
     against two lines of text instead of one. Spanning it across both rows is
     what keeps this corner exactly as tall as it was when the handle stood
     here alone: the text column is shorter than the mark, so the mark is
     still what sets the height, and the chip row below it has not moved.

     No max-width any more. The old card had one, and it never held anything:
     the handle is a single unbreakable word, so a narrow box does not wrap it,
     it just lets it hang out of the far side. What keeps it clear of the two
     buttons in the other corner is the size step at the bottom of this block,
     which is a thing the text actually obeys. */
  .brand {
    display: grid;
    grid-template-columns: auto 1fr;
    align-items: center;
    column-gap: 10px;
    width: auto;
  }
  /* The one sentence on this site that says what it is: every place on this
     map is a place I have eaten in myself. A stranger who arrives on a phone
     and never reads it is looking at a map of pins like any other, so it is
     no longer dropped here — it is the first thing the page says, and then it
     stops saying it, the same bargain the rail buttons below make with their
     labels. On a line of its own under the mark and the handle, across both
     columns, where it has the width to be prose rather than three words a
     line.

     Collapsed by default and opened by .hint-open from assets/app.js: the
     height animates so the sentence unrolls under the mark instead of
     appearing over the map. max-height is a ceiling for the transition to
     run to, set well past the three lines the longest translation takes. */
  .brand .tagline {
    grid-row: 3;
    grid-column: 1 / -1;
    max-width: min(78vw, 320px);
    margin: 0;
    max-height: 0;
    overflow: hidden;
    opacity: 0;
    font-size: 13px;
    line-height: 1.4;
    transition: opacity .3s var(--ease), max-height .38s var(--ease),
                margin-top .38s var(--ease);
  }
  .brand.hint-open .tagline {
    max-height: 6em;
    margin-top: 7px;
    opacity: 1;
  }

  /* The chip row begins where the mark ends, so a sentence unrolling under
     the mark lands straight on top of Filters. It makes room instead: the
     row slides down by the height of the line for as long as the line is up,
     and comes back up with it — the corner empties in the order it filled.
     --tell-h is measured in openBrandHint rather than guessed here, because
     three lines of Estonian are not two lines of English. */
  body.brand-telling .filter-bar { transform: translateY(var(--tell-h, 46px)); }

  /* The mark and the name are laid out against the handle and the sentence,
     which are not inside this box, so up here it stops being a box and lets
     its two children stand in the grid themselves. Anything measuring it —
     placeRail in assets/app.js — measures the mark and the handle instead. */
  .brand-head { display: contents; }

  .brand-mark { width: 40px; height: 40px; }
  /* Down the left of both text lines, whichever of the two is drawn: the ring
     is what the mark sits inside once a story is up. */
  .brand-mark,
  .brand-ring { grid-row: 1 / span 2; }

  /* The ring comes in a step with the circle it is drawn round, and the paper
     between the two comes in with it. */
  .brand-ring::before { inset: -4px; }
  .brand-ring .brand-mark { box-shadow: 0 0 0 2px var(--paper), var(--lift); }

  /* The name, at the size a name is given at the top of a profile rather
     than the 25px it takes on a screen with room for it. It was hidden here
     for as long as the handle was standing in for it, which left this corner
     with nothing that went home — the mark opens the stories, and a visitor
     who wanted the whole map back had nowhere to press. */
  .brand .wordmark {
    grid-row: 1;
    grid-column: 2;
    font-size: 15px;
    line-height: 1.2;
    white-space: nowrap;
  }
  /* And the handle under it, in the same face a step down, which is how a
     username is printed under a name. Not the mono the desktop sets it in:
     seventeen characters of it are half as wide again in mono as in the
     display face, and that width is the whole argument up here — what the
     two lines have to stay clear of is the PLACES button in the other
     corner. */
  .brand .ig-link {
    grid-row: 2;
    grid-column: 2;
    /* As wide as the handle and no wider. A grid item fills its track unless
       it is told not to, and this one takes its pointer events back — left to
       stretch, it would lie across the corner under the PLACES button and
       take the presses meant for it. */
    justify-self: start;
    font-family: var(--display);
    font-size: 12px;
    font-weight: 600;
    letter-spacing: -.01em;
    line-height: 1.2;
    color: var(--muted);
    white-space: nowrap;
  }
  .brand .ig-link:hover,
  .brand .ig-link:focus-visible { color: var(--accent); }

  /* A 320px screen has about 150px left over once PLACES and the language
     button have taken theirs, and the mark and two lines of text at full size
     want more than that. Everything comes down a step here, which is what
     keeps the last letter of each line off the PLACES button on the narrowest
     phone still being sold — measured there at 99px and 81px against the 107
     the corner has. */
  @media (max-width: 380px) {
    .brand { column-gap: 8px; }
    .brand-mark { width: 34px; height: 34px; }
    .brand .wordmark { font-size: 12.5px; }
    .brand .ig-link { font-size: 10.5px; }
    .brand .tagline { font-size: 12.5px; max-width: 82vw; }
    .brand-ring::before { inset: -3px; }
  }

  .controls { gap: 6px; }
  .btn { min-width: 34px; height: 34px; }

  #lang-switch { position: relative; overflow: visible; }
  /* The code on this button was inheriting the body serif at 15px, which put
     it a size and a typeface away from LIST sitting right under it. It is the
     same mono the row of codes uses on a desktop. */
  .btn-lang-now {
    display: flex;
    align-items: center;
    gap: 5px;
    padding: 0 10px;
    font-family: var(--mono);
    font-size: 11px;
    font-weight: 500;
    letter-spacing: .1em;
  }
  .btn-lang-now .caret { display: flex; }
  .btn-lang-now svg {
    width: 8px;
    height: 5px;
    fill: none;
    stroke: currentColor;
    stroke-width: 1.6;
    stroke-linecap: round;
    stroke-linejoin: round;
    transition: transform .15s var(--ease);
  }
  #lang-switch.is-open .btn-lang-now svg { transform: rotate(180deg); }

  .lang-list {
    display: none;
    position: absolute;
    z-index: 1;
    top: calc(100% + 6px);
    right: 0;
    flex-direction: column;
    background: var(--paper);
    border: 1px solid var(--hairline);
    border-radius: var(--radius-soft);
    box-shadow: var(--shadow);
    overflow: hidden;
  }
  #lang-switch.is-open .lang-list { display: flex; }
  .lang-list .btn {
    display: flex;
    align-items: center;
    justify-content: flex-start;
    gap: 9px;
    padding: 0 12px;
    min-width: 0;
  }
  .lang-list .btn + .btn { border-left: 0; border-top: 1px solid var(--hairline); }
  .lang-list .lang-name {
    display: block;
    font-family: var(--body);
    font-size: 13px;
    letter-spacing: 0;
  }
  .btn-wide { padding: 0 11px; gap: 5px; }

  .filter-bar { --chrome-h: 36px; left: 10px; right: 10px; }

  /* --kbd is what the on-screen keyboard covers. Lifting the sheet by it and
     taking the same off the height keeps the top edge exactly where it was,
     so the field you are typing in does not end up behind the keyboard. */
  .panel {
    top: auto;
    left: 0;
    right: 0;
    bottom: var(--kbd, 0px);
    width: auto;
    max-width: none;
    height: calc(var(--sheet-h) - var(--kbd, 0px));
    border-width: 1px 0 0;
    border-radius: 14px 14px 0 0;
    transform: translateY(calc(100% + 20px));
    box-shadow: 0 -6px 34px rgba(20, 26, 31, .16);
  }
  .panel {
    transition: transform .34s var(--ease), height .28s var(--ease),
                visibility 0s linear .34s, background-color .25s var(--ease);
  }
  .panel.is-open {
    transition: transform .34s var(--ease), height .28s var(--ease),
                visibility 0s, background-color .25s var(--ease);
  }
  .panel.is-dragging { transition: none; }

  /* A finger-sized strip along the top of the sheet: drag it to resize, tap
     it to swap between the two heights. touch-action keeps the browser from
     claiming the gesture as a page scroll. */
  .sheet-grip {
    display: block;
    position: absolute;
    z-index: 3;
    top: 0;
    left: 0;
    right: 0;
    height: 48px;
    touch-action: none;
    cursor: grab;
  }
  .sheet-grip:active { cursor: grabbing; }
  .sheet-grip::before {
    content: "";
    position: absolute;
    top: 9px;
    left: 50%;
    transform: translateX(-50%);
    width: 34px;
    height: 3px;
    border-radius: 100px;
    background: var(--hairline);
    transition: background .15s var(--ease), width .15s var(--ease);
  }
  .sheet-grip:focus-visible { outline: none; }
  .sheet-grip:focus-visible::before,
  .panel.is-dragging .sheet-grip::before { background: var(--accent); width: 46px; }
  /* The sheet is a phone screen, not a page: the rows get tighter so more of
     the list is above the fold, and the heading stops shouting. */
  .panel { --panel-pad: 18px; --panel-pad-t: 48px; --search-field-h: 40px; }
  .panel-scroll { padding-bottom: calc(26px + var(--safe-b)); }
  .list-group { font-size: 19px; }
  .search { margin-top: 12px; }
  /* 16px is the smallest a field can be before iOS zooms the page to reach
     it, and it never zooms back out: the panel is left a seventh too wide,
     with the send arrow and the badge on the right of every row off the side
     of the screen and the whole map pannable sideways. The height moves with
     it through the token.

     The chat's field is the same field and takes the same floor. It had been
     missed since the chat was written, which is how a first question came
     back with an answer nobody could read the right-hand edge of. */
  .search-input,
  .ask-input { font-size: 16px; }
  .list-label { margin-top: 20px; padding: 8px var(--panel-pad) 7px; padding-right: calc(var(--panel-pad) + 34px); }
  .list-label:first-of-type { margin-top: 10px; }
  .list-row { padding: 10px 2px; column-gap: 8px; }
  .list-name { font-size: 15.5px; }
  .list-sub { margin-top: 3px; gap: 10px; }
  .list-sub .deal-mark { padding: 1px 6px; font-size: 9.5px; }
  /* Narrower screen, narrower badge — the name is what the row is for and it
     gets the space back. */
  .depth-mark { gap: 4px; padding: 2px 7px; font-size: 9px; letter-spacing: .08em; }
  .depth-mark svg { width: 8px; height: 8px; }
  /* On a phone this is the way out of a sheet standing over the whole screen,
     so it is a target rather than a mark. It sits in the grip bar, which is
     the sheet's header: the pill to grab in the middle, the way out on the
     right, and the content scrolling underneath both. */
  .panel-close {
    top: 4px;
    right: 8px;
    width: 40px;
    height: 40px;
    color: var(--ink);
  }
  .panel-close svg { width: 19px; height: 19px; stroke-width: 1.8; }

  /* The mark follows it: a target on a phone rather than a mark, kept the
     same distance to its left as the close is from the edge. */
  .panel-save {
    top: 4px;
    right: 56px;
    min-width: 40px;
    height: 40px;
  }
  .panel-save:not(.has-n) { width: 40px; }
  .panel-save svg { width: 20px; height: 20px; stroke-width: 1.8; }
  .save-n { font-size: 13px; }

  /* The sheet used to cover the chip row, which is why the row used to be
     hidden while the panel was open. At the top of the screen it clears even
     the fully dragged-up sheet, so it stays where it is and the filters can
     be changed with the list open. */

  /* Two stops. A detail sheet opens at the full one: a place you tapped is a
     page to read, and at the half stop the reel under the name was sliced
     across the bottom edge of the screen. Drag the grip down for the half
     stop, where the map keeps the other half and the chosen pin has somewhere
     to be. The map is not worth much while you are reading anyway, so the
     rail steps out of the way at full height.

     Surprise me opens at the half stop instead — a place nobody chose is a
     question about where it is — which is also what keeps the rail on screen
     for the press that rolls again. selectPlace() in assets/app.js decides
     which of the two a place arrives at; both stops are these two rules. */
  body.panel-detail { --sheet-h: min(50vh, 470px); }
  body.panel-detail { --sheet-h: min(calc(var(--vph, 100dvh) * .5), 470px, calc(var(--vph, 100dvh) - 110px - var(--safe-t))); }
  body.panel-detail.sheet-full { --sheet-h: min(88vh, 780px); }
  body.panel-detail.sheet-full { --sheet-h: min(calc(var(--vph, 100dvh) * .88), 780px, calc(var(--vph, 100dvh) - 110px - var(--safe-t))); }
  body.sheet-full .rail { opacity: 0; pointer-events: none; }
  .rail { transition: opacity .2s var(--ease); }

  /* With the sheet up, a vertically centred rail sits behind it and Surprise
     me becomes unreachable — which kills the whole point of the button. Lay
     it out along the strip above the sheet instead. */
  body.panel-open .rail {
    top: auto;
    bottom: calc(var(--sheet-h) + 10px + var(--safe-b));
    transform: none;
    flex-direction: row;
    align-items: center;
    gap: 8px;
  }

  /* The rail introduces itself, then gets out of the way.

     A phone has no hover and no title tooltip, so the desktop pill lost its
     label here and the rail arrived as a column of bare discs: a head and
     shoulders, a die, a speech bubble, a play triangle, a coloured dot, a
     crosshair and a question mark, floating over a map, saying nothing about
     what pressing them does. People did not press them.
     So on a phone they now open wearing their label, in the order they are
     stacked, hold it long enough to read, and collapse to the icon so the map
     gets its screen back. The rail is body copy for four seconds and chrome
     after that.

     Every one of them is the same shape: an icon at the left, a label that
     grows out of nothing beside it, and a pill drawn around the pair — which
     is one rule now that the swatch and the crosshair wear the same pill as
     the ones above them. */
  .rail-label {
    display: block;
    min-width: 0;
    overflow: hidden;
    opacity: 0;
    transition: opacity .22s var(--ease);
  }
  .hint-open > .rail-label { opacity: 1; }

  /* Icon, then a track for the words that grows from nothing to exactly the
     width of them: 0fr to 1fr, which an auto-width grid resolves against the
     label's own max-content. A fixed ceiling in ch cannot do both halves of
     that job — one wide enough for "Показать моё местоположение" makes "Red"
     snap open in a tenth of the time, and one tuned to "Red" puts an ellipsis
     through the label explaining the button. This way every language gets the
     same slide and none of them gets cut.

     Collapsed it is a 35.6px disc — 21.6px of icon, 6px each side, the border
     on top of that — so the buttons still read as one column of circles down
     the edge of the map. One set of numbers for all seven, because every icon
     on the rail is drawn at the same size, the swatch's dot included. */
  .rail-btn {
    display: grid;
    grid-auto-flow: column;
    grid-template-columns: auto 0fr;
    align-items: center;
    justify-content: center;
    overflow: hidden;
    width: auto;
    min-width: 35.6px;
    height: 35.6px;
    padding: 0 6px;
    gap: 0;
    transition: border-color .15s var(--ease), color .15s var(--ease),
                grid-template-columns .34s var(--ease),
                padding .34s var(--ease), gap .34s var(--ease);
  }
  .rail-btn.hint-open {
    grid-template-columns: auto 1fr;
    padding: 0 13px 0 6px;
    gap: 8px;
  }

  .rail { gap: 8px; }

  .place-name { font-size: 26px; }
  .photo-grid { grid-template-columns: repeat(3, 1fr); }
  .toast { bottom: calc(74px + var(--safe-b)); }
}

@media (max-width: 420px) {
  .facts { grid-template-columns: 1fr; gap: 2px 0; }
  .facts dd { margin-bottom: 8px; }
}

/* ---------- reduced motion ---------- */

@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: .001ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: .001ms !important;
    scroll-behavior: auto !important;
  }
}

/* ---------- print: the list, on paper ---------- */

@media print {
  #map, .controls, .filter-bar, .rail, .brand, .panel-close, .panel-save, .toast, .lightbox, .stories { display: none !important; }
  body { overflow: visible; background: #fff; }
  .panel {
    position: static;
    transform: none;
    visibility: visible;
    width: auto;
    height: auto;
    border: 0;
    box-shadow: none;
  }
  .panel-scroll { overflow: visible; padding: 0; }
  .panel::before { display: none; }
  .list-label { position: static; margin-inline: 0; padding-inline: 0; }
  /* A player prints as a blank box the height of a reel, and on paper the
     panel has no padding for it to bleed through either. The link to the
     post underneath it is the part worth printing. */
  .reel-frame { display: none; }
  .search { display: none; }
}
