/* Landmark Hunter — design system for a 600×600 additive waveguide.
 *
 * #000000 is transparent on this display: black is not a colour, it is the real
 * world showing through. Every surface the user should perceive as a surface is
 * a non-zero dark value from the token set below. Never paint pure black on
 * purpose, and never paint a full-bleed mid-tone (it reads as grey haze).
 *
 * Sizes are in dp, which is px at this viewport. See CLAUDE.md § Layout.
 */

:root {
  --bg-primary:     #0a0a0f;
  --bg-secondary:   #131318;
  --bg-tertiary:    #1c1e21;
  --bg-card:        #1a1a22;
  --accent:         #00d4ff;
  --accent-green:   #00ff88;
  --danger:         #ff4444;
  --text-primary:   #e8e8f0;
  --text-secondary: #8888a0;
  --focus-glow:     rgba(0, 212, 255, 0.4);

  --safe:    8px;
  --gap:     8px;
  --radius:  12px;
  --target:  88px;   /* fixed interactive height */
  --scrim:   64px;   /* standard list scrim */

  /* Motion. Container-only scaling: the squeeze is 8dp on an 88dp target. */
  --squeeze:        0.91;
  --unsqueeze:      1.0989;   /* 1 / 0.91 — keeps content at 1× */
  --focus-on:       475ms cubic-bezier(0.6, 0, 0.4, 1);
  --focus-off:      625ms cubic-bezier(0.24, 0.24, 0.6, 1);
  --color-on:       300ms cubic-bezier(0.4, 0.04, 0.5, 1);
  --color-off:      400ms cubic-bezier(0.2, 0.24, 0.9, 1);
  --press:          100ms cubic-bezier(0.68, 0, 0.29, 1);
}

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

html, body {
  width: 600px;
  height: 600px;
  overflow: hidden;              /* the viewport does not scroll — regions do */
  background: #000;              /* transparent: the world */
  color: var(--text-primary);
  font-family: system-ui, -apple-system, "Segoe UI", sans-serif;
  -webkit-font-smoothing: antialiased;
}

body { padding: var(--safe); }

#shell {
  width: 584px;
  height: 584px;
  background: var(--bg-primary);
  border-radius: 16px;
  overflow: hidden;
  position: relative;
}

#app, .screen { width: 100%; height: 100%; }

.screen {
  display: flex;
  flex-direction: column;
  gap: var(--gap);
  padding: 16px;
}

/* A failed request never blanks a region. It used to dim the whole screen to 55%,
 * which is the wrong instinct on an additive waveguide: less light means less
 * legible against the street, so the punishment for a dropped packet was landing
 * on the content the walker still needs. Instead the content is left alone and the
 * shell says so, quietly, in the corner. */
.stale-note {
  position: absolute;
  top: 8px;
  right: 8px;
  z-index: 5;
  padding: 4px 8px;
  border-radius: 8px;
  background: var(--bg-tertiary);
  color: var(--text-secondary);
  opacity: 0;
  transition: opacity var(--color-off);
  pointer-events: none;
}
#shell[data-stale="1"] .stale-note { opacity: 1; transition: opacity var(--color-on); }

.mock-note {
  position: absolute;
  top: 8px;
  left: 8px;
  z-index: 5;
  padding: 4px 8px;
  border-radius: 8px;
  background: var(--bg-tertiary);
  color: var(--accent-green);
  opacity: 0;
  transition: opacity var(--color-off);
  pointer-events: none;
}
#shell[data-mock="1"] .mock-note { opacity: 1; transition: opacity var(--color-on); }

/* ---------- type scale ----------
 * Every size in this scale is >=18dp. Meta1/Meta2 used to sit at 12/10dp — legible
 * on a 150dp waveguide glance, but a QA pass measuring raw px flags anything under
 * 14dp as a hard failure and recommends 18dp everywhere. Below 18dp exists nowhere
 * now, so there's no borderline size left to regress into; Meta1/Meta2 keep their
 * distinct *role* (secondary color, smaller weight, no strong) rather than a
 * distinct size. */
