/* ==========================================================================
   Fast-Mood — pixel-art design system

   Rules the whole UI obeys:
   • every dimension is a multiple of 4px (--u), so nothing lands off-grid
   • no blur, ever: depth comes from hard 2px bevels, never from box-shadow blur
   • no border-radius: corners are cut with clip-path when they must be soft
   • canvases render nearest-neighbour so pixels stay pixels at any zoom
   ========================================================================== */

@import url('https://fonts.googleapis.com/css2?family=Press+Start+2P&family=Silkscreen:wght@400;700&display=swap');

:root {
  --u: 4px;

  --void:    #070b16;
  --deep:    #0b1020;
  --panel:   #111a33;
  --panel-2: #16203d;
  --raise:   #1d2a4d;
  --line:    #26355a;
  --line-2:  #3b4d7d;
  --ink:     #eef3ff;
  --dim:     #8fa3c8;
  --dimmer:  #5d6f95;

  --joy:      #ffd23f;
  --love:     #ff5c8a;
  --calm:     #4ec9b0;
  --energy:   #ff8f1f;
  --surprise: #35d0ff;
  --sadness:  #5b8dee;
  --fear:     #a06cf5;
  --anger:    #e23c3c;

  --accent:  var(--joy);
  --good:    #4ec9b0;
  --bad:     #e23c3c;

  --font-display: 'Press Start 2P', 'Silkscreen', ui-monospace, monospace;
  --font-ui:      'Silkscreen', ui-monospace, 'Courier New', monospace;

  --hud-w: 264px;
}

/* --------------------------------------------------------------- reset -- */
*, *::before, *::after { box-sizing: border-box; }

/* `hidden` means not rendered, and no layout rule gets a vote.
   The browser's own [hidden] rule carries the same specificity as any class
   here, and loses to it simply for being in the user-agent sheet — so a single
   `display:` on a component silently disabled the attribute. That is how the
   send button and the "place me on the map" button ended up side by side: the
   script had hidden one of them, and .fm-btn kept painting it anyway. Every
   toggled component was patching this one at a time; this settles it once. */
[hidden] { display: none !important; }

html, body {
  margin: 0;
  padding: 0;
  height: 100%;
  background: var(--void);
  color: var(--ink);
  font-family: var(--font-ui);
  font-size: 14px;
  line-height: 1.5;
  -webkit-font-smoothing: none;
  font-smooth: never;
  text-rendering: optimizeSpeed;
}

body {
  /* faint CRT scanlines — 2px period, so it stays on the pixel grid */
  background-image: repeating-linear-gradient(
    to bottom,
    rgba(255, 255, 255, 0.016) 0 1px,
    transparent 1px 2px
  );
  overscroll-behavior: none;
}

canvas { image-rendering: pixelated; image-rendering: crisp-edges; display: block; }
/* height:auto is not decoration. Every <img> here carries width and height
   attributes so the browser can reserve the space before the file arrives —
   and those attributes become the used height the moment CSS sets a width.
   The shared card is 1200x630 declared and 100% wide applied, so on a phone it
   was drawn 390 wide and 630 tall: the same picture, stretched. */
img { image-rendering: pixelated; max-width: 100%; height: auto; }

a { color: var(--accent); text-decoration: none; }
a:hover { text-decoration: underline; }

button, input, select, textarea { font: inherit; color: inherit; }

::selection { background: var(--accent); color: var(--void); }

/* ------------------------------------------------------------ scrollbar -- */
* { scrollbar-width: thin; scrollbar-color: var(--line-2) transparent; }
*::-webkit-scrollbar { width: 8px; height: 8px; }
*::-webkit-scrollbar-track { background: var(--deep); }
*::-webkit-scrollbar-thumb { background: var(--line-2); border: 2px solid var(--deep); }

/* ------------------------------------------------------- pixel surfaces -- */
/* The bevel: 2px light on top/left, 2px dark on bottom/right. One shadow
   declaration, zero blur — the whole 3D language of the UI comes from here. */
.px {
  background: var(--panel);
  box-shadow:
    inset 2px 2px 0 0 var(--raise),
    inset -2px -2px 0 0 var(--void),
    0 0 0 2px var(--line);
}

.px--sunken {
  background: var(--deep);
  box-shadow:
    inset 2px 2px 0 0 var(--void),
    inset -2px -2px 0 0 var(--raise),
    0 0 0 2px var(--line);
}

.px--flat { background: var(--panel); box-shadow: 0 0 0 2px var(--line); }

/* --------------------------------------------------------------- layout -- */
.fm-app {
  display: flex;
  flex-direction: column;
  min-height: 100vh;
  min-height: 100dvh;
}

/* Pinned at every width.
   It was only stuck on phones, on the grounds that the map fills the screen
   there — but a long page is a long page on a desktop too, and the navigation
   travelling off the top means scrolling back up to reach it. The compact
   behaviour below stays a small-screen affair: on a wide screen there is room
   for the name and no reason to take it away. */
.fm-pinned {
  position: sticky;
  top: 0;
  /* Above the map and its canvas layers, below the prompt and the toast. */
  z-index: 40;
}

.fm-top {
  display: flex;
  align-items: center;
  gap: calc(var(--u) * 3);
  padding: calc(var(--u) * 2) calc(var(--u) * 3);
  background: var(--deep);
  box-shadow: 0 2px 0 0 var(--line), 0 4px 0 0 var(--void);
  position: relative;
  z-index: 30;
  flex-wrap: wrap;
}

.fm-logo {
  display: flex;
  align-items: center;
  gap: calc(var(--u) * 2);
  font-family: var(--font-display);
  font-size: 15px;
  letter-spacing: 1px;
  color: var(--ink);
  white-space: nowrap;
}

/* The shadow belongs to the letters. On the flex parent it applied to the mark
   as well, which put a yellow smear behind a 16-pixel drawing. */
.fm-logo > span {
  text-shadow: 2px 2px 0 var(--void), 4px 4px 0 rgba(255, 210, 63, 0.25);
}

.fm-logo__mark {
  /* A multiple of the 16px grid, so every art pixel lands on whole screen
     pixels and the edges stay hard. */
  width: 32px;
  height: 32px;
  image-rendering: pixelated;
  flex: none;
}
.fm-logo b { color: var(--accent); }
.fm-logo:hover { text-decoration: none; }

.fm-nav { display: flex; gap: calc(var(--u) * 2); margin-left: auto; align-items: center; flex-wrap: wrap; }

/* The language picker, wide where there is room and narrow where there is not.
 *
 * A native select is as wide as its longest option, and the twelfth language
 * made that "Bahasa Indonesia". On a desktop that is fine and the full name is
 * the right thing to show. On a phone it was 175px of a 360px bar, and the
 * single reason the header ran to three rows. */
.fm-lang { display: flex; gap: calc(var(--u)); align-items: center; position: relative; }

/* Shown only below the breakpoint; see the compact rules further down. */
.fm-lang__code { display: none; }

.fm-nav a {
  color: var(--dim);
  text-transform: uppercase;
  font-size: 12px;
  letter-spacing: 1px;
  padding: calc(var(--u)) calc(var(--u) * 2);
}
.fm-nav a:hover, .fm-nav a[aria-current='page'] {
  color: var(--ink);
  background: var(--panel);
  box-shadow: 0 0 0 2px var(--line);
  text-decoration: none;
}

/* --------------------------------------------------------------- stage --- */
.fm-stage {
  position: relative;
  flex: 1;
  /* Fills the window below the header, so the map keeps its full-screen feel
     and the crawlable band sits just off-screen until you scroll. */
  min-height: min(78vh, 900px);
  background: var(--void);
}

/* Everything that must sit *on* the map is inside this box. On a wide screen it
   fills the stage and the HUD panels float over it; on a narrow one it becomes a
   fixed-height band and the panels flow underneath — without this wrapper the
   overlays anchor to the whole column and the zoom controls end up on top of
   the legend. */
.fm-viewport {
  position: absolute;
  inset: 0;
  overflow: hidden;
  touch-action: none;
}

#fm-map { position: absolute; inset: 0; width: 100%; height: 100%; cursor: grab; }
#fm-map.is-dragging { cursor: grabbing; }

.fm-hud {
  position: absolute;
  z-index: 10;
  width: var(--hud-w);

  /* Capped against the window, not against the parent, and that is not a
     preference.

     This was `calc(100% - var(--u) * 8)`. The percentage resolves against
     .fm-stage, which is a flex item of .fm-app — and .fm-app has min-height
     but no height, so its own height comes from its content and is indefinite.
     A percentage max-height whose containing block has an indefinite height
     computes to `none`. The cap therefore did nothing at all, `overflow-y`
     never had anything to clamp, and the right-hand panel — the one with the
     most in it — grew past "add my mood" and out of the bottom of the map.

     .fm-viewport escaped it because `inset: 0` is resolved from the used
     height after layout, which is why the map itself was the right size while
     the panels over it were not.

     min(78vh, 900px) is .fm-stage's own min-height, so the stage is never
     shorter than this and the panel can never reach past it. When the stage is
     taller the panel simply scrolls a little sooner, which costs nothing.

     The 28 is where the panel has to stop, counted up from the bottom of the
     stage, and it is not a round number chosen for looks:

         12u   .fm-actions sits at bottom: 12u
        + 8u   and "add my mood" is about that tall (10px display type in
               2u of vertical padding)
        + 4u   a gap, so the two are not touching
        + 4u   the panel's own top: 4u
        ----
         28u

     The first version subtracted 8u, which capped the panel inside the map and
     nothing more: it still ran 60-odd pixels past the top of the button and
     stopped 16px short of the bottom edge. The button is on the right, which is
     this panel's own side, so clearing the frame was never the requirement —
     clearing the button is. */
  max-height: calc(min(78vh, 900px) - var(--u) * 28);
  overflow-y: auto;
  overscroll-behavior: contain;
  padding: calc(var(--u) * 3);
  background: rgba(11, 16, 32, 0.92);
  box-shadow:
    inset 2px 2px 0 0 var(--raise),
    inset -2px -2px 0 0 var(--void),
    0 0 0 2px var(--line);
}

.fm-hud--left  { top: calc(var(--u) * 4); left: calc(var(--u) * 4); }
.fm-hud--right { top: calc(var(--u) * 4); right: calc(var(--u) * 4); }

.fm-hud h2 {
  font-family: var(--font-display);
  font-size: 9px;
  letter-spacing: 1px;
  color: var(--dim);
  margin: 0 0 calc(var(--u) * 3);
  text-transform: uppercase;
}

