/* =====================================================================
   quest.css - the CHROME layer. Loads after every component sheet.

   WAS: the PC-98 / pixel-retro skin (paper panels, 2.5px ink borders,
   hard offset shadows, checker trims, uppercase everything). That skin is
   RETIRED -- owner-approved, Aug 2026. The audit found this one file was
   most of what made the site read as a different language per tab.

   IS: the quiet chrome of the unified language. Soft surfaces on --card,
   one shadow, 16px radius, hairline --line borders, sentence case. The
   only uppercase left anywhere is the eyebrow (11px, .12em) -- small and
   spaced, which reads as calm rather than as shouting.

   Everything here is a theme.css token. Pixel art belongs to SPRITES
   ONLY; the chrome never imitates it.
   ===================================================================== */

/* ---------------- page frame ---------------- */
body { background: var(--paper); }
#app { background: var(--paper); }
#app::before { display: none; }   /* no paper-grain vignette */

/* The time-reactive wash behind the inner pages. It used to be a full
   sunset gradient (a second visual language on top of the paper one); it
   is a whisper now -- Portal.updateSky paints it, css keeps it faint. */
#app-sky {
    position: fixed;
    inset: 0;
    max-width: var(--content-max);
    margin: 0 auto;
    z-index: 0;
    pointer-events: none;
    opacity: .5;
    transition: background 1s;
}
body[data-page="home"] #app-sky,
body[data-page="picker"] #app-sky { display: none; }

/* ---------------- header ---------------- */
#app-header {
    background: color-mix(in srgb, var(--paper) 88%, transparent);
    -webkit-backdrop-filter: blur(12px);
    backdrop-filter: blur(12px);
    border-bottom: 1px solid var(--line);
    box-shadow: none;
    padding-top: calc(var(--space-md) + env(safe-area-inset-top, 0));
    padding-bottom: 12px;
}
#app-header .brand {
    font-family: var(--display);
    font-size: 21px;
    font-weight: 600;
    letter-spacing: .01em;
    text-transform: none;
}
#app-header .brand::before { display: none; }   /* no sticker pip */
#app-header .brand .amp { font-family: var(--script); font-style: normal; color: var(--rose); font-size: 1.15em; }

.hud-flowers {
    font-family: var(--sans);
    font-weight: 600;
    font-size: var(--px-sm);
    color: var(--ink-soft);
    background: var(--card);
    border: 1px solid var(--line);
    border-radius: var(--radius-pill);
    box-shadow: var(--shadow-sm);
    padding: 6px 12px 6px 9px;
}
/* top-right profile door */
.hdr-btn {
    width: 44px; height: 44px; flex: none;
    display: grid; place-items: center;
    color: var(--ink-soft);
    background: var(--card);
    border: 1px solid var(--line);
    border-radius: var(--radius-pill);
    box-shadow: var(--shadow-sm);
    text-decoration: none;
    transition: color var(--dur-med), background var(--dur-med);
}
.hdr-btn:active { color: var(--rose); }
.hdr-btn:focus-visible { outline: 2px solid var(--rose); outline-offset: 2px; }
.hdr-btn svg { width: 21px; height: 21px; }
body[data-page="profile"] .hdr-btn { background: var(--rose); color: #fff; border-color: transparent; }

/* ---------------- bottom nav + camera FAB ---------------- */
#app-nav {
    background: color-mix(in srgb, var(--card) 94%, transparent);
    -webkit-backdrop-filter: blur(14px);
    backdrop-filter: blur(14px);
    border: 1px solid var(--line);
    border-radius: var(--radius);
    box-shadow: var(--shadow);
    gap: 2px;
    padding: 0 8px env(safe-area-inset-bottom, 0);
}
#app-nav a {
    font-family: var(--sans);
    font-weight: 600;
    font-size: var(--px-xs);
    letter-spacing: .04em;
    text-transform: none;
    color: var(--dim);
    border: 0;
    border-radius: var(--radius-sm);
    gap: 3px;
    padding: 8px 2px;
    min-height: 46px;
    justify-content: center;
    transition: color var(--dur-med);
}
#app-nav a.active { color: var(--rose); background: none; box-shadow: none; }
#app-nav a.active .nav-ico { transform: none; }
#app-nav a:focus-visible { outline: 2px solid var(--rose); outline-offset: -4px; }
/* the active pip comes back as one small rose dot -- quieter than a filled tab */
#app-nav a.active::after {
    content: '';
    display: block;
    width: 4px; height: 4px;
    border-radius: 50%;
    background: var(--rose);
    margin-top: -1px;
}
#nav-cam {
    -webkit-appearance: none; appearance: none;
    flex: none;
    width: 56px; height: 56px;
    margin-top: -28px;
    display: grid; place-items: center;
    color: #fff;
    background: var(--rose);
    border: 3px solid var(--card);
    border-radius: 50%;
    box-shadow: var(--shadow);
    cursor: pointer;
    transition: transform var(--dur-fast);
}
#nav-cam:active { transform: scale(.96); }
#nav-cam:focus-visible { outline: 2px solid var(--rose); outline-offset: 3px; }
#nav-cam .nav-ico { width: 26px; height: 26px; display: block; }

