/* 台北戀愛模擬 — dark theme, scoped to this demo */

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

/* [hidden] must beat the display rules below, which the UA sheet loses to. */
[hidden] {
  display: none !important;
}

/*
 * Two registers, because the app does two different things.
 *
 * Finding someone is wayfinding: paper, hairlines, signage numerals, her line
 * colour as the only saturated thing on the page. Talking to her is a room at
 * night: the paper drops away and her colour becomes the light in it.
 *
 * The split also settles an old fight. The basemap is light; the chrome used
 * to be near-black, so the tiles had to be dimmed to 0.72 to stop the two
 * shouting at each other. A paper-coloured map view lets Voyager be itself.
 */
:root {
  /* Wayfinding — the map view */
  --paper: #e8e6df;
  --paper-raised: #f3f2ec;
  --ink: #15171c;
  --ink-soft: #5c5f68;
  --rule: #c3bfb4;

  /* Night — the conversation */
  --night: #15171c;
  --night-raised: #1f222a;
  --night-ink: #edecE8;
  --night-soft: #8d9099;
  --night-rule: #2e323c;

  --good: #2f7d55;
  --bad: #a83550;

  /* Radius is hierarchy, not a house style: a room has no corners to round,
     a card has small ones, a pill is a pill. One value everywhere is the
     giveaway. */
  --r-card: 4px;
  --r-pill: 999px;

  /* Rounded gothic is what Taipei MRT signage is set in, so the numerals —
     the day count, the affection figure — carry it. CJK falls through to the
     system face, which means only the Latin file is ever downloaded. */
  --font-cjk: "PingFang TC", "Noto Sans TC", "Microsoft JhengHei", sans-serif;
  --font-sign: "M PLUS Rounded 1c", var(--font-cjk);

  /* Basemap dimmer. Near 1 now that the chrome no longer fights it — the
     tiles only need taking off full brightness, not pushing to dusk. */
  --map-dim: 0.94;
}

html,
body {
  height: 100%;
  /* 行動瀏覽器的網址列會伸縮，100%／100vh 都把它算進去，畫面底部會被裁掉。
     dvh 是唯一跟著縮的單位；不支援的瀏覽器落回上面那行。 */
  height: 100dvh;
  font-family: var(--font-cjk);
  font-size: 15px;
  line-height: 1.6;
  background: var(--paper);
  color: var(--ink);
  overflow: hidden;
}

/* The day count and the names are set in the signage face; digits are tabular
   so the day counter does not shift as it climbs. */
.day-badge span,
.girl-name,
.stage-nameplate,
h1,
h2 {
  font-family: var(--font-sign);
  font-variant-numeric: tabular-nums;
}

.muted {
  color: var(--ink-soft);
}

/* ── Top bar ─────────────────────────────────────────────────────────── */
/*
 * Wayfinding chrome: one hairline, no fill, nothing competing with the map.
 * The day count is the only figure here, so it is the only thing set in the
 * signage face.
 */

.topbar {
  display: flex;
  align-items: center;
  gap: 0.85rem;
  height: var(--topbar-h);
  padding: 0 1rem;
  background: var(--paper);
  border-bottom: 1px solid var(--rule);
}

.topbar h1 {
  font-size: 1rem;
  font-weight: 600;
  letter-spacing: 0.06em;
}

.back {
  color: var(--ink-soft);
  text-decoration: none;
  font-size: 0.85rem;
}

@media (hover: hover) {
  .back:hover {
    color: var(--ink);
  }
}

.spacer {
  flex: 1;
}

/* No border, no pill — the number is doing the work, so let it. */
.day-badge {
  font-size: 0.8rem;
  color: var(--ink-soft);
  letter-spacing: 0.04em;
}

.day-badge span {
  font-family: var(--font-sign);
  font-variant-numeric: tabular-nums;
  font-size: 1.15rem;
  font-weight: 700;
  color: var(--ink);
  margin: 0 0.1em;
}

.boot-status {
  font-size: 0.78rem;
  color: var(--ink-soft);
}

.confess-btn,
.ghost-btn {
  font: inherit;
  font-size: 0.82rem;
  padding: 0.4rem 0.9rem;
  border-radius: var(--r-card);
  cursor: pointer;
  border: 1px solid var(--rule);
  background: transparent;
  color: var(--ink-soft);
}

@media (hover: hover) {
  .ghost-btn:hover {
    color: var(--ink);
    border-color: var(--ink-soft);
  }
}

/* The one button that changes the game permanently, so it is the one filled
   shape in the bar. Flat ink, not a gradient — a gradient here would just be
   decoration competing with three line colours already on screen. */
.confess-btn {
  background: var(--ink);
  border-color: var(--ink);
  color: var(--paper);
  font-weight: 600;
}

@media (hover: hover) {
  .confess-btn:hover {
    background: var(--bad);
    border-color: var(--bad);
  }
}

/* ── Layout ──────────────────────────────────────────────────────────── */

.layout {
  display: grid;
  grid-template-columns: 290px 1fr;
  height: calc(100dvh - var(--topbar-h));
}

.roster {
  border-right: 1px solid var(--rule);
  padding: 0.85rem;
  display: flex;
  flex-direction: column;
  gap: 0.7rem;
  overflow-y: auto;
  background: var(--paper);
}

/*
 * The card is a station board. Her name is the biggest thing on the paper and
 * her colour is the only saturated area on it — that is where the boldness is
 * spent, so everything else here stays quiet: no fill, no shadow, one rule.
 */