/* Any block followed by a section heading gets breathing room. */
.fm-hud > * + h2 { margin-top: calc(var(--u) * 5); }

/* ------------------------------------------------------------- kpi tile -- */
.fm-kpi { display: grid; grid-template-columns: 1fr 1fr; gap: 2px; margin-bottom: calc(var(--u) * 3); }

.fm-kpi__cell {
  background: var(--deep);
  padding: calc(var(--u) * 2);
  box-shadow: inset 2px 2px 0 0 var(--void), inset -2px -2px 0 0 var(--panel-2);
}

.fm-kpi__label { font-size: 10px; color: var(--dimmer); text-transform: uppercase; letter-spacing: 1px; }
.fm-kpi__value { font-family: var(--font-display); font-size: 14px; margin-top: var(--u); color: var(--ink); }
.fm-kpi__value small { font-size: 9px; color: var(--dim); }

.fm-delta { font-size: 11px; }
.fm-delta--up   { color: var(--good); }
.fm-delta--down { color: var(--bad); }
.fm-delta--flat { color: var(--dimmer); }

/* --------------------------------------------------------------- gauge --- */
/* Segmented pixel gauge: 20 discrete blocks, no gradients. */
.fm-gauge { display: flex; gap: 2px; height: calc(var(--u) * 4); margin: calc(var(--u) * 2) 0; }
.fm-gauge__seg { flex: 1; background: var(--panel-2); }
.fm-gauge__seg.is-on { background: var(--accent); box-shadow: inset 0 -2px 0 0 rgba(0, 0, 0, 0.35); }

/* The heading's escape hatch to the page that explains the numbers. */
.fm-hud__more {
  float: right;
  font-family: var(--font-body, inherit);
  font-size: 9px;
  letter-spacing: 0;
  text-transform: none;
  color: var(--dim);
}
.fm-hud__more:hover { color: var(--accent); }

/* The panel heading, doubling as the way to the page behind it. Inherits the
   heading entirely — it should read as a title first and reveal itself as a
   link on approach, not announce itself as one. */
.fm-hud__link {
  color: inherit;
  text-decoration: none;
}
.fm-hud__link:hover { color: var(--accent); text-decoration: underline; }
.fm-hud__link:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }

/* The place the panel is about, beside the heading rather than in it: a name
   that arrives after the page has drawn should not change the heading's
   weight, and it may be long. */
.fm-hud__where {
  font-family: var(--font-body, inherit);
  font-size: 9px;
  letter-spacing: 0;
  text-transform: none;
  color: var(--dimmer);
}

.fm-gauge { cursor: help; }
.fm-gauge:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }

/* ---------------------------------------------------------- mood legend -- */
.fm-legend { display: flex; flex-direction: column; gap: 2px; }

/* A family does two things, so its row holds two controls: the filter, which is
   most of the width, and a door to the family's own page. The wrapper exists
   because a link cannot live inside a button. */
.fm-legend__item { display: flex; gap: 2px; }
.fm-legend__item > .fm-legend__row { flex: 1; min-width: 0; }

.fm-legend__go {
  display: flex;
  align-items: center;
  justify-content: center;
  flex: none;
  width: calc(var(--u) * 6);
  background: var(--deep);
  color: var(--dimmer);
  text-decoration: none;
  font-size: 13px;
  line-height: 1;
}
.fm-legend__go:hover { background: var(--panel-2); color: var(--accent); }
.fm-legend__go:focus-visible { outline: 2px solid var(--accent); outline-offset: -2px; }

.fm-legend__row {
  display: grid;
  grid-template-columns: calc(var(--u) * 4) minmax(0, 62px) 1fr auto;
  align-items: center;
  gap: calc(var(--u) * 2);
  padding: calc(var(--u)) calc(var(--u) * 1.5);
  background: var(--deep);
  cursor: pointer;
  border: 0;
  width: 100%;
  text-align: left;
  font-size: 12px;
  color: var(--dim);
}
.fm-legend__row:hover { background: var(--panel-2); color: var(--ink); }
.fm-legend__row.is-off { opacity: 0.35; }

.fm-swatch {
  width: calc(var(--u) * 3);
  height: calc(var(--u) * 3);
  box-shadow: inset -2px -2px 0 0 rgba(0, 0, 0, 0.45), 0 0 0 1px var(--void);
}

.fm-legend__bar { height: calc(var(--u) * 2); background: var(--panel-2); position: relative; }
.fm-legend__bar i { display: block; height: 100%; }

/* A legend row without a name column (hover card, news list) collapses to
   three tracks; the modifier keeps those aligned too. */
.fm-legend__row--wide { grid-template-columns: calc(var(--u) * 4) 1fr auto; }

.fm-legend__name {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  font-size: 11px;
  text-transform: uppercase;
  letter-spacing: 0.5px;
}

/* Tells people the squares are clickable, which nothing else did. */
.fm-hint {
  margin: calc(var(--u) * 3) 0 0;
  padding: var(--u) calc(var(--u) * 2);
  background: var(--panel-2);
  border-left: 3px solid var(--accent);
  font-size: 11px;
  color: var(--dim);
}

/* --------------------------------------------------------------- chips --- */
.fm-chips { display: flex; flex-wrap: wrap; gap: var(--u); }

/* The four sources on one line, in every language, all four readable.
 *
 * They wrapped to two rows, which made one filter look like two unrelated pairs
 * and pushed the mix down the panel. One row is not free: 240px of row against
 * "Menschen Social KI Nachrichten", which wants over 270 at the shared chip
 * size. Something has to give, and it must not be the words — a filter whose
 * label is cut to "Nachrich…" cannot be read, which is worse than the wrapping
 * this replaces. Equal columns were tried first and did exactly that.
 *
 * So the type gives: 9px and no letter-spacing for this row only, matching the
 * panel's own headings, with the padding tightened to match. Chips size to
 * their own word and shrink only if they must, so "KI" does not sit in a
 * quarter of the row while "Nachrichten" runs out of it. */
.fm-chips--sources {
  flex-wrap: nowrap;
  gap: calc(var(--u) / 2);
}
.fm-chips--sources .fm-chip {
  flex: 0 1 auto;
  min-width: 0;
  font-size: 9px;
  letter-spacing: 0;
  padding: var(--u) calc(var(--u) / 2);
  white-space: nowrap;
}

.fm-chip {
  font-size: 11px;
  text-transform: uppercase;
  letter-spacing: 1px;
  padding: calc(var(--u)) calc(var(--u) * 2);
  background: var(--panel-2);
  color: var(--dim);
  border: 0;
  cursor: pointer;
  box-shadow: inset 1px 1px 0 0 var(--raise), inset -1px -1px 0 0 var(--void);
}
.fm-chip:hover { color: var(--ink); }
.fm-chip.is-on { background: var(--accent); color: var(--void); font-weight: 700; }

/* A nuance chip carries its family's colour, so a row of them reads as a mood
   mix rather than as a list of words with numbers after them. */
.fm-chip--tinted {
  display: inline-flex;
  align-items: center;
  gap: calc(var(--u) * 1.5);
  border-left: 3px solid currentColor;
  cursor: help;
}
.fm-chip--tinted .fm-swatch { flex: none; }

/* -------------------------------------------------------------- buttons -- */
.fm-btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: calc(var(--u) * 2);
  padding: calc(var(--u) * 2) calc(var(--u) * 4);
  font-family: var(--font-display);
  font-size: 10px;
  letter-spacing: 1px;
  text-transform: uppercase;
  color: var(--ink);
  background: var(--raise);
  border: 0;
  cursor: pointer;
  box-shadow:
    inset 2px 2px 0 0 var(--line-2),
    inset -2px -2px 0 0 var(--void),
    0 0 0 2px var(--line);
  transition: none;
}
.fm-btn:hover { background: var(--line); }
.fm-btn:active {
  box-shadow:
    inset -2px -2px 0 0 var(--line-2),
    inset 2px 2px 0 0 var(--void),
    0 0 0 2px var(--line);
  transform: translate(1px, 1px);
}
.fm-btn:disabled { opacity: 0.4; cursor: not-allowed; }

