/* Mobile first: everything below is the phone layout; desktop tweaks are at the end. */

:root {
  color-scheme: dark;
  --ink: #0b0912;                 /* page base + scrim colour */
  --text: #ece8f5;
  --muted: #b4aec8;
  --line-text: #f3eefc;
  --serif: "Iowan Old Style", "Palatino Linotype", Palatino, "Book Antiqua", Georgia, "Noto Serif", serif;
  --sans: system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
  --accent: #a070ff;
  --on-accent: #120d1c;
  /* Conversation-screen art treatment. Brightness sets readability (it was
     measured against the single brightest pixel of every scene); saturation
     brings back the colour that darkening drains. Re-measure if you change it. */
  --chat-brightness: 0.18;
  --chat-saturate: 2;
}

/* One rule per scene. Only the matching rule applies, so only that image loads.
   body[data-scene] is the session scene (accent, audio); .art[data-scene] is
   what each walk layer currently shows (image, glow colour and position).
   --light-x/y: where the light source sits in the image (for the glow pulse). */
body[data-scene="arch"],    .art[data-scene="arch"] { --accent: #f0a860; --scene-img: url("scenes/arch.webp");     --light-x: 73%; --light-y: 75%; }
body[data-scene="sword"],   .art[data-scene="sword"] { --accent: #a070ff; --scene-img: url("scenes/sword.webp");    --light-x: 30%; --light-y: 58%; }
body[data-scene="campfire"],.art[data-scene="campfire"] { --accent: #ff9d4d; --scene-img: url("scenes/campfire.webp"); --light-x: 48%; --light-y: 81%; }
body[data-scene="woods"],   .art[data-scene="woods"] { --accent: #4fd6c8; --scene-img: url("scenes/woods.webp");    --light-x: 39%; --light-y: 37%; }
body[data-scene="stones"],  .art[data-scene="stones"] { --accent: #5ce08a; --scene-img: url("scenes/stones.webp");   --light-x: 39%; --light-y: 51%; }
body[data-scene="bridge"],  .art[data-scene="bridge"] { --accent: #e8445c; --scene-img: url("scenes/bridge.webp");   --light-x: 49%; --light-y: 88%; }
body[data-scene="dragon"],  .art[data-scene="dragon"] { --accent: #e8803c; --scene-img: url("scenes/dragon.webp");      --light-x: 60%; --light-y: 21%; }
body[data-scene="troll"],   .art[data-scene="troll"] { --accent: #f0b45a; --scene-img: url("scenes/troll.webp");       --light-x: 60%; --light-y: 44%; }
body[data-scene="islands"], .art[data-scene="islands"] { --accent: #5ad9e8; --scene-img: url("scenes/islands.webp");     --light-x: 29%; --light-y: 58%; }
body[data-scene="temple"],  .art[data-scene="temple"] { --accent: #b8c4e8; --scene-img: url("scenes/temple.webp");      --light-x: 78%; --light-y: 18%; }
body[data-scene="guardians"],.art[data-scene="guardians"] { --accent: #d9453f; --scene-img: url("scenes/guardians.webp");   --light-x: 21%; --light-y: 19%; }

* { box-sizing: border-box; }

/* The hidden attribute always wins. Author rules like .ask { display: grid }
   would otherwise override the browser's own [hidden] style, and app.js relies
   on hidden to keep the "something else" states mutually exclusive. */
[hidden] { display: none !important; }

html, body { height: 100%; overflow: hidden; }

body {
  margin: 0;
  background: var(--ink);
  color: var(--text);
  font: 16px/1.5 var(--sans);
  overflow: hidden;
  -webkit-font-smoothing: antialiased;
}

/* ---- frame ---------------------------------------------------------- */

/* Everything lives in .frame. On a portrait phone it is the whole screen;
   on wider screens it becomes a centred column (see "wider screens" below). */
.frame {
  position: relative;
  width: 100%;
  height: 100dvh;
  margin: 0 auto;
  overflow: hidden;
}

/* ---- scene ---------------------------------------------------------- */

.scene {
  position: absolute;
  inset: 0;
  z-index: 0;
  overflow: hidden;
  background: var(--ink);
  container-type: size;           /* lets .art size itself like background-size: cover */
  transition: filter 700ms ease;
  /* Motion amplitudes (1 = full). The chat screen cuts them to about a third. */
  --drift: 1;
  --glow-lo: 0.3;
  --glow-hi: 0.6;
}

/* The image, sized and centred exactly like background-size: cover, so the
   glow can be positioned in the image's own coordinates and ride with it.
   Two stacked copies (.a below, .b on top) make the "walking in" motion. */
.art {
  position: absolute;
  left: 50%;
  top: 50%;
  width: max(100cqw, 100cqh * 9 / 16);
  height: max(100cqh, 100cqw * 16 / 9);
  margin-left: calc(max(100cqw, 100cqh * 9 / 16) / -2);
  margin-top: calc(max(100cqh, 100cqw * 16 / 9) / -2);
  background: var(--scene-img) center / 100% 100% no-repeat;
  transform-origin: 50% 50%;
  will-change: transform, opacity;
  /* Walk: both copies zoom 1.0 -> 1.5 over 40 s, linear, forever; .b runs half
     a cycle ahead. Only the 1.0 -> 1.25 half of each copy is ever visible: .b
     covers .a whenever it is opaque. The crossfade (1.25x leaving, 1.0x
     arriving) lasts 4% of the cycle (1.6 s) to keep the double exposure brief.
     Each layer's cycle boundary falls while it is fully hidden, with 1% (0.4 s)
     of margin either side, so walk.js can swap it to the next scene unseen.
     Delays start the page just after a crossfade (.b fully in at 1.025x). */
  animation: walk 40s linear -22s infinite, walk-fade-under 40s linear -22s infinite;
}
.art.b {
  animation: walk 40s linear -2s infinite, walk-fade-over 40s linear -2s infinite;
}

/* Accent light pulsing at the scene's light source. */
.glow {
  position: absolute;
  left: var(--light-x, 50%);
  top: var(--light-y, 70%);
  width: 44%;
  aspect-ratio: 1;
  margin: -22% 0 0 -22%;          /* % of .art width: centres the 44% circle */
  border-radius: 50%;
  background: radial-gradient(closest-side,
    color-mix(in srgb, var(--accent) 85%, transparent),
    color-mix(in srgb, var(--accent) 30%, transparent) 40%,
    transparent);
  mix-blend-mode: screen;
  opacity: var(--glow-lo);
  animation: glow-pulse 3.5s ease-in-out infinite alternate; /* 7 s cycle */
  will-change: opacity;
}

/* Soft, wide mist drifting sideways. */
.mist {
  position: absolute;
  inset: 0 -50%;                  /* 200% wide so it can drift without edges showing */
  background: radial-gradient(ellipse 34% 26% at 50% 60%, rgb(255 255 255 / 0.07), transparent);
  animation: mist-drift 70.5s ease-in-out infinite alternate;  /* 141 s cycle: never in step with the 90 s Ken Burns for long */
  will-change: transform;
}

/* Forward motion: grow, with a slight downward slide so the ground moves past
   (translate is 8% of the zoom, far inside the edge margin at every scale). */
@keyframes walk {
  from { transform: translateY(0) scale(1); }
  to   { transform: translateY(4%) scale(1.5); }
}
/* Top copy: fades in over the bottom one, holds, fades out to reveal it.
   Holds at 0 until 1% and at 1 until 51%: the swap margins (see .art). */
@keyframes walk-fade-over {
  0%   { opacity: 0; }
  1%   { opacity: 0; }
  5%   { opacity: 1; }
  51%  { opacity: 1; }
  55%  { opacity: 0; }
  100% { opacity: 0; }
}
/* Bottom copy: always fully opaque while it can be seen; it only switches on
   or off at moments when the top copy covers it completely, so the scene
   never dims mid-crossfade. */
@keyframes walk-fade-under {
  0%     { opacity: 1; }
  55%    { opacity: 1; }
  55.1%  { opacity: 0; }
  99.9%  { opacity: 0; }
  100%   { opacity: 1; }
}
@keyframes mist-drift {
  from { transform: translateX(calc(-12% * var(--drift))); }
  to   { transform: translateX(calc(12% * var(--drift))); }
}
@keyframes glow-pulse {
  from { opacity: var(--glow-lo); }
  to   { opacity: var(--glow-hi); }
}

/* Waiting / ended: soft vertical scrim, clear at top, ~75% at the bottom.
   ::after so it paints over the art, glow and mist. */
.scene::after {
  content: "";
  position: absolute;
  inset: 0;
  background: linear-gradient(to bottom, rgb(11 9 18 / 0) 0%, rgb(11 9 18 / 0.75) 100%);
  transition: opacity 700ms ease;
}

/* Conversation: darken the art itself (not a black layer over it, which
   greys it out) and push saturation back up so it keeps its colour.
   Motion drops to about a third: movement behind text is distracting. */
body[data-state="chat"] .scene {
  filter: brightness(var(--chat-brightness)) saturate(var(--chat-saturate));
  --drift: 0.33;
  --glow-lo: 0.4;
  --glow-hi: 0.5;
}
body[data-state="chat"] .scene::after { opacity: 0; }

/* The walk keeps running, unchanged, on the conversation screen: no pause or
   duration change, so nothing recomputes and nothing jumps when chat opens.
   The existing chat dim (--chat-brightness) keeps text readable; checked
   across the full 40 s cycle. */

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

#chat { position: relative; z-index: 1; }
#landing, #faded, #lobby, #chat, #fade { display: none; }
body[data-state="landing"] #landing,
body[data-state="faded"]   #faded,
body[data-state="waiting"] #lobby { display: flex; }

/* A conversation ending: the line holds, then fades; the landing fades in
   after it (app.js switches state at FADED_MS). Opacity only. */
body[data-state="faded"] #faded { animation: faded-out 2.6s ease forwards; }
body[data-state="landing"] #landing { animation: landing-in 700ms ease both; }
@keyframes faded-out { 0%, 75% { opacity: 1; } 100% { opacity: 0; } }
@keyframes landing-in { from { opacity: 0; } to { opacity: 1; } }
body[data-state="chat"]    #chat  { display: grid; }
body[data-state="waiting"] #fade,
body[data-state="chat"]    #fade  { display: block; }

a { color: var(--accent); }

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

button {
  font: inherit;
  cursor: pointer;
  border-radius: 999px;
  padding: 10px 18px;
  min-height: 44px;
}
button:disabled { opacity: 0.45; cursor: default; }

.primary {
  background: var(--accent);
  color: var(--on-accent);
  border: 0;
  font-weight: 600;
}

.ghost {
  background: rgb(11 9 18 / 0.45);
  color: var(--text);
  border: 1px solid rgb(236 232 245 / 0.22);
}

/* ---- waiting + ended ------------------------------------------------ */

.centered {
  position: absolute;
  inset: 0;
  z-index: 1;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 28px;
  padding: 24px max(24px, env(safe-area-inset-right)) 24px max(24px, env(safe-area-inset-left));
  text-align: center;
}

.line {
  position: relative;
  margin: 0;
  max-width: 18em;
  font: italic 400 26px/1.35 var(--serif);
  text-wrap: balance;
  color: var(--line-text);
  text-shadow: 0 1px 2px rgb(0 0 0 / 0.7), 0 0 18px rgb(0 0 0 / 0.55);
}
/* Landing: the name, in the same serif as the waiting line, larger. */
.title {
  position: relative;
  margin: 0;
  font: italic 400 46px/1.1 var(--serif);
  color: var(--line-text);
  text-shadow: 0 1px 2px rgb(0 0 0 / 0.7), 0 0 18px rgb(0 0 0 / 0.55);
}
#landing {
  gap: 18px;
  /* Sit the group a little below centre: keeps the title clear of the band
     (34-37% of the height) where woods' lantern passes during the walk. */
  padding-top: calc(24px + 16dvh);
}
#landing .primary { margin-top: 14px; }

/* Waiting: the three dots after "finding someone who's still awake" fade in
   one after another, slowly, so the search reads as live. Opacity only. */
.dots span { animation: dot 2.4s ease-in-out infinite; opacity: 0.15; }
.dots span:nth-child(2) { animation-delay: 0.4s; }
.dots span:nth-child(3) { animation-delay: 0.8s; }
@keyframes dot { 0%, 100% { opacity: 0.15; } 30%, 70% { opacity: 1; } }

/* A soft local halo just behind the line, so stars/lanterns behind the
   text can't wash it out. It fades to nothing well inside the screen.
   Checked across the full Ken Burns / mist / glow range: >= 6.7:1. */
.line::before,
.title::before {
  content: "";
  position: absolute;
  inset: -36px -48px;
  z-index: -1;
  background: radial-gradient(closest-side, rgb(11 9 18 / 0.6), rgb(11 9 18 / 0.45) 55%, rgb(11 9 18 / 0) 100%);
}

/* Fade sits bottom-centre while waiting (to leave the queue), top-right in chat. */
#fade {
  position: absolute;
  z-index: 2;
  left: 50%;
  bottom: calc(24px + env(safe-area-inset-bottom));
  transform: translateX(-50%);
}
body[data-state="chat"] #fade {
  left: auto;
  right: calc(max(12px, env(safe-area-inset-right)) + 40px + 8px);   /* left of #sound */
  top: calc(10px + env(safe-area-inset-top));
  bottom: auto;
  transform: none;
  padding: 6px 14px;
  min-height: 36px;
  font-size: 14px;
}

/* Sound toggle: quiet icon button, top-right corner in every state. */
#sound {
  position: absolute;
  z-index: 2;
  top: calc(10px + env(safe-area-inset-top));
  right: max(12px, env(safe-area-inset-right));
  width: 40px;
  height: 36px;
  min-height: 0;
  padding: 0;
  display: grid;
  place-items: center;
  color: var(--muted);
}
#sound[hidden] { display: none; }
#sound svg { fill: none; stroke: currentColor; stroke-width: 1.6; stroke-linecap: round; stroke-linejoin: round; }
#sound .spk { fill: currentColor; stroke: none; }
#sound[aria-pressed="false"] .on,
#sound[aria-pressed="true"] .off { display: none; }
#sound[aria-pressed="true"] { color: var(--accent); border-color: color-mix(in srgb, var(--accent) 45%, transparent); }

/* While sound fades in, ring the toggle so it's obvious where the sound came
   from and how to stop it. Opacity only. */
#sound::after {
  content: "";
  position: absolute;
  inset: -4px;
  border-radius: 999px;
  box-shadow: 0 0 0 2px var(--accent), 0 0 18px color-mix(in srgb, var(--accent) 55%, transparent);
  opacity: 0;
  transition: opacity 600ms ease;
  pointer-events: none;
}
#sound.arriving::after { opacity: 1; }

/* iOS only, once per session: the ringer switch mutes Web Audio. */
#sound-hint {
  position: absolute;
  z-index: 2;
  top: calc(10px + env(safe-area-inset-top) + 36px + 10px);
  right: max(12px, env(safe-area-inset-right));
  max-width: min(240px, 70%);
  margin: 0;
  padding: 6px 12px;
  border-radius: 12px;
  background: rgb(11 9 18 / 0.72);
  color: var(--text);
  font-size: 13px;
  line-height: 1.35;
  text-align: right;
  opacity: 0;
  transition: opacity 700ms ease;
  pointer-events: none;
}
#sound-hint.shown { opacity: 1; }

/* ---- conversation --------------------------------------------------- */

#chat {
  height: 100%;
  grid-template-rows: auto 1fr auto auto;
  padding:
    env(safe-area-inset-top)
    env(safe-area-inset-right)
    env(safe-area-inset-bottom)
    env(safe-area-inset-left);
}

.bar {
  padding: 14px 132px 12px 16px;  /* right padding leaves room for #fade + #sound */
  border-bottom: 1px solid rgb(236 232 245 / 0.08);
}

.small {
  min-height: 32px;
  padding: 4px 12px;
  font-size: 13px;
}

#reroll { margin-top: 8px; color: var(--muted); }

#prompt {
  margin: 0;
  font: italic 17px/1.4 var(--serif);
  color: var(--text);
  border-left: 2px solid var(--accent);
  padding-left: 10px;
}

#messages {
  list-style: none;
  margin: 0;
  padding: 16px;
  overflow-y: auto;
  overscroll-behavior: contain;
  display: flex;
  flex-direction: column;
  gap: 8px;
}

#messages li {
  padding: 9px 13px;
  border-radius: 16px;
  max-width: 85%;
  white-space: pre-wrap;
  overflow-wrap: anywhere;
}
/* Bubbles: translucent, tinted toward the scene accent, so they sit in the
   image rather than on top of it. No backdrop blur: the art is already
   darkened on this screen, and re-blurring a moving background every frame
   costs battery on phones. */
#messages .theirs {
  align-self: flex-start;
  background: color-mix(in srgb, var(--accent) 12%, rgb(18 14 28 / 0.5));
  border: 1px solid color-mix(in srgb, var(--accent) 16%, transparent);
  border-bottom-left-radius: 4px;
}
#messages .mine {
  align-self: flex-end;
  background: color-mix(in srgb, var(--accent) 26%, rgb(18 14 28 / 0.5));
  border: 1px solid color-mix(in srgb, var(--accent) 30%, transparent);
  border-bottom-right-radius: 4px;
}