.girl-card {
  font: inherit;
  text-align: left;
  color: var(--ink);
  background: var(--paper-raised);
  border: 1px solid var(--rule);
  border-left: 4px solid var(--accent);
  border-radius: var(--r-card);
  padding: 0.7rem 0.8rem;
  cursor: pointer;
  display: flex;
  flex-direction: column;
  gap: 0.5rem;
}

@media (hover: hover) {
  .girl-card:hover {
    border-color: var(--accent);
  }
}

.girl-card-head {
  display: flex;
  /* center, not baseline — the portrait has no baseline to sit on. */
  align-items: center;
  gap: 0.55rem;
}

.girl-avatar {
  width: 36px;
  height: 36px;
  flex: none;
  border-radius: 50%;
  object-fit: cover;
  object-position: top center;
  border: 2px solid var(--accent);
  background: var(--paper);
}

.girl-name {
  /* Takes the slack so the mood chip stays pinned right, which
     justify-content used to do before the portrait joined the row. */
  flex: 1;
  font-weight: 800;
  font-size: 1.12rem;
  letter-spacing: 0.04em;
  color: var(--ink);
}

.girl-mood {
  font-size: 0.75rem;
  color: var(--ink-soft);
  white-space: nowrap;
}

.girl-tagline,
.girl-where {
  font-size: 0.76rem;
  color: var(--ink-soft);
}


/* ── Map ─────────────────────────────────────────────────────────────── */

.map-panel {
  position: relative;
}

#map {
  height: 100%;
  width: 100%;
  /* Shown only while tiles load — the paper tone, so the panel reads as part
     of the page instead of flashing a hole in it before the first tiles. */
  background: var(--paper);
}

/* Dim only the tile pane: markers, tooltips, and controls live in sibling panes
   and keep their full contrast. Dimming drops label contrast too, so nudge it
   back with a slight contrast bump. */
.leaflet-tile-pane {
  filter: brightness(var(--map-dim)) contrast(1.06) saturate(0.88);
}

.map-hint {
  position: absolute;
  left: 50%;
  bottom: 14px;
  transform: translateX(-50%);
  z-index: 500;
  font-size: 0.75rem;
  color: var(--ink-soft);
  background: color-mix(in srgb, var(--paper) 92%, transparent);
  border: 1px solid var(--rule);
  border-radius: var(--r-pill);
  padding: 0.3rem 0.85rem;
  pointer-events: none;
}

/* Leaflet pins ---------------------------------------------------------- */

.girl-pin-wrap {
  background: none;
  border: none;
}

.girl-pin {
  position: relative;
  /* Set from map.js so the size lives in one place — the icon anchor has to be
     derived from the same number or the pin drifts off its location. */
  width: var(--pin-size, 28px);
  height: var(--pin-size, 28px);
  border-radius: 50% 50% 50% 6px;
  transform: rotate(-45deg);
  background: var(--pin);
  /* A white ring and one hairline is enough separation from the basemap now
     that the tiles are barely dimmed. The drop shadow that used to sit under
     this was the accessory to take off. */
  border: 2px solid #fff;
  box-shadow: 0 0 0 1px #0000002e;
  display: grid;
  place-items: center;
}

/* Sits on top of .girl-pin-face, which stays in the markup as the fallback:
   if the image 404s it removes itself and the letter is already there. The
   45deg undoes the teardrop's rotation so the face is upright, and the top
   crop is because these renders put the head at the top of the frame. */
.girl-pin-photo {
  position: absolute;
  inset: 0;
  /* Explicit, not left to inset: an <img> is a replaced element, so with
     width/height auto it draws at its intrinsic size (540px here) and the
     opposite inset is dropped as over-constrained. Only a stated size makes it
     fill the pin. 100% is the padding box, i.e. inside the white border. */
  width: 100%;
  height: 100%;
  border-radius: 50%;
  object-fit: cover;
  object-position: top center;
  transform: rotate(45deg);
  background: var(--pin);
}

/* The pin is 28px so it does not shout over the basemap, but a 28px tap target
   is under any touch guideline — so the hit area is padded back out to ~44px
   with a transparent overlay. Clicks on it still bubble to the icon element
   Leaflet bound its handler to. */
.girl-pin::after {
  content: "";
  position: absolute;
  inset: -8px;
}

.girl-pin-face {
  transform: rotate(45deg);
  font-weight: 700;
  font-size: 0.82rem;
  color: #1a1a24;
}

.girl-pin.alert {
  animation: pin-pulse 1.4s ease-in-out infinite;
}

.girl-pin-badge {
  position: absolute;
  top: -5px;
  right: -5px;
  z-index: 1;
  transform: rotate(45deg);
  background: var(--bad);
  color: #fff;
  font-size: 0.6rem;
  font-weight: 700;
  width: 15px;
  height: 15px;
  border-radius: 50%;
  display: grid;
  place-items: center;
}

@keyframes pin-pulse {
  0%,
  100% {
    box-shadow:
      0 0 0 1px #00000033,
      0 3px 10px #0000005c;
  }
  50% {
    box-shadow:
      0 0 0 7px #e0708a4d,
      0 3px 10px #0000005c;
  }
}

/* ── Dialog overlay ──────────────────────────────────────────────────── */
/*
 * The other register. Finding someone happens on paper; talking to her happens
 * in a room at night, and her line colour is the light in it — injected as
 * --accent when the dialog opens, so the same component is lit differently
 * depending on who is in it. Nothing else in here is coloured.
 */

