/* ============================================================================
   Wish We Were Here — UI shell
   Deliberately quiet: warm, low-contrast chrome that never competes with the
   scene. The diorama is the content; the UI gets out of its way.
   ========================================================================= */

:root {
  --ink:            #f4ede4;
  --ink-soft:       rgba(244, 237, 228, 0.68);
  --ink-faint:      rgba(244, 237, 228, 0.42);

  --glass:          rgba(14, 22, 33, 0.62);
  --glass-strong:   rgba(12, 19, 29, 0.82);
  --hairline:       rgba(255, 255, 255, 0.14);
  --hairline-soft:  rgba(255, 255, 255, 0.08);

  --ember:          #e9b872;
  --ember-deep:     #c88f45;
  --ember-glow:     rgba(233, 184, 114, 0.32);

  --radius:         14px;
  --radius-sm:      10px;

  --font: "Segoe UI", -apple-system, BlinkMacSystemFont, Tahoma, Geneva, Verdana, sans-serif;
  --font-display: "Iowan Old Style", "Palatino Linotype", Palatino, Georgia, serif;

  --ease: cubic-bezier(0.22, 0.61, 0.36, 1);

  /*
   * The screen's own edges, on a device that has any.
   *
   * The viewport is `viewport-fit=cover` and an installed copy runs its content under the status bar
   * and the home indicator (`black-translucent`), so every piece of chrome pinned to an edge is
   * offset by the inset as well as by its own margin. `env()` is 0 everywhere else — a desktop
   * browser, a phone in a tab — so the layout is unchanged where there is nothing to avoid, and
   * `tools/phone.mjs` reads the base term of each `calc()` for exactly that reason.
   */
  --safe-t: env(safe-area-inset-top, 0px);
  --safe-r: env(safe-area-inset-right, 0px);
  --safe-b: env(safe-area-inset-bottom, 0px);
  --safe-l: env(safe-area-inset-left, 0px);
}

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

html, body {
  width: 100%;
  height: 100%;
  overflow: hidden;
  background: #0a121c;
  color: var(--ink);
  font-family: var(--font);
  -webkit-font-smoothing: antialiased;
  -webkit-tap-highlight-color: transparent;
  overscroll-behavior: none;
}

#viewport {
  position: fixed;
  inset: 0;
  touch-action: none;   /* we handle gestures ourselves */
}

#viewport canvas { display: block; }

/* ── Boot ─────────────────────────────────────────────────────────────── */