.fm-btn--primary { background: var(--accent); color: var(--void); }
.fm-btn--primary:hover { background: #ffe27a; }
.fm-btn--primary { box-shadow: inset 2px 2px 0 0 #fff2b0, inset -2px -2px 0 0 #a8791a, 0 0 0 2px var(--void); }

.fm-btn--block { width: 100%; }
.fm-btn--sm { padding: var(--u) calc(var(--u) * 2); font-size: 9px; }

/* ------------------------------------------------------------- zoom bar -- */
/* Deliberately loud: on a full-bleed map these are the only affordances, so
   they get a solid panel, real labels and a bright accent rather than two
   cryptic glyphs lost against the ocean. */
.fm-zoombar {
  position: absolute;
  left: calc(var(--u) * 4);
  bottom: calc(var(--u) * 12);
  z-index: 12;
  display: flex;
  flex-direction: column;
  gap: calc(var(--u));
  padding: calc(var(--u) * 2);
  background: rgba(11, 16, 32, 0.94);
  box-shadow:
    inset 2px 2px 0 0 var(--raise),
    inset -2px -2px 0 0 var(--void),
    0 0 0 2px var(--line);
}

.fm-zoombar__pair { display: flex; gap: calc(var(--u)); }

.fm-zoombar button {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: calc(var(--u) * 1.5);
  min-height: calc(var(--u) * 10);
  padding: 0 calc(var(--u) * 2);
  font-family: var(--font-display);
  font-size: 10px;
  letter-spacing: 1px;
  background: var(--raise);
  color: var(--ink);
  border: 0;
  cursor: pointer;
  white-space: nowrap;
  box-shadow: inset 2px 2px 0 0 var(--line-2), inset -2px -2px 0 0 var(--void), 0 0 0 2px var(--void);
}
.fm-zoombar button:hover { background: var(--line-2); }
.fm-zoombar button:active { transform: translate(1px, 1px); }

/* +/- are square; the labelled actions stretch to the panel width. */
.fm-zoombar__pair button { flex: 1; font-size: 14px; }
.fm-zoombar__wide { width: 100%; }
.fm-zoombar__wide--accent { background: var(--accent); color: var(--void); }
.fm-zoombar__wide--accent:hover { background: #ffe27a; }
.fm-zoombar__wide--accent {
  box-shadow: inset 2px 2px 0 0 #fff2b0, inset -2px -2px 0 0 #a8791a, 0 0 0 2px var(--void);
}

/* "Whole world" is the way back out, and it was the quietest control on the
   map. A slow breath on its marker is enough to find it without pulling the
   eye off the map — and it only runs while there is somewhere to go back
   from, which app.js decides by watching the zoom. */
@keyframes fm-world-breathe {
  0%, 100% { opacity: 1; }
  50%      { opacity: 0.25; }
}

.fm-zoombar__wide.is-callable i { animation: fm-world-breathe 2.4s ease-in-out infinite; }
.fm-zoombar__wide.is-callable { color: var(--ink); }
.fm-zoombar__wide:hover i { animation: none; opacity: 1; }

@media (prefers-reduced-motion: reduce) {
  .fm-zoombar__wide.is-callable i { animation: none; }
}

.fm-zoombar i {
  display: block;
  width: calc(var(--u) * 3);
  height: calc(var(--u) * 3);
  flex: none;
}

/* Sits on the map, above the ticker. Declared here rather than inline so the
   narrow-screen rules can move it.

   The wrapper carries the position so a second button — sharing, once there is
   something to share — sits beside the first instead of under it. */
.fm-actions {
  position: absolute;
  right: calc(var(--u) * 4);
  bottom: calc(var(--u) * 12);
  z-index: 14;
  display: flex;
  gap: calc(var(--u) * 2);
}

/* ------------------------------------------------------ placing a pin ---- */
/* Geolocation can be denied, blocked by a VPN, or simply unavailable. This is a
   map — tapping it is the obvious way to answer "where are you?", and it beats
   the old behaviour of filing the mood at 20N 0E, in the Gulf of Guinea. */
.fm-placing {
  position: absolute;
  top: calc(var(--u) * 4);
  left: 50%;
  transform: translateX(-50%);
  z-index: 22;
  display: flex;
  align-items: center;
  gap: calc(var(--u) * 3);
  padding: calc(var(--u) * 2) calc(var(--u) * 3);
  background: var(--accent);
  color: var(--void);
  font-size: 12px;
  font-weight: 700;
  box-shadow: 0 0 0 2px var(--void), 0 4px 0 0 rgba(0, 0, 0, 0.4);
  animation: fm-placing-pulse 1.4s steps(2) infinite;
}

@keyframes fm-placing-pulse { 50% { background: #ffe27a; } }

@media (prefers-reduced-motion: reduce) {
  .fm-placing { animation: none; }
}

/* The cursor is the affordance: crosshair means "click to drop yourself". */
.fm-viewport.is-placing #fm-map { cursor: crosshair; }
.fm-viewport.is-placing { box-shadow: inset 0 0 0 3px var(--accent); }

/* The recovery link stays folded away until asked for. */
.fm-reveal { margin-top: calc(var(--u) * 3); }
.fm-reveal summary {
  cursor: pointer;
  color: var(--accent);
  font-size: 12px;
  text-decoration: underline;
}
.fm-reveal[open] summary { margin-bottom: calc(var(--u) * 2); }

.fm-warn {
  font-size: 11px;
  color: var(--ink);
  background: var(--panel-2);
  padding: calc(var(--u) * 2);
  margin: 0 0 calc(var(--u) * 2);
  border-left: 3px solid var(--bad);
}

.fm-sep { color: var(--dimmer); font-size: 11px; }

.fm-link {
  background: none;
  border: 0;
  padding: 0;
  color: var(--accent);
  font: inherit;
  font-size: 11px;
  cursor: pointer;
  text-decoration: underline;
}

/* -------------------------------------------------------- detail panel --- */
/* Opens over the map when a cell is clicked. Moves with steps() rather than a
   smooth curve: everything else here sits on a pixel grid, and an eased
   transition would be the one thing that does not. */
.fm-detail {
  position: absolute;
  top: calc(var(--u) * 4);
  right: calc(var(--u) * 4);
  z-index: 18;
  width: min(340px, calc(100% - var(--u) * 8));
  max-height: calc(100% - var(--u) * 20);
  overflow-y: auto;
  overscroll-behavior: contain;

  /* Scrolling in here stays in here.
     Reach the bottom of the panel and the browser hands the remaining scroll
     to whatever is behind it — which on this page is the map and then the
     document, so reading to the end of a cell's notes dragged the page out
     from under the panel. `contain` stops the chain at this box without
     locking the page, which is what `none` would do. */
  overscroll-behavior: contain;

  padding: calc(var(--u) * 4);
  background: rgba(11, 16, 32, 0.97);
  box-shadow:
    inset 2px 2px 0 0 var(--raise),
    inset -2px -2px 0 0 var(--void),
    0 0 0 2px var(--line),
    0 8px 0 0 rgba(0, 0, 0, 0.45);
  transform: translateX(12px);
  opacity: 0;
  transition: transform 120ms steps(4), opacity 120ms steps(4);
}
.fm-detail.is-open { transform: none; opacity: 1; }

@media (prefers-reduced-motion: reduce) {
  .fm-detail { transition: none; }
}

.fm-detail__close { position: absolute; top: calc(var(--u) * 2); right: calc(var(--u) * 2); }
.fm-detail__loading { color: var(--dim); font-size: 12px; padding: calc(var(--u) * 4) 0; }

.fm-detail__place {
  font-size: 11px;
  text-transform: uppercase;
  letter-spacing: 1px;
  color: var(--dim);
  padding-right: calc(var(--u) * 8);
}

.fm-detail__headline {
  font-family: var(--font-display);
  font-size: 16px;
  margin: var(--u) 0 calc(var(--u) * 2);
  line-height: 1.4;
}

.fm-detail__sub { font-size: 12px; color: var(--dimmer); margin-bottom: calc(var(--u) * 3); }

.fm-detail__index {
  padding: calc(var(--u) * 2);
  background: var(--deep);
  margin-bottom: calc(var(--u) * 3);
  box-shadow: inset 2px 2px 0 0 var(--void), inset -2px -2px 0 0 var(--panel-2);
}
.fm-detail__number { font-family: var(--font-display); font-size: 22px; margin: var(--u) 0; }

.fm-detail h3 {
  font-family: var(--font-display);
  font-size: 9px;
  letter-spacing: 1px;
  text-transform: uppercase;
  color: var(--dim);
  margin: calc(var(--u) * 4) 0 calc(var(--u) * 2);
}

.fm-detail__news {
  display: block;
  font-size: 12px;
  color: var(--dim);
  padding: var(--u) 0;
  border-bottom: 1px solid var(--panel-2);
}
.fm-detail__news:hover { color: var(--ink); }

.fm-detail__foot { display: flex; gap: var(--u); margin-top: calc(var(--u) * 4); }
.fm-detail__foot .fm-btn { flex: 1; }

/* --------------------------------------------------------- labelled bars -- */
/* A name, a bar and a number. The swatch-and-percentage rows this replaces were
   unreadable unless you had already memorised the colour code. */
.fm-bars { display: flex; flex-direction: column; gap: 2px; }

.fm-bar {
  display: grid;
  grid-template-columns: minmax(0, 74px) 1fr auto;
  align-items: center;
  gap: calc(var(--u) * 1.5);
  font-size: 12px;
  color: var(--dim);
}

.fm-bar__name { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.fm-bar__track { height: calc(var(--u) * 3); background: var(--panel-2); }
.fm-bar__track i { display: block; height: 100%; box-shadow: inset 0 -2px 0 0 rgba(0, 0, 0, 0.3); }
.fm-bar__value { color: var(--ink); font-size: 11px; }

.fm-chip--nuance { padding-left: calc(var(--u) * 1.5); }
.fm-chip--nuance b { color: var(--ink); }

.fm-quote {
  margin: 0 0 var(--u);
  padding: calc(var(--u) * 1.5) calc(var(--u) * 2);
  background: var(--deep);
  border-left: 3px solid var(--accent);
  font-size: 12px;
  color: var(--ink);
}
.fm-quote__mood {
  display: block;
  font-size: 10px;
  text-transform: uppercase;
  letter-spacing: 1px;
  margin-bottom: 2px;
}

/* --------------------------------------------------------------- ticker -- */
.fm-ticker {
  position: absolute;
  left: 0;
  right: 0;
  bottom: 0;
  z-index: 11;
  height: calc(var(--u) * 8);
  display: flex;
  align-items: center;
  gap: calc(var(--u) * 6);
  padding: 0 calc(var(--u) * 3);
  background: rgba(7, 11, 22, 0.94);
  box-shadow: 0 -2px 0 0 var(--line);
  overflow: hidden;
  white-space: nowrap;
  font-size: 12px;
  color: var(--dim);
}

.fm-ticker__track { display: flex; gap: calc(var(--u) * 8); animation: fm-scroll 60s linear infinite; }
.fm-ticker:hover .fm-ticker__track { animation-play-state: paused; }
.fm-ticker__item { display: inline-flex; align-items: center; gap: calc(var(--u) * 2); }

@keyframes fm-scroll {
  from { transform: translateX(0); }
  to   { transform: translateX(-50%); }
}

@media (prefers-reduced-motion: reduce) {
  .fm-ticker__track { animation: none; }
}

/* --------------------------------------------------------------- prompt -- */
/* The mood prompt is a bottom sheet on mobile, a floating panel on desktop. */
.fm-prompt {
  position: absolute;
  z-index: 25;
  left: 50%;
  bottom: calc(var(--u) * 12);
  transform: translateX(-50%);
  width: min(560px, calc(100% - var(--u) * 8));
  padding: calc(var(--u) * 4);
  background: var(--panel);
  box-shadow:
    inset 2px 2px 0 0 var(--raise),
    inset -2px -2px 0 0 var(--void),
    0 0 0 2px var(--line),
    0 8px 0 0 rgba(0, 0, 0, 0.5);
}


.fm-prompt__q {
  font-family: var(--font-display);
  font-size: 13px;
  line-height: 1.8;
  margin: 0 0 calc(var(--u) * 4);
  color: var(--ink);
}
.fm-prompt__q em { color: var(--accent); font-style: normal; }

.fm-prompt__hint { font-size: 12px; color: var(--dimmer); margin: 0 0 calc(var(--u) * 3); }

/* mood grid: 8 primary families, always 4 across */
.fm-moods { display: grid; grid-template-columns: repeat(4, 1fr); gap: calc(var(--u) * 2); }

.fm-mood {
  position: relative;
  padding: calc(var(--u) * 2) var(--u);
  background: var(--deep);
  border: 0;
  cursor: pointer;
  text-align: center;
  color: var(--dim);
  font-size: 11px;
  text-transform: uppercase;
  letter-spacing: 1px;
  box-shadow: inset 2px 2px 0 0 var(--panel-2), inset -2px -2px 0 0 var(--void);
}
.fm-mood:hover { color: var(--ink); }
.fm-mood.is-on { color: var(--void); font-weight: 700; }
.fm-mood.is-on .fm-mood__face { filter: none; }

.fm-mood__face {
  display: block;
  width: calc(var(--u) * 12);
  height: calc(var(--u) * 12);
  margin: 0 auto calc(var(--u));
}

/* nuance pills appear once a family is chosen */
.fm-nuances { margin-top: calc(var(--u) * 3); display: flex; flex-wrap: wrap; gap: var(--u); }

.fm-pill {
  font-size: 11px;
  padding: var(--u) calc(var(--u) * 2);
  background: var(--deep);
  color: var(--dim);
  border: 0;
  cursor: pointer;
  box-shadow: inset 1px 1px 0 0 var(--panel-2), inset -1px -1px 0 0 var(--void);
}
.fm-pill.is-on { background: var(--ink); color: var(--void); }

/* A light walks the nuances, one at a time.
   They are small, dim and easy to skim past, so the sweep says "these are
   yours to pick" without spending a caption on it. No blur, per the house
   rule: the highlight is one step up the surface ramp and one step up the
   bevel — exactly what :hover does by hand, just arriving on its own.

   --i is the pill's rank and --n the family's nuance count, both set in JS.
   The cycle outlasts the sweep, so the wave crosses the row and then rests
   before coming round again. */
@keyframes fm-pill-sweep {
  0%, 16%, 100% {
    background: var(--deep);
    color: var(--dim);
    box-shadow: inset 1px 1px 0 0 var(--panel-2), inset -1px -1px 0 0 var(--void);
  }
  6% {
    background: var(--raise);
    color: var(--ink);
    box-shadow: inset 1px 1px 0 0 var(--line-2), inset -1px -1px 0 0 var(--void);
  }
}

.fm-nuances .fm-pill {
  animation: fm-pill-sweep calc(var(--n, 6) * 140ms + 1800ms) linear infinite;
  animation-delay: calc(var(--i, 0) * 140ms);
}

/* It is an invitation, and it stops the moment it is taken: a chosen nuance,
   one under the cursor, or any pick at all in this family. */
.fm-nuances .fm-pill.is-on,
.fm-nuances .fm-pill:hover,
.fm-nuances.is-touched .fm-pill { animation: none; }

@media (prefers-reduced-motion: reduce) {
  .fm-nuances .fm-pill { animation: none; }
}

/* intensity: 5 discrete blocks, keyboard accessible */
.fm-intensity { display: flex; gap: 2px; margin-top: calc(var(--u) * 3); }
.fm-intensity button {
  flex: 1;
  height: calc(var(--u) * 5);
  background: var(--deep);
  border: 0;
  cursor: pointer;
  box-shadow: inset 1px 1px 0 0 var(--panel-2), inset -1px -1px 0 0 var(--void);
}
.fm-intensity button.is-on { background: var(--accent); }

.fm-note {
  width: 100%;
  margin-top: calc(var(--u) * 3);
  padding: calc(var(--u) * 2);
  background: var(--void);
  border: 0;
  color: var(--ink);
  font-size: 13px;
  resize: none;
  box-shadow: inset 2px 2px 0 0 var(--void), inset -2px -2px 0 0 var(--panel-2), 0 0 0 2px var(--line);
}
.fm-note:focus { outline: 2px solid var(--accent); outline-offset: 2px; }

.fm-prompt__foot {
  display: flex;
  align-items: center;
  gap: calc(var(--u) * 3);
  margin-top: calc(var(--u) * 4);
  flex-wrap: wrap;
}
.fm-prompt__foot .fm-btn { flex: 1; }

.fm-loc {
  font-size: 11px;
  color: var(--dimmer);
  display: flex;
  align-items: center;
  gap: var(--u);
  width: 100%;
}
.fm-loc b { color: var(--dim); font-weight: 400; }

/* --------------------------------------------------------------- tooltip -- */
/* Follows the cursor over the map: without it the colours are a puzzle, since
   the right-hand HUD is hidden on narrow screens. */
.fm-tip {
  position: fixed;
  z-index: 20;
  min-width: 132px;
  max-width: 220px;
  padding: calc(var(--u) * 2);
  background: rgba(7, 11, 22, 0.96);
  box-shadow: inset 1px 1px 0 0 var(--raise), 0 0 0 2px var(--line);
  pointer-events: none;
  font-size: 12px;
}

.fm-tip__place {
  font-size: 11px;
  text-transform: uppercase;
  letter-spacing: 1px;
  color: var(--dim);
  margin-bottom: calc(var(--u) * 1.5);
  padding-bottom: var(--u);
  border-bottom: 1px solid var(--line);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.fm-tip__mood {
  display: flex;
  align-items: center;
  gap: calc(var(--u) * 1.5);
  font-family: var(--font-display);
  font-size: 10px;
  letter-spacing: 1px;
  margin-bottom: var(--u);
}

.fm-tip__count { color: var(--dimmer); font-size: 11px; margin-bottom: calc(var(--u) * 1.5); }

.fm-tip__row {
  display: grid;
  grid-template-columns: calc(var(--u) * 3) 1fr auto;
  align-items: center;
  gap: var(--u);
  color: var(--dim);
  font-size: 11px;
  line-height: 1.7;
}

/* ---------------------------------------------------------------- toast -- */
.fm-toast {
  position: fixed;
  left: 50%;
  bottom: calc(var(--u) * 6);
  transform: translateX(-50%);
  z-index: 200;
  padding: calc(var(--u) * 2) calc(var(--u) * 4);
  background: var(--panel);
  color: var(--ink);
  font-size: 12px;
  box-shadow: 0 0 0 2px var(--line), 0 4px 0 0 rgba(0, 0, 0, 0.5);
  /* A server error message carries a file path and a line number, and is worth
     reading in full -- it must wrap rather than run off the side of the screen. */
  max-width: min(92vw, 560px);
  line-height: 1.45;
  overflow-wrap: anywhere;
}
.fm-toast--bad { background: var(--bad); color: #fff; }

/* ---------------------------------------------------------------- modal -- */
.fm-modal {
  position: fixed;
  inset: 0;
  z-index: 100;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: calc(var(--u) * 4);
  background: rgba(7, 11, 22, 0.88);
}

.fm-modal__box {
  width: min(520px, 100%);
  max-height: 86vh;
  overflow-y: auto;
  overscroll-behavior: contain;
  padding: calc(var(--u) * 4);
  background: var(--panel);
  box-shadow: inset 2px 2px 0 0 var(--raise), inset -2px -2px 0 0 var(--void), 0 0 0 2px var(--line);
}

.fm-modal__head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: calc(var(--u) * 3);
  margin-bottom: calc(var(--u) * 4);
}
.fm-modal__head h2 { font-family: var(--font-display); font-size: 12px; margin: 0; }

.fm-x {
  width: calc(var(--u) * 7);
  height: calc(var(--u) * 7);
  background: var(--deep);
  border: 0;
  color: var(--dim);
  cursor: pointer;
  font-family: var(--font-display);
  font-size: 10px;
  box-shadow: inset 1px 1px 0 0 var(--panel-2), inset -1px -1px 0 0 var(--void);
}
.fm-x:hover { color: var(--ink); background: var(--bad); }

/* ------------------------------------------------------- streak & twin --- */
/* The two things shown at the peak moment — right after posting — so they sit
   above the share buttons, not below them. */
.fm-streak {
  display: flex;
  align-items: center;
  gap: calc(var(--u) * 2);
  margin-bottom: calc(var(--u) * 3);
  padding: calc(var(--u) * 2);
  background: var(--deep);
  font-size: 12px;
  box-shadow: inset 2px 2px 0 0 var(--panel-2), inset -2px -2px 0 0 var(--void);
}

.fm-streak__count {
  font-family: var(--font-display);
  font-size: 18px;
  color: var(--accent);
  min-width: calc(var(--u) * 8);
  text-align: center;
}

.fm-streak__link { margin-left: auto; font-size: 11px; white-space: nowrap; }

.fm-twin {
  margin-bottom: calc(var(--u) * 3);
  padding: calc(var(--u) * 2) calc(var(--u) * 3);
  background: var(--deep);
  border-left: 4px solid var(--accent);
  font-size: 13px;
}
.fm-twin p { margin: 0; color: var(--ink); }
.fm-twin--alone { border-left-color: var(--line-2); }
.fm-twin--alone p { color: var(--dim); }

.fm-twin__label {
  font-size: 10px;
  text-transform: uppercase;
  letter-spacing: 1px;
  color: var(--dimmer);
  margin-bottom: var(--u);
}

.fm-twin__nuance { margin-top: var(--u) !important; color: var(--dim) !important; font-size: 12px; }

/* ------------------------------------------------------------- calendar -- */
/* Sixty squares. A visible run is what makes a streak worth not breaking. */
.fm-cal {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(calc(var(--u) * 4), 1fr));
  gap: 3px;
  margin: calc(var(--u) * 2) 0;
}

.fm-cal__day {
  aspect-ratio: 1;
  background: var(--panel-2);
  box-shadow: inset 1px 1px 0 0 var(--void);
}
.fm-cal__day.is-on { background: var(--accent); box-shadow: inset -1px -1px 0 0 rgba(0, 0, 0, 0.4); }

/* ---------------------------------------------------------------- share -- */
.fm-share-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(96px, 1fr)); gap: calc(var(--u) * 2); }

.fm-share-btn {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: var(--u);
  padding: calc(var(--u) * 2) var(--u);
  background: var(--deep);
  border: 0;
  color: var(--dim);
  cursor: pointer;
  font-size: 11px;
  box-shadow: inset 2px 2px 0 0 var(--panel-2), inset -2px -2px 0 0 var(--void);
}
.fm-share-btn:hover { color: var(--ink); background: var(--panel-2); }
/* X's brand colour is #111111 on a #0b1020 tile: the swatch vanished and the
   label "X" was all that showed, which reads as a broken icon rather than a
   network. A frame gives every swatch an edge, whichever end of the ramp the
   brand sits at. */
.fm-share-btn i {
  display: block;
  width: calc(var(--u) * 6);
  height: calc(var(--u) * 6);
  box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.35);
}

/* ------------------------------------------------------- shared mood page -- */
/* The card someone followed a link to see, and the two things to do about it.
   Narrow: the picture, then the panel under it. Wide: the two side by side,
   because a 1200-pixel image with its actions a scroll below is a page where
   the actions are never seen.

   Beside, not on top. The panel used to be positioned absolutely over the
   card's right edge, and it was wrong in both directions at once: it hid the
   right third of the picture — which is where the card puts "share your mood"
   and the index figure — and, at twelve share targets in two columns, it was
   roughly 200px taller than the 472px card, so its bottom rows sat over the
   footer. Neither was visible when the panel held four buttons. An overlay
   whose height is decided by a list that grows cannot be positioned against a
   picture whose height is fixed; a grid column has no such argument with
   itself. */
.fm-shared {
  /* The card is 1200x630. Past about 900 it stops being a preview and starts
     being a poster, and the eye has to travel the width of the screen to get
     from the picture to what to do next. */
  max-width: 900px;
}

.fm-card-preview {
  display: block;
  width: 100%;
  box-shadow: 0 0 0 2px var(--line);
}

.fm-shared__side { margin-top: calc(var(--u) * 4); }
.fm-shared__side h3 { margin: calc(var(--u) * 5) 0 calc(var(--u) * 2); }
.fm-shared__cta { margin-top: 0 !important; }

@media (min-width: 1080px) {
  /* Picture, then panel. `minmax(0, 1fr)` and not `1fr`: a grid column's
     default minimum is its content, and the card is a 1200px-wide image, so
     plain `1fr` refuses to go below 1200 and pushes the panel off the row. */
  .fm-shared {
    display: grid;
    grid-template-columns: minmax(0, 1fr) 232px;
    gap: calc(var(--u) * 4);
    align-items: start;
  }

  .fm-shared__side {
    margin: 0;
    padding: calc(var(--u) * 3);

    /* Solid, not translucent. It sits beside a busy map with text on it, and
       anything see-through reads as part of the picture. */
    background: var(--panel);
    box-shadow:
      inset 2px 2px 0 0 var(--raise),
      inset -2px -2px 0 0 var(--void),
      0 0 0 2px var(--line);
  }

  .fm-shared__side h3 { font-size: 11px; }
}

/* ---------------------------------------------------------------- pages -- */
.fm-page { max-width: 1180px; margin: 0 auto; padding: calc(var(--u) * 8) calc(var(--u) * 4) calc(var(--u) * 16); width: 100%; }

.fm-page h1 {
  font-family: var(--font-display);
  font-size: 20px;
  line-height: 1.6;
  margin: 0 0 calc(var(--u) * 6);
}
.fm-page h2 {
  font-family: var(--font-display);
  font-size: 13px;
  margin: calc(var(--u) * 10) 0 calc(var(--u) * 4);
  color: var(--accent);
}
.fm-page h3 { font-size: 14px; text-transform: uppercase; letter-spacing: 1px; color: var(--dim); margin: calc(var(--u) * 6) 0 calc(var(--u) * 2); }
.fm-chips + h3 { margin-top: calc(var(--u) * 8); }
.fm-page p  { max-width: 68ch; color: var(--dim); }
.fm-page li { color: var(--dim); margin-bottom: var(--u); }

.fm-grid { display: grid; gap: calc(var(--u) * 4); }
.fm-grid--2 { grid-template-columns: repeat(auto-fit, minmax(320px, 1fr)); }
.fm-grid--3 { grid-template-columns: repeat(auto-fit, minmax(240px, 1fr)); }
.fm-grid--4 { grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); }

.fm-card { padding: calc(var(--u) * 4); background: var(--panel); box-shadow: inset 2px 2px 0 0 var(--raise), inset -2px -2px 0 0 var(--void), 0 0 0 2px var(--line); }
.fm-card h3 { margin-top: 0; }

.fm-stat { text-align: left; }
.fm-stat__label { font-size: 11px; text-transform: uppercase; letter-spacing: 1px; color: var(--dimmer); }
.fm-stat__value { font-family: var(--font-display); font-size: 22px; margin: calc(var(--u) * 2) 0; }
.fm-stat__note  { font-size: 11px; color: var(--dim); }

/* Just the figures now.
 *
 * This was a flex row so a 52px icon could reserve its own width beside the
 * numbers, with a media query dropping it on narrow phones where it took two
 * fifths of the box. The icons are gone from both pages, so the row, the
 * reserved width and the query go with them — a layout kept for an element
 * that no longer exists is a trap for whoever next wonders why the box is
 * flex. `__body` stays: it is the wrapper both templates render. */
.fm-stat__body { min-width: 0; }

/* Pixel art has to land on whole pixels or the grid blurs. */
.fm-pix { display: block; image-rendering: pixelated; }

/* The level in words, under the gauge it describes. Brighter than the note
   below it — this is the reading, not a footnote to it — and it keeps the row
   height stable when it is empty, so a panel that fails to load does not make
   the cards beside it jump. */
.fm-stat__band {
  margin-top: calc(var(--u) * 2);
  font-size: 11px;
  color: var(--ink);
  min-height: 1em;
}

.fm-table { width: 100%; border-collapse: collapse; font-size: 12px; }
.fm-table th {
  text-align: left;
  font-size: 10px;
  text-transform: uppercase;
  letter-spacing: 1px;
  color: var(--dimmer);
  padding: var(--u) calc(var(--u) * 2);
  border-bottom: 2px solid var(--line);
}
.fm-table td { padding: calc(var(--u) * 1.5) calc(var(--u) * 2); border-bottom: 1px solid var(--panel-2); color: var(--dim); }
.fm-table tr:hover td { background: var(--panel-2); color: var(--ink); }

.fm-chart { width: 100%; height: 180px; background: var(--void); box-shadow: inset 2px 2px 0 0 var(--void), 0 0 0 2px var(--line); }
#s-mix { cursor: crosshair; }

.fm-foot {
  padding: calc(var(--u) * 6) calc(var(--u) * 4);
  background: var(--deep);
  box-shadow: 0 -2px 0 0 var(--line);
  font-size: 11px;
  color: var(--dimmer);
  display: flex;
  gap: calc(var(--u) * 4);
  flex-wrap: wrap;
  align-items: center;
}
.fm-foot a { color: var(--dim); }

/* The hubs, set apart from the two obligations beside them. Accented because
   they are where a reader goes next; About and Privacy stay quiet because
   nobody arrives wanting them, and making everything loud makes nothing loud. */
.fm-link--go { color: var(--accent); }
.fm-foot a.fm-link--go { color: var(--accent); }
.fm-foot a.fm-link--go:hover { color: #ffe27a; }

/* Its own line on a narrow screen, beside the note on a wide one.
 *
 * Pushed to the right, so every link in the footer sits on one side and the
 * note on the other. margin-left:auto here rather than on .fm-right, because
 * two auto margins in one flex row split the free space between them and would
 * have parked the hubs in the middle — .fm-right keeps its utility meaning
 * everywhere else and only gives it up inside this footer. */
.fm-foot__nav { display: flex; flex-wrap: wrap; align-items: center; gap: 0; margin-left: auto; }
.fm-foot .fm-right { margin-left: 0; }

/* Stacked and left-aligned once the footer wraps: a right-pushed group on its
   own line reads as a mistake rather than as alignment. */
@media (max-width: 640px) {
  .fm-foot__nav { margin-left: 0; }
}

/* ------------------------------------------------------- crawlable band -- */
/* Real content, not a hidden keyword dump: the map is a <canvas>, so this is
   the only version of the page a search engine or a screen reader can read.
   It sits below the map and is reached by scrolling. */
.fm-seo {
  padding: calc(var(--u) * 8) calc(var(--u) * 4);
  max-width: 1180px;
  margin: 0 auto;
  width: 100%;
  font-size: 13px;
  color: var(--dim);
}

.fm-seo h2 {
  font-family: var(--font-display);
  font-size: 13px;
  color: var(--accent);
  margin: 0 0 calc(var(--u) * 4);
}

.fm-seo h3 {
  font-size: 12px;
  text-transform: uppercase;
  letter-spacing: 1px;
  color: var(--dimmer);
  margin: calc(var(--u) * 6) 0 calc(var(--u) * 2);
}

.fm-seo p { max-width: 68ch; }

.fm-seo ul {
  list-style: none;
  padding: 0;
  margin: 0;
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(240px, 1fr));

  /* Room between the columns, not just between the rows.
     At a 4px gap the figure ending one card sat against the swatch beginning
     the next, so four aligned entries read as one run-on line. The row gap
     stays tight — these are dense lists and should look it. */
  gap: calc(var(--u) * 2) calc(var(--u) * 7);
}

.fm-seo li { padding: var(--u) 0; }

/* One row shape for every list in this block.
   They had three: a flex row here, plain wrapped text there, and a count pushed
   into a rigid column somewhere else — so three lists that summarise one thing
   read as three fragments. Swatch, what it is, then the number. */
.fm-seo__list li {
  display: grid;
  grid-template-columns: auto minmax(0, 1fr) auto;
  gap: calc(var(--u) * 2);

  /* Aligned to the first line, not to the middle or the baseline.
     The eight family links are whole sentences and run to three lines at
     240px; centred, their figure floated halfway down an empty column, which
     is what made that list look broken. */
  align-items: start;
}

/* The swatch sits on the first line of the text it belongs to, not above it. */
.fm-seo__list .fm-swatch { margin-top: 3px; }

.fm-seo__list .fm-seo__what { min-width: 0; }

/* The country's family, and each nuance count: present, subordinate, and on
   its own line so a two-word family name cannot widen the column. */
.fm-seo__list .fm-seo__what small { display: block; font-size: 10px; }

.fm-seo__num {
  text-align: right;
  font-size: 11px;
  color: var(--dim);
  white-space: nowrap;
}

 /* Under the figure, not beside it: inline, the unit was what widened the
    column and pushed the eight sentences into a fourth line. */
.fm-seo__num small {
  display: block;
  color: var(--dimmer);
  font-size: 10px;
}

/* The eight sentences need more room before they wrap than a country name
   does; one column fewer is the difference between two lines and four. */
.fm-seo__list--wide { grid-template-columns: repeat(auto-fill, minmax(320px, 1fr)) !important; }

/*
 * The eight family links recede; the mood inside them does not.
 *
 * Every link on the site is accent yellow, which is right where a link is one
 * thing among many. Here it is eight consecutive lines, and eight yellow
 * sentences stacked is a block of yellow — the accent stops marking anything.
 * The coloured mood inside each sentence is what should carry the eye, and it
 * cannot while competing with the eight lines it sits in.
 *
 * So the sentence is set in the same --dimmer as the "(n countries)" count that
 * closes the line: everything that is not the mood name now reads as one quiet
 * layer, and the eight coloured words are the only thing with weight. The <b>
 * carries its family colour inline, which outranks this.
 *
 * Measured: --dimmer on --bg is 3.76:1, under the 4.5:1 AA asks for body text.
 * Deliberate, and the reason it is safe here is that the sentence is not the
 * information — the coloured mood is, at 7:1 or better for all eight, and the
 * whole line is a link with a hover and focus state that goes to full accent.
 * If this ever becomes the only way to read something, use --dim (7.43:1).
 */
.fm-seo__list a { color: var(--dimmer); }
.fm-seo__list a:hover { color: var(--accent); text-decoration: underline; }

/* ------------------------------------------------------------- loading --- */
.fm-boot {
  position: absolute;
  inset: 0;
  z-index: 40;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: calc(var(--u) * 4);
  background: var(--void);
  font-family: var(--font-display);
  font-size: 11px;
  color: var(--dim);
  text-align: center;
  padding: calc(var(--u) * 4);
}

.fm-boot__bar { width: min(320px, 80%); height: calc(var(--u) * 4); background: var(--deep); box-shadow: 0 0 0 2px var(--line); }
.fm-boot__bar i { display: block; height: 100%; width: 0; background: var(--accent); transition: width 0.2s steps(8); }

.fm-blink { animation: fm-blink 1s steps(2) infinite; }
@keyframes fm-blink { 50% { opacity: 0; } }

/* ------------------------------------------------------------ utilities -- */
/* ------------------------------------------------- ranked & index lists -- */
/* The family, city and hub pages are lists of places with a number each. The
   bar is the number: a reader takes in a length before they read a figure, and
   these pages exist to be skimmed. Grid-aligned like everything else. */
.fm-rank { list-style: none; margin: 0 0 calc(var(--u) * 6); padding: 0; counter-reset: rank; }

.fm-rank li {
  display: flex;
  align-items: center;
  gap: calc(var(--u) * 2);
  padding: calc(var(--u) * 2);
  border-bottom: 2px solid var(--line);
  font-size: 12px;
}
.fm-rank li::before {
  counter-increment: rank;
  content: counter(rank);
  min-width: calc(var(--u) * 6);
  color: var(--dimmer);
  font-family: var(--font-display);
  font-size: 10px;
}
.fm-rank a { color: var(--ink); text-decoration: none; }
.fm-rank a:hover { color: var(--accent); text-decoration: underline; }

/* The bar needs a track, not a width of its own.
   Sized directly, it had to be capped to stop a long row pushing the figure
   off the line — and the cap then flattened everything above it, so 53% and
   43% drew the same length and the ranking stopped being readable. A fixed
   track takes the space; the fill is the share of it, honestly proportional
   to 100 rather than rescaled to whoever leads. */
.fm-rank__track {
  flex: none;
  width: 38%;
  height: calc(var(--u) * 2);
  margin-left: auto;
  background: var(--deep);
  box-shadow: inset 1px 1px 0 0 var(--void);
}

.fm-rank__bar {
  display: block;
  height: 100%;
  min-width: 2px;
  box-shadow: inset -1px -1px 0 0 rgba(0, 0, 0, 0.45);
}

/* The nuance line under a country's or city's mood mix.
   Quiet on purpose: the mix is the finding, these are the detail under it, and
   a second block of coloured chips would compete with the ranking it belongs
   to. The family colour lands on the word itself — six swatches on one line is
   more punctuation than information. */
.fm-nuance-line {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: calc(var(--u) * 1.5);
  margin: calc(var(--u) * 4) 0 0;
  font-size: 11px;
  line-height: 1.9;
  color: var(--dim);
}

.fm-nuance-line__label {
  text-transform: uppercase;
  letter-spacing: 1px;
  font-size: 10px;
  color: var(--dimmer);
  margin-right: var(--u);
}

.fm-nuance-line__item { display: inline-flex; align-items: baseline; gap: var(--u); }
.fm-nuance-line__item b { font-size: 10px; color: var(--dimmer); font-weight: normal; }
.fm-nuance-line__sep { color: var(--dimmer); }

/* The save-your-journal row, above everything else on /me. Baseline-aligned so
   the reason reads as part of the button's sentence rather than as a caption
   under it; wraps to its own line when there is no room beside. */
.fm-keep-top {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: calc(var(--u) * 3);
  margin: calc(var(--u) * 6) 0 0;
  font-size: 11px;
}

/* ---------------------------------------------------------- the companion -- */
/* The creature made of the moods somebody has recorded. Art on the left, what
   it is and why on the right; under 520px it stacks, because a 112px drawing
   beside three sentences leaves the sentences about nine characters wide. */
.fm-companion__body {
  display: flex;
  align-items: center;
  gap: calc(var(--u) * 5);
  margin-top: calc(var(--u) * 3);
}

.fm-companion__art {
  flex: none;
  /* The whole point is that it is made of pixels. Without this the browser
     smooths a 24-pixel drawing scaled seven times into a watercolour. */
  image-rendering: pixelated;

  /* Alive, barely.
     Whole pixels and nothing else: the drawing is a 24-grid scaled 7x, so it
     moves in steps of 7px and never lands between two of its own pixels. A
     fractional translate re-samples the whole creature and undoes the
     `pixelated` above — which is the trap here, because it looks fine in
     motion and blurry only at rest.
     `steps()` for the same reason: this is a sprite, not a balloon, and eased
     motion reads as rubber. */
  animation: fm-bob 3.2s steps(2, jump-none) infinite alternate;
  will-change: transform;
}

@keyframes fm-bob {
  from { transform: translateY(0); }
  to   { transform: translateY(-7px); }
}

/* Asleep breathes instead of bobbing: slower, and downward, so a sleeping
   animal settles rather than hovers. */
.fm-companion.is-asleep .fm-companion__art {
  animation-name: fm-breathe;
  animation-duration: 5.4s;
}

@keyframes fm-breathe {
  from { transform: translateY(0); }
  to   { transform: translateY(7px); }
}

/* An idle decoration is the clearest case there is for honouring this: nothing
   here is communicated by the movement. */
@media (prefers-reduced-motion: reduce) {
  .fm-companion__art { animation: none; }
}

.fm-companion__text { min-width: 0; }
.fm-companion__state { margin: 0 0 calc(var(--u) * 2); font-size: 12px; }
.fm-companion__act { margin: calc(var(--u) * 4) 0 0; }

/* Asleep is dimmer, not greyer.
   Desaturating it would throw away the one thing the drawing is carrying — the
   reader's own mood mix, banded down its body. A sleeping animal you can still
   recognise as yours is the difference between "come back" and "you lost it". */
.fm-companion.is-asleep .fm-companion__art { opacity: 0.72; }

@media (max-width: 520px) {
  .fm-companion__body {
    flex-direction: column;
    align-items: flex-start;
    gap: calc(var(--u) * 3);
  }
}

/* ------------------------------------------------------ the eight families -- */
/* /moods was eight rows of name, bar and percentage, which is all a list has
   room for. The nuances are the point of the page — 48 words the site records
   on every submission and showed only inside a canvas panel — and six of them
   per family do not fit in a table cell. */
.fm-families {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(260px, 1fr));
  gap: calc(var(--u) * 4);
  margin: calc(var(--u) * 6) 0;
}

.fm-family {
  padding: calc(var(--u) * 4);
  background: var(--panel);
  box-shadow:
    inset 2px 2px 0 0 var(--raise),
    inset -2px -2px 0 0 var(--void),
    0 0 0 2px var(--line);
}

.fm-family__head {
  display: flex;
  align-items: baseline;
  gap: calc(var(--u) * 2);
  margin: 0 0 calc(var(--u) * 2);
  font-size: 13px;
}

.fm-family__head a { text-decoration: none; }
.fm-family__head a:hover { text-decoration: underline; }

/* Pushed to the far end rather than given a margin: the family name is as long
   as its language makes it, and "Überraschung" and "Joy" must both leave the
   figure in the same place. */
.fm-family__head .fm-mono { margin-left: auto; font-size: 11px; }

/* The track is laid out for a flex row elsewhere, where it takes 38% and an
   auto margin. Here it is the full width of a card and the only thing on its
   line.

   `display: block` is the part that matters, and its absence is what made the
   first version draw eight full-height columns: the track is a <span>, and on
   an inline box width and height are ignored — so the fill inside it, at
   height:100% of nothing, grew to the height of the card. */
.fm-family .fm-rank__track {
  display: block;
  width: 100%;
  margin-left: 0;
}

.fm-family__sub {
  margin: calc(var(--u) * 4) 0 calc(var(--u) * 2);
  font-family: inherit;
  font-size: 10px;
  text-transform: uppercase;
  letter-spacing: 1px;
  color: var(--dimmer);
}

.fm-nuances-list {
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  flex-wrap: wrap;
  gap: var(--u);
}

.fm-nuances-list li {
  display: flex;
  align-items: baseline;
  gap: var(--u);
  padding: var(--u) calc(var(--u) * 2);
  background: var(--deep);
  font-size: 11px;
  color: var(--dim);
}

.fm-nuances-list b { color: var(--ink); font-size: 10px; }

/* A nuance nobody has picked this week is still part of what the family means,
   so it is listed — dimmer, and without a count. Printing "0" six times would
   turn a definition into a table of failures, and hiding them would make the
   vocabulary look smaller than it is. */
.fm-nuance--quiet { opacity: 0.55; }

.fm-family__foot { margin: calc(var(--u) * 4) 0 0; font-size: 11px; color: var(--dim); }

/* Provenance above inference, stacked: they answer different questions, and
   four things on one line read as four labels of equal weight. */
.fm-signal__meta {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: 2px;
  margin-left: auto;
  text-align: right;
}

/* The mood, named rather than hinted at. A coloured dot said "this headline has
   a mood" and put which one in a title attribute — invisible on a phone. Dark
   text on the mood's own colour, because the palette is bright and white on
   yellow is unreadable. */
.fm-badge {
  padding: 1px calc(var(--u) * 1.5);
  background: var(--line);
  color: var(--void);
  font-size: 9px;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.5px;
  white-space: nowrap;
}

/* Where a headline came from: provenance, not the headline. Small enough that
   the eye reads the title first and finds the publisher only when it looks. */
.fm-source {
  font-size: 10px;
  color: var(--dimmer);
  text-transform: uppercase;
  letter-spacing: 0.5px;
}

/*
 * The eight families as filters, and the legend for the table below them.
 *
 * Each chip carries its own colour, so the row teaches the mapping the table
 * then uses — the reader never holds it in their head. The selected one fills
 * with that colour rather than growing a border, because a border shifts the
 * text by a pixel and eight chips shifting is a row that flickers.
 */
.fm-moodbar {
  display: flex;
  flex-wrap: wrap;
  gap: 2px;
  margin: calc(var(--u) * 4) 0;
}

.fm-moodchip {
  display: inline-flex;
  align-items: center;
  gap: calc(var(--u) * 1.5);
  padding: calc(var(--u) * 1.5) calc(var(--u) * 2);
  background: var(--deep);
  border: 0;
  color: var(--dim);
  font-size: 10px;
  text-transform: uppercase;
  letter-spacing: 0.5px;
  cursor: pointer;
}
.fm-moodchip b { color: var(--ink); font-variant-numeric: tabular-nums; }
.fm-moodchip:hover { background: var(--panel-2); color: var(--ink); }

/* Filled with the family's own colour when chosen. Dark text on it, because
   the palette is bright and white on yellow cannot be read. */
.fm-moodchip.is-on {
  background: var(--chip, var(--accent));
  color: var(--void);
}
.fm-moodchip.is-on b { color: var(--void); }
.fm-moodchip.is-on .fm-swatch { box-shadow: inset 0 0 0 1px rgba(0, 0, 0, 0.4); }

/* Nobody leading in it: shown, greyed, unclickable. A row of chips that changes
   length between visits is a row nobody learns. */
.fm-moodchip.is-empty { opacity: 0.4; cursor: default; }

.fm-row { display: flex; align-items: center; gap: calc(var(--u) * 2); flex-wrap: wrap; }

/* The band sits under the figure rather than beside it: the column is narrow and
   two things on one line there would wrap unpredictably. */
.fm-table--rank .fm-source { display: block; }

/* The twenty busiest countries: a colour to recognise, a number to compare.
   auto-fill rather than a fixed count, so the block is four across on a desk
   and two on a phone without a media query saying so. */
.fm-tiles {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(148px, 1fr));
  gap: 2px;
  margin: 0 0 calc(var(--u) * 6);
}