/* magic camera popup (toggled by the FAB) */
#cam-pop {
    position: fixed;
    bottom: calc(var(--nav-height) + 34px);
    left: 50%; transform: translateX(-50%);
    z-index: 95;
    width: min(268px, calc(100vw - 48px));
    padding: 20px 18px 18px;
    text-align: center;
    background: var(--card);
    border: 1px solid var(--line);
    border-radius: var(--radius);
    box-shadow: var(--pop-shadow);
    animation: qPopIn .24s var(--ease) both;
}
#cam-pop .cp-eyebrow {
    font-family: var(--sans);
    font-weight: 600;
    font-size: var(--px-xs);
    letter-spacing: var(--track-eyebrow);
    text-transform: uppercase;
    color: var(--dim);
}
#cam-pop .cp-ico { margin: 12px auto 0; width: 38px; height: 38px; color: var(--rose); }
#cam-pop .cp-note {
    font-family: var(--sans);
    font-size: var(--text-sm); font-weight: 400;
    color: var(--ink-soft);
    line-height: 1.5;
    margin-top: 10px;
}
#cam-pop .cp-open {
    display: block;
    font-family: var(--sans);
    font-weight: 600;
    font-size: var(--px-md);
    letter-spacing: 0;
    color: #fff;
    background: var(--rose);
    border: 1px solid transparent;
    border-radius: var(--radius-pill);
    box-shadow: none;
    text-decoration: none;
    padding: 13px 0;
    margin-top: 14px;
    min-height: 44px;
}
#cam-pop .cp-open--alt { background: transparent; color: var(--ink); border-color: var(--line); margin-top: 8px; }
#cam-pop .cp-hint {
    font-family: var(--sans); font-weight: 600;
    font-size: var(--px-xs); letter-spacing: var(--track-eyebrow);
    text-transform: uppercase; color: var(--dim); margin-top: 12px;
}
@keyframes qPopIn { from { opacity: 0; transform: translate(-50%, 8px); } to { opacity: 1; transform: translate(-50%, 0); } }

/* ---------------- shared chrome ---------------- */
/* THE eyebrow. The one allowed uppercase on the whole site. */
.eyebrow {
    font-family: var(--sans);
    font-weight: 600;
    font-size: var(--px-xs);
    letter-spacing: var(--track-eyebrow);
    text-transform: uppercase;
    color: var(--dim);
    background: none;
    border-radius: 0;
    padding: 0;
}
.title {
    font-family: var(--display);
    font-weight: 600;
    font-size: var(--text-2xl);
    line-height: 1.12;
    letter-spacing: 0;
    text-transform: none;
    color: var(--ink);
}
.title .amp { font-family: var(--script); font-style: normal; color: var(--rose); }
.subtitle { font-family: var(--display); font-weight: 500; color: var(--ink-soft); }

#app-header .crumb {
    font-family: var(--sans);
    font-weight: 600;
    font-size: var(--px-xs);
    letter-spacing: var(--track-eyebrow);
    text-transform: uppercase;
    color: var(--dim);
    border: 1px solid var(--line);
    box-shadow: var(--shadow-sm);
    border-radius: var(--radius-pill);
    padding: 6px 12px;
}

/* cards + the little "window" title bar */
.card, .window, .gw {
    background: var(--card);
    border: 1px solid var(--line);
    border-radius: var(--radius);
    box-shadow: var(--shadow);
}
.window > .win-bar {
    position: relative;
    background: transparent;
    padding: 16px var(--space-lg) 0;
    font-family: var(--sans);
    font-weight: 600;
    font-size: var(--px-xs);
    letter-spacing: var(--track-eyebrow);
    text-transform: uppercase;
    color: var(--dim);
    border-bottom: 0;
}
.window > .win-bar::before { display: none; }   /* the checker trim is gone */
.window > .win-bar .dot { display: none; }
.window > .win-body { padding: 12px var(--space-lg) var(--space-lg); }

/* buttons: primary = rose fill, secondary = ghost. Pills, both. */
.btn {
    font-family: var(--sans);
    font-weight: 600;
    font-size: var(--px-md);
    text-transform: none;
    letter-spacing: 0;
    color: var(--ink);
    background: var(--card);
    border: 1px solid var(--line);
    border-radius: var(--radius-pill);
    box-shadow: none;
    padding: 13px 22px;
    min-height: 46px;
    transition: transform var(--dur-fast), background var(--dur-med), color var(--dur-med);
}
.btn:active { transform: scale(var(--tap-scale)); box-shadow: none; }
.btn:focus-visible { outline: 2px solid var(--rose); outline-offset: 2px; }
.btn-accent {
    background: var(--rose);
    border-color: transparent;
    color: #fff;
    text-shadow: none;
}
.btn-accent:active { background: var(--accent-deep); }
.btn-lg { padding: 16px 26px; font-size: var(--px-lg); min-height: 52px; }

.chip {
    font-family: var(--sans);
    font-weight: 600;
    font-size: var(--px-xs);
    letter-spacing: .02em;
    color: var(--ink-soft);
    background: var(--card);
    border: 1px solid var(--line);
    border-radius: var(--radius-pill);
    box-shadow: none;
    padding: 5px 12px;
}
.hud-flowers, #app-header .crumb { box-shadow: var(--shadow-sm); }

/* inputs: radius 12, 16px type (iOS never zooms) */
.field {
    font-family: var(--sans);
    font-size: 16px;
    background: var(--card);
    border: 1px solid var(--line);
    border-radius: var(--radius-sm);
    padding: 14px 16px;
    box-shadow: none;
}
.field:focus {
    border-color: var(--rose);
    box-shadow: 0 0 0 3px color-mix(in srgb, var(--rose) 18%, transparent);
}