/* A prompt that arrived mid-conversation: styled like the pinned prompt,
   not like a message bubble. */
#messages li.prompt {
  align-self: stretch;
  max-width: none;
  margin: 10px 0;
  padding: 2px 0 2px 10px;
  border-radius: 0;
  background: none;
  border-left: 2px solid var(--accent);
  font: italic 16px/1.4 var(--serif);
  color: var(--text);
}

/* "Something else" ask, in the header under the prompt. Which of the three
   states shows is decided only by the hidden attribute (see app.js setAsk);
   these rules just style the panels. Quiet, never modal; the line along the
   bottom counts down to the ask expiring on its own. */
.ask {
  display: grid;
  grid-template-columns: 1fr auto;
  align-items: center;
  column-gap: 12px;
  position: relative;
  overflow: hidden;
  margin-top: 10px;
  padding: 8px 12px 10px;
  border-radius: 12px;
  background: color-mix(in srgb, var(--accent) 14%, rgb(16 12 26 / 0.92));
  border: 1px solid color-mix(in srgb, var(--accent) 45%, transparent);
}
.ask-head { margin: 0; grid-column: 1; font-size: 14px; line-height: 1.35; color: var(--text); }
.ask-note { margin: 2px 0 0; grid-column: 1; font-size: 12px; color: var(--muted); }
#ask-yes { grid-column: 2; grid-row: 1 / span 2; }
.ask::after {
  content: "";
  position: absolute;
  left: 0; right: 0; bottom: 0;
  height: 2px;
  background: var(--accent);
  opacity: 0.7;
  transform-origin: left center;
  animation: ask-countdown var(--ask-ttl, 90s) linear forwards;
}
@keyframes ask-countdown { from { transform: scaleX(1); } to { transform: scaleX(0); } }