.boot {
  position: fixed;
  inset: 0;
  display: grid;
  place-items: center;
  background: radial-gradient(120% 90% at 50% 30%, #12202f 0%, #080e16 70%);
  z-index: 60;
  transition: opacity 0.6s var(--ease);
}
.boot[hidden] { display: none; }
.boot.is-fading { opacity: 0; pointer-events: none; }

.boot-inner { text-align: center; max-width: 30rem; padding: 2rem; }

.boot-spinner {
  width: 30px; height: 30px;
  margin: 0 auto 1.25rem;
  border: 2px solid var(--hairline);
  border-top-color: var(--ember);
  border-radius: 50%;
  animation: spin 0.9s linear infinite;
}
@keyframes spin { to { transform: rotate(360deg); } }

.boot-text { color: var(--ink-soft); font-size: 0.94rem; letter-spacing: 0.02em; }
.boot-error { color: var(--ink); font-size: 1.05rem; margin-bottom: 0.6rem; }
.boot-detail { color: var(--ink-faint); font-size: 0.85rem; line-height: 1.6; }

/*
 * With JavaScript off, the boot overlay never exists — the `<noscript>` block is the whole page.
 * It carries the same dark ground, so a browser that cannot run the app says why instead of
 * showing an empty white screen.
 */
.boot-noscript {
  position: fixed;
  inset: 0;
  display: grid;
  place-items: center;
  padding: 2rem 1.25rem;
  background: radial-gradient(120% 90% at 50% 30%, #12202f 0%, #080e16 70%);
}

/* ── Landing ──────────────────────────────────────────────────────────── */

.landing {
  position: fixed;
  inset: 0;
  display: grid;
  place-items: center;
  z-index: 50;
  background: radial-gradient(120% 90% at 50% 35%, rgba(10,18,28,0.35) 0%, rgba(6,10,16,0.88) 75%);
  backdrop-filter: blur(3px);
  animation: fade-in 0.9s var(--ease) both;
}
.landing[hidden] { display: none; }
.landing.is-leaving { animation: fade-out 0.55s var(--ease) both; }

@keyframes fade-in  { from { opacity: 0; } to { opacity: 1; } }
@keyframes fade-out { from { opacity: 1; } to { opacity: 0; visibility: hidden; } }

.landing-card {
  text-align: center;
  max-width: 34rem;
  padding: 2.5rem 2rem;
  animation: rise 1.1s var(--ease) both;
  animation-delay: 0.15s;
}
@keyframes rise {
  from { opacity: 0; transform: translateY(14px); }
  to   { opacity: 1; transform: none; }
}

.landing-title {
  font-family: var(--font-display);
  font-size: clamp(2.1rem, 7vw, 3.4rem);
  font-weight: 400;
  letter-spacing: 0.01em;
  line-height: 1.1;
  margin-bottom: 0.55rem;
  text-shadow: 0 2px 30px rgba(0, 0, 0, 0.55);
}

/*
 * The German word for what this is, and the only place in the UI it appears: the language has
 * a single noun for "a place you long to be", and the gloss under it is the app's thesis — the
 * sentence every diorama here is trying to say back. `lang="de"` is on the element so a screen
 * reader pronounces it as German rather than mangling it as English.
 *
 * Quiet, and above the tagline rather than instead of it: it is poetry next to the title, not
 * a feature, and someone who does not read German loses nothing but the echo.
 */
.landing-word {
  font-family: var(--font-display);
  font-size: clamp(0.92rem, 2.6vw, 1.08rem);
  letter-spacing: 0.02em;
  color: var(--ink-soft);
  margin-bottom: 0.55rem;
}

.landing-word-gloss {
  display: block;
  margin-top: 0.22rem;
  font-family: var(--font);
  font-size: 0.7rem;
  letter-spacing: 0.13em;
  text-transform: uppercase;
  color: var(--ink-faint);
}

.landing-tagline {
  color: var(--ember);
  font-size: clamp(0.86rem, 2.4vw, 1rem);
  letter-spacing: 0.16em;
  text-transform: uppercase;
  margin-bottom: 1.5rem;
}

.landing-blurb {
  color: var(--ink-soft);
  font-size: 0.98rem;
  line-height: 1.75;
  margin: 0 auto 2rem;
  max-width: 27rem;
}

.landing-actions {
  display: flex;
  gap: 0.75rem;
  justify-content: center;
  flex-wrap: wrap;
}

/*
 * The notice's door, at the foot of the title screen.
 *
 * Pinned to the overlay rather than added to the card, because `.landing` centres its card and does
 * not scroll: one more line inside it would push a short screen into a clip at both ends, and the
 * visitor who most needs the notice is not one who will scroll a title screen. Quiet, second to the
 * two buttons the card exists for, and still a real tap target on a phone (see the coarse rules).
 */
.landing-legal {
  position: absolute;
  left: 0;
  right: 0;
  bottom: calc(1.1rem + var(--safe-b));
  text-align: center;
}

.landing-legal-link {
  font: inherit;
  font-size: 0.74rem;
  letter-spacing: 0.06em;
  padding: 0.35rem 0.55rem;
  color: var(--ink-faint);
  background: none;
  border: 0;
  border-radius: 6px;
  cursor: pointer;
  text-decoration: underline;
  text-decoration-color: var(--hairline);
  text-underline-offset: 0.22em;
  transition: color 0.18s var(--ease), text-decoration-color 0.18s var(--ease);
}
.landing-legal-link:hover { color: var(--ember); text-decoration-color: var(--ember); }
.landing-legal-link:focus-visible {
  outline: 2px solid rgba(233, 184, 114, 0.6);
  outline-offset: 2px;
}

/* ── The MCP bridge, inside the settings popup ────────────────────────── */

/*
 * It used to be a strip pinned to the bottom-left of the viewport. It now lives inside the
 * settings popup behind the world's name, which is where someone looks for it — so it is
 * plain flow with no positioning of its own: a disclosure that positioned itself would
 * escape the card it sits in.
 */
.mcp-more {
  text-align: left;
}

.mcp-summary,
.whats-new-summary {
  cursor: pointer;
  list-style: none;
  padding: 0.2rem 0;
  font-size: 0.7rem;
  font-weight: 600;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--ink-faint);
  transition: color 0.18s var(--ease);
}
.mcp-summary::-webkit-details-marker,
.whats-new-summary::-webkit-details-marker { display: none; }
.mcp-summary:hover,
.whats-new-summary:hover { color: var(--ink); }
.mcp-summary:focus-visible,
.whats-new-summary:focus-visible {
  outline: 2px solid rgba(233, 184, 114, 0.6);
  outline-offset: -2px;
  color: var(--ink);
}
.mcp-summary::after,
.whats-new-summary::after {
  content: '+';
  float: right;
  color: var(--ember);
  font-weight: 400;
}
.mcp-more[open] .mcp-summary::after,
.whats-new[open] .whats-new-summary::after { content: '−'; }

.mcp-body {
  padding: 0.75rem 0 0;
  margin-top: 0.15rem;
  border-top: 1px solid var(--hairline-soft);
}

/*
 * What's new: the same disclosure as the bridge above, holding a list rather than prose. Twelve
 * releases, each a version and a line — six shown, six behind the second disclosure at the foot of
 * the list, and the oldest dropped as a release prepends to it, so it can never grow into a history
 * nobody reads in a popup.
 */
/*
 * A line of its own worth of air above, because it is not more of the paragraph above it. The build
 * number, this disclosure and the copyright notice are three different kinds of small print — a
 * fact, a control, a notice — and stacked at half a line each they read as one grey block, which is
 * how "Version 0.15.0 / What's new / © 2026 Michael Haar" arrived as a single lump.
 */
.whats-new {
  text-align: left;
  margin-top: 1.05rem;
}

.whats-new-body {
  padding: 0.6rem 0 0;
  margin-top: 0.15rem;
  border-top: 1px solid var(--hairline-soft);
}

.whats-new-version {
  margin-top: 0.7rem;
  font-size: 0.72rem;
  font-weight: 600;
  letter-spacing: 0.06em;
  color: var(--ink-faint);
}
.whats-new-version:first-child { margin-top: 0; }

.whats-new-lines {
  margin-top: 0.2rem;
  padding-left: 0.95rem;
  list-style: disc;
  color: var(--ink-soft);
  font-size: 0.78rem;
  line-height: 1.55;
}
.whats-new-lines li { margin-top: 0.15rem; }
.whats-new-lines li::marker { color: var(--hairline); }

/*
 * The older half of the window, one press away: the same disclosure as the popup's others, a size
 * down, because it is a second thought rather than a section. It carries the ember `+`/`−` and no
 * border and no top rule of its own — the list above it simply continues once it is open — and it
 * is a `<details>` rather than script so it still works with JavaScript off.
 */
.whats-new-more {
  margin-top: 0.9rem;
}

.whats-new-more-summary {
  cursor: pointer;
  list-style: none;
  padding: 0.15rem 0;
  font-size: 0.72rem;
  letter-spacing: 0.04em;
  color: var(--ink-faint);
  transition: color 0.18s var(--ease);
}
.whats-new-more-summary::-webkit-details-marker { display: none; }
.whats-new-more-summary:hover { color: var(--ink); }
.whats-new-more-summary:focus-visible {
  outline: 2px solid rgba(233, 184, 114, 0.6);
  outline-offset: -2px;
  color: var(--ink);
}
.whats-new-more-summary::after {
  content: ' +';
  color: var(--ember);
}
.whats-new-more[open] .whats-new-more-summary::after { content: ' −'; }

.whats-new-older { padding-top: 0.55rem; }

.mcp-lead {
  color: var(--ink-soft);
  font-size: 0.82rem;
  line-height: 1.6;
  margin: 0.75rem 0 0.85rem;
}

/*
 * A definition list rather than prose, because half the audience is a program reading the
 * page. Compact two-column on anything wider than a phone, stacked on one.
 */
.mcp-facts {
  display: grid;
  grid-template-columns: minmax(7rem, auto) 1fr;
  gap: 0.4rem 0.85rem;
  font-size: 0.76rem;
  line-height: 1.55;
}

.mcp-facts dt {
  color: var(--ember);
  font-size: 0.63rem;
  font-weight: 600;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  padding-top: 0.18rem;
}

.mcp-facts dd {
  color: var(--ink-soft);
  min-width: 0;
  overflow-wrap: anywhere;
}

.mcp-facts code {
  font-family: ui-monospace, "Cascadia Mono", "SFMono-Regular", Menlo, Consolas, monospace;
  font-size: 0.94em;
  color: var(--ink);
}

/* ── Presentation mode: a received wish ───────────────────────────────── */

/*
 * A wish arrives to be looked at. The editing chrome is removed in `main.js` (the elements
 * are hidden outright), and this is the body-level switch for what is left: the hint bar's
 * gesture advice, which is advice about editing.
 *
 * Orbit stays — looking around is viewing, not editing — so the viewport keeps its
 * `touch-action: none` and OrbitControls keeps its listeners. Only the dragger is switched
 * off, and it is switched off in code as well as here: a mode that exists only in CSS is one
 * a stray pointer event can still walk into.
 */
body.is-presenting .hint { display: none; }

/*
 * "Create your own" takes the corner Send would have had.
 *
 * While presenting, Send is hidden — a received scene is not yours to pass on — so the one action
 * of *this* moment belongs where the one action always is: bottom right, alone. The placement is
 * asserted in tools/phone.mjs, alongside the arithmetic that keeps it beside the icon row.
 */
.present-bar {
  position: fixed;
  right: calc(1.25rem + var(--safe-r));
  bottom: calc(1.15rem + var(--safe-b));
  z-index: 30;
  animation: rise 0.6s var(--ease) both;
}
.present-bar[hidden] { display: none; }

.present-bar .btn {
  font-size: 0.95rem;
  padding: 0.75rem 1.9rem;
  box-shadow: 0 10px 30px var(--ember-glow);
}

@media (max-width: 720px) {
  /*
   * The MCP disclosure, on a phone.
   *
   * One column only: two columns of mono text at 320px is unreadable. Nothing to reposition
   * any more — it lives inside the settings popup, and the popup is already full-width here
   * (see `.settings-card` in the settings block).
   */
  .mcp-facts { grid-template-columns: 1fr; gap: 0.15rem 0; }
  .mcp-facts dd { margin-bottom: 0.55rem; }
  .mcp-facts dt { padding-top: 0.3rem; }

  /* Bottom-right, the corner Send occupies while editing. Only the music icon is left in the
     toolbar while presenting, so the pill fits beside it even at 320px (see tools/phone.mjs). */
  .present-bar { right: calc(0.7rem + var(--safe-r)); bottom: calc(0.7rem + var(--safe-b)); }
  .present-bar .btn { width: auto; }
}

/* ── Buttons ──────────────────────────────────────────────────────────── */

.btn {
  font: inherit;
  font-size: 0.94rem;
  border-radius: 999px;
  padding: 0.72rem 1.7rem;
  cursor: pointer;
  border: 1px solid transparent;
  transition: transform 0.18s var(--ease), background 0.18s var(--ease),
              border-color 0.18s var(--ease), box-shadow 0.18s var(--ease);
}
.btn:active:not(:disabled) { transform: translateY(1px) scale(0.99); }

/*
 * A button that is off rather than merely unclickable — the two doors to the clipboard loop while an
 * agent drives the tab (`agentMode` in `src/main.js`). It keeps its shape and its place, and dims the
 * way the toolbar's icons dim, so "not now" is visible without the control moving or vanishing. The
 * hover and active states are guarded above rather than overridden here: a button that lights up under
 * the pointer and does nothing is the one thing a disabled control must not do.
 */
.btn:disabled { opacity: 0.42; cursor: default; }

.btn-primary {
  background: linear-gradient(150deg, var(--ember) 0%, var(--ember-deep) 100%);
  color: #241703;
  font-weight: 600;
  box-shadow: 0 6px 24px var(--ember-glow);
}
.btn-primary:hover:not(:disabled) { box-shadow: 0 8px 30px rgba(233, 184, 114, 0.45); transform: translateY(-1px); }

.btn-ghost {
  background: rgba(255, 255, 255, 0.05);
  color: var(--ink-soft);
  border-color: var(--hairline);
}
.btn-ghost:hover:not(:disabled) { background: rgba(255, 255, 255, 0.11); color: var(--ink); }

/* ── HUD ──────────────────────────────────────────────────────────────── */

.hud {
  position: fixed;
  top: 0; left: 0; right: 0;
  display: flex;
  justify-content: space-between;
  align-items: flex-start;
  gap: 1rem;
  padding: calc(1.15rem + var(--safe-t)) calc(1.25rem + var(--safe-r)) 1.15rem calc(1.25rem + var(--safe-l));
  z-index: 20;
  pointer-events: none;
  animation: fade-in 0.8s var(--ease) both;
}
.hud[hidden] { display: none; }
.hud > * { pointer-events: auto; }

.hud-scene {
  background: var(--glass);
  backdrop-filter: blur(14px);
  -webkit-backdrop-filter: blur(14px);
  border: 1px solid var(--hairline);
  border-radius: var(--radius);
  padding: 0.8rem 1.05rem;
  box-shadow: 0 10px 34px rgba(0, 0, 0, 0.42);
  max-width: 60vw;
}

/*
 * The world's name is a button: it opens the settings popup. It keeps the display face and
 * the size it had as a caption — the only additions are a quiet gear and a hover state, so
 * nothing moves and it still reads as the name of the place rather than as a control.
 */
.hud-title {
  font-family: var(--font-display);
  font-size: 1.06rem;
  font-weight: 400;
  letter-spacing: 0.01em;
  white-space: nowrap;
}

.hud-title-btn {
  font: inherit;
  color: inherit;
  display: inline-flex;
  align-items: center;
  gap: 0.42rem;
  /*
   * Padding for the fingertip, negative margin so the title does not move: the name is a
   * control now, and an 18px line of text is not a tap target. What is left is a 34px-tall
   * button sitting exactly where the caption sat before (asserted in tools/phone.mjs).
   */
  padding: 0.35rem 0.3rem;
  margin: -0.35rem -0.3rem;
  background: transparent;
  border: 0;
  cursor: pointer;
  border-radius: 6px;
}

.hud-title-mark {
  font-size: 0.86em;
  color: var(--ink-faint);
  transition: color 0.18s var(--ease), transform 0.22s var(--ease);
}
.hud-title-btn:hover .hud-title-mark { color: var(--ember); }
.hud-title-btn[aria-expanded="true"] .hud-title-mark {
  color: var(--ember);
  transform: rotate(35deg);
}
.hud-title-btn:focus-visible {
  outline: 2px solid rgba(233, 184, 114, 0.6);
  outline-offset: 3px;
}
.hud-subtitle {
  color: var(--ember);
  font-size: 0.76rem;
  letter-spacing: 0.07em;
  text-transform: uppercase;
  margin-top: 0.2rem;
}
/* A place that brought its own stage names no world, and an empty line would still take a line and a
   margin. Stated here rather than left to the UA stylesheet, which a later `display` would override. */
.hud-subtitle[hidden] { display: none; }

.hud-controls {
  display: flex;
  flex-direction: column;
  gap: 0.6rem;
  align-items: flex-end;
}
/* Likewise for the postcard row, which a place with no looks of its own does not have: the label goes
   with it, or an empty heading sits over nothing. A class rather than `hidden`, which presentation
   mode owns — a received wish takes the whole editing row away, and this is the narrower fact that
   there is nothing in it to switch between. */
.hud-controls[hidden] { display: none; }

/*
 * A place that brought one look and no choice has no row to show, so the row goes — heading and all.
 * (The fullscreen control is *not* in here: it lives in every state, so it cannot live inside a box
 * that three of them take away.)
 */
.hud-controls.is-empty { display: none; }

/* ── Fullscreen ───────────────────────────────────────────────────────── */

/*
 * Fullscreen: a bare glyph over the top-right corner.
 *
 * No background and no border, on purpose. The control is in every mode, and in three of them the box
 * it would otherwise sit in — the postcard row — is not on screen at all, so a plate around it would
 * read as a piece of that row that failed to leave. What is left is the glyph, which the stylesheet
 * only ever recolours: faint at rest, ink on hover, ember when the screen is already filled.
 *
 * `position: fixed` rather than a row in the HUD, so it is the corner of the *screen* in every mode.
 * It is a child of `#hud`, which is what makes it appear with the HUD and stay off the landing screen.
 */
.screen-btn {
  font: inherit;
  position: fixed;
  top: calc(1.15rem + var(--safe-t));
  right: calc(1.25rem + var(--safe-r));
  z-index: 21;
  width: 2.1rem;
  height: 2.1rem;
  display: grid;
  place-items: center;
  color: var(--ink-faint);
  background: none;
  border: 0;
  border-radius: var(--radius-sm);
  cursor: pointer;
  transition: color 0.18s var(--ease), background 0.18s var(--ease);
}
.screen-btn[hidden] { display: none; }
.screen-btn:hover { color: var(--ink); background: rgba(255, 255, 255, 0.08); }
.screen-btn:focus-visible { outline: 2px solid rgba(233, 184, 114, 0.6); outline-offset: 2px; }
.screen-btn.is-active { color: var(--ember); background: rgba(233, 184, 114, 0.14); }

.screen-glyph {
  width: 1.05rem;
  height: 1.05rem;
  fill: none;
  stroke: currentColor;
  stroke-width: 1.7;
  stroke-linecap: round;
  stroke-linejoin: round;
}
/* Two glyphs in one button: four brackets opening outwards, and the same four turned inwards. */
.screen-in { display: none; }
.screen-btn.is-active .screen-out { display: none; }
.screen-btn.is-active .screen-in { display: block; }

/*
 * Immersive: fullscreen, with the *editing* chrome gone.
 *
 * A class rather than presentation mode, and that distinction is load-bearing — presentation empties
 * the undo history, which would be a strange thing for a fullscreen button to do to an editor. So this
 * hides only what is drawn: the toolbar, Send, the gesture hint and the postcard row. The dragger, the
 * history and the keyboard shortcuts are untouched, the button above stays to lead back out, and the
 * browser's own Escape does the same thing.
 */
body.is-immersive .toolbar,
body.is-immersive .send,
body.is-immersive .hint,
body.is-immersive .hud-controls { display: none; }

.panel {
  background: var(--glass);
  backdrop-filter: blur(14px);
  -webkit-backdrop-filter: blur(14px);
  border: 1px solid var(--hairline);
  border-radius: var(--radius);
  padding: 0.55rem 0.7rem;
  box-shadow: 0 10px 34px rgba(0, 0, 0, 0.42);
  max-width: min(22rem, 72vw);
}

.panel-label {
  display: block;
  font-size: 0.6rem;
  font-weight: 600;
  letter-spacing: 0.13em;
  text-transform: uppercase;
  color: var(--ink-faint);
  margin-bottom: 0.36rem;
}

/*
 * `hidden` has to win.
 *
 * A `.btn` carries `display: inline-flex`, and a class rule beats the user agent's
 * `[hidden] { display: none }` — so every button the app hides *by attribute* stayed on screen. In
 * presentation mode that meant a received idyll showed the whole editing toolbar, the ⟲ menu ("Somewhere
 * else, or start over") among it, and the trim and postcard buttons appeared before they had anything
 * to do. Reported from a link, by the author, twice — the second time as "all the edit buttons should
 * not be visible in View mode".
 */
[hidden] { display: none !important; }

.chip-group { display: flex; gap: 0.3rem; flex-wrap: wrap; }


/*
 * Deliberately small. These are a row of five labels over a picture, not a settings
 * dialog: on a phone the whole HUD has to stay out of the diorama's way.
 */
.chip {
  font: inherit;
  font-size: 0.69rem;
  line-height: 1.2;
  padding: 0.24rem 0.55rem;
  border-radius: 999px;
  cursor: pointer;
  color: var(--ink-soft);
  background: rgba(255, 255, 255, 0.06);
  border: 1px solid var(--hairline-soft);
  transition: all 0.18s var(--ease);
  white-space: nowrap;
}
.chip:hover { background: rgba(255, 255, 255, 0.14); color: var(--ink); }
.chip.is-active {
  background: linear-gradient(150deg, var(--ember) 0%, var(--ember-deep) 100%);
  border-color: rgba(255, 226, 178, 0.55);
  color: #241703;
  font-weight: 600;
  box-shadow: 0 0 14px var(--ember-glow);
}

.select {
  font: inherit;
  font-size: 0.82rem;
  color: var(--ink);
  background: rgba(255, 255, 255, 0.07);
  border: 1px solid var(--hairline);
  border-radius: var(--radius-sm);
  padding: 0.45rem 0.6rem;
  width: 100%;
  cursor: pointer;
}
.select option { background: #101a26; color: var(--ink); }

/* ── Hint bar ─────────────────────────────────────────────────────────── */

.hint {
  position: fixed;
  bottom: calc(1.15rem + var(--safe-b));
  left: 50%;
  transform: translateX(-50%);
  z-index: 20;
  display: flex;
  gap: 0.6rem;
  align-items: center;
  background: var(--glass);
  backdrop-filter: blur(14px);
  -webkit-backdrop-filter: blur(14px);
  border: 1px solid var(--hairline);
  border-radius: 999px;
  padding: 0.55rem 1.15rem;
  font-size: 0.79rem;
  color: var(--ink-soft);
  box-shadow: 0 8px 26px rgba(0, 0, 0, 0.4);
  pointer-events: none;
  animation: fade-in 1s var(--ease) both;
  animation-delay: 0.4s;
  max-width: calc(100vw - 2rem);
}
.hint[hidden] { display: none; }
.hint-sep { color: var(--ink-faint); }
.hint.is-dimmed { opacity: 0.32; transition: opacity 1.4s var(--ease); }

.hint kbd {
  font: inherit;
  font-size: 0.72em;
  padding: 0.1em 0.4em;
  border-radius: 4px;
  background: rgba(255, 255, 255, 0.12);
  border: 1px solid var(--hairline);
  color: var(--ink);
}

/* Turning rather than moving — a distinct cursor makes the mode legible. */
body.is-rotating #viewport { cursor: crosshair; }

/* ── A received wish ──────────────────────────────────────────────────── */

.wish-card {
  position: fixed;
  inset: 0;
  z-index: 52;
  display: grid;
  place-items: center;
  padding: 1.25rem;
  background: radial-gradient(120% 90% at 50% 40%, rgba(12, 20, 30, 0.4) 0%, rgba(5, 8, 13, 0.86) 78%);
  backdrop-filter: blur(3px);
  -webkit-backdrop-filter: blur(3px);
  animation: fade-in 0.7s var(--ease) both;
}
.wish-card[hidden] { display: none; }
.wish-card.is-leaving { animation: fade-out 0.5s var(--ease) both; }

.wish-card-inner {
  text-align: center;
  max-width: 30rem;
  padding: 2.2rem 1.8rem;
  animation: rise 0.9s var(--ease) both;
  animation-delay: 0.25s;
}

.wish-card-kicker {
  font-size: 0.68rem;
  font-weight: 600;
  letter-spacing: 0.18em;
  text-transform: uppercase;
  color: var(--ink-faint);
  margin-bottom: 0.9rem;
}

.wish-card-title {
  font-family: var(--font-display);
  font-size: clamp(1.7rem, 5.5vw, 2.5rem);
  font-weight: 400;
  line-height: 1.15;
  margin-bottom: 0.7rem;
  text-shadow: 0 2px 28px rgba(0, 0, 0, 0.6);
}

.wish-card-note {
  color: var(--ember);
  font-size: 1rem;
  font-style: italic;
  line-height: 1.6;
  margin-bottom: 0.5rem;
  max-width: 24rem;
  margin-inline: auto;
}
.wish-card-note[hidden] { display: none; }

.wish-card-actions {
  display: flex;
  gap: 0.7rem;
  justify-content: center;
  flex-wrap: wrap;
  margin-top: 1.8rem;
}

/* ── Toolbar ──────────────────────────────────────────────────────────── */

.toolbar {
  position: fixed;
  left: calc(1.25rem + var(--safe-l));
  bottom: calc(1.15rem + var(--safe-b));
  z-index: 20;
  display: flex;
  flex-direction: column;
  /*
   * One gap for everything in this corner. The AI pill is the column's last child, so the space
   * between it and the icon row above it is this number, not an offset computed from the pill's
   * height: that arithmetic was wrong by most of the gap, because a line of text is taller than
   * its font-size, and no arithmetic survives a longer label or a different font.
   */
  gap: 0.4rem;
}

/* The icons themselves: a column here, a row on a phone (see the phone sheet). It is also the
   box the ⟲ menu is positioned against, so it is `relative` even though nothing else needs it —
   and `flex-start`, because a stretched item is as wide as the AI pill below it, which would open
   the menu a pill's width away from the button that summoned it. */
.toolbar-icons {
  position: relative;
  align-self: flex-start;
  display: flex;
  flex-direction: column;
  gap: 0.4rem;
}
.toolbar[hidden] { display: none; }

.icon-btn {
  font: inherit;
  font-size: 1rem;
  width: 2.35rem;
  height: 2.35rem;
  display: grid;
  place-items: center;
  color: var(--ink-soft);
  background: var(--glass);
  backdrop-filter: blur(14px);
  -webkit-backdrop-filter: blur(14px);
  border: 1px solid var(--hairline);
  border-radius: 50%;
  cursor: pointer;
  transition: all 0.18s var(--ease);
  box-shadow: 0 6px 20px rgba(0, 0, 0, 0.35);
}
.icon-btn:hover:not(:disabled) { color: var(--ink); background: var(--glass-strong); }
.icon-btn:disabled { opacity: 0.28; cursor: default; }

/* A sticky mode has to look stuck: ↻ on means the next drag turns the prop. */
.icon-btn.is-active {
  color: #241703;
  background: linear-gradient(150deg, var(--ember) 0%, var(--ember-deep) 100%);
  border-color: rgba(255, 226, 178, 0.6);
  box-shadow: 0 0 16px var(--ember-glow);
}

/* ── Send, on its own in the bottom-right ─────────────────────────────── */

/*
 * The most prominent control after the diorama itself, and deliberately a labelled pill
 * rather than another circle: it is the one action the app exists for. Pinned by `right` and
 * `bottom` so it owns the corner outright, with the toasts stacked above it.
 */
.send {
  position: fixed;
  right: calc(1.25rem + var(--safe-r));
  bottom: calc(1.15rem + var(--safe-b));
  z-index: 22;
  animation: fade-in 0.5s var(--ease) both;
}
.send[hidden] { display: none; }

.send-btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 0.5rem;
  padding: 0.7rem 1.3rem 0.7rem 1.1rem;
  font-size: 0.95rem;
  /* One line, always: the pill shares the bottom edge with the icon row on a narrow phone. */
  white-space: nowrap;
  box-shadow: 0 10px 30px var(--ember-glow);
}

/*
 * The share mark: arrow out of a box. One glyph for both buttons that send — the standalone Send
 * pill and the sheet button inside the panel — because two different marks for the same action on
 * the same screen is how a UI starts disagreeing with itself.
 */
.share-glyph {
  width: 1.05rem;
  height: 1.05rem;
  fill: none;
  stroke: currentColor;
  stroke-width: 1.6;
  stroke-linecap: round;
  stroke-linejoin: round;
}

/* The word never breaks: the pill shares the bottom edge with the icon row on a narrow phone. */
.send-label { white-space: nowrap; }

/* ── Ask your AI, at the foot of the editing tools ────────────────────── */

/*
 * The second door, and deliberately the quiet one.
 *
 * Send owns the bottom-right corner and the ember pill, because sending a place is what the app
 * is for. This is the last child of the bottom-left icon column — an editing tool among the
 * editing tools — and wears the ghost style, so the one action everyone takes keeps looking like
 * the one action worth taking. It used to be a disclosure inside the Send panel, which put a form
 * for a chatbot between someone and their link.
 *
 * It carries no positioning of its own: the column's `gap` is what stands it off the ⟲ above it,
 * which is the only way the spacing can be the same as the icons' by construction rather than by
 * arithmetic.
 */
.ai-launcher { display: flex; }

/* Presenting has no chat loop: asking an assistant to rewrite someone else's idyll is editing it.
   Hiding the whole item, not just the button, so the column closes up to the corner and stays
   level with "Create your own" on the other side. */
body.is-presenting .ai-launcher { display: none; }

.ai-open {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 0.45rem;
  padding: 0.6rem 1.1rem 0.6rem 0.95rem;
  font-size: 0.88rem;
  /*
   * A comfortable tap target on its own, and the pill's line on a phone: the label is one line, so
   * this is the height the wrap below produces.
   */
  min-height: 2.25rem;
  box-shadow: 0 8px 24px rgba(0, 0, 0, 0.35);
}
/* An author `display` beats the `hidden` attribute, so the state needs a rule of its own —
   this is the button presentation mode hides, alongside `.ai-launcher`. */
.ai-open[hidden] { display: none; }

/* A solid mark rather than the stroked share glyph: a sparkle is a shape, not a line drawing. */
.ai-glyph { width: 1rem; height: 1rem; fill: currentColor; }

.ai-label { white-space: nowrap; }

/* ── Settings, behind the world's name ────────────────────────────────── */

/*
 * A transparent full-screen layer, with the card anchored under the title in the top-left:
 * the popup grows out of the element that opened it, so there is never a question about
 * where it came from or where it went.
 *
 * The layer is full-screen for one reason — a press anywhere in it counts as a press outside
 * the card, which is how the popup closes (the listener is in hud.js). It does not block the
 * diorama: `pointer-events: none` on the layer and `auto` on the card, so a prop can still be
 * dragged while the popup is open. That is what non-modal means here.
 */
.settings {
  position: fixed;
  inset: 0;
  z-index: 24;
  pointer-events: none;
}
.settings[hidden] { display: none; }

.settings-card {
  position: absolute;
  top: 5.9rem;
  left: 1.25rem;
  pointer-events: auto;
  width: min(26rem, calc(100vw - 2.5rem));
  max-height: min(74vh, 40rem);
  overflow-y: auto;
  padding: 1.35rem 1.35rem 1.2rem;
  text-align: left;
  background: var(--glass);
  backdrop-filter: blur(16px);
  -webkit-backdrop-filter: blur(16px);
  border: 1px solid var(--hairline);
  border-radius: var(--radius);
  box-shadow: 0 22px 60px rgba(0, 0, 0, 0.55);
  animation: rise 0.32s var(--ease) both;
}
/*
 * It is focused when it opens, so a keyboard starts inside the popup. A ring around the whole
 * card would be noise: the heading is what the eye lands on anyway.
 */
.settings-card:focus { outline: none; }

/*
 * The same popup, opened from the title screen's own door (see `.landing-legal`).
 *
 * Two things change, and both are about where it was opened from. It has to sit *above* the title
 * screen — `.landing` is z-index 50, and a notice that opens behind the screen that summoned it is
 * a notice nobody reads — while staying under `.boot` (60), the one thing that may cover the whole
 * page. And the sound row goes: before Begin there is no world, so there is no audio to shape, and
 * a slider for it would be a control for something that does not exist yet.
 *
 * **The changelog stays.** It was hidden here once, on the argument that a changelog is not a
 * notice — which made one popup into two different documents depending on the door, and was
 * reported as a missing section rather than read as a decision. Someone who has not pressed Begin
 * is exactly the person asking what changed since last time, and the sound row is the only thing
 * that genuinely has to go. Do not re-add the rule.
 *
 * With the sound row gone the *language* group is what the popup starts with — it sits directly under
 * the sound row now — so it takes the first-group treatment `.settings-section:first-of-type` gives to
 * the sound row: no hairline above the first thing in the card. About follows it and keeps its own
 * hairline, which is the separation the two groups should have.
 */
.settings.is-legal { z-index: 55; }
.settings.is-legal #settings-sound { display: none; }
.settings.is-legal #settings-lang {
  margin-top: 0.15rem;
  padding-top: 0;
  border-top: 0;
}