/* dialogue kit */
.dlg-name {
    font-family: var(--sans); font-weight: 600;
    font-size: var(--px-xs); letter-spacing: var(--track-eyebrow);
    border: 0; border-radius: var(--radius-pill);
    box-shadow: none;
}
.dlg-portrait { border-radius: var(--radius-sm); background: var(--card-2); border: 1px solid var(--line); }
.dlg-panel { border: 1px solid var(--line); border-radius: var(--radius); box-shadow: var(--shadow); }
.dlg-choice {
    font-family: var(--sans); font-weight: 600;
    font-size: var(--px-md);
    text-transform: none;
    background: var(--card);
    border: 1px solid var(--line);
    border-radius: var(--radius-pill);
    box-shadow: none;
    min-height: 46px;
}
.dlg-choice--accent { background: var(--rose); border-color: transparent; color: #fff; text-shadow: none; }
.dlg-choice:focus-visible { outline: 2px solid var(--rose); outline-offset: 2px; }

.live-card { border: 1px solid var(--line); border-radius: var(--radius); box-shadow: var(--shadow); }
.live-card .lc-tag {
    border: 0; border-radius: var(--radius-pill); box-shadow: none;
    font-family: var(--sans); font-weight: 600;
    font-size: var(--px-xs); letter-spacing: var(--track-eyebrow);
    background: var(--rose);
}
.live-card .lc-t { font-family: var(--display); font-weight: 600; font-size: var(--text-lg); color: var(--ink); }
.state-chip, .best-chip {
    font-family: var(--sans); font-weight: 600;
    font-size: var(--px-xs); letter-spacing: .06em;
    border: 1px solid var(--line); border-radius: var(--radius-pill);
    box-shadow: none;
}
.badge-float {
    font-family: var(--sans); font-weight: 600; font-size: var(--px-xs);
    background: var(--rose); border-color: transparent; box-shadow: var(--shadow-sm);
    border-radius: var(--radius-pill);
}

/* ONE divider motif per page: a hairline with a small rose diamond. */
.divider { color: var(--blush); margin: var(--space-lg) 0; }
.divider::before, .divider::after {
    height: 1px; width: 54px; border-radius: 0;
    background: linear-gradient(90deg, transparent, var(--line));
}
.divider .gem { width: 6px; height: 6px; background: var(--blush); }

/* =====================================================================
   HOME -- the scene.

   WAS a full-bleed dark "Wedding Quest" stage (six decorative systems),
   then a crest over three cards.

   IS a room. The couple's crest, one slim "now" line, and then every
   pixel that is left goes to the scene: a crowd of guests behind, the
   two of them with the guest's own character in front, an ambient chip
   that suggests what to do, and the VN dock at the bottom when somebody
   is actually talking. QuestStage in portal.js drives it; every id is
   still qs-.

   ZERO SCROLL IS A LAYOUT INVARIANT, not a media query. Home pins #app
   to exactly one viewport, #app-content is a flex column that clips, and
   .hs-scene is the single flex:1 remainder. Nothing on this page can sum
   past the viewport because only ONE box is allowed to grow, and that box
   is measured, never content-sized. Add a row here and the scene shrinks
   -- it does not push a scrollbar.
   ===================================================================== */
body[data-page="home"] { overflow: hidden; }
body[data-page="home"] #app {
    height: 100dvh;
    min-height: 100dvh;
    overflow: hidden;
}
body[data-page="home"] #app-content {
    padding: var(--space-sm) var(--page-pad) calc(var(--nav-height) + 22px);
    display: flex;
    flex-direction: column;
    flex: 1 1 auto;
    min-height: 0;
    overflow: hidden;
}

.hstage {
    position: relative;
    display: flex;
    flex-direction: column;
    flex: 1 1 auto;
    min-height: 0;
    /* the band the dock takes when it opens. QuestStage.syncDockHeight
       measures the real dock and overwrites this; the value here is the
       two-line dock and is only ever the first frame's fallback. */
    --hs-dockh: 132px;
}

/* ---- the crest ---- */
.hs-crest { text-align: center; padding: var(--space-lg) 0 var(--space-md); }
.hs-crest .hs-eyebrow {
    font-family: var(--sans); font-weight: 600;
    font-size: var(--px-xs); letter-spacing: var(--track-eyebrow);
    text-transform: uppercase; color: var(--dim);
}
.hs-crest .hs-names {
    font-family: var(--display);
    font-weight: 600;
    font-size: clamp(30px, 8.5vw, 42px);
    line-height: 1.1;
    color: var(--ink);
    margin-top: 10px;
    white-space: nowrap;
}
/* the ampersand is the ONE script glyph on this page */
.hs-crest .hs-amp {
    font-family: var(--script);
    font-weight: 400;
    font-size: 1.25em;
    color: var(--rose);
    padding: 0 .12em;
    vertical-align: -.06em;
}
.hs-crest .hs-rule {
    display: flex; align-items: center; justify-content: center; gap: 10px;
    margin: 12px auto 0; width: 140px;
}
.hs-crest .hs-rule::before, .hs-crest .hs-rule::after {
    content: ''; flex: 1; height: 1px; background: var(--line);
}
.hs-crest .hs-rule i {
    width: 5px; height: 5px; background: var(--blush);
    transform: rotate(45deg); flex: none;
}
.hs-crest .hs-date {
    font-family: var(--sans); font-weight: 500;
    font-size: var(--text-sm); color: var(--dim);
    margin-top: 12px;
}