.h1    { font-size: 28px; font-weight: 700; line-height: 1.15; }
.h2    { font-size: 22px; font-weight: 700; line-height: 1.2; }
.b1    { font-size: 18px; font-weight: 400; }
.b2    { font-size: 18px; font-weight: 400; }
.meta1 { font-size: 18px; font-weight: 400; color: var(--text-secondary); }
.meta2 { font-size: 18px; font-weight: 400; color: var(--text-secondary); }
.strong { font-weight: 700; }

.hdr {
  min-height: 64px;
  margin-top: 8px;              /* 8dp body padding + 8 + 8 ≈ 24dp from the top */
  display: flex;
  flex-direction: column;
  justify-content: center;
  gap: 2px;
  flex: 0 0 auto;
}

.fill { flex: 1 1 auto; min-height: 0; display: flex; flex-direction: column; gap: var(--gap); }

/* ---------- focus ----------
 * Only the container scales. Content counter-scales so text never shrinks.
 */
.focusable {
  border: 2px solid transparent;
  border-radius: var(--radius);
  background: var(--bg-card);
  color: var(--text-primary);
  transform: scale(1);
  transition: transform var(--focus-off),
              border-color var(--color-off),
              background-color var(--color-off),
              box-shadow var(--color-off);
  outline: none;
  cursor: default;
  display: flex;
  align-items: center;
  justify-content: center;
  -webkit-tap-highlight-color: transparent;
}

.focusable > .c {
  display: flex;
  align-items: center;
  gap: 10px;
  opacity: 0.8;
  transform: scale(1);
  transition: opacity var(--color-off), transform var(--focus-off);
  text-align: center;
}

.focusable:focus {
  transform: scale(var(--squeeze));
  border-color: var(--accent);
  box-shadow: 0 0 20px var(--focus-glow);
  transition: transform var(--focus-on),
              border-color var(--color-on),
              background-color var(--color-on),
              box-shadow var(--color-on);
}

.focusable:focus > .c {
  opacity: 1;
  transform: scale(var(--unsqueeze));
  transition: opacity var(--color-on), transform var(--focus-on);
}

.focusable[data-pressed="1"] {
  background: var(--bg-tertiary);
  transition: background-color var(--press), transform var(--press);
}

/* ---------- buttons ---------- */
.btn {
  height: var(--target);
  flex: 0 0 var(--target);
  width: 100%;
}

.btn.primary { background: var(--bg-tertiary); }
.btn.danger:focus { border-color: var(--danger); box-shadow: 0 0 20px rgba(255, 68, 68, 0.35); }

.row { display: flex; gap: var(--gap); flex: 0 0 auto; }
.row > .btn { flex: 1 1 0; }

/* ---------- interest grid ---------- */
.grid {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: var(--gap);
  overflow-y: auto;
  height: 100%;
  padding: 4px;                  /* room for the focus squeeze + glow */
  scrollbar-width: none;
}
.grid::-webkit-scrollbar { display: none; }

.chip { height: var(--target); justify-content: flex-start; padding: 0 14px; }
.chip > .c { justify-content: flex-start; text-align: left; }

/* selection mark: a ring that fills, drawn in CSS so it needs no asset */
.mark {
  width: 20px; height: 20px;
  flex: 0 0 20px;
  border: 2px solid var(--text-secondary);
  border-radius: 50%;
  transition: background-color var(--color-off), border-color var(--color-off);
}
.chip[aria-checked="true"] { background: var(--bg-secondary); }
.chip[aria-checked="true"] .mark {
  background: var(--accent-green);
  border-color: var(--accent-green);
  transition: background-color var(--color-on), border-color var(--color-on);
}
.chip[aria-checked="true"] > .c { opacity: 1; }

/* ---------- settings ----------
 * A section head inside the two-column grid, so the whole screen can be one
 * scrolling region rather than three that each need their own scrim.
 */
.grid > .full { grid-column: 1 / -1; }