.dialog {
  position: fixed;
  inset: 0;
  z-index: 1000;
  background: color-mix(in srgb, var(--ink) 82%, transparent);
  backdrop-filter: blur(6px);
  display: grid;
  place-items: center;
  padding: 1.5rem;
  color: var(--night-ink);
}

.dialog-inner {
  width: min(1050px, 100%);
  height: min(660px, 100%);
  display: grid;
  /* minmax(0, …), not plain fr. `1fr` is `minmax(auto, 1fr)`, and `auto` means
     "at least as wide as the content" — the suggestion chips are nowrap, so
     their intrinsic width was inflating this column until the panel ran off
     the side of the screen. */
  grid-template-columns: minmax(0, 1.15fr) minmax(0, 1fr);
  background: var(--night);
  border: 1px solid var(--night-rule);
  border-radius: 6px;
  overflow: hidden;
  position: relative;
}

.dialog-close {
  position: absolute;
  top: 10px;
  right: 12px;
  z-index: 5;
  font: inherit;
  font-size: 1rem;
  width: 30px;
  height: 30px;
  border-radius: var(--r-card);
  border: 1px solid var(--night-rule);
  background: color-mix(in srgb, var(--night) 80%, transparent);
  color: var(--night-soft);
  cursor: pointer;
}

@media (hover: hover) {
  .dialog-close:hover {
    color: var(--night-ink);
  }
}

/* Stage ----------------------------------------------------------------- */

.stage {
  min-width: 0;
  position: relative;
  background: var(--night);
  display: grid;
  place-items: center;
  overflow: hidden;
}

/* The one piece of colour in the room: a low wash of her line colour behind
   the avatar, so switching girls changes the light rather than a label. */