/*
 * And it opens where its own door is. The card's usual corner is the one the ⚙ button lives in, so
 * the popup lands beside the control that summoned it; the title screen's door is the About link at
 * the foot of the screen, centred, and a notice that opens in the far top corner from the link that
 * asked for it reads as some other box arriving by accident.
 *
 * So it is anchored to the bottom and centred, clearing the link *and* its coarse-pointer hit area:
 * the link sits 1.1rem off the bottom, is about 1.8rem tall, and grows a further 0.55rem of
 * invisible tap area above that — 3.8rem leaves it room rather than a hair.
 *
 * Centred with the insets and their margins rather than `translateX(-50%)`: `rise` animates
 * `transform`, and the card's `both` fill-mode would put the offset back to `none` the moment the
 * animation finished — the card would jump half its width to the right a third of a second after
 * opening.
 */
.settings.is-legal .settings-card {
  top: auto;
  bottom: 3.8rem;
  left: 0;
  right: 0;
  margin-left: auto;
  margin-right: auto;
}

.settings-section {
  margin-top: 1.2rem;
  padding-top: 1.05rem;
  border-top: 1px solid var(--hairline-soft);
}
/*
 * The bridge section ships hidden and is revealed only on a local origin (see
 * `shouldShowBridgeDocs` in src/bridge/mcp/local.js), so the deployed page never tells a phone
 * to run npm. Stated here rather than relied on from the UA stylesheet, because a later
 * `display` on this class would silently switch the hiding off.
 */