.fm-tile {
  display: grid;
  grid-template-columns: 1fr auto;
  align-items: baseline;
  gap: 0 calc(var(--u) * 2);
  padding: calc(var(--u) * 2);
  background: var(--deep);
  border-left: 4px solid var(--line);
  color: var(--ink);
  text-decoration: none;
}
.fm-tile:hover { background: var(--panel-2); }
.fm-tile:focus-visible { outline: 2px solid var(--accent); outline-offset: -2px; }

.fm-tile__name {
  font-size: 12px;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.fm-tile__index { font-size: 15px; }
.fm-tile__mood {
  grid-column: 1 / -1;
  font-size: 10px;
  color: var(--dimmer);
  text-transform: uppercase;
  letter-spacing: 0.5px;
}

/* A shortcut above a list that already holds the same destinations. */
.fm-jump { margin: 0 0 calc(var(--u) * 5); }
.fm-jump select { max-width: 100%; }

.fm-index {
  list-style: none;
  margin: 0 0 calc(var(--u) * 6);
  padding: 0;
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(240px, 1fr));
  gap: 2px;
}
.fm-index li {
  display: flex;
  align-items: center;
  gap: calc(var(--u) * 2);
  padding: calc(var(--u) * 2);
  background: var(--deep);
  font-size: 12px;
}
.fm-index a { color: var(--ink); text-decoration: none; }
.fm-index a:hover { color: var(--accent); text-decoration: underline; }
.fm-index .fm-mono { margin-left: auto; }