.stage::before {
  content: "";
  position: absolute;
  inset: 0;
  background: radial-gradient(
    120% 80% at 50% 100%,
    color-mix(in srgb, var(--accent, #6b6f7a) 26%, transparent),
    transparent 70%
  );
  pointer-events: none;
}

sv-presenter {
  width: 100%;
  height: 100%;
  display: block;
  position: relative;
}

.stage-status {
  position: absolute;
  font-size: 0.85rem;
  color: var(--night-soft);
}

/* Her name, set the same way it is on the station board, so the two views
   agree about what the biggest thing on screen is. */
.stage-nameplate {
  position: absolute;
  left: 14px;
  bottom: 14px;
  z-index: 2;
  display: flex;
  gap: 0.5rem;
  align-items: baseline;
  font-size: 1.15rem;
  font-weight: 800;
  letter-spacing: 0.04em;
  text-shadow: 0 1px 12px var(--night);
}

.stage-nameplate .muted {
  font-size: 0.78rem;
  font-weight: 400;
  color: var(--night-soft);
  letter-spacing: 0;
}

.mood-chip {
  position: absolute;
  right: 14px;
  bottom: 14px;
  z-index: 2;
  font-size: 0.75rem;
  color: var(--night-soft);
  background: color-mix(in srgb, var(--night) 75%, transparent);
  border: 1px solid var(--night-rule);
  border-radius: var(--r-pill);
  padding: 0.25rem 0.7rem;
}

.mood-chip.speaking {
  border-color: var(--accent, var(--night-soft));
  color: var(--night-ink);
}

/* Conversation ---------------------------------------------------------- */

.convo {
  display: flex;
  flex-direction: column;
  border-left: 1px solid var(--night-rule);
  /* Both axes: the same min-*: auto default that pushed the input off the
     bottom also pushed this column off the side. */
  min-height: 0;
  min-width: 0;
  background: var(--night);
}

.chat-log {
  min-width: 0;
  /* min-height: 0 is load-bearing. A flex item defaults to min-height: auto and
     refuses to shrink below its content, so as soon as another row joined this
     column — the suggestions, then the target picker — the input at the bottom
     was pushed out of the panel and clipped. The log is the only thing here
     that should give way; everything else is flex: none below. */
  flex: 1 1 0;
  min-height: 0;
  overflow-y: auto;
  padding: 0.9rem 1rem;
  display: flex;
  flex-direction: column;
  gap: 0.55rem;
}

.msg {
  max-width: 85%;
  min-width: 0;
  overflow-wrap: anywhere;
  padding: 0.55rem 0.8rem;
  font-size: 0.88rem;
  line-height: 1.6;
  white-space: pre-wrap;
  word-break: break-word;
}

/* Asymmetric on purpose: you get a bubble, she gets the room. Two mirrored
   bubbles is the messaging-app default and says nothing about who is who. */
.msg.user {
  align-self: flex-end;
  background: var(--night-raised);
  border-radius: 12px 12px 3px 12px;
}

.msg.assistant {
  align-self: stretch;
  max-width: none;
  padding-left: 0.85rem;
  border-left: 2px solid var(--accent, var(--night-rule));
}

.msg.error {
  align-self: center;
  background: transparent;
  color: var(--bad);
  font-size: 0.78rem;
}

/* Suggested replies — sits between the log and the input, so it reads as
   "things you could say" rather than part of the conversation. */
/*
 * One scrolling row, not a stack. Three sentences stacked ate about 130px above
 * the input — on a phone that is a third of the conversation. Laid on their
 * side they cost one line, and swiping through them is the same gesture as
 * reading them.
 */
.suggestions {
  min-width: 0;
  flex: none;
  display: flex;
  gap: 0.35rem;
  padding: 0.5rem 1rem;
  overflow-x: auto;
  overflow-y: hidden;
  scroll-snap-type: x proximity;
  -webkit-overflow-scrolling: touch;
  scrollbar-width: none;
}

.suggestions::-webkit-scrollbar {
  display: none;
}

.suggestion {
  font: inherit;
  font-size: 0.8rem;
  line-height: 1.3;
  text-align: left;
  cursor: pointer;
  flex: 0 0 auto;
  max-width: min(78%, 22rem);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  scroll-snap-align: start;
  padding: 0.42rem 0.75rem;
  border-radius: var(--r-pill);
  border: 1px solid var(--night-rule);
  background: transparent;
  color: var(--night-ink);
  transition:
    border-color 0.15s,
    background 0.15s;
}

@media (hover: hover) {
  .suggestion:hover:not(:disabled) {
    border-color: var(--accent, var(--night-soft));
    background: var(--night-raised);
  }
}

.suggestion:disabled {
  opacity: 0.4;
  cursor: default;
}

.chat-form {
  min-width: 0;
  flex: none;
  display: flex;
  gap: 0.5rem;
  padding: 0.8rem 1rem;
  border-top: 1px solid var(--night-rule);
}

.chat-form input {
  flex: 1;
  min-width: 0;
  font: inherit;
  font-size: 0.88rem;
  padding: 0.55rem 0.75rem;
  border-radius: var(--r-card);
  border: 1px solid var(--night-rule);
  background: var(--night-raised);
  color: var(--night-ink);
}

.chat-form input::placeholder {
  color: var(--night-soft);
}

.chat-form input:focus {
  border-color: var(--accent, var(--night-soft));
}

.chat-form button {
  font: inherit;
  font-size: 0.85rem;
  padding: 0.55rem 1.1rem;
  border-radius: var(--r-card);
  border: 1px solid var(--accent, var(--night-soft));
  cursor: pointer;
  background: var(--accent, var(--night-soft));
  color: var(--night);
  font-weight: 700;
}

.chat-form button:disabled {
  opacity: 0.4;
  cursor: default;
}

/* ── Ending ──────────────────────────────────────────────────────────── */
/*
 * The confession is the one irreversible act in the game, so it gets the room
 * to itself: night register, no map behind it, and the three choices weighted
 * only by each girl's own line colour.
 */

.ending-inner {
  width: min(560px, 100%);
  background: var(--night);
  border: 1px solid var(--night-rule);
  border-radius: 6px;
  padding: 2rem;
  text-align: center;
  color: var(--night-ink);
  display: flex;
  flex-direction: column;
  gap: 1rem;
}

.ending-inner h2 {
  font-size: 1.35rem;
  font-weight: 800;
  letter-spacing: 0.04em;
}

.ending-choices {
  display: flex;
  flex-direction: column;
  gap: 0.6rem;
}

.ending-choice {
  font: inherit;
  cursor: pointer;
  text-align: left;
  padding: 0.75rem 1rem;
  border-radius: var(--r-card);
  background: transparent;
  border: 1px solid var(--night-rule);
  border-left: 4px solid var(--accent);
  color: var(--night-ink);
  display: flex;
  flex-direction: column;
  gap: 0.15rem;
}

@media (hover: hover) {
  .ending-choice:hover {
    background: var(--night-raised);
    border-color: var(--accent);
  }
}

.ending-choice b {
  font-family: var(--font-sign);
  font-size: 1.05rem;
  font-weight: 800;
  letter-spacing: 0.04em;
}

.ending-choice span {
  font-size: 0.75rem;
  color: var(--night-soft);
  font-variant-numeric: tabular-nums;
}

.ending-line {
  font-size: 1.05rem;
  line-height: 1.75;
}

.ending-other {
  font-size: 0.82rem;
  color: var(--night-soft);
}

/* ── Toasts ──────────────────────────────────────────────────────────── */
/*
 * Toasts land on the map, so they belong to the paper register — the girl's
 * line colour is the only thing carried over, since that is the whole content
 * of the message: which of them moved.
 */

.toasts {
  position: fixed;
  right: 16px;
  bottom: 16px;
  z-index: 2000;
  display: flex;
  flex-direction: column;
  gap: 0.5rem;
}

.toast {
  background: var(--paper-raised);
  border: 1px solid var(--rule);
  border-left: 4px solid var(--accent);
  border-radius: var(--r-card);
  padding: 0.55rem 0.9rem;
  font-size: 0.8rem;
  color: var(--ink);
  box-shadow: 0 6px 18px #00000024;
  transition:
    opacity 0.4s,
    transform 0.4s;
}

.toast.out {
  opacity: 0;
  transform: translateX(12px);
}

/* 觸控裝置沒有 hover 可用，按下的當下要看得出來點到了。 */
.girl-card:active,
.suggestion:active:not(:disabled),
.ending-choice:active {
  transform: scale(0.985);
}

.hint-narrow {
  display: none;
}

/* ── Tablets ─────────────────────────────────────────────────────────── */
/*
 * The roster used to be hidden below 900px. It is the only place the game
 * state is visible — three affection bars, who is jealous, where everyone is —
 * so hiding it means playing blind. Narrow it instead.
 */
@media (max-width: 900px) {
  .layout {
    grid-template-columns: 216px 1fr;
  }
  .roster {
    padding: 0.6rem;
  }
  .girl-tagline {
    display: none;
  }
  .dialog-inner {
    grid-template-columns: 1fr;
    grid-template-rows: minmax(0, 1fr) minmax(0, 1.15fr);
  }
  .convo {
    border-left: none;
    border-top: 1px solid var(--night-rule);
  }
}

/* ── Phones ──────────────────────────────────────────────────────────── */
/*
 * A phone has no room for a column beside the map, so the roster becomes a
 * snapping strip underneath it — still three cards, still the tap target that
 * opens a conversation, just laid on its side.
 *
 * env(safe-area-inset-*) resolves to 0 here because the viewport meta does not
 * set viewport-fit=cover: the browser already insets the visual viewport, so
 * nothing sits under a notch or the home indicator. The max() wrappers are
 * there so that turning cover on later stays correct.
 */
@media (max-width: 640px) {
  :root {
    --topbar-h: 52px;
  }

  .topbar {
    gap: 0.45rem;
    padding: 0 max(0.6rem, env(safe-area-inset-right)) 0
      max(0.6rem, env(safe-area-inset-left));
  }

  /* "← Demos" → "←". Seven items in a 52px row overflow at 360px, and the
     arrow alone still reads as back. */
  .back-label {
    display: none;
  }

  .topbar h1 {
    font-size: 0.95rem;
    white-space: nowrap;
  }

  .day-badge {
    font-size: 0.72rem;
    padding: 0.15rem 0.5rem;
  }

  /* Duplicated by the roster and the stage once anything is running, and it is
     what tips the row into overflowing. */
  .boot-status {
    display: none;
  }

  .confess-btn,
  .ghost-btn {
    font-size: 0.78rem;
    padding: 0.5rem 0.7rem;
    min-height: 40px;
  }

  /* Map on top, roster below. Source order is roster-then-map, hence order. */
  .layout {
    grid-template-columns: 1fr;
    grid-template-rows: minmax(0, 1fr) auto;
  }

  .map-panel {
    order: 1;
  }

  .roster {
    order: 2;
    flex-direction: row;
    overflow-x: auto;
    overflow-y: hidden;
    scroll-snap-type: x mandatory;
    -webkit-overflow-scrolling: touch;
    border-right: none;
    border-top: 1px solid var(--rule);
    gap: 0.55rem;
    padding: 0.6rem 0.6rem max(0.6rem, env(safe-area-inset-bottom));
  }

  .girl-card {
    flex: 0 0 min(74vw, 240px);
    scroll-snap-align: start;
    padding: 0.6rem 0.7rem;
    gap: 0.34rem;
  }

  .hint-wide {
    display: none;
  }

  .hint-narrow {
    display: inline;
  }

  .map-hint {
    font-size: 0.7rem;
    padding: 0.25rem 0.6rem;
    max-width: calc(100vw - 1.6rem);
  }

  /* Full screen. A centred card wastes height a phone does not have, and the
     rounded corners just letterbox the avatar. */
  .dialog {
    padding: 0;
  }

  .dialog-inner {
    width: 100%;
    height: 100dvh;
    border: none;
    border-radius: 0;
    grid-template-columns: minmax(0, 1fr);
    /* The stage gives way first. With the keyboard up dvh shrinks, the stage
       floors at 150px, and the input keeps its place instead of being pushed
       off the bottom. */
    grid-template-rows: clamp(150px, 34dvh, 300px) minmax(0, 1fr);
  }

  .dialog-close {
    width: 40px;
    height: 40px;
  }

  /* The stage floors at 150px on a phone. Three cards from the top plus the
     nameplate at the bottom is the one combination that can meet in the
     middle, so they get tighter rather than taller. */
  .stage-meter {
    font-size: 0.66rem;
    padding: 0.1rem 0.4rem;
  }

  .stage-meters {
    gap: 0.2rem;
  }

  .chat-log {
    padding: 0.7rem 0.8rem;
  }

  .msg {
    max-width: 92%;
  }

  .suggestions {
    padding: 0.45rem 0.8rem;
  }

  .suggestion {
    padding: 0.55rem 0.7rem;
  }

  .chat-form {
    padding: 0.6rem 0.8rem max(0.6rem, env(safe-area-inset-bottom));
  }

  /* 16px is the threshold below which iOS Safari zooms the page on focus —
     and it does not zoom back out afterwards. */
  .chat-form input {
    font-size: 16px;
    padding: 0.6rem 0.75rem;
  }

  .chat-form button {
    min-height: 44px;
    padding: 0.6rem 0.9rem;
  }

  /* Bottom-right lands squarely on the input and the send button. */
  .toasts {
    top: calc(var(--topbar-h) + 0.5rem);
    right: 0.6rem;
    bottom: auto;
    left: 0.6rem;
    align-items: center;
  }

  .toast.out {
    transform: translateY(-12px);
  }

  .ending-inner {
    height: 100dvh;
    border: none;
    border-radius: 0;
    justify-content: center;
    padding: 1.5rem 1.15rem max(1.5rem, env(safe-area-inset-bottom));
  }
}




/* Who said it. Unlabelled bubbles say nothing about which of them answered,
   and in a group scene that is the only thing that matters. */
.msg-who {
  display: block;
  font-size: 0.7rem;
  font-weight: 700;
  letter-spacing: 0.04em;
  color: var(--accent, var(--night-soft));
  margin-bottom: 0.15rem;
}

/*
 * Both verdicts on one line. This row is the group scene's whole argument —
 * one sentence, two scores, and the reasons underneath saying why they differ.
 */
/* Someone else is standing where she is — the reason to go, or to stay away. */
.girl-card .together {
  margin-left: 0.4rem;
  font-size: 0.68rem;
  color: var(--ink);
  border-bottom: 1px solid var(--ink);
}

/* Two people are offering you an opening. The colour and the name say whose
   you would be taking — the line still goes to both of them. */
/* Whose opening it is, without spending width on her name. */
.suggestions.paired .suggestion {
  border-color: color-mix(in srgb, var(--accent) 55%, var(--night-rule));
}

.suggestions.paired .suggestion::before {
  content: "";
  display: inline-block;
  width: 6px;
  height: 6px;
  border-radius: 50%;
  background: var(--accent);
  margin-right: 0.4rem;
  vertical-align: 0.05em;
}

/* ── Sheets: inbox, day end, ending, name ─────────────────────────── */
/* One panel style for every "the day paused" moment — night register, no map behind it. */

.sheet {
  width: min(560px, 100%);
  background: var(--night);
  border: 1px solid var(--night-rule);
  border-radius: 6px;
  padding: 1.6rem 1.4rem;
  color: var(--night-ink);
  display: flex;
  flex-direction: column;
  gap: 0.9rem;
}

.sheet h2 {
  font-size: 1.25rem;
  font-weight: 800;
  letter-spacing: 0.04em;
}

.sheet .muted {
  color: var(--night-soft);
}

.sheet input[type="text"] {
  font: inherit;
  font-size: 16px;
  padding: 0.6rem 0.75rem;
  border-radius: var(--r-card);
  border: 1px solid var(--night-rule);
  background: var(--night-raised);
  color: var(--night-ink);
}

.inbox-list,
.dayend-list {
  display: flex;
  flex-direction: column;
  gap: 0.6rem;
}

.inbox-item {
  padding: 0.6rem 0.8rem;
  border-left: 3px solid var(--accent);
  background: var(--night-raised);
  border-radius: var(--r-card);
  line-height: 1.6;
}

.inbox-item b {
  display: block;
  font-size: 0.75rem;
  color: var(--accent);
  letter-spacing: 0.04em;
  margin-bottom: 0.15rem;
}

.dayend-list p {
  color: var(--night-soft);
  line-height: 1.7;
}

/* Today's fixed event, over the map (§2.1). */
.event-banner {
  position: absolute;
  top: 12px;
  left: 50%;
  transform: translateX(-50%);
  z-index: 500;
  max-width: calc(100% - 2rem);
  font-size: 0.8rem;
  color: var(--ink);
  background: color-mix(in srgb, var(--paper-raised) 94%, transparent);
  border: 1px solid var(--rule);
  border-radius: var(--r-card);
  padding: 0.4rem 0.9rem;
  pointer-events: none;
}

/* ── Who you are talking to (group only) ───────────────────────────── */

.targets {
  min-width: 0;
  overflow-x: auto;
  scrollbar-width: none;
  flex: none;
  display: flex;
  gap: 0.4rem;
  padding: 0.5rem 1rem 0;
  align-items: center;
}

.target {
  font: inherit;
  font-size: 0.75rem;
  display: inline-flex;
  align-items: center;
  gap: 0.35rem;
  padding: 0.25rem 0.6rem 0.25rem 0.25rem;
  border-radius: var(--r-pill);
  border: 1px solid var(--night-rule);
  background: transparent;
  color: var(--night-soft);
  cursor: pointer;
}

.target img {
  width: 22px;
  height: 22px;
  border-radius: 50%;
  object-fit: cover;
  object-position: top center;
}

/* The selected one is filled, not merely outlined — at this size an outline
   reads as a hover state rather than a commitment. */
.target.on {
  border-color: var(--accent);
  background: color-mix(in srgb, var(--accent) 22%, transparent);
  color: var(--night-ink);
  font-weight: 700;
}

/* Aimed at someone, in the transcript. */
.msg-toward {
  display: block;
  font-size: 0.68rem;
  font-weight: 700;
  color: var(--accent);
  opacity: 0.85;
  margin-bottom: 0.1rem;
}

/* ── Afterparty (§5.5) ──────────────────────────────────────────────── */

.afterparty {
  flex: none;
  padding: 0.7rem 1rem;
  border-top: 1px solid var(--night-rule);
  display: flex;
  flex-direction: column;
  gap: 0.5rem;
}

.afterparty p {
  font-size: 0.85rem;
  color: var(--night-soft);
}

.afterparty-choices {
  display: flex;
  flex-wrap: wrap;
  gap: 0.4rem;
}

.afterparty-choice {
  font: inherit;
  font-size: 0.82rem;
  padding: 0.5rem 0.9rem;
  border-radius: var(--r-card);
  border: 1px solid var(--accent);
  background: transparent;
  color: var(--night-ink);
  cursor: pointer;
}

@media (hover: hover) {
  .afterparty-choice:hover {
    background: var(--night-raised);
  }
}

/* ── Two or three of them at once ────────────────────────────────────── */
/*
 * Every presenter gets the same sceneId, so the split is the only thing telling
 * you there is more than one of them — the background is continuous on purpose.
 *
 * Flex, not grid tracks. Giving the speaker a two-column span in a
 * three-column grid makes four column-units and wraps the third avatar onto a
 * second row, so the stage snapped between two different layouts every time
 * someone started or stopped talking. Growing a flex item changes only its
 * width, and it can be animated.
 */

.stage.pair,
.stage.trio {
  display: flex;
  align-items: stretch;
}

/* A hidden presenter is display:none and takes no width, so someone leaving
   mid-scene closes the gap instead of leaving a hole — but display is not
   animatable, hence the entering/leaving classes stage.js drives around it. */
.stage.pair > sv-presenter,
.stage.trio > sv-presenter {
  flex: 1 1 0;
  min-width: 0;
  min-height: 0;
  opacity: 1;
  outline: 2px solid transparent;
  outline-offset: -2px;
  transition:
    flex-grow 0.35s ease,
    opacity 0.3s ease,
    filter 0.3s ease,
    outline-color 0.25s ease;
}

/* §9.2 「1 大 2 小」—— 寬度在變，版面不重排。 */
.stage.pair > sv-presenter.speaking,
.stage.trio > sv-presenter.speaking {
  flex-grow: 1.9;
  outline-color: var(--accent);
}

/* Whoever is listening recedes. Width alone is a weak signal at phone size;
   letting the light come off the others makes it obvious who has the floor. */
.stage:has(> sv-presenter.speaking) > sv-presenter:not(.speaking) {
  filter: saturate(0.7) brightness(0.68);
}

/* Fading in: the class is removed a frame after unhiding, so the browser has a
   starting value to animate from. */
.stage > sv-presenter.entering {
  opacity: 0;
}

/* Walking out: collapse the column and fade, then stage.js hides it. */
.stage > sv-presenter.leaving {
  flex-grow: 0.001;
  opacity: 0;
}

/* Booting: she has a box, she is just not visible in it yet.
   The presenter engine will not finish initializing inside an element it
   cannot measure, and `hidden` is display:none — so present() unhides her
   veiled and show() lifts the veil once she is Ready. Must stay below the
   .stage.pair / .stage.trio rule above, which sets opacity at the same
   specificity. */
.stage > sv-presenter.veiled {
  opacity: 0;
  pointer-events: none;
}

@media (prefers-reduced-motion: reduce) {
  .stage.pair > sv-presenter,
  .stage.trio > sv-presenter {
    transition: none;
  }
  .stage:has(> sv-presenter.speaking) > sv-presenter:not(.speaking) {
    filter: none;
  }
}


/* ── Pins carry the only visible state (§7.4) ───────────────────────── */

.girl-pin-faces {
  position: absolute;
  inset: 0;
  display: flex;
  border-radius: 50%;
  overflow: hidden;
  transform: rotate(45deg);
}

.girl-pin-faces img,
.girl-pin-faces .girl-pin-face {
  flex: 1;
  min-width: 0;
  height: 100%;
  object-fit: cover;
  object-position: top center;
  transform: none;
}

.girl-pin.pair {
  width: calc(var(--pin-size, 30px) + 14px);
}

.girl-pin-mood {
  position: absolute;
  right: -7px;
  top: -7px;
  z-index: 1;
  transform: rotate(45deg);
  font-size: 0.8rem;
  line-height: 1;
  filter: drop-shadow(0 1px 1px #0006);
}

.girl-pin-count {
  position: absolute;
  left: -6px;
  top: -6px;
  z-index: 1;
  transform: rotate(45deg);
  width: 16px;
  height: 16px;
  border-radius: 50%;
  background: var(--ink);
  color: var(--paper);
  font-size: 0.62rem;
  font-weight: 700;
  display: grid;
  place-items: center;
}

/* Sulking: a second ring in the warning colour, pulsing once to be noticed. */
.girl-pin.sulk {
  box-shadow:
    0 0 0 1px #0000002e,
    0 0 0 4px color-mix(in srgb, var(--bad) 55%, transparent);
}

/* Today is spent — the pins dim but stay, so the map still says where they were. */
.girl-pin.done {
  opacity: 0.45;
  filter: saturate(0.4);
}

.girl-card.off {
  opacity: 0.55;
  cursor: default;
}

.girl-mood.sulk {
  outline: 2px solid var(--bad);
  outline-offset: 2px;
  border-radius: 50%;
}

.ending-spots {
  display: flex;
  flex-direction: column;
  gap: 0.5rem;
}

.ending-other.crash {
  color: var(--bad);
}

/* A stage direction, not a line anyone said — so it does not borrow the error
   colour, and it does not get a name tag. */
.msg.note {
  align-self: center;
  background: transparent;
  color: var(--night-soft);
  font-size: 0.78rem;
}

/* The recap replaces days of scroll-back, so it reads as a margin note above
   the conversation rather than as its first line: full width, a rule under it,
   and the day sitting outside the sentence it dates. */
.msg.recap {
  align-self: stretch;
  max-width: none;
  padding: 0 0 0.6rem;
  border-bottom: 1px solid var(--night-rule);
  line-height: 1.55;
}

.msg.recap > b {
  display: block;
  margin-bottom: 0.3rem;
  color: var(--night-soft);
  font-size: 0.7rem;
  font-weight: 600;
  letter-spacing: 0.04em;
}

.msg.recap ul {
  margin: 0;
  padding: 0;
  list-style: none;
  display: flex;
  flex-direction: column;
  gap: 0.28rem;
}

/* The remembered thing is the point; the day only files it. */
.msg.recap li {
  color: var(--night-ink);
}

.msg.recap i {
  margin-right: 0.4rem;
  color: var(--night-soft);
  font-style: normal;
  font-variant-numeric: tabular-nums;
}

/* An unkept promise is a debt, not a note — it gets the warning colour. */
.msg.recap .recap-owed {
  margin: 0.4rem 0 0;
  color: var(--bad);
}

/* ── The numbers ────────────────────────────────────────────────────── */
/*
 * The spec keeps affection and jealousy hidden and lets expressions carry them
 * (§7.4). Showing them is a deliberate reversal: a player who cannot see the
 * figure cannot tell a good turn from a lucky one, and the Judge's scoring is
 * the most interesting thing in this build to be able to read.
 */

.meters {
  display: flex;
  gap: 0.6rem;
  font-size: 0.72rem;
  color: var(--ink-soft);
}

.meters b,
.stage-meter b {
  font-family: var(--font-sign);
  font-variant-numeric: tabular-nums;
  font-size: 0.95rem;
  color: var(--accent);
}

.meters .jealous b {
  color: var(--bad);
}

/*
 * On stage, one per person in the scene. Top-left, because the close button
 * owns the top-right corner and the nameplate the bottom-left — these two were
 * sitting on exactly the same coordinates.
 */
.stage-meters {
  position: absolute;
  left: 12px;
  top: 10px;
  z-index: 2;
  display: flex;
  flex-direction: column;
  gap: 0.3rem;
  align-items: flex-start;
  /* Never grow into the close button's corner. */
  max-width: calc(100% - 60px);
}

.stage-meter {
  display: inline-flex;
  align-items: baseline;
  gap: 0.3rem;
  white-space: nowrap;
  font-size: 0.7rem;
  color: var(--night-soft);
  background: color-mix(in srgb, var(--night) 78%, transparent);
  border: 1px solid var(--night-rule);
  border-left: 3px solid var(--accent);
  border-radius: var(--r-card);
  padding: 0.16rem 0.5rem;
}

.stage-meter b {
  color: var(--night-ink);
}

.stage-meter i {
  font-style: normal;
  color: var(--bad);
}

.stage-meter.sulk {
  border-color: var(--bad);
  border-left-color: var(--bad);
}

/* What one line was worth. Centred and quiet — it is a footnote to her reply,
   not a message in the conversation. */
.delta {
  align-self: center;
  font-size: 0.72rem;
  font-variant-numeric: tabular-nums;
  color: var(--night-soft);
  letter-spacing: 0.03em;
}

.delta .up {
  color: var(--good);
}

.delta .down {
  color: var(--bad);
}

/* ── Someone is about to answer ─────────────────────────────────────── */
/*
 * The gap between one line finishing and the next appearing is three API calls
 * long. Without something in it the game looks stopped; with her name on it,
 * the wait is her thinking — and it also says who is coming, which is half the
 * tension in a group scene.
 */

.msg.thinking {
  align-self: stretch;
  max-width: none;
  padding-left: 0.85rem;
  border-left: 2px solid var(--accent, var(--night-rule));
}

.dots {
  display: inline-flex;
  gap: 4px;
  align-items: center;
  height: 1.4em;
}

.dots i {
  width: 5px;
  height: 5px;
  border-radius: 50%;
  background: var(--accent, var(--night-soft));
  opacity: 0.25;
  animation: dot 1.1s ease-in-out infinite;
}

.dots i:nth-child(2) {
  animation-delay: 0.16s;
}

.dots i:nth-child(3) {
  animation-delay: 0.32s;
}

@keyframes dot {
  30% {
    opacity: 1;
    transform: translateY(-2px);
  }
}

@media (prefers-reduced-motion: reduce) {
  .dots i {
    animation: none;
    opacity: 0.6;
  }
}

/* ── Appearing ───────────────────────────────────────────────────────── */
/*
 * Panels are toggled with [hidden], and display is not animatable — but an
 * animation does run when an element becomes displayed, which is enough for
 * everything that opens here. Short and once: this is furniture arriving, not
 * an effect.
 */

.dialog {
  animation: veil-in 0.2s ease;
}

.dialog-inner,
.sheet {
  animation: panel-in 0.26s cubic-bezier(0.2, 0.7, 0.3, 1);
}

@keyframes veil-in {
  from {
    opacity: 0;
  }
}

@keyframes panel-in {
  from {
    opacity: 0;
    transform: translateY(10px);
  }
}

/* A new line in the log slides the last inch rather than appearing. */
.msg,
.suggestion {
  animation: line-in 0.22s ease;
}

@keyframes line-in {
  from {
    opacity: 0;
    transform: translateY(4px);
  }
}

@media (prefers-reduced-motion: reduce) {
  .dialog,
  .dialog-inner,
  .sheet,
  .msg,
  .suggestion {
    animation: none;
  }
}

/* ── Quality floor ───────────────────────────────────────────────────── */

/*
 * Keyboard focus. The UA ring disappears against both registers, so state one
 * that works on either — her line colour where there is one, the current text
 * colour where there is not (the top bar).
 */
a:focus-visible,
button:focus-visible,
input:focus-visible,
.girl-card:focus-visible,
.suggestion:focus-visible,
.ending-choice:focus-visible {
  outline: 2px solid var(--accent, currentColor);
  outline-offset: 2px;
}

/*
 * The one piece of motion that runs without being asked for is the jealousy
 * pulse on a pin, and it runs precisely to pull the eye — which is what this
 * preference asks not to happen. Everything else here answers a click and can
 * simply arrive instantly.
 */
@media (prefers-reduced-motion: reduce) {
  *,
  *::before,
  *::after {
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.01ms !important;
    scroll-behavior: auto !important;
  }
}