.settings-section[hidden] { display: none; }
/*
 * The first group has nothing above it to be separated from, and the top-right corner belongs
 * to the × button. The popup carries no title of its own: it is a list of things about the
 * world you are already looking at, and repeating the app's name and tagline at the top of it
 * was saying the same thing twice. Its accessible name is an `aria-label` on the card.
 */
.settings-section:first-of-type {
  margin-top: 0.15rem;
  padding-top: 0;
  border-top: 0;
}

/*
 * **No rule between Sound and Language.** They are the two controls a visitor reaches for first — is
 * there sound, and in which language — and the author asked for the line between them to go, so the
 * pair reads as one block of controls rather than as two sections that happen to sit next to each
 * other. Only the hairline is gone: the gap above the language group is unchanged, and About and the
 * guide pointer below it keep theirs, which is what separates the controls from the prose.
 *
 * The legal form strips this rule's padding as well, because there the sound row is *hidden* and the
 * language group is genuinely first — see `.settings.is-legal #settings-lang`. The two rules are
 * deliberately about different things and neither is redundant.
 */
#settings-lang { border-top: 0; }

.settings-subheading {
  font-size: 0.62rem;
  font-weight: 600;
  letter-spacing: 0.13em;
  text-transform: uppercase;
  color: var(--ink-faint);
  margin-bottom: 0.6rem;
}