/* A cycle chip reads "label / current value", stacked. The row is a fixed 88 dp
 * and the value pushed out to the right edge would sit under the focus squeeze. */
.chip.cycle > .c { flex-direction: column; align-items: flex-start; gap: 2px; }
.chip.cycle .cycle-value { color: var(--accent); }

/* Dimmed, not hidden: a control that has nothing to act on is still worth showing
 * as a control, so the screen does not change shape underneath the walker.
 * `candidates()` already skips [disabled], so the D-pad steps over it. */
.focusable[disabled] { opacity: 0.45; }

/* ---------- scrims ----------
 * Required on any scrolling container: the edge gradient is the only signal
 * that there is more content, since there is no visible scrollbar.
 */
.scroll-wrap { position: relative; overflow: hidden; }
.scroll-wrap::before,
.scroll-wrap::after {
  content: "";
  position: absolute;
  left: 0; right: 0;
  height: var(--scrim);
  pointer-events: none;
  z-index: 2;
  opacity: 1;
  transition: opacity var(--color-off);
}
.scroll-wrap::before { top: 0;    background: linear-gradient(to bottom, var(--bg-primary), transparent); }
.scroll-wrap::after  { bottom: 0; background: linear-gradient(to top,    var(--bg-primary), transparent); }
/* app.js sets data-scroll; "none" means it all fits, so no scrim at all */
.scroll-wrap[data-scroll="none"]::before,
.scroll-wrap[data-scroll="none"]::after,
.scroll-wrap[data-scroll="top"]::before,
.scroll-wrap[data-scroll="bottom"]::after { opacity: 0; }

.stack { display: flex; flex-direction: column; gap: var(--gap); }

/* ---------- walk screen ---------- */
.walk-top { display: flex; gap: var(--gap); flex: 0 0 auto; align-items: stretch; }