.fm-dot { width: var(--u); height: var(--u); flex: none; }

/*
 * The notes wall.
 *
 * One column, not the card grid the place index uses: these are sentences, and
 * a sentence read across a 240px column is read twice. Width is capped for the
 * same reason — a note stretched over a 1600px screen is a line nobody follows
 * back to the start of.
 */
.fm-notes {
  list-style: none;
  margin: 0 0 calc(var(--u) * 6);
  padding: 0;
  display: grid;
  gap: 2px;
  max-width: 70ch;
}
.fm-notes__item {
  padding: calc(var(--u) * 2) calc(var(--u) * 3);
  background: var(--deep);
}
.fm-notes__text {
  margin: 0;
  font-size: 13px;
  line-height: 1.5;
  overflow-wrap: anywhere;
}
.fm-notes__meta {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: calc(var(--u) * 2);
  margin: calc(var(--u) * 2) 0 0;
  font-size: 11px;
}
/* Pushed to the end of the row so every card's link lands in one column. */
.fm-notes__meta a { color: var(--dim); margin-left: auto; }
.fm-notes__meta a:hover { color: var(--accent); }

.fm-notes__filters {
  display: flex;
  flex-wrap: wrap;
  align-items: flex-end;
  gap: calc(var(--u) * 3);
  margin: calc(var(--u) * 4) 0;
}
/* Long place names would otherwise stretch one select across the whole row. */
.fm-notes__filters select { max-width: 220px; }