.settings-note {
  color: var(--ink-soft);
  font-size: 0.8rem;
  line-height: 1.65;
}
.settings-note[hidden] { display: none; }
.settings-note code {
  font-family: ui-monospace, "Cascadia Mono", "SFMono-Regular", Menlo, Consolas, monospace;
  font-size: 0.94em;
  color: var(--ink);
}

#volume-note { margin-top: 0.5rem; }

/*
 * The pointer to the guide, at the foot of the popup: a note rather than a group of its own, because it is
 * one sentence — but it is *separated* like a group. The privacy paragraph above it is the end of About,
 * and with only a margin between them the pointer read as one more sentence of that notice rather than as
 * something to press: an instruction to follow, sitting in the middle of a description of what the app
 * does with your data. So it borrows the popup's own group rule — the same hairline, the same air on
 * either side — which is what the author asked for. The bridge's section below it is `hidden` on the
 * deployed site, which makes this the last thing in the popup for most visitors.
 */
.settings-guide {
  margin-top: 1.2rem;
  padding-top: 1.05rem;
  border-top: 1px solid var(--hairline-soft);
}

/*
 * The build number, above the copyright notice. Quieter than the notice it sits over — it is a
 * fact to quote, not something to read on the way past — but not so quiet that it cannot be found.
 */
.settings-version {
  margin-top: 0.7rem;
  font-size: 0.72rem;
  letter-spacing: 0.06em;
  color: var(--ink-faint);
  opacity: 0.75;
}

/*
 * The language row, in its own group under Sound: the same `chip-group` the postcard looks use — one row
 * that wraps, so five languages cost two lines on a phone and one everywhere else, with no new control
 * to lay out. Its heading is the popup's ordinary subheading, like Sound and About above and below it.
 *
 * Each language is named in its own language: someone who cannot read the one on screen has to be able
 * to find theirs, and "German" is no help to a German speaker who is looking at Chinese.
 */

/*
 * A language chip: the flag, then the language's own name. The chip is a flex row so the two sit on one
 * baseline, and the flag is a fixed small box — `flex: 0 0 auto` so a long name (中文（简体）) cannot
 * squeeze it — with an inset hairline rather than a border, because France's and Spain's white bands
 * would otherwise dissolve into the glass. The SVG keeps its own aspect ratio, so the Union Jack stays
 * square while the horizontal flags are wider boxes: `height` is the constant here.
 *
 * The flag is measured in `em`, not `rem`, so it is a *character* in the label's own type: a large one,
 * at the height of a capital, sitting in the same line as the name and inside the same button. And
 * `inline-block` is load-bearing rather than tidy — an inline span cannot be sized, so its width and
 * height are ignored, and the SVG inside it (which is `width: 100%`) then stretches to the button and
 * wraps onto its own line above the name. That is exactly what 0.19.2 shipped, because this chip rule
 * was written as `.lang-choices` while the markup carries `id="lang-choices"`: the class selector
 * matched nothing, so the chip never became a flex row. `tools/phone.mjs` now holds the two together.
 */
#lang-choices .chip {
  display: inline-flex;
  align-items: center;
  gap: 0.4rem;
}

.lang-flag {
  display: inline-block;
  flex: 0 0 auto;
  width: 1.05em;
  height: 0.78em;
  vertical-align: -0.06em;
  border-radius: 2px;
  overflow: hidden;
  box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.3);
}

.lang-flag svg { display: block; width: 100%; height: 100%; }

/*
 * CJK has no spaces, so a line may break between any two characters — which turns a two-word button
 * into "带我去" / "别处" in the middle of a pill. `keep-all` holds a *label* together and lets the
 * pill grow instead, while `line-break: strict` keeps the punctuation rules right in the paragraphs
 * that still wrap. No font is bundled: the stack falls through to whatever CJK face the device has,
 * which is the only honest option for a static site with no webfonts.
 */