/* ---- the "now" strip: one line, dinner-night utility only ---- */
.hs-nowline {
    position: relative;
    flex: none;
    display: flex; align-items: baseline; gap: 8px;
    margin-top: 2px;
    padding: 9px 14px 11px;
    background: var(--card);
    border: 1px solid var(--line);
    border-radius: var(--radius-pill);
    box-shadow: var(--shadow-sm);
    overflow: hidden;
}
.hs-nowline .qp-nowtitle {
    font-size: var(--text-base);
    margin-top: 0;
    flex: 1;
    min-width: 0;
    white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.hs-nowline .qp-nowtime { flex: none; }

/* ---- the strip AS THE JOIN DOOR (.qz-live) --------------------------
   THE ONE THING A TIPSY GUEST MUST FIND AT 9PM. It replaced a card that
   had a pulsing dot, a bold title and a full-width accent button, and the
   first pass replaced all of that with a border tint and 11px grey type
   reading "Tap to join" -- quieter than the thing it stood in for. So the
   whole strip fills rose, the type goes white, and the dot comes back.
   No shouting is still respected: sentence case, no pixel face. */
.hs-nowline.qz-live {
    cursor: pointer;
    background: var(--rose);
    border-color: transparent;
}
.hs-nowline.qz-live .qp-nowtag { color: #fff; }
.hs-nowline.qz-live .qp-nowtitle { color: #fff; }
.hs-nowline.qz-live .qp-nowtime {
    color: #fff;
    font-size: var(--text-sm);
    padding: 3px 11px;
    border-radius: var(--radius-pill);
    background: color-mix(in srgb, #fff 22%, transparent);
}
.hs-nowline.qz-live .qp-track { background: color-mix(in srgb, #fff 28%, transparent); }
.hs-nowline.qz-live .qp-pct { background: #fff; }
.hs-nowline.qz-live:focus-visible { outline: 2px solid var(--rose); outline-offset: 3px; }
/* the live dot: exists only in the live state */
.qp-livedot { display: none; }
.hs-nowline.qz-live .qp-livedot {
    display: block; flex: none;
    width: 8px; height: 8px; border-radius: 50%;
    background: #fff;
    align-self: center;
    animation: qLiveDot 1.4s ease-in-out infinite;
}
@keyframes qLiveDot {
    0%, 100% { opacity: 1;  transform: scale(1); }
    50%      { opacity: .45; transform: scale(.72); }
}
/* a kahoot_url the sheet cannot resolve. Staff-only (see QuestStage.render):
   the quiz does NOT go live, and the crew get told which field to look at. */
.hs-nowline.qz-err { border-color: color-mix(in srgb, var(--gold) 60%, var(--line)); }
.hs-nowline.qz-err .qp-nowtime { color: var(--accent-deep); }
.hs-nowline .qp-track {
    position: absolute; left: 0; right: 0; bottom: 0; height: 2px;
    background: var(--line); overflow: hidden;
}
.hs-nowline .qp-pct { display: block; height: 100%; width: 0%; background: var(--rose); transition: width .5s var(--ease); }

/* =====================================================================
   THE SCENE. One flex:1 box, and everything in it is absolute.

   There is NO background art for this zone and none is coming, so the
   depth is built out of three cheap things: a vertical wash that
   deepens toward the ground, one hairline horizon, and a crowd that
   gets smaller and dimmer as it goes back. The crowd IS the busy-ness;
   the chrome stays quiet on purpose.

   --hs-trio IS THE UNIT OF THE ROOM. Yi Fang's sprite box is the tallest
   thing on stage, so every other height is written as a fraction of it:
   Jiahao, the guest, both crowd bands. Change this one clamp and the
   whole room rescales in proportion -- which is the only way the depth
   hierarchy below stays true on a 320px phone and a 430px one.
   (QuestStage.TRIO_MIN/VW/MAX mirrors these three numbers, because the
   guest's avatar bakes px into its own markup. A test pins the pair.)

   --hs-floor was --hs-dockpad, and the rename is the fix: the dock does
   not live in the scene any more (see .hs-dock), so this is no longer a
   reservation for it. It is the strip of ground the cast stands on. */
.hs-scene {
    position: relative;
    flex: 1 1 auto;
    min-height: 0;
    overflow: hidden;
    margin-top: var(--space-sm);
    margin-bottom: 0;
    --hs-trio: clamp(94px, 29vw, 126px);
    --hs-floor: 44px;
    /* THE COMPRESSION. .hstage--docked hands the dock its band by taking
       it off the scene, and the scene is the single flex grower, so this
       one property moves the trio, their nametags and the whole crowd up
       together. 240ms, per the brief. */
    transition: margin-bottom .24s var(--ease);
}
.hstage--docked .hs-scene { margin-bottom: var(--hs-dockh); }
/* the ground wash -- paper, one shade deeper as it falls */
.hs-ground {
    position: absolute; inset: 0; pointer-events: none;
    background:
        linear-gradient(to bottom,
            transparent 0%,
            transparent 50%,
            color-mix(in srgb, var(--card-warm) 55%, transparent) 80%,
            color-mix(in srgb, var(--card-warm) 88%, transparent) 100%),
        radial-gradient(120% 62% at 50% 108%, color-mix(in srgb, var(--blush) 34%, transparent), transparent 70%);
}
/* one hairline, fading at both ends. That is the whole horizon. It sits
   just above the far band's heads now that the crowd stands further back;
   at 44% it was floating well clear of anything and read as a stray rule. */
.hs-horizon {
    position: absolute; left: 0; right: 0; top: 52%; height: 1px;
    pointer-events: none;
    background: linear-gradient(90deg, transparent, var(--line) 22%, var(--line) 78%, transparent);
}

/* ---- the crowd (behind everything) ---- */
.hs-crowd { position: absolute; inset: 0; pointer-events: none; }
.hs-cw {
    position: absolute;
    display: block;
    padding: 0; border: 0; background: none;
    transform: translateX(-50%);
    animation: qCrowdBob var(--bd, 5s) ease-in-out var(--dl, 0s) infinite;
    -webkit-tap-highlight-color: transparent;
}
.hs-cw img {
    display: block;
    width: 100%; height: 100%;
    object-fit: contain; object-position: bottom center;
    image-rendering: pixelated;      /* SPRITES keep their pixels */
    transform: scaleX(var(--fx, 1));
    pointer-events: none;
}
/* ---- THE DEPTH HIERARCHY (owner, Aug 2026) --------------------------
   "really cramped... no separation of the couple + user's avatar VS the
   rest behind."

   And he was right: the crowd was drawn at .55 and .40 of the SAME clamp
   the foreground used, standing 42px and 96px above a floor the trio's
   feet were only ~21px above. A near-band guest was two thirds of the
   couple's height, one sprite-width behind them, at 80% opacity. That is
   not a background, that is a fourth row of cast.

   The three levers, all pulled at once, all off --hs-trio:

     SIZE      .34 near / .26 far (was .55 / .40 of a smaller unit)
     DISTANCE  the near band's feet sit ~86px above the floor while the
               trio's feet sit ~21px above it -- 65px of clear ground at
               375x812, where the brief asked for 48
     LIGHT     .65 / .45 opacity (was .80 / .55), on top of the existing
               desaturation

   Read them together and the trio is unmistakably the foreground. */
.hs-cw--b1 {
    width: calc(var(--hs-trio) * .34);
    height: calc(var(--hs-trio) * .34);
    bottom: calc(var(--hs-floor) + 86px);
    opacity: .65;
    filter: saturate(.86);
}
.hs-cw--b2 {
    width: calc(var(--hs-trio) * .26);
    height: calc(var(--hs-trio) * .26);
    bottom: calc(var(--hs-floor) + 148px);
    opacity: .45;
    /* NO BLUR. These sprites are animated APNGs, so a blur filter makes the
       compositor re-rasterise every one of them on every frame of their own
       loop -- the most expensive half-pixel of depth on the page. Smaller,
       dimmer and desaturated carries the distance on its own. */
    filter: saturate(.7);
}
/* THE NARROW CAP. Twelve guests across a 375px phone is a queue, not a
   crowd; four of them (HOME_CROWD `wide`) stand down under 400px, leaving
   four per band and never the one you can talk to. */
@media (max-width: 399px) {
    .hs-cw--wide { display: none; }
}
/* the ONE crowd guest you can talk to: a real button, a real 44px target.
   It is the brightest thing in the crowd and still dimmer than the trio --
   it used to be opacity .92 with no desaturation at all, which put a crowd
   sprite in the same light as the couple. */
.hs-cw--tap {
    pointer-events: auto;
    cursor: pointer;
    min-width: 44px; min-height: 44px;
    opacity: .78; filter: saturate(.9);
    transition: transform var(--dur-med), opacity var(--dur-med), filter var(--dur-med);
}
.hs-cw--tap:focus-visible { outline: 2px solid var(--rose); outline-offset: 3px; border-radius: var(--radius-sm); }
.hs-cw--tap.dim { opacity: .5; filter: saturate(.7); }
.hs-cw--tap.talk { transform: translateX(-50%) translateY(-5px); }
@keyframes qCrowdBob { 0%, 100% { translate: 0 0; } 50% { translate: 0 -3px; } }

/* ---- the field: whatever the crowd leaves, the trio takes ---- */
.hs-field {
    position: absolute;
    left: 0; right: 0; bottom: var(--hs-floor); top: 0;
    display: flex; align-items: flex-end; justify-content: center;
    pointer-events: none;
}

/* ---- the cast: the two of them, and you ---- */
.hs-cast {
    position: relative;
    display: flex; align-items: flex-end; justify-content: center;
    /* tighter than it was (3vw): the guest's character grew by ~40px and
       the row has to hold three characters at full height on a 320px
       phone without any of them shrinking */
    gap: clamp(2px, 2vw, 14px);
    width: 100%;
    pointer-events: auto;
}
/* the soft ground they stand on -- a wash, not a painted floor */
.hs-cast::after {
    content: '';
    position: absolute; left: 8%; right: 8%; bottom: 14px; height: 26px;
    background: radial-gradient(60% 100% at 50% 100%, color-mix(in srgb, var(--blush) 55%, transparent), transparent 72%);
    pointer-events: none;
}
.hs-who {
    position: relative;
    -webkit-appearance: none; appearance: none;
    /* 1px, not 2: the guest's character is ~45px wider than it was and the
       row has to hold three characters at full height on a 320px phone */
    background: none; border: 0; padding: 0 1px;
    display: flex; flex-direction: column; align-items: center; gap: 6px;
    min-width: 44px; min-height: 44px;
    cursor: pointer;
    -webkit-tap-highlight-color: transparent;
    transform-origin: bottom center;
    transition: transform var(--dur-med), opacity var(--dur-med), filter var(--dur-med);
}
.hs-who:focus-visible { outline: 2px solid var(--rose); outline-offset: 4px; border-radius: var(--radius-sm); }
.hs-who.dim { opacity: .55; filter: saturate(.7); }
.hs-who.talk { transform: translateY(-6px); }
/* THE BOXES ARE THE ART NOW. Every sprite box used to be square, which
   meant Yi Fang carried ~12px of empty box either side of her and the
   mushroom carried ~17px, and those 30 wasted pixels were exactly what
   the guest's character needed to stand at their height. Each box is
   sized to its own frame sheet's aspect instead:
     Yi Fang  156x176  ->  w = h * .886
     Jiahao   126x112  ->  w = h * 1.125, at .65 of the trio (he is a
                           short round mushroom; matching HEIGHT would
                           make him a metre wide)
   which also retires the .82 shrink that used to live on his <img>. */
.hs-sprbox { display: block; position: relative; animation: qBob 5s ease-in-out infinite; }
#qs-mush .hs-sprbox {
    height: calc(var(--hs-trio) * .65);
    width: calc(var(--hs-trio) * .65 * 1.125);
}
#qs-vio  .hs-sprbox {
    height: var(--hs-trio);
    width: calc(var(--hs-trio) * .886);
    animation-duration: 5.6s;
}
.hs-sprbox img {
    position: absolute; inset: 0; width: 100%; height: 100%;
    object-fit: contain; object-position: bottom center;
    image-rendering: pixelated;      /* SPRITES keep their pixels */
    pointer-events: none;
}
#qs-mush .hs-sprbox img { transform: scaleX(-1); transform-origin: bottom center; }
.hs-tag {
    font-family: var(--sans); font-weight: 600;
    font-size: var(--px-xs); letter-spacing: .04em;
    color: var(--ink-soft);
}
/* THE GUEST'S OWN CHARACTER, at the couple's height (owner: "the user
   avatar should be big too"). The BOX is sized in JS, not here: every
   avatar type bakes its own px into its markup, and a guest's art may be
   a wide sprite, a tall sprite, a cropped photo or a bobblehead composite,
   each of which fills a contain box differently. QuestStage.guestPx()
   solves the box backwards from a target drawn height of .92 x --hs-trio;
   see GUEST_FILL there for the per-type measurements. */
.hs-who--you { padding-bottom: 2px; }
.hs-who--you .hs-tag { color: var(--rose); }
.hs-guestbox { display: block; animation: qBob 5.2s ease-in-out infinite; }
.hs-guestbox img, .hs-guestbox .mbox { display: block; image-rendering: pixelated; }
@keyframes qBob { 0%, 100% { transform: translateY(0); } 50% { transform: translateY(-4px); } }

/* ---- the tap prompt -------------------------------------------------
   A pip is a PROMISE that a sprite does something. It is per sprite and
   it retires the first time that sprite is tapped (localStorage), so a
   guest who already knows is never nagged again. */
.hs-pip {
    position: absolute; top: -4px; left: 50%;
    margin-left: -6px;
    width: 12px; height: 12px; line-height: 12px; text-align: center;
    font-family: var(--sans); font-size: 12px;
    color: var(--rose);
    pointer-events: none;
    animation: qPipBob 1.5s ease-in-out infinite;
}
#qs-vio .hs-pip { animation-delay: .35s; }
#qs-you .hs-pip { animation-delay: .7s; }
#qs-crowdtap .hs-pip { animation-delay: 1.05s; top: -10px; }
@keyframes qPipBob {
    0%, 100% { transform: translateY(0); opacity: .9; }
    50%      { transform: translateY(4px); opacity: .5; }
}

/* ---- AMBIENT: the room talking ---------------------------------------
   It must never read as dialogue. No portrait, no nameplate, no button,
   no card: just dim italic UI type.

   IT IS A ROW, NOT AN OVERLAY (owner, Aug 2026: "the random text... is
   overlapping, i dont like the position"). It used to be absolutely
   positioned inside .hs-scene, 30px above the dock's floor line, which
   put it straight through the trio's nametags on a short phone and over
   the crowd on a tall one. It now owns a slot of its own in the column,
   in the airspace between the now strip and the scene, where it cannot
   overlap anything because nothing else is in its row.

   flex: none is load-bearing -- see the "exactly three growers" invariant
   at the top of this section. A row that grows would break zero-scroll.

   .hidden does NOT collapse it. While somebody is talking the line is
   invisible but its row still stands, so opening the dock does not also
   jolt the whole column up by 44px. */
.hs-amb {
    position: relative;
    flex: none;
    display: flex; align-items: center; justify-content: center;
    min-height: 44px;
    padding: 8px 10px 4px;
    text-align: center;
    cursor: pointer;
    -webkit-tap-highlight-color: transparent;
    transition: opacity .2s var(--ease);
}
.hs-amb.hidden { opacity: 0; pointer-events: none; }
.hs-amb.in .hs-ambtext { animation: qAmbIn .5s var(--ease) both; }
.hs-amb:focus-visible { outline: 2px solid var(--rose); outline-offset: 4px; border-radius: var(--radius-pill); }
.hs-ambtext {
    font-family: var(--sans);
    font-style: italic;
    font-weight: 500;
    font-size: 13.5px;
    line-height: 1.45;
    color: color-mix(in srgb, var(--ink) 65%, transparent);
    text-shadow: 0 1px 0 color-mix(in srgb, var(--paper) 70%, transparent);
    max-width: 34ch;
}
@keyframes qAmbIn { from { opacity: 0; transform: translateY(5px); } to { opacity: 1; transform: translateY(0); } }

/* ---- THE DOCK: the VN rectangle -------------------------------------
   Owner, Aug 2026: "the dialogue box is overlapping everything."

   It used to be absolute at the foot of the SCENE, painted over the floor
   the trio stand on. It is absolute at the foot of the STAGE now -- below
   the scene, above the tab bar -- and it never overlaps the room, because
   .hstage--docked shortens .hs-scene by exactly --hs-dockh first. The
   band the dock occupies is a band the scene has given up.

   min-height holds the measured band even while the typewriter has the
   text half-written, so the dock does not visibly grow as it types. */
.hs-dock {
    position: absolute;
    left: 0; right: 0; bottom: 0;
    z-index: 5;
    min-height: var(--hs-dockh);
    background: var(--card);
    border: 1px solid var(--line);
    border-radius: var(--radius);
    box-shadow: var(--shadow);
    padding: 14px 16px 12px;
    cursor: pointer;
    animation: qDockIn .22s var(--ease) both;
}
.hs-dock.hidden { display: none; }
@keyframes qDockIn { from { opacity: 0; transform: translateY(10px); } to { opacity: 1; transform: translateY(0); } }
.qp-nowtag {
    font-family: var(--sans); font-weight: 600;
    font-size: var(--px-xs); letter-spacing: var(--track-eyebrow);
    text-transform: uppercase;
    color: var(--rose);
    background: none; border: 0; padding: 0;
}
.qp-nowtime { font-family: var(--sans); font-weight: 600; font-size: var(--px-xs); letter-spacing: .06em; color: var(--dim); }
.qp-nowtitle { font-family: var(--display); font-weight: 600; font-size: var(--text-xl); line-height: 1.15; margin-top: 6px; color: var(--ink); }
@keyframes qCaret { 0%, 100% { opacity: 1; } 50% { opacity: .45; } }

/* the dock, in dialogue mode. THE OTHER TREATMENT: a name, a portrait
   and a button -- everything the ambient chip deliberately refuses.

   TWO COLUMNS. The portrait used to `float: left` and the name and the
   dialogue flowed AROUND it, so any line long enough wrapped underneath
   the picture -- the owner's "the word schedule cuts below the profile
   picture". A float cannot be told not to do that. A grid can: the
   portrait has a fixed column, the name / text / CTA share the other one,
   and there is no way for content to reach the space under the picture
   because that space belongs to the portrait's column. */
.qp-dlg {
    display: grid;
    grid-template-columns: 44px minmax(0, 1fr);
    gap: 12px;
    align-items: start;
}
.qp-body { min-width: 0; }
.qp-dlgname {
    display: block;
    font-family: var(--sans); font-weight: 600;
    font-size: var(--px-xs); letter-spacing: var(--track-eyebrow);
    text-transform: uppercase;
    color: var(--vio-deep);
    background: none; border: 0; padding: 0;
}
.qp-dlgname--mush  { color: var(--mush-deep); }
.qp-dlgname--you   { color: var(--accent-deep); }
.qp-dlgname--crowd { color: var(--dim); }
.qp-port {
    /* its own grid column -- the float is what let text under it */
    width: 44px; height: 44px;
    margin: 0;
    position: relative;
    background: var(--card-2);
    border: 1px solid var(--line);
    border-radius: var(--radius-sm);
    overflow: hidden;
}
.qp-port img, .qp-port .mbox {
    position: absolute; inset: 3px;
    width: calc(100% - 6px); height: calc(100% - 6px);
    object-fit: contain; object-position: bottom center;
    image-rendering: pixelated;
}
/* A REAL FADE, on the one event that warrants one. This used to be a bare
   `transition: opacity` left over from a twin cross-fade that no longer
   exists -- there is only ever one portrait in the box, and nothing
   animated its opacity, so it cost a comment's worth of confusion and
   bought nothing. render() now rebuilds the portrait ONLY when the
   speaker changes, and adds this class when it does. */
.qp-port--swap img, .qp-port--swap .mbox { animation: qPortIn .18s var(--ease) both; }
@keyframes qPortIn { from { opacity: 0; } to { opacity: 1; } }
.qp-dlgtext {
    margin: 4px 0 0;
    font-family: var(--sans);
    font-size: var(--text-base); line-height: 1.5;
    /* two lines, not three: every row the dock does not need is a row of
       scene the trio keeps, because the scene pays for this band */
    min-height: 2.6em;
    color: var(--ink);
}
/* the CTA sits UNDER the text, right-aligned, and inside the content
   column -- never beside the portrait, never below it */
.qp-dlgfoot { display: flex; justify-content: flex-end; gap: 10px; margin-top: 8px; min-height: 36px; align-items: center; }
.qp-act {
    font-family: var(--sans); font-weight: 600; font-size: var(--px-sm);
    color: #fff; background: var(--rose);
    border: 0; border-radius: var(--radius-pill);
    box-shadow: none;
    padding: 9px 16px;
    min-height: 36px;
    display: inline-flex; align-items: center;
    cursor: pointer;
}
.qp-caret { color: var(--blush); font-size: 13px; animation: qCaret 1.4s ease-in-out infinite; }

/* THE TWO CARDS ARE GONE (owner, Aug 2026): "remove the 'leave a wish'
   and 'our little adventure' part, give more canvas space for the
   mushrooms and the character on screen". Both doors still exist -- the
   wishing wall is the bottom nav AND the front mushroom's route, and the
   chapters are the Play tab AND an ambient suggestion. */

/* demo scrubber -- staff only, behind a long-press on the date line */
.qs-scrub {
    position: absolute; top: 8px; right: 0; z-index: 30;
    width: 248px; padding: 14px 16px;
    background: var(--card);
    border: 1px solid var(--line);
    border-radius: var(--radius);
    box-shadow: var(--pop-shadow);
    animation: qPopInPlain .22s var(--ease) both;
    cursor: default;
}
.qs-scrub .sc-eyebrow {
    font-family: var(--sans); font-weight: 600;
    font-size: var(--px-xs); letter-spacing: var(--track-eyebrow);
    text-transform: uppercase; color: var(--dim);
}
.qs-scrub input[type="range"] { width: 100%; margin: 12px 0 6px; accent-color: var(--rose); }
.qs-scrub .sc-row { display: flex; justify-content: space-between; align-items: center; }
.qs-scrub .sc-read { font-family: var(--sans); font-weight: 600; font-size: var(--px-md); color: var(--ink); }
.qs-scrub .sc-live {
    font-family: var(--sans); font-weight: 600; font-size: var(--px-xs);
    color: var(--ink-soft); background: var(--card);
    border: 1px solid var(--line); border-radius: var(--radius-pill);
    padding: 6px 12px; cursor: pointer;
    box-shadow: none;
}
.qs-scrub .sc-live.on { color: #fff; background: var(--rose); border-color: transparent; }
.qs-scrub .sc-note {
    margin-top: 10px; padding-top: 9px;
    border-top: 1px solid var(--line);
    font-family: var(--sans); font-size: var(--text-xs); line-height: 1.4;
    color: var(--dim);
}
.qs-scrub .sc-note b { color: var(--ink); font-weight: 600; }
@keyframes qPopInPlain { from { opacity: 0; transform: translateY(6px); } to { opacity: 1; transform: translateY(0); } }

/* ---------------- Play tab: stage games readout ---------------- */
.stage-card {
    display: block;
    text-decoration: none;
    color: inherit;
    background: var(--card);
    border: 1px solid var(--line);
    border-radius: var(--radius);
    box-shadow: var(--shadow);
    overflow: hidden;
    margin-bottom: var(--space-md);
}
.stage-card .sg-trim { display: none; }
.stage-card .sg-row { padding: 16px 18px; display: flex; align-items: center; justify-content: space-between; gap: 12px; }
.stage-card .sg-t {
    font-family: var(--sans); font-weight: 600;
    font-size: var(--px-xs); letter-spacing: var(--track-eyebrow);
    text-transform: uppercase; color: var(--dim);
    display: block; line-height: 1.3;
}
.stage-card .sg-note {
    font-family: var(--display); font-weight: 600;
    font-size: var(--text-lg); color: var(--ink);
    display: block; margin-top: 4px; text-transform: none;
}
.stage-card .sg-note.open { color: var(--accent-deep); }
.stage-card .sg-badge {
    font-family: var(--sans); font-weight: 600; font-size: var(--px-xs);
    letter-spacing: var(--track-eyebrow); text-transform: uppercase;
    border: 1px solid var(--line); border-radius: var(--radius-pill);
    padding: 7px 13px;
    white-space: nowrap;
}
.stage-card .sg-badge--ready { color: #fff; background: var(--rose); border-color: transparent; box-shadow: none; }
.stage-card .sg-badge--locked { color: var(--dim); background: var(--card-2); }

/* short phones: the crest gives ground first, the scene never does.
   The scene is the flex remainder, so every pixel the crest hands back
   goes straight to the room -- nothing here has to be re-measured. */
@media (max-height: 700px) {
    .hs-crest { padding: var(--space-md) 0 var(--space-sm); }
    .hs-crest .hs-names { font-size: clamp(26px, 7.5vw, 34px); }
    .hs-crest .hs-date { margin-top: 8px; }
    .hs-scene { --hs-floor: 32px; }
    /* the gap between the crowd's feet and the trio's has to survive a
       short phone -- these keep it at ~54px and ~48px, the floor of the
       brief, rather than letting the bands slide back down onto the cast */
    .hs-cw--b1 { bottom: calc(var(--hs-floor) + 72px); }
    .hs-cw--b2 { bottom: calc(var(--hs-floor) + 124px); }
    .hs-amb { min-height: 38px; padding-top: 4px; }
}
@media (max-height: 620px) {
    .hs-crest { padding: var(--space-sm) 0 4px; }
    .hs-crest .hs-rule { display: none; }
    .hs-scene { --hs-floor: 24px; }
    .hs-cw--b1 { bottom: calc(var(--hs-floor) + 76px); }
    .hs-cw--b2 { bottom: calc(var(--hs-floor) + 128px); }
}

/* =====================================================================
   THE FLOATING QUIZ PILL (Portal.paintQuizPill)

   The global half of the join door. Home has the strip; this is what the
   other pages get, and it exists because the MC flips the quiz on while
   half the room is reading the wishing wall. It floats ABOVE the tab bar,
   never over it, and never appears inside the journey or the PlayStory
   takeover. Rose fill and a live dot: quiet chrome, unmissable colour.
   ===================================================================== */
.qz-pill {
    position: fixed;
    left: 50%;
    bottom: calc(var(--nav-height) + 10px);
    transform: translateX(-50%);
    z-index: 70;                     /* under the nav (90), over the page */
    display: flex; align-items: center; gap: 2px;
    max-width: calc(100vw - 24px);
    padding: 4px;
    background: var(--card);
    border: 1px solid color-mix(in srgb, var(--rose) 35%, var(--line));
    border-radius: var(--radius-pill);
    box-shadow: var(--shadow);
    animation: qzPillIn .35s var(--ease) both;
}
/* the auth-retry banner claims the same slot; stack rather than overlap */
.qz-pill--stacked { bottom: calc(var(--nav-height) + 78px); }
@keyframes qzPillIn {
    from { transform: translateX(-50%) translateY(10px); opacity: 0; }
    to   { transform: translateX(-50%) translateY(0);    opacity: 1; }
}
.qz-go {
    display: flex; align-items: center; gap: 8px;
    min-height: 40px;
    padding: 0 16px;
    border: 0; border-radius: var(--radius-pill);
    background: var(--rose); color: #fff;
    font-family: var(--sans); font-weight: 600; font-size: var(--text-sm);
    white-space: nowrap;
    cursor: pointer;
    box-shadow: none;
}
.qz-go:active { background: var(--accent-deep); }
.qz-sep { opacity: .6; }
.qz-dot {
    flex: none; width: 8px; height: 8px; border-radius: 50%;
    background: #fff;
    animation: qLiveDot 1.4s ease-in-out infinite;
}
.qz-x {
    flex: none;
    min-width: 36px; min-height: 40px;
    padding: 0 6px;
    background: none; border: 0;
    color: var(--dim); font-size: 14px; line-height: 1;
    cursor: pointer;
}
.qz-go:focus-visible, .qz-x:focus-visible { outline: 2px solid var(--rose); outline-offset: 2px; }

/* ---------------- reduced motion ----------------
   The pips go static (they still point, they just stop bouncing) and the
   ambient chip plain-fades instead of typing (that half is in JS).

   The CROWD is the case CSS cannot win: `animation: none` stops the bob,
   but those sprites are APNGs and no stylesheet can pause a PNG loop.
   QuestStage.freezeCrowd() draws frame one to a canvas and swaps the img.
   The rule below is still correct, it is just not the whole job. */
@media (prefers-reduced-motion: reduce) {
    .hs-sprbox, .hs-guestbox, .qp-caret, .hs-pip, .hs-cw, .hs-dock,
    .hs-amb.in .hs-ambtext, .qp-livedot, .qz-dot, .qz-pill,
    .qp-port--swap img, .qp-port--swap .mbox,
    .stage-card .sg-badge--ready, #cam-pop { animation: none !important; }
    .hs-who, .hs-cw--tap, .btn { transition: none; }
    #app-sky { transition: none; }
    .hs-nowline .qp-pct { transition: none; }
    /* the scene still gives the dock its band -- it just does it at once */
    .hs-scene { transition: none; }
}