/* One control, labelled. Five bare dropdowns in a row gave the reader nothing
   to go on but whichever option happened to be showing — "Anywhere" next to
   "Any mood" next to "Last 7 days" reads as three unrelated words. */
.fm-field {
  display: flex;
  flex-direction: column;
  gap: var(--u);
  margin: 0;
  min-width: 0;
}
.fm-field > label {
  font-size: 9px;
  text-transform: uppercase;
  letter-spacing: 1px;
  color: var(--dimmer);
}

/* The buttons belong on the controls' baseline, not stacked under an empty
   label of their own. */
.fm-field--actions {
  flex-direction: row;
  align-items: center;
  gap: calc(var(--u) * 2);
}

/* A jump box that is also a field.
 *
 * .fm-jump leaves a gap under itself and .fm-field zeroes the margin; they have
 * equal specificity and .fm-field is declared second, so the pair silently lost
 * the gap and the country search sat flush against the table below it. Naming
 * the combination fixes it where it can be seen, rather than by reordering two
 * rules 80 lines apart and hoping nobody reorders them back. */
.fm-field.fm-jump { margin-bottom: calc(var(--u) * 6); }

.fm-notes__pager {
  display: flex;
  align-items: center;
  gap: calc(var(--u) * 3);
}

.fm-crumbs {
  font-size: 11px;
  color: var(--dimmer);
  margin-bottom: calc(var(--u) * 3);
}
.fm-crumbs a { color: var(--dim); }