:lang(zh) { line-break: strict; }
:lang(zh) .btn,
:lang(zh) .chip,
:lang(zh) .ai-label,
:lang(zh) .panel-label,
:lang(zh) .settings-subheading { word-break: keep-all; }

/*
 * The copyright line, under the build number: the same quiet register, because it is a fact to be
 * able to find rather than a thing to read on the way past. It carries the author's name itself
 * now — it used to be a second "Author:" line above it, naming the same person twice.
 */
.settings-copyright {
  margin-top: 0.95rem;
  font-size: 0.72rem;
  letter-spacing: 0.06em;
  color: var(--ink-faint);
}

/*
 * The author's name is a mailto link — the one place in the app where someone might want to say
 * something back, and it should not mean copying an address out of a canvas or a toast. In the
 * copyright notice rather than a line of its own, so the notice and the way to reply are one line.
 */
.settings-mail {
  color: var(--ink-soft);
  text-decoration: underline;
  text-decoration-color: var(--hairline);
  text-underline-offset: 0.2em;
  transition: color 0.18s var(--ease), text-decoration-color 0.18s var(--ease);
}
.settings-mail:hover { color: var(--ember); text-decoration-color: var(--ember); }
.settings-mail:focus-visible {
  outline: 2px solid rgba(233, 184, 114, 0.6);
  outline-offset: 2px;
  border-radius: 4px;
}

/* Phone: the full width under the title, stopping before the bottom of the screen. */
@media (max-width: 720px) {
  .settings-card {
    top: 5.4rem;
    left: 0.75rem;
    right: 0.75rem;
    width: auto;
    max-height: min(68vh, 32rem);
    padding: 1.2rem 1.1rem 1.05rem;
  }

  /* The legal popup keeps its own door: the same bottom-centre anchoring, with the phone's insets,
     and it climbs from the link rather than growing down from the title. */
  .settings.is-legal .settings-card {
    top: auto;
    bottom: 3.8rem;
    left: 0.75rem;
    right: 0.75rem;
    width: auto;
  }
}

/* ── Volume, in that popup ────────────────────────────────────────────── */

/*
 * It used to be a pill pinned to the bottom-left of the screen. Same row, and no chrome of
 * its own now: the popup is the panel, and a panel inside a panel is one too many.
 */
.volume {
  display: flex;
  align-items: center;
  gap: 0.6rem;
}
.volume[hidden] { display: none; }

.volume-label {
  font-size: 0.63rem;
  font-weight: 600;
  letter-spacing: 0.13em;
  text-transform: uppercase;
  color: var(--ink-faint);
}

/*
 * The mute toggle, at the head of the volume row. It was a ♪ in the toolbar; the popup's quieter
 * register is the right one for it here — no plate of its own, just the glyph and a ring on
 * hover or focus, so the row still reads as "one sound setting" rather than a control and a half.
 */
.volume-mute {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 1.7rem;
  height: 1.7rem;
  padding: 0;
  border: 0;
  border-radius: 50%;
  background: none;
  color: var(--ink-soft);
  cursor: pointer;
  transition: color 0.18s var(--ease), background 0.18s var(--ease);
}
.volume-mute:hover { color: var(--ink); background: var(--glass-strong); }
.volume-mute:focus-visible {
  outline: 2px solid rgba(233, 184, 114, 0.6);
  outline-offset: 2px;
}

.volume-glyph {
  width: 1.05rem;
  height: 1.05rem;
  display: block;
  fill: none;
  stroke: currentColor;
  stroke-width: 1.5;
  stroke-linecap: round;
  stroke-linejoin: round;
}

/*
 * Muted is the slash, drawn across the glyph: the cone and the wave stay either way, so the button
 * is still recognisably the sound control when there is no sound. `.is-off` is the state class
 * main.js sets — the same one the toolbar's ♪ used, since the name is about the state and not
 * about which button carries it.
 */
.volume-mute .volume-slash { display: none; }
.volume-mute.is-off { color: var(--ink-faint); }
.volume-mute.is-off .volume-slash { display: block; }

.volume-slider {
  -webkit-appearance: none;
  appearance: none;
  flex: 1;
  min-width: 6rem;
  max-width: 13rem;
  height: 3px;
  border-radius: 2px;
  background: rgba(255, 255, 255, 0.18);
  outline: none;
  cursor: pointer;
}
.volume-slider:disabled { opacity: 0.4; cursor: default; }
.volume-slider::-webkit-slider-thumb {
  -webkit-appearance: none;
  appearance: none;
  width: 13px;
  height: 13px;
  border-radius: 50%;
  background: var(--ember);
  border: 0;
  cursor: pointer;
  box-shadow: 0 0 8px var(--ember-glow);
}
.volume-slider::-moz-range-thumb {
  width: 13px;
  height: 13px;
  border: 0;
  border-radius: 50%;
  background: var(--ember);
  cursor: pointer;
}

/* ── A draft that no longer matches the world ─────────────────────────── */

/*
 * Sits below the HUD rather than over it: the scene title names the world the notice is
 * talking about, so covering it would hide half the explanation. Deliberately a card and not
 * a toast — it offers a choice, and a choice that vanishes on a timer is not a choice.
 */
.draft-notice {
  position: fixed;
  top: 6.2rem;
  left: 50%;
  transform: translateX(-50%);
  z-index: 45;
  width: min(30rem, calc(100vw - 1.6rem));
  max-height: calc(100vh - 14rem);
  overflow-y: auto;
  padding: 1rem 1.1rem 0.85rem;
  text-align: left;
  background: var(--glass-strong);
  backdrop-filter: blur(16px);
  -webkit-backdrop-filter: blur(16px);
  border: 1px solid var(--hairline);
  border-radius: var(--radius);
  box-shadow: 0 18px 48px rgba(0, 0, 0, 0.55);
  animation: rise 0.42s var(--ease) both;
}
.draft-notice[hidden] { display: none; }
.draft-notice.is-leaving { animation: fade-out 0.3s var(--ease) both; }

.draft-notice-heading {
  font-family: var(--font-display);
  font-size: 1.1rem;
  font-weight: 400;
  line-height: 1.25;
  margin-bottom: 0.4rem;
}

.draft-notice-text {
  color: var(--ink-soft);
  font-size: 0.86rem;
  line-height: 1.6;
  margin-bottom: 0.9rem;
}

.draft-notice-actions {
  display: flex;
  gap: 0.55rem;
  flex-wrap: wrap;
}
.draft-notice-actions .btn {
  font-size: 0.88rem;
  padding: 0.6rem 1.2rem;
}

.draft-notice-note {
  margin-top: 0.7rem;
  font-size: 0.74rem;
  color: var(--ink-faint);
}

/* ── The ⟲ menu ───────────────────────────────────────────────────────── */

/*
 * Two actions behind one icon: "A new wish" and "Reset". Styled as a small card of the
 * same glass as everything else, at the same type scale as a .chip — it is a menu of the
 * toolbar's own actions, not a dialog.
 *
 * On desktop the toolbar is a column at the bottom-left, so the menu opens to the right of
 * it, bottom-aligned with the button it came from. On a phone the toolbar is a row across
 * the bottom (see the media query below), where to the right would run off-screen; there
 * it opens *above* the icon instead. Full-width rows on a phone, because 2.4rem is the
 * smallest thing a thumb reliably hits.
 *
 * Anchored to `.toolbar-icons` rather than to the column, which is as wide as the AI pill at
 * its foot — see `src/ui/hud.js`, which appends it there.
 */
.menu {
  position: absolute;
  left: calc(100% + 0.5rem);
  bottom: 0;
  z-index: 22;
  min-width: 11.5rem;
  max-width: min(15rem, calc(100vw - 4rem));
  display: flex;
  flex-direction: column;
  padding: 0.28rem;
  background: var(--glass-strong);
  backdrop-filter: blur(14px);
  -webkit-backdrop-filter: blur(14px);
  border: 1px solid var(--hairline);
  border-radius: var(--radius-sm);
  box-shadow: 0 14px 38px rgba(0, 0, 0, 0.5);
  animation: fade-in 0.16s var(--ease) both;
}
.menu[hidden] { display: none; }

.menu-item {
  font: inherit;
  font-size: 0.72rem;
  line-height: 1.2;
  letter-spacing: 0.01em;
  min-height: 2.4rem;
  display: flex;
  align-items: center;
  text-align: left;
  padding: 0.5rem 0.7rem;
  border: 0;
  border-radius: 7px;
  color: var(--ink-soft);
  background: transparent;
  cursor: pointer;
  white-space: nowrap;
  transition: background 0.16s var(--ease), color 0.16s var(--ease);
}
.menu-item:hover,
.menu-item:focus-visible { background: rgba(255, 255, 255, 0.12); color: var(--ink); }
.menu-item:active { background: rgba(255, 255, 255, 0.18); }