.minimap {
  width: 200px; height: 200px;
  flex: 0 0 200px;
  background: var(--bg-primary);      /* never a filled tile: haze over the world */
}
.minimap .range       { fill: none; stroke: #2a3038; stroke-width: 1; }
.minimap .range-inner { stroke-dasharray: 3 6; }

/* Bright strokes on nothing. Widths are deliberately thin and the palette
 * deliberately narrow: on an additive waveguide, area reads as haze and only
 * line contrast survives. Water is an outline, never a fill, for the same reason. */
.minimap .road        { fill: none; stroke: #55626e; stroke-width: 1.6;
                        stroke-linecap: round; stroke-linejoin: round; }
.minimap .road-major  { fill: none; stroke: var(--text-primary); stroke-width: 2.4;
                        stroke-linecap: round; stroke-linejoin: round; }
.minimap .water       { fill: none; stroke: #2f6f8f; stroke-width: 1.6;
                        stroke-linejoin: round; }
.minimap .pip         { fill: var(--accent); }
.minimap .user        { fill: var(--accent-green); }

.status {
  flex: 1 1 auto;
  min-width: 0;
  background: var(--bg-card);
  border-radius: var(--radius);
  padding: 14px;
  display: flex;
  flex-direction: column;
  justify-content: center;
  gap: 6px;
}

/* The alert slot and the nearby grid share the middle of the screen: whichever has
 * content fills it. The other is rendered empty rather than removed — the slot is
 * always in the DOM — so it has to be told to stop claiming its share, or an empty
 * region would take half the space from the one with something in it. */
.alert-slot { flex: 1 1 auto; min-height: 0; display: flex; }
.nearby     { flex: 1 1 auto; min-height: 0; }
.walk [data-empty="1"] { flex: 0 0 0; min-height: 0; }

/* With both regions empty (no fix yet) the menu still belongs at the bottom. */
.walk > .btn { margin-top: auto; }

.card {
  flex: 1 1 auto;
  background: var(--bg-card);
  border-radius: var(--radius);
  border: 1px solid var(--bg-tertiary);
  padding: 14px;
  display: flex;
  flex-direction: column;
  gap: 8px;
  min-height: 0;
}
.card-head { display: flex; flex-direction: column; gap: 2px; }
.card-teaser {
  flex: 1 1 auto;
  min-height: 0;
  overflow: hidden;
  color: var(--text-primary);
  /* three lines, then stop — an alert is a glance, not a read */
  display: -webkit-box;
  -webkit-line-clamp: 3;
  -webkit-box-orient: vertical;
}
.card .row { flex: 0 0 var(--target); }

/* ---------- nearby grid ----------
 * Reuses .grid (two columns, scrolling, scrimmed) with fixed 88 dp rows, so a long
 * name grows lines inside its tile instead of resizing the row next to it.
 */
.nearby-grid { grid-auto-rows: var(--target); align-content: start; }

.tile { padding: 0 12px; justify-content: flex-start; }
.tile > .c {
  flex-direction: column;
  align-items: flex-start;
  gap: 2px;
  text-align: left;
  /* The focus squeeze shrinks the tile by 9% while the counter-scale keeps the text
   * at 1x, so text laid out to the full width would spill over the focused border.
   * Wrapping to the squeezed width instead means the same words fit either way. */
  width: calc(100% * var(--squeeze));
}
/* The row is a fixed 88dp — grid-auto-rows below — so nothing in it may grow past
 * one wrap. Name has its own 2-line clamp; the kind/distance line underneath gets
 * one line and an ellipsis instead of a second wrap, which is what the 12px→18px
 * bump would otherwise have turned into silent vertical overflow of the tile. */
.tile .meta1 {
  width: 100%;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
/* Two lines, then stop: a Wikipedia article title can run to a full sentence, and
 * pushing the category out of the tile would cost the one fact that tells places
 * apart. */
.tile-name {
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
  overflow: hidden;
}

/* Focus restored after a poll replaced the tile under the walker. Instant, because
 * the alternative is a 475 ms squeeze pulsing every eight seconds on whatever they
 * are pointing at — an idle animation, which is the one thing the battery cannot
 * afford and the eye reads as a glitch. */
.focusable[data-quiet], .focusable[data-quiet] > .c { transition-duration: 1ms; }

/* ---------- detail screen ---------- */
.narr { flex-direction: row; align-items: stretch; }

.char {
  /* Ten expressions in one row, 1408x145, so the sheet is exactly 10x the element
   * wide and `auto` keeps the frame square to the element. A cue is one
   * background-position write — no reflow, no second request. */
  flex: 0 0 150px;
  width: 150px;
  height: 155px;
  align-self: center;
  background-image: url("/static/sprites/character.png");
  background-repeat: no-repeat;
  background-size: 1000% auto;
  background-position-x: 0;
  background-position-y: center;
  /* No transition: a mouth that eases between frames is a mouth that is late. */
}

/* Nine visemes onto five drawn mouths. The sheet is expressions, not a Preston
 * Blair strip, so shapes that differ in the lips but not in the opening share a
 * frame — at 150 dp on a waveguide, "how open" is all that reads anyway.
 *   col 0 closed  1 slight  2 teeth  5 open  6 wide
 * The column numbers are the one thing here worth checking on-device; they are all
 * in this table, so a correction is a number. */
.char[data-viseme="0"],                      /* A  closed (M/B/P) */
.char[data-viseme="8"] { background-position-x: 0; }              /* X  rest */
.char[data-viseme="1"],                      /* B  slightly open */
.char[data-viseme="5"] { background-position-x: 11.111%; }        /* F  puckered */
.char[data-viseme="6"],                      /* G  teeth on lip  */
.char[data-viseme="7"] { background-position-x: 22.222%; }        /* H  tongue up */
.char[data-viseme="2"],                      /* C  open */
.char[data-viseme="4"] { background-position-x: 55.556%; }        /* E  rounded */
.char[data-viseme="3"] { background-position-x: 66.667%; }        /* D  wide */

.subtitle {
  height: 100%;
  overflow-y: auto;
  padding: 4px 2px;
  line-height: 1.45;
  scrollbar-width: none;
}
.subtitle::-webkit-scrollbar { display: none; }

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