.fm-mono { font-variant-numeric: tabular-nums; }
.fm-muted { color: var(--dimmer); }
.fm-right { margin-left: auto; }
.fm-row { display: flex; align-items: center; gap: calc(var(--u) * 2); }
.fm-stack { display: flex; flex-direction: column; gap: calc(var(--u) * 2); }
.fm-sr {
  position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px;
  overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; border: 0;
}

:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }

/* ----------------------------------------------------------- responsive -- */
/* Below this width the two HUD panels cannot both float over the map without
   burying it, so the layout stops overlaying and starts stacking. The panels
   are never hidden: the world and local figures are the point of the page, and
   dropping them on a phone would be losing the content, not adapting it. */
@media (max-width: 1080px) {
  :root { --hud-w: auto; }

  .fm-stage {
    display: flex;
    flex-direction: column;
    min-height: 0;
  }

  .fm-viewport {
    position: relative;
    inset: auto;
    width: 100%;
    height: clamp(320px, 56vh, 620px);
    flex: none;
  }

  .fm-hud,
  .fm-hud--left,
  .fm-hud--right {
    position: static;
    width: auto;
    max-height: none;
    margin: calc(var(--u) * 3);
    background: var(--deep);

    /* Back in the flow, so it must not hold on to the page's scroll.
       Containment is for a box that floats *over* the page — reaching the end
       of it should not drag what is behind it. These panels are not over
       anything down here; they are the page. Left contained, `overflow-y: auto`
       from the base rule still made each one a scroll container, and every one
       of them swallowed the gesture at its own boundary — which is every
       moment, since nothing overflows once max-height is gone. Scrolling the
       area under the map simply stopped working. */
    overflow: visible;
    overscroll-behavior: auto;
  }

  /* Two columns of panels on a tablet, one on a phone. */
  .fm-hud--left, .fm-hud--right { margin-bottom: 0; }
  .fm-hud--right { margin-bottom: calc(var(--u) * 3); }
}

@media (min-width: 861px) and (max-width: 1080px) {
  .fm-stage { display: grid; grid-template-columns: 1fr 1fr; }
  .fm-viewport { grid-column: 1 / -1; }
}

/* --------------------------------------------------------------- mobile cta --
   On a phone the map is a block in a page that scrolls, so the navigation used
   to travel off the top and the action went with it. Wide screens keep the
   button floating over the map, where it is always beside what it acts on. */
.fm-cta { display: none; }