/* ── Sharing ──────────────────────────────────────────────────────────── */

.share {
  position: fixed;
  inset: 0;
  z-index: 54;
  display: grid;
  place-items: center;
  padding: 1.25rem;
  background: radial-gradient(120% 90% at 50% 35%, rgba(10, 18, 28, 0.55) 0%, rgba(6, 10, 16, 0.9) 75%);
  backdrop-filter: blur(4px);
  -webkit-backdrop-filter: blur(4px);
  animation: fade-in 0.32s var(--ease) both;
}
.share[hidden] { display: none; }
.share.is-leaving { animation: fade-out 0.24s var(--ease) both; }

.share-panel {
  position: relative;
  width: min(32rem, 100%);
  /*
   * Capped and scrollable. A share link is the whole scene in the address bar, and the box it
   * lands in wraps it over several lines — without this, a rich scene pushes "Copy" and "Save
   * postcard" off the bottom of a short screen, where they cannot be reached at all.
   */
  max-height: calc(100vh - 2rem);
  overflow-y: auto;
  background: var(--glass-strong);
  backdrop-filter: blur(18px);
  -webkit-backdrop-filter: blur(18px);
  border: 1px solid var(--hairline);
  border-radius: 20px;
  padding: 1.9rem 1.9rem 1.5rem;
  box-shadow: 0 26px 70px rgba(0, 0, 0, 0.6);
  animation: rise 0.4s var(--ease) both;
  animation-delay: 0.04s;
}

.share-title {
  font-family: var(--font-display);
  font-size: 1.45rem;
  font-weight: 400;
  margin-bottom: 0.4rem;
}

.share-blurb {
  color: var(--ink-soft);
  font-size: 0.88rem;
  line-height: 1.6;
  margin-bottom: 1.3rem;
}

.share-link-row {
  display: flex;
  gap: 0.5rem;
  align-items: stretch;
}

.share-link {
  font: inherit;
  font-size: 0.82rem;
  flex: 1 1 auto;
  min-width: 0;
  padding: 0.62rem 0.75rem;
  color: var(--ink);
  background: rgba(255, 255, 255, 0.06);
  border: 1px solid var(--hairline);
  border-radius: var(--radius-sm);
  /* The link is meant to be read and selected, so let it be. */
  text-overflow: ellipsis;
}
.share-link:focus {
  outline: none;
  border-color: rgba(233, 184, 114, 0.55);
  background: rgba(255, 255, 255, 0.09);
}

.share-link-row .btn {
  flex: 0 0 auto;
  padding: 0.62rem 1.2rem;
  border-radius: var(--radius-sm);
}

/*
 * The platform share sheet, when the browser has one. Full width above the link, because on a
 * phone it is the action that matters: it opens the apps people send from, and it can carry the
 * postcard image with the link. `[hidden]` is stated because `display: flex` would otherwise beat
 * the user-agent's rule and leave a dead button on a desktop.
 */
.share-native {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 0.5rem;
  width: 100%;
  margin-bottom: 1.15rem;
  white-space: nowrap;
}
.share-native[hidden] { display: none; }

/*
 * The sender's name. Optional, remembered, and carried in the link so the arrival can name them
 * instead of saying "someone" — which is the difference between a message and a mystery.
 */
.share-from {
  font: inherit;
  font-size: 0.85rem;
  width: 100%;
  padding: 0.6rem 0.75rem;
  margin-bottom: 1.15rem;
  color: var(--ink);
  background: rgba(255, 255, 255, 0.06);
  border: 1px solid var(--hairline);
  border-radius: var(--radius-sm);
}
.share-from::placeholder { color: var(--ink-faint); }
.share-from:focus {
  outline: none;
  border-color: rgba(233, 184, 114, 0.55);
  background: rgba(255, 255, 255, 0.09);
}

.share-size {
  margin-top: 0.5rem;
  font-size: 0.74rem;
  color: var(--ink-faint);
}

/*
 * The trim, as a link inside the size line rather than a button beside the link: it is an answer to
 * what that line says, and a third button in the action row read as a third way to send the place.
 */
.share-trim {
  font: inherit;
  color: var(--ember);
  background: none;
  border: 0;
  padding: 0;
  margin-left: 0.18rem;
  cursor: pointer;
  text-decoration: underline;
  text-underline-offset: 2px;
}
.share-trim:hover { color: var(--ink); }

/* Copy leads the pair, at the row's own rhythm: no offset of its own to move it, or the label under it. */
.share-short { flex: 0 0 auto; }

/* The image is its own decision, so it gets its own section instead of sharing the link's row. */
.share-section {
  margin-top: 1.2rem;
  padding-top: 1.1rem;
  border-top: 1px solid var(--hairline-soft);
}
.share-section .wizard-label { margin-bottom: 0.5rem; }

/*
 * The two things you can *do* with a link, on one row: Copy (which sends it) and Save postcard (which
 * always arrives).
 *
 * The row separates *itself* from the label below it. That is the whole point of `margin-bottom` here:
 * it used to be the Copy button's own `margin-top` — from an older layout, when Copy sat directly
 * under the share sheet — that pushed the row's box down and so gave "The link" its air. When the
 * button was hidden by a gate that should never have existed, that stray margin went with it and the
 * label moved up under the buttons: one defect that looked like two.
 */
.share-actions {
  display: flex;
  gap: 0.55rem;
  margin-top: 1.2rem;
  margin-bottom: 0.9rem;
  padding-top: 1.1rem;
  border-top: 1px solid var(--hairline-soft);
  flex-wrap: wrap;
}

.share-note {
  margin-top: 0.7rem;
  font-size: 0.78rem;
  color: var(--ember);
  min-height: 1.1rem;
}

/* ── Ask your AI ──────────────────────────────────────────────────────── */

/*
 * The chat loop's own dialog: the same shell as Send's (see `.share` and `.share-panel`), because
 * it is the same kind of thing — a card over the diorama that has to fit a phone and scroll — and
 * two shells would be two sets of phone arithmetic to keep right.
 *
 * The launcher that opens it is styled up with the toolbar, in the bottom-left corner.
 */
.ai-panel .wizard-label { margin-top: 1rem; display: block; }

/*
 * The two introductions, one per mode, and only one of them is ever unhidden: the panel is asked
 * for two different things by two different doors, and copy that hedged both would say neither.
 * The `[hidden]` rule is stated rather than left to the UA sheet for the reason `.ai-open[hidden]`
 * gives — the moment either block is given a `display` of its own, the attribute would stop working.
 */
.ai-intro[hidden] { display: none; }

.ai-reply {
  width: 100%;
  margin-top: 0.35rem;
  padding: 0.6rem 0.7rem;
  border-radius: 10px;
  border: 1px solid var(--hairline-soft);
  background: var(--field, rgba(255, 255, 255, 0.06));
  color: var(--ink);
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
  font-size: 0.76rem;
  line-height: 1.35;
  resize: vertical;
}

.ai-reply::placeholder { color: var(--ink-faint); }
.ai-reply:focus { outline: none; border-color: var(--ember); }

/* The status line doubles as the error line, so the tone has to be visible without shouting. */
.share-note.is-good { color: var(--ink-soft); }
.share-note.is-bad { color: var(--ember-bright, var(--ember)); }

/*
 * The panel's switch to the other road: a line of text at the foot, under the status, quiet enough
 * that it never competes with Apply. A bare button with no chrome of its own rather than a third pill
 * — it is a correction for someone who came in the wrong door, not an action the panel is offering —
 * and it underlines on hover so it still reads as pressable without a box around it.
 */
.ai-switch { margin-top: 0.9rem; text-align: center; }

.ai-switch-btn {
  font: inherit;
  font-size: 0.78rem;
  background: none;
  border: 0;
  padding: 0.2rem 0.3rem;
  color: var(--ink-faint);
  text-decoration: underline;
  text-underline-offset: 0.18em;
  cursor: pointer;
}

.ai-switch-btn:hover,
.ai-switch-btn:focus-visible { color: var(--ink); }

@media (max-width: 720px) {
  .share-panel { padding: 1.5rem 1.25rem 1.2rem; border-radius: 16px; }
  .share-link-row { flex-wrap: wrap; }
  .share-link-row .btn { flex: 1 1 auto; }
  .share-actions .btn { flex: 1 1 auto; }
}

/* ── Toasts ───────────────────────────────────────────────────────────── */