#notice {
  margin: 0;
  padding: 0 16px;
  min-height: 0;
  font-size: 13px;
  color: var(--muted);
}
#notice:not(:empty) { padding-bottom: 6px; }

#composer {
  display: flex;
  align-items: flex-end;
  gap: 8px;
  padding: 10px 12px 12px;
  border-top: 1px solid rgb(236 232 245 / 0.08);
  background: rgb(11 9 18 / 0.6);
}

textarea {
  flex: 1;
  min-height: 44px;
  max-height: 30dvh;
  resize: none;
  background: rgb(26 21 40 / 0.9);
  color: var(--text);
  border: 1px solid rgb(236 232 245 / 0.14);
  border-radius: 22px;
  padding: 10px 16px;
  font: 16px/1.4 var(--sans);   /* 16px stops iOS zooming on focus */
}
textarea::placeholder { color: var(--muted); }
textarea:focus-visible { outline: none; border-color: var(--accent); box-shadow: 0 0 0 1px var(--accent); }

#send {
  background: var(--accent);
  color: var(--on-accent);
  border: 0;
  font-weight: 600;
}

/* ---- wider screens -------------------------------------------------- */

/* The art is 9:16 portrait. Once the screen is wider than that (tablets,
   desktops, landscape phones) the frame becomes a centred column sized to the
   art's native ratio, so the whole scene shows instead of a zoomed crop.
   360px floor keeps the chat usable on short landscape screens (the art then
   crops a little top/bottom rather than the column getting too narrow). */
@media (min-aspect-ratio: 10/16) {
  .frame {
    width: clamp(360px, calc(100dvh * 9 / 16), 100vw);
    box-shadow: 0 0 0 1px rgb(236 232 245 / 0.06), 0 0 80px rgb(0 0 0 / 0.6);
  }
}

@media (prefers-reduced-motion: reduce) {
  /* No movement at all: static image, glow held steady, mist parked. */
  .scene, .scene::after, #sound::after, #sound-hint { transition: none; }
  .art, .mist, .glow { animation: none; }
  #faded, #landing { animation: none !important; }
  .dots span { animation: none; opacity: 1; }
  .ask::after { animation: none; }
  .art.b { animation: none; visibility: hidden; }   /* one static copy; forward motion is a vestibular trigger.
                                                      (.art.b's own animation rule is more specific than .art) */
  .glow { opacity: 0.45; }
}