@media (max-width: 860px) {
  /* The pinning itself is unconditional now — see .fm-pinned near the top.
     One wrapper rather than two sticky children: both anchored to the top
     would land on each other, and the button has to follow whatever height
     the nav wraps to. */

  .fm-cta {
    display: flex;
    gap: calc(var(--u) * 2);
    padding: var(--u) calc(var(--u) * 3) calc(var(--u) * 2);
    background: var(--deep);
    box-shadow: 0 2px 0 0 var(--line), 0 4px 0 0 var(--void);
  }

  .fm-cta[hidden] { display: none; }

  .fm-cta__btn {
    /* The row is the full width whether it holds one button or two: the
       primary action takes whatever sharing does not, and a thumb should never
       have to aim for it. */
    display: flex;
    flex: 1;
    min-width: 0;
    justify-content: center;
  }

  /* Sized to its own label so the action that matters keeps the room — and
     when it is hidden, flex: 1 above gives the whole row back to that action.
     No [hidden] rule needed here: the global one near the top wins outright. */
  .fm-cta__share {
    flex: none;
  }

  /*
   * Both labels, on one line, in every language.
   *
   * Press Start 2P is a fixed-width pixel font: every glyph advances exactly
   * one em, so the width of a label is its character count times the font size,
   * with no help from narrow letters. "MODIFIER MON HUMEUR" beside "PARTAGER"
   * is 27 characters — 351px at the old 12px plus 1px tracking, on a 360px
   * screen that had 280px to give. It wrapped, and German and Portuguese are
   * the same length.
   *
   * Three things buy that back, in order of what they cost the design: the
   * tracking, which a fixed-width font needs least; the side padding, which was
   * generous; and last the size, which only shrinks on the screens that cannot
   * fit it. At 360px this lands at 9.4px and 253px of text in 296px of room.
   */
  .fm-cta__btn,
  .fm-cta__share {
    font-size: clamp(9px, 2.6vw, 12px);
    letter-spacing: 0;
    white-space: nowrap;
    padding: calc(var(--u) * 2) calc(var(--u) * 2);
  }

  /* The floating pair on the map. Two buttons offering the same thing is worse
     than the button being out of reach, which is what this replaces — the
     pinned bar carries both actions at this width. */
  .fm-actions { display: none; }

  .fm-prompt {
    position: fixed;
    left: 0;
    right: 0;
    bottom: 0;
    width: 100%;
    transform: none;
    max-height: 82vh;
    overflow-y: auto;
    overscroll-behavior: contain;
    /* Clear of the home indicator on a phone. */
    padding-bottom: max(calc(var(--u) * 4), env(safe-area-inset-bottom));
  }

  .fm-moods { grid-template-columns: repeat(4, 1fr); gap: var(--u); }
  .fm-mood { font-size: 9px; padding: var(--u) 2px; }
  .fm-mood__face { width: calc(var(--u) * 9); height: calc(var(--u) * 9); }

  /*
   * The ticker, on a phone too.
   *
   * It was hidden here because a 32px strip is a real bite out of a map that is
   * already only 56vh tall. But the map without it is a grid of coloured cells,
   * and the ticker is the only place the words behind those cells surface at
   * all — which is worth more on the screen where there is no room for the
   * panels that carry them.
   *
   * Touches pass straight through. A band across the bottom of the map that
   * swallowed pans would be worse than no ticker at all, and nothing in it is
   * tappable: the per-item tooltips are a pointer affordance that a touch
   * screen never sees.
   */
  .fm-ticker {
    pointer-events: none;
    font-size: 11px;
    gap: calc(var(--u) * 4);
  }

  /* A sheet rather than a floating card once there is no room beside the map. */
  .fm-detail {
    position: fixed;
    inset: auto 0 0 0;
    width: 100%;
    max-height: 76vh;
    transform: translateY(12px);
    padding-bottom: max(calc(var(--u) * 4), env(safe-area-inset-bottom));
  }
  .fm-detail.is-open { transform: none; }

  /* Lifted clear of the ticker: 12px of its own gap, plus the strip's 32px.
     Its z-index already put it in front, which would have left the scrolling
     text running behind the buttons and readable in pieces. */
  /*
   * A row along the bottom, not a tower in the corner.
   *
   * Stacked, this was 152px on a map at most 620px tall — a fifth of the thing
   * the page exists to show, spent on four controls. In a line it is 52px, and
   * every target stays 44px square, which is the number that matters on a
   * phone.
   */
  .fm-zoombar {
    bottom: calc(var(--u) * 11);
    left: calc(var(--u) * 3);
    right: calc(var(--u) * 3);
    flex-direction: row;
    flex-wrap: wrap;
    align-items: stretch;
    padding: var(--u);
    gap: var(--u);
  }

  .fm-zoombar__pair { flex: none; gap: var(--u); }

  /* A floor rather than a fixed width: both sit on the row when there is room,
     and the second drops below rather than overflowing when there is not.
     Measured at 320, 360, 390 and 430 with the longest labels shipped. */
  .fm-zoombar__wide {
    width: auto;
    flex: 1 1 calc(var(--u) * 24);
    min-width: 0;
    padding: 0 calc(var(--u) * 2);
    gap: var(--u);
  }

  /* The swatch is decorative and costs 20px a button — at 360px that is the
     difference between the label fitting and the row spilling over. */
  .fm-zoombar__wide i { display: none; }

  /* Shrinks the two wide buttons, which carry words. */
  .fm-zoombar button { min-height: calc(var(--u) * 11); font-size: 8px; }

  /* But not the pair, which carries a glyph.
     A minus sign and a plus sign are the whole label, so 9px leaves nothing to
     aim at or to read. The rule above used to catch them as well — same
     specificity as the 14px on line 419, and later in the file, so it won. */
  .fm-zoombar__pair button {
    font-size: 18px;
    min-height: calc(var(--u) * 11);
    /* Square, and a comfortable thumb target. */
    min-width: calc(var(--u) * 11);
    padding: 0;
    /* Press Start 2P sits high in its box; the glyphs need nudging to look
       centred rather than measured to be. */
    line-height: 1;
  }

  /* The floating button used to be stretched across the bottom of the map here.
     It is hidden at this width now — the pinned one above the map does the job
     without covering the cells someone is trying to read. */

  .fm-logo { font-size: 12px; gap: var(--u); }

  /* The mark alone, from the start rather than once the page has moved.
     The bar is pinned here, so it is charged against every screen of a page
     that is mostly map. Waiting for a scroll meant the bar opened at three rows
     and settled to one, which is a layout shift on arrival — and the row it
     saved was needed before the reader had done anything.

     Screen-reader treatment rather than display:none, and this is not a
     nicety: the mark carries alt="" precisely because the wordmark says the
     name, so hiding the word outright leaves a link containing one decorative
     image and no accessible name — the link home, on the narrowest screens,
     for the readers most dependent on it being announced. */
  .fm-logo__word {
    position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px;
    overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; border: 0;
  }

  /* And the menu closes up to the mark instead of staying pushed to the far
     side with a gap where the name used to be. */
  .fm-nav { margin-left: calc(var(--u) * 2); }

  /* One row, and the type pays for it.
     Press Start 2P is fixed-width, so a nav item costs exactly its character
     count — and the longest label is not English: "Статистика" is ten
     characters where "Stats" is five. A step down the scale buys the widest
     language the same fit as the narrowest. */
  .fm-nav a { font-size: 10px; letter-spacing: 0; }

  /* The code is what shows; the select is what works.
   *
   * Narrowing the select on its own would truncate the closed label to
   * "Bahasa Indone…", which is worse than a code and looks broken. So the code
   * is drawn as an ordinary chip and the real control is laid transparently
   * over it at full size: the tap target is the whole chip, and what opens is
   * the platform's own picker, which lists every language at full length in its
   * own sheet. Nobody chooses from an abbreviation — they choose from Русский.
   *
   * The select keeps its label, its focus and its keyboard behaviour, because
   * it is still the element being operated. Only its paint is borrowed. */
  .fm-lang__code {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    min-width: calc(var(--u) * 9);
    padding: var(--u) calc(var(--u) * 1.5);
    background: var(--panel-2);
    color: var(--dim);
    font-size: 10px;
    letter-spacing: 0;
    line-height: 1;
  }

  .fm-lang__select {
    position: absolute;
    /* Taller than the chip it covers, and deliberately.
       The chip is 18px because that is what the bar can spend on paint; a thumb
       needs more than a bar is willing to look like. Being absolutely
       positioned, the control can be bigger than the thing it appears to be —
       so the hit area grows into the header's own padding, top and bottom,
       while the drawing stays the size the row was designed around. Horizontal
       inset stays 0: growing sideways would steal taps from the nav link next
       to it, which is a worse failure than a small target. */
    inset: calc(var(--u) * -2) 0;
    width: 100%;
    padding: 0;
    margin: 0;
    opacity: 0;
    /* Above the chip it is painting over, so the tap lands on the control and
       not on the decoration. */
    z-index: 1;
  }

  /* The outline belongs to the thing the reader can see. Without this a
     keyboard user tabs to an invisible element and nothing appears to happen. */
  .fm-lang:focus-within .fm-lang__code {
    outline: 2px solid var(--accent);
    outline-offset: 2px;
    color: var(--ink);
  }
  /* 16, not 24. This breakpoint is not only phones — a narrowed desktop window
     hits it at device pixel ratio 1, where 24 gives one and a half screen
     pixels per art pixel and the edges go soft. Measured: 1.5. Whole multiples
     only, which is the rule the rest of this stylesheet keeps. */
  .fm-logo__mark { width: 16px; height: 16px; }
  .fm-top { gap: calc(var(--u) * 2); padding: calc(var(--u) * 2); }
  .fm-nav a { padding: var(--u); }

  .fm-page { padding: calc(var(--u) * 5) calc(var(--u) * 3) calc(var(--u) * 10); }
  .fm-page h1 { font-size: 15px; }
  .fm-page h2 { font-size: 11px; }
  .fm-stat__value { font-size: 17px; }
  .fm-seo ul { grid-template-columns: 1fr; }

  /* Tables are the one thing that genuinely cannot reflow. */
  .fm-table { display: block; overflow-x: auto; white-space: nowrap; }
}

/* A tooltip that cannot be hovered is not a tooltip that should not exist.
   This used to be `@media (hover: none) { display: none }`, which reads as a
   sensible guard against a tip sticking after a tap — and silently deleted
   every explanation on the site for anyone on a touchscreen laptop, where the
   primary pointer is reported as coarse even with a mouse plugged in. The
   sticking is handled where it belongs, in tip.js: a tap shows the tip, the
   next tap or a few seconds takes it away. */

/* ------------------------------------------------------------- consent -- */
/* Anchored to the bottom rather than laid over the middle: the map is the
   page, and a banner across it hides the thing someone came to look at. */
.fm-consent {
  position: fixed;
  left: 0;
  right: 0;
  bottom: 0;

  /* Behind anything that is asking the reader a question.
     It was 180, which put it in front of the mood prompt (25) and the cell
     panel (18) — and on a phone all three are bottom sheets, so the banner
     landed squarely on the prompt's buttons. Consent is not urgent: it waits
     at the foot of the page until there is nothing on top of it, and it is
     still above the map and the HUD, which is all it needs. The toast (200)
     and the modal (100) stay in front, as they were. */
  z-index: 17;
  display: flex;
  align-items: center;
  justify-content: center;
  flex-wrap: wrap;
  gap: calc(var(--u) * 3);
  padding: calc(var(--u) * 3) calc(var(--u) * 4)
           max(calc(var(--u) * 3), env(safe-area-inset-bottom));
  background: var(--panel);
  color: var(--ink);
  box-shadow: 0 -2px 0 0 var(--line), 0 -4px 0 0 rgba(0, 0, 0, 0.5);
  font-size: 11px;
  line-height: 1.5;
}

.fm-consent[hidden] { display: none; }

.fm-consent__text { margin: 0; max-width: 68ch; }
.fm-consent__text a { color: var(--accent); }

.fm-consent__actions {
  display: flex;
  gap: var(--u);
  flex: none;
}

@media (max-width: 640px) {
  .fm-consent { flex-direction: column; align-items: stretch; gap: calc(var(--u) * 2); }
  /* Equal weight: refusing must not be the harder button to hit. */
  .fm-consent__actions { justify-content: stretch; }
  .fm-consent__actions .fm-btn { flex: 1; justify-content: center; }
}