/*
 * Stacked above the Send button rather than sitting in the corner: that corner belongs to the
 * one action worth a single press, and a toast sliding in over it would cover the very thing
 * it is usually talking about ("Copied." / "Someone sent you this place.").
 */
.toasts {
  position: fixed;
  bottom: calc(1.15rem + 3.2rem + var(--safe-b));
  right: calc(1.25rem + var(--safe-r));
  z-index: 40;
  display: flex;
  flex-direction: column;
  gap: 0.5rem;
  align-items: flex-end;
  pointer-events: none;
}

.toast {
  background: var(--glass-strong);
  backdrop-filter: blur(14px);
  -webkit-backdrop-filter: blur(14px);
  border: 1px solid var(--hairline);
  border-radius: var(--radius-sm);
  padding: 0.65rem 1rem;
  font-size: 0.84rem;
  color: var(--ink);
  box-shadow: 0 10px 30px rgba(0, 0, 0, 0.45);
  animation: toast-in 0.35s var(--ease) both;
  max-width: 20rem;
}
.toast.is-leaving { animation: toast-out 0.3s var(--ease) both; }

@keyframes toast-in  { from { opacity: 0; transform: translateY(8px); } to { opacity: 1; transform: none; } }
@keyframes toast-out { to { opacity: 0; transform: translateY(6px); } }

/* ── Responsive ───────────────────────────────────────────────────────── */

/*
 * The icon column and the AI pill are one stack in the bottom-left corner, spaced by the column's
 * own `gap` (see `.toolbar`), and it sits in the corner it always had: the lift that used to clear
 * a volume pill is gone with the pill. Send has the opposite corner to itself, and presenting has
 * no chat loop at all — `.ai-launcher` is removed outright, so the column closes up and stays level
 * with "Create your own" on the other side.
 */

/*
 * The orbit line goes where there is not room for three things along the bottom: on a phone,
 * where a finger drags the sky and the props alike anyway, and in a narrow desktop window, where
 * the centred hint would otherwise reach the AI pill in the bottom-left corner. What is left —
 * drag to move, Shift (or ↻) to turn — is the part a first-time editor needs.
 */
@media (max-width: 1000px) {
  .hint-orbit { display: none; }
}

/* Fine pointers can hold Shift; coarse ones cannot, so the hints differ. */
.hint-coarse { display: none; }

@media (pointer: coarse) {
  .hint-fine { display: none; }
  .hint-coarse { display: inline; }

  /*
   * A finger is not a mouse. The icons cannot simply grow — four of them and the Send pill
   * have to fit one 320px line — so each one gains an invisible hit area instead, grown by
   * half the gap between them: any larger and two neighbours would overlap, and a tap landing
   * between two icons would go to whichever came later in the DOM.
   */
  .icon-btn { position: relative; }
  .icon-btn::after {
    content: '';
    position: absolute;
    inset: -0.2rem;
    border-radius: 50%;
  }

  /*
   * The title screen's legal link gets the same treatment, and needs it more: it is the smallest
   * text in the app and it is reached for by someone who does not yet trust the site. Alone in its
   * corner, so the hit area can be generous without stealing a tap from a neighbour.
   */
  .landing-legal-link { position: relative; }
  .landing-legal-link::after {
    content: '';
    position: absolute;
    inset: -0.55rem -0.9rem;
    border-radius: 8px;
  }

  /* A grade is a word you tap, not a 21px sliver. The row already scrolls sideways. */
  .chip {
    display: inline-flex;
    align-items: center;
    min-height: 2.1rem;
  }

  /* A 13px thumb on a 3px track is a coin toss under a fingertip. */
  .volume-slider { height: 4px; }
  .volume-slider::-webkit-slider-thumb { width: 20px; height: 20px; }
  .volume-slider::-moz-range-thumb { width: 20px; height: 20px; }
}

@media (max-width: 720px) {
  .hud {
    flex-direction: column;
    align-items: stretch;
    padding: calc(0.7rem + var(--safe-t)) calc(0.7rem + var(--safe-r)) 0.7rem calc(0.7rem + var(--safe-l));
    gap: 0.5rem;
  }
  .hud-scene { max-width: none; }
  .hud-controls { align-items: stretch; flex-direction: row; gap: 0.5rem; }
  /* The glyph follows the HUD's own insets in, which are smaller here. It overlays the card. */
  .screen-btn { top: calc(0.7rem + var(--safe-t)); right: calc(0.7rem + var(--safe-r)); }
  .panel { max-width: none; flex: 1 1 auto; min-width: 0; }

  /* One line of small pills, scrolled sideways rather than stacked over the scene. */
  .chip-group {
    flex-wrap: nowrap;
    overflow-x: auto;
    scrollbar-width: none;
    -webkit-overflow-scrolling: touch;
  }
  .chip-group::-webkit-scrollbar { display: none; }

  /*
   * The hint lifts clear of the stack on the left — the AI pill, then the icon row above it —
   * which is why it starts at 6.6rem rather than over the bottom edge. It also drops the orbit
   * line here, as it does in any window under 1000px (see above).
   */
  .hint {
    font-size: 0.7rem;
    padding: 0.45rem 0.8rem;
    gap: 0.35rem;
    bottom: calc(6.6rem + var(--safe-b));
  }

  /*
   * The volume row is inside the settings popup now, so there is nothing to reposition here —
   * only the label to drop, and the slider capped: the × button shares the top-right corner of
   * that popup, and a full-width slider would run underneath it.
   */
  .volume-label { display: none; }
  .volume-slider { max-width: 11rem; }

  /*
   * A row along the bottom-left that hugs its own width, rather than icons spread across the full
   * width: Send is a fixed pill in the opposite corner, so a stretched row would run underneath it.
   * Four 2.15rem icons and their gaps leave the right corner clear even at 320px — the arithmetic
   * is asserted in tools/phone.mjs, because this is the one line where one more pixel of padding
   * breaks the layout.
   *
   * The pill stays the *column's* second child on a phone too — the icons become a row inside it —
   * so it is always on its own line, spaced from them by the same 0.4rem they have between them.
   * A wrapping row instead would put the pill back beside the icons on the widths where it fits
   * (351–464px, which is a large phone) and the Send pill would be sitting on top of it.
   */
  .toolbar {
    /* Stated, not inherited from the desktop sheet: this is what keeps the pill on its own line. */
    flex-direction: column;
    left: calc(0.7rem + var(--safe-l));
    right: auto;
    bottom: calc(0.7rem + var(--safe-b));
    gap: 0.4rem;
  }
  .toolbar-icons {
    flex-direction: row;
    gap: 0.4rem;
  }

  /* The pill is at the same height as Send's. */
  .ai-open { padding: 0.6rem 0.95rem 0.6rem 0.8rem; font-size: 0.86rem; }

  .icon-btn { width: 2.15rem; height: 2.15rem; font-size: 0.95rem; }
  .send { right: calc(0.7rem + var(--safe-r)); bottom: calc(0.7rem + var(--safe-b)); }
  .send-btn { padding: 0.6rem 1.05rem 0.6rem 0.9rem; font-size: 0.88rem; }

  /*
   * The × grows on a phone. It is the only way out of the share and settings panels besides a
   * press outside, and 32px is under a fingertip. (Both panels use it, hence one rule.)
   */
  .wizard-close { width: 2.4rem; height: 2.4rem; font-size: 1.4rem; }

  /* The share panel sits inset from the edges at this size rather than touching them. */
  .share { padding: 0.9rem; }

  /*
   * The ⟲ menu, on a phone: above the icon rather than beside it. To the right it would
   * leave the screen — the toolbar is a row that already spans the width — and the row is
   * never taller than an icon, so opening upward can never push the menu off the top
   * either. Rows stretch, so the tap target is the full width of the card.
   */
  .menu {
    left: auto;
    right: 0;
    bottom: calc(100% + 0.5rem);
    min-width: 10rem;
    max-width: none;
  }
  .menu-item { min-height: 2.6rem; font-size: 0.8rem; }

  .toasts { left: calc(0.7rem + var(--safe-l)); right: calc(0.7rem + var(--safe-r)); bottom: calc(9.4rem + var(--safe-b)); align-items: stretch; }
  .toast { max-width: none; }

  /* The HUD is taller here (title over a filter row), so the notice starts below it. */
  .draft-notice {
    top: 7.6rem;
    max-height: calc(100vh - 15rem);
    padding: 0.9rem 0.9rem 0.75rem;
  }
  .draft-notice-actions .btn { flex: 1 1 auto; }
}

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


/* The release's date, beside its number: faint, so the number stays the thing you read. */
.whats-new-date { color: var(--ink-faint); font-weight: 400; }
