/* ============================================================================
   home.painterly.css — Fable-lore painterly restyle of the lobby.
   A/B test: in home.html swap the <link> between ./home.css (old) and
   ./home.painterly.css (this). Markup is shared; this file is self-contained.

   Register: aged parchment + candle-warm gold on deep umber, engraved serif
   display (Cinzel), humanist serif body (EB Garamond), a painted hero bg,
   vignette + film grain. No emoji, no SaaS blue, no glassmorphism.
   ============================================================================ */

/* ---- design tokens ---- */
:root {
    /* dark UA widgets: autofill dropdown, scrollbars, native controls */
    color-scheme: dark;

    /* The act-1 lobby diorama backdrop doubles as the page art everywhere the
       3D stage isn't live (logged out, transitions, no-WebGL) — one world,
       not two paintings. Re-rendered by assets/scenes/lobby/act1.py. */
    --bg-art: url("/viewer/lobby/act1.jpg");

    --ink:        #ece3cf;   /* parchment text */
    --ink-dim:    #b3a684;   /* muted label */
    --ink-faint:  #80765e;   /* hints / footers */
    --gold:       #c8a45c;   /* primary accent */
    --gold-bright:#ecca80;   /* hover / glow */
    --gold-deep:  #8a6d36;   /* borders at rest */
    --danger:     #b5503f;   /* oxblood */
    --moss:       #7c9466;   /* info / positive */

    --bg-0:       #0c0a07;   /* deepest umber */
    --panel:      rgba(26, 21, 15, 0.82);
    --panel-edge: rgba(200, 164, 92, 0.28);
    --panel-edge-hi: rgba(236, 202, 128, 0.7);

    --font-title: 'Cinzel Decorative', 'Cinzel', serif;
    --font-head:  'Cinzel', serif;
    --font-body:  'EB Garamond', Georgia, serif;
    --font-script:'Tangerine', cursive;

    --radius: 4px;
    --ease: cubic-bezier(.2,.7,.3,1);
}

* { box-sizing: border-box; }
[v-cloak] { display: none; }

/* ---- page + painted background ---- */
html, body {
    margin: 0; padding: 0;
    /* NO `min-width: 100vw`. 100vw INCLUDES the scrollbar gutter, so the
       moment anything makes the page scroll vertically the body is laid out
       wider than the visible area — the whole centred lobby sat off to the
       left until the scrollbar went away, then jumped right. Block-level body
       is already the full content width. */
    min-height: 100vh;
    color: var(--ink);
    font-family: var(--font-body);
    font-size: 17px;
    letter-spacing: .01em;
    user-select: none;
    overflow-x: hidden;
    background: var(--bg-0);
}

/* layered backdrop: art → warm wash → vignette → grain.
   NB: pseudos use NON-negative z-index; #ui is lifted to z-index:1. A
   negative-z-index pseudo would paint behind body's opaque --bg-0 fill. */
/* PERF: this layer drifts forever (kenburns) — it must stay transform-only.
   No `filter:` here (forces per-frame re-raster) and nothing blended above
   it (mix-blend-mode over a moving layer re-blends the full viewport every
   frame → a Chrome core pinned at 100% on an idle lobby, 2026-07-07). */
body::before {
    content: "";
    position: fixed; inset: 0; z-index: 0; pointer-events: none;
    /* the art is already moody + vignetted — keep washes light so it reads */
    background:
        linear-gradient(180deg, rgba(8,7,12,0.45) 0%, rgba(8,6,8,0.10) 30%, rgba(6,5,4,0.30) 100%),
        var(--bg-art) center center / cover no-repeat;
    animation: kenburns 60s ease-in-out infinite alternate;
    will-change: transform;
}
/* deep vignette + film grain on top of the art — plain low-opacity overlay,
   NOT mix-blend-mode (see perf note above) */
body::after {
    content: "";
    position: fixed; inset: 0; z-index: 0; pointer-events: none;
    background:
        radial-gradient(ellipse 90% 85% at 50% 50%, transparent 62%, rgba(6,4,3,0.7) 100%),
        url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='120' height='120'><filter id='n'><feTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='2'/></filter><rect width='100%25' height='100%25' filter='url(%23n)' opacity='0.5'/></svg>");
    background-size: cover, 180px 180px;
    opacity: .28;
    transition: opacity .5s ease;
}
/* `art-ready` is set by the head script when the backdrop image has decoded.
   Before that there is nothing under this layer but flat umber, and grain +
   vignette on flat umber is a grey wash over the whole window — which is the
   "whitish overlay" the lobby opened on. */
html:not(.art-ready) body::after { opacity: 0; }
@keyframes kenburns {
    from { transform: scale(1.0) translate(0, 0); }
    to   { transform: scale(1.06) translate(-1.2%, -1%); }
}

#ui {
    position: relative; z-index: 1;   /* above the backdrop pseudos */
    min-height: 100vh;
    display: flex; flex-direction: column; align-items: center;
    padding: 24px;
    animation: rise .9s var(--ease) both;
}
/* Opacity only — deliberately NOT a translate. `#ui` is min-height:100vh, so
   nudging it down 10px made the document 100vh+10px tall for the length of
   the animation: a scrollbar appeared and then vanished at 0.9 s, which is
   what shifted the whole lobby sideways just after it finished loading. */
@keyframes rise { from { opacity: 0; } to { opacity: 1; } }

/* ---- title ---- */
#title {
    line-height: 0;
    text-align: center;
    position: relative;
    isolation: isolate; /* keeps the ::before glow behind the glyphs, not behind #ui */
    margin: 40px 0 32px;
}
#title img {
    /* painted wordmark ships its own gold glow + teal wisp base */
    width: clamp(280px, 42vw, 620px);
    height: auto;
}
/* Static dark scrim behind the wordmark — the painted gold ships its own
   glow; this just deepens the bg under it so gold never sits on gold. */
#title::before {
    content: "";
    position: absolute; inset: -22% -6%; z-index: -1; pointer-events: none;
    background: radial-gradient(ellipse 60% 75% at 50% 55%,
        rgba(8,7,5,0.45), rgba(8,7,5,0.22) 55%, transparent 78%);
}
.dim { color: var(--ink-faint); font-size: 13px; font-style: italic; }
.warn { color: var(--danger); }

/* ---- shared panel skin ---- */
.modal-content, #connection, .menu_button, #description, .player-card,
.rail-section, .game-card, .class-card, .alert, .mode-option, .server-choice, .region_choice {
    background-color: var(--panel);
    border: 1px solid var(--panel-edge);
    border-radius: var(--radius);
    box-shadow:
        inset 0 1px 0 rgba(255,235,190,0.06),
        inset 0 0 28px rgba(0,0,0,0.45),
        0 8px 28px rgba(0,0,0,0.5);
    backdrop-filter: blur(2px);
}

/* ---- inputs ---- */
input {
    font-family: var(--font-body);
    padding: 11px 14px; margin: 6px 4px;
    font-size: 16px; color: var(--ink);
    background: rgba(12,9,6,0.7);
    border: 1px solid var(--gold-deep);
    border-radius: var(--radius);
    transition: border-color .18s, box-shadow .18s;
}
input:focus {
    outline: none;
    border-color: var(--gold);
    box-shadow: 0 0 0 1px rgba(200,164,92,0.35), 0 0 14px rgba(200,164,92,0.25);
}
input::placeholder { color: rgba(180,166,132,0.45); font-style: italic; }
.input_error { border-color: var(--danger) !important; }

/* Chrome paints autofilled fields with its own light background + near-black
   text; `background` can't override it — only an inset shadow can. The long
   transition kills the yellow flash on submit. */
input:-webkit-autofill,
input:-webkit-autofill:hover,
input:-webkit-autofill:focus,
input:-webkit-autofill:active {
    -webkit-text-fill-color: var(--ink);
    caret-color: var(--ink);
    box-shadow: inset 0 0 0 1000px rgba(12,9,6,0.92);
    transition: background-color 9999s ease-in-out 0s;
}
input:-webkit-autofill:focus {
    box-shadow:
        inset 0 0 0 1000px rgba(12,9,6,0.92),
        0 0 0 1px rgba(200,164,92,0.35),
        0 0 14px rgba(200,164,92,0.25);
}

/* ---- buttons: parchment plates with a gold edge ---- */
.btn {
    position: relative;
    padding: 12px 26px;
    border: 1px solid var(--gold-deep);
    border-radius: var(--radius);
    font-family: var(--font-head);
    font-weight: 600;
    font-size: 15px;
    letter-spacing: .08em;
    cursor: var(--cur-point, pointer);
    color: var(--ink);
    background:
        linear-gradient(180deg, rgba(48,38,24,0.92), rgba(28,21,13,0.96));
    box-shadow: inset 0 1px 0 rgba(255,235,190,0.10), 0 3px 10px rgba(0,0,0,0.45);
    transition: color .15s, border-color .15s, box-shadow .2s, transform .12s var(--ease);
    margin: 5px;
    display: inline-flex; align-items: center; justify-content: center; gap: 8px;
}
.btn:hover {
    color: var(--gold-bright);
    border-color: var(--gold);
    box-shadow: inset 0 1px 0 rgba(255,235,190,0.14), 0 0 18px rgba(200,164,92,0.35);
}
.btn:active { transform: translateY(1px); }

.btn-primary {
    color: #1a130a;
    background: linear-gradient(180deg, var(--gold-bright), var(--gold) 55%, var(--gold-deep));
    border-color: var(--gold-bright);
    text-shadow: 0 1px 0 rgba(255,245,215,0.5);
    font-weight: 700;
}
.btn-primary:hover {
    color: #1a130a;
    box-shadow: 0 0 24px rgba(236,202,128,0.55), inset 0 1px 0 rgba(255,250,230,0.5);
}
.btn-secondary { /* inherits the parchment plate */ }

/* Google sign-in deliberately keeps the parchment plate instead of Google's
   white Roboto brand button — that button in this lobby reads as a broken
   third-party embed. The coloured mark alone carries the recognition, which is
   the part players actually scan for. Full width because it stands for both
   "log in" and "create account", so it can't sit in the two-up row above. */
.btn-google { width: 100%; margin-left: 0; margin-right: 0; font-size: 13px; }
/* Gold disc, dark glyph — the same plate as .btn-primary, one size down. The
   white disc it replaced was the one spot on the whole lobby that wasn't in
   the palette, so it read as a pasted-in third-party badge. */
.btn-google .g-mark {
    display: inline-flex; align-items: center; justify-content: center;
    width: 18px; height: 18px; border-radius: 50%;
    background: linear-gradient(180deg, var(--gold-bright), var(--gold) 55%, var(--gold-deep));
    color: #1a130a;
    font-family: var(--font-head); font-weight: 700; font-size: 12px;
    letter-spacing: 0;
    text-shadow: 0 1px 0 rgba(255,245,215,0.45);
    box-shadow: inset 0 1px 0 rgba(255,250,230,0.5), 0 1px 3px rgba(0,0,0,0.45);
}
/* Warms with the label on hover, like every other gold element here. */
.btn-google:hover .g-mark { box-shadow: inset 0 1px 0 rgba(255,250,230,0.6), 0 0 10px rgba(236,202,128,0.5); }
.btn-danger {
    color: #f6e3dd;
    background: linear-gradient(180deg, rgba(150,64,50,0.95), rgba(96,38,30,0.96));
    border-color: rgba(181,80,63,0.8);
}
.btn-danger:hover { color: #fff; box-shadow: 0 0 18px rgba(181,80,63,0.5); }

.btn.large { padding: 15px 36px; font-size: 17px; letter-spacing: .12em; }
.btn.hero {
    padding: 18px 72px; font-size: 21px; letter-spacing: .16em;
    min-width: 300px;
}
.btn-compact { padding: 8px 20px; font-size: 13px; font-weight: 500; margin: 0; }
/* quiet destructive action — oxblood text on the parchment plate, not a red slab */
.btn-quiet-danger { color: var(--danger); border-color: rgba(181,80,63,0.45); }
.btn-quiet-danger:hover { color: #e8907f; border-color: var(--danger); box-shadow: 0 0 14px rgba(181,80,63,0.35); }
.btn.disabled { opacity: 0.4; pointer-events: none; filter: grayscale(0.6); }

/* ---- account chip (top-right, logged in) ---- */
#connection {
    position: fixed; top: 16px; right: 16px;
    padding: 14px 18px;
    display: flex; flex-direction: column; align-items: center; gap: 10px;
    min-width: 170px;
    z-index: 10;
}
.conn-row {
    display: flex; align-items: center; justify-content: space-between;
    gap: 14px; width: 100%;
}
#pseudo { font-family: var(--font-head); font-size: 17px; color: var(--gold-bright); font-weight: 600; cursor: var(--cur-point, pointer); letter-spacing: .04em; }
#pseudo:hover { text-shadow: 0 0 12px rgba(236,202,128,0.5); }

/* ---- top-left menu ---- */
#top_left_menu {
    position: fixed; top: 16px; left: 16px;
    display: flex; gap: 8px;
    z-index: 10;
}
.menu_button {
    padding: 11px 22px;
    cursor: var(--cur-point, pointer);
    font-family: var(--font-head);
    font-size: 14px;
    font-weight: 600;
    letter-spacing: .08em;
    color: var(--ink-dim);
    transition: color .15s, border-color .15s, box-shadow .2s;
}
.menu_button:hover { color: var(--gold-bright); border-color: var(--gold); box-shadow: 0 0 14px rgba(200,164,92,0.3); }

/* ---- description / welcome ---- */
#description {
    text-align: center; max-width: 580px; margin: 28px auto;
    padding: 30px 38px;
    position: relative; z-index: 2;   /* above the ambient 3D stage (fixed, z 0) */
}
#description h2 {
    font-family: var(--font-head); font-weight: 600;
    margin: 0 0 14px; color: var(--gold-bright);
    font-size: 27px; letter-spacing: .04em;
}
#description p { margin: 8px 0; color: var(--ink-dim); font-size: 18px; }

/* ---- centered login (logged out) ---- */
.login-panel { padding: 34px 44px 26px; }
.login-panel p { line-height: 1.45; font-size: 16px; }
.login-panel .dim { margin-top: 16px; }
.login-form {
    display: flex; flex-direction: column; gap: 2px;
    max-width: 360px; margin: 20px auto 0;
}
.login-form input { width: 100%; margin: 5px 0; text-align: center; font-size: 17px; }
.login-actions { display: flex; gap: 10px; margin-top: 12px; }
.login-actions .btn { margin: 0; white-space: nowrap; }
.login-actions .btn-primary { flex: 1.3; font-size: 16px; }
.login-actions .btn-secondary { flex: 1; font-size: 13px; padding-left: 14px; padding-right: 14px; }

/* ---- player cards (live inside the right rail) ---- */
.player-card {
    display: flex; align-items: center; gap: 16px;
    padding: 12px 22px;
    cursor: var(--cur-point, pointer);
    min-width: 260px;
    transition: border-color .15s, box-shadow .2s, transform .12s var(--ease);
}
.player-card:hover { border-color: var(--gold); transform: translateY(-2px); box-shadow: 0 0 18px rgba(200,164,92,0.3); }
.player-card.active {
    border-color: var(--gold-bright);
    box-shadow: 0 0 22px rgba(236,202,128,0.4), inset 0 0 24px rgba(200,164,92,0.12);
}
.player-glyph { font-family: var(--font-head); font-size: 56px; line-height: 1; color: var(--gold); }
.player-info { display: flex; flex-direction: column; gap: 2px; }
.player-name { font-family: var(--font-head); font-size: 22px; color: var(--ink); letter-spacing: .03em; }
.player-details { font-size: 16px; font-style: italic; color: var(--ink-faint); }
/* Guest deadline, pinned to the card's right edge — a plate, not a footnote:
   it has to survive being read past a 56px class glyph and a 22px name. */
.player-expiry {
    margin-left: auto; text-align: center;
    padding: 5px 12px;
    font-family: var(--font-head); font-weight: 600;
    font-size: 13px; letter-spacing: .06em; text-transform: uppercase;
    color: #ffb3a2;
    background: rgba(181, 80, 63, 0.24);
    border: 1px solid var(--danger);
    border-radius: var(--radius);
    box-shadow: inset 0 0 14px rgba(181,80,63,0.35), 0 0 12px rgba(181,80,63,0.28);
    white-space: nowrap;
}

/* ---- lobby hero stage (D2R): full-bleed diorama behind the chrome ---- */
#lobby_stage {
    position: fixed; inset: 0; z-index: 0;
    overflow: hidden;              /* fit:"cover" — the stage overflows and crops here */
    /* TRANSPARENT, not --bg-0. The container exists (and must be laid out, the
       stage measures clientWidth) from the moment the mount starts, but its
       backdrop lands an image-fetch later — an opaque fill here was a black
       screen for exactly that long. Transparent lets the CSS art below show
       through until the room has its picture; `stage-live` (which hides that
       art) is added on `painted`, not on mount. */
    background: transparent;
}
/* The 3D stage owns the whole frame — stop paying for the kenburns art
   drifting invisibly underneath it (it's a permanent compositor layer). */
body.stage-live::before, body.stage-live::after { display: none; }

/* ---- side rails: characters left, games right ---- */
.lobby-rail {
    position: fixed; top: 50%; transform: translateY(-50%);
    /* centred vertically; capped so a long list never slides under the fixed
       corner chrome (menu / account chip above, plate margin below) */
    max-height: calc(100vh - 220px);
    width: 330px; z-index: 5;
    display: flex; flex-direction: column; justify-content: center;
    pointer-events: none;          /* the sections take the pointer, the empty rail doesn't */
}
.lobby-rail > * { pointer-events: auto; }
#rail_left  { left: 36px; }
#rail_right { right: 36px; }
.rail-section {
    padding: 14px 16px;
    display: flex; flex-direction: column; gap: 8px;
}
/* section hugs its content; past the rail's height the LIST scrolls while the
   title and the actions row stay pinned */
.rail-grow { flex: 0 1 auto; min-height: 0; max-height: 100%; }
.rail-list {
    overflow-y: auto; min-height: 0;
    display: flex; flex-direction: column; gap: 4px;
    padding-right: 4px;
    scrollbar-width: thin;
    scrollbar-color: var(--gold-deep) rgba(0,0,0,0.25);
}
.rail-list::-webkit-scrollbar { width: 8px; }
.rail-list::-webkit-scrollbar-track { background: rgba(0,0,0,0.25); border-radius: 4px; }
.rail-list::-webkit-scrollbar-thumb {
    background: linear-gradient(180deg, rgba(200,164,92,0.75), rgba(138,109,54,0.85));
    border-radius: 4px;
    border: 1px solid rgba(0,0,0,0.4);
}
.rail-list::-webkit-scrollbar-thumb:hover { background: var(--gold); }
.rail-title {
    font-family: var(--font-head); font-size: 13px; font-weight: 600;
    letter-spacing: .16em; text-transform: uppercase;
    text-align: center;
    color: var(--ink-faint);
    padding-bottom: 8px; border-bottom: 1px solid var(--panel-edge);
}
/* cards inside the rail: quiet rows on the section panel, not stacked panels */
.lobby-rail .player-card, .lobby-rail .game-card {
    min-width: 0;
    background: transparent; box-shadow: none; backdrop-filter: none;
    border-color: transparent;
}
.lobby-rail .player-card { padding: 8px 12px; gap: 12px; }
.lobby-rail .player-card:hover, .lobby-rail .game-card:hover {
    transform: none;
    border-color: var(--gold);
    box-shadow: 0 0 14px rgba(200,164,92,0.25);
}
.lobby-rail .player-card.active {
    border-color: var(--gold-bright);
    background: rgba(200,164,92,0.10);
    box-shadow: 0 0 16px rgba(236,202,128,0.3), inset 0 0 18px rgba(200,164,92,0.10);
}
.lobby-rail .player-glyph { width: 54px; height: 54px; font-size: 36px; line-height: 54px; }
.lobby-rail .player-name { font-size: 18px; }
.lobby-rail .player-details { font-size: 14px; }
.lobby-rail .game-card { padding: 8px 12px; }
.lobby-rail .dim { padding: 2px 12px 6px; text-align: center; }
.rail-row { display: flex; gap: 8px; margin-top: 8px; }
.rail-row .btn { flex: 1 1 0; padding: 12px 14px; font-size: 14px; margin: 0; white-space: nowrap; }

/* ---- hero plate: name + Play under the figure on stage ---- */
#hero_plate {
    position: fixed; left: 50%; bottom: 4vh; transform: translateX(-50%);
    z-index: 5;
    display: flex; flex-direction: column; align-items: center;
    text-align: center; pointer-events: none;   /* only the button takes clicks */
}
#hero_plate .btn { pointer-events: auto; margin: 18px 0 0; }
.hp-name {
    font-family: var(--font-head); font-weight: 600;
    font-size: clamp(28px, 3.2vw, 44px);
    letter-spacing: .18em; text-transform: uppercase;
    color: var(--gold-bright);
    text-shadow: 0 2px 12px #000, 0 0 30px #000;
    line-height: 1.15;
}
.hp-sub {
    font-size: clamp(14px, 1.2vw, 17px); font-style: italic;
    letter-spacing: .1em; color: var(--ink-dim);
    text-shadow: 0 2px 10px #000, 0 0 22px #000;
    margin-top: 2px;
}

/* ---- skeletons: shown while the roster is in flight (home.js roster_loaded)
   Same rails / same plate / same box metrics as the loaded lobby, so the data
   lands into the layout it already occupies. Animation is opacity-only and
   lives on the container, not per-box (one compositor layer, no paint work) —
   see the "infinite CSS anims" rule. */
.skel { pointer-events: none; }
.skel .rail-title { color: var(--ink-faint); opacity: .55; }
.skel .skel-card { border-color: transparent; cursor: default; }
.skel-box {
    background: linear-gradient(180deg, rgba(200,164,92,0.13), rgba(200,164,92,0.06));
    border: 1px solid rgba(200,164,92,0.12);
    border-radius: var(--radius);
    /* staggered so the rail reads as a list waking up, not one flat block */
    animation: skel-breathe 1.9s ease-in-out infinite;
    animation-delay: calc(var(--skel-i, 0) * .12s);
}
@keyframes skel-breathe { 0%,100% { opacity: .5; } 50% { opacity: .95; } }
@media (prefers-reduced-motion: reduce) { .skel-box { animation: none; opacity: .7; } }

.skel-glyph { width: 44px; height: 44px; border-radius: 50%; flex: 0 0 auto; }
.skel-line { height: 12px; }
.skel-name { width: 128px; height: 15px; }
.skel-sub  { width: 92px;  height: 11px; margin-top: 5px; }
.skel-btn  { height: 44px; flex: 1 1 0; }
/* game skeleton has no glyph — keep its rows on the card's left edge */
.skel .game-card { min-height: 44px; }

.skel-plate-name { width: min(46vw, 380px); height: clamp(30px, 3.4vw, 47px); }
.skel-plate-sub  { width: min(30vw, 240px); height: 17px; margin-top: 8px; }
.skel-plate-btn  { width: 300px; height: 63px; margin-top: 18px; }

/* ---- region toggle (account chip) ---- */
#region_toggle {
    display: flex; align-items: center; gap: 6px;
}
.region_label {
    font-family: var(--font-head); font-size: 11px;
    letter-spacing: .12em; text-transform: uppercase;
    color: var(--ink-faint);
    margin-right: 4px;
}
.region_choice {
    padding: 4px 13px;
    cursor: var(--cur-point, pointer);
    font-family: var(--font-head);
    font-size: 13px; letter-spacing: .04em;
    color: var(--ink-dim);
    transition: border-color .15s, color .15s, background .15s, transform .12s var(--ease);
}
.region_choice:hover { border-color: var(--gold); color: var(--ink); }
.region_choice.active {
    border-color: var(--gold-bright);
    background: rgba(200,164,92,0.16);
    color: var(--gold-bright);
}

/* ---- game cards (live inside the right rail) ---- */
.game-card {
    padding: 11px 18px;
    cursor: var(--cur-point, pointer);
    min-width: 170px;
    transition: border-color .15s, box-shadow .2s, transform .12s var(--ease);
}
.game-card:hover { border-color: var(--gold); transform: translateY(-2px); box-shadow: 0 0 14px rgba(200,164,92,0.3); }
.game-name { font-family: var(--font-head); color: var(--ink); font-size: 16px; }
.game-players { color: var(--ink-faint); font-size: 13px; }

/* ---- modals ---- */
.modern-modal {
    position: fixed; inset: 0;
    background: rgba(6,4,3,0.66);
    display: flex; align-items: center; justify-content: center;
    z-index: 100;
    backdrop-filter: blur(3px);
    animation: fade .25s ease both;
}
@keyframes fade { from { opacity: 0; } to { opacity: 1; } }
.modal-content {
    padding: 30px 34px;
    min-width: 340px;
    max-width: 740px;
    max-height: 90vh;
    overflow-y: auto;
    position: relative;
    animation: rise .35s var(--ease) both;
}
.large-modal { min-width: min(1020px, 94vw); max-width: 1060px; }
.small-modal { min-width: 380px; max-width: 440px; }
.modal-close {
    position: absolute; top: 1px; right: 1px;
    width: 44px; height: 44px;
    display: flex; align-items: center; justify-content: center;
    font-size: 26px; cursor: var(--cur-point, pointer); color: var(--ink-faint); line-height: 1;
}
.modal-close:hover { color: var(--gold-bright); }
.modal-header {
    margin-bottom: 20px; padding-bottom: 14px;
    border-bottom: 1px solid var(--panel-edge);
}
.modal-title {
    font-family: var(--font-head); margin: 0; font-weight: 600;
    color: var(--gold-bright); letter-spacing: .05em; font-size: 24px;
}
.modal-body { display: flex; flex-direction: column; gap: 14px; font-size: 17px; }
.modal-body p { margin: 0; color: var(--ink-dim); }
.modal-actions { margin-top: 20px; display: flex; gap: 10px; justify-content: flex-end; }

/* ---- account modal ---- */
.account-row {
    display: flex; justify-content: space-between; align-items: center;
    gap: 14px; padding: 10px 0;
    border-bottom: 1px solid var(--panel-edge);
}
.account-row > span:first-child { color: var(--ink-faint); font-family: var(--font-head); font-size: 14px; letter-spacing: .05em; }
.account-row:last-child { border-bottom: none; }
/* Display name — clickable since it stopped being a login handle. The "edit"
   affordance stays faint until hover so the row still reads as a value. */
.account-editable { cursor: var(--cur-point, pointer); }
/* At rest the "edit" tag takes NO width, so the name sits flush right like
   every other value in this panel — an invisible-but-laid-out label held it
   short of the edge and made the column look misaligned. `display:none` can't
   animate, so it collapses via max-width instead: zero to content width, with
   the margin animating alongside so nothing jumps.
   Hover is on the ROW, not the name: the target is the whole line, which is a
   much easier thing to hit than the text itself. */
.account-editable em {
    display: inline-block; white-space: nowrap; overflow: hidden;
    font-style: normal; font-family: var(--font-head); font-size: 12px;
    letter-spacing: .06em; color: var(--ink-faint);
    opacity: 0; max-width: 0; margin-left: 0;
    transition: opacity .15s ease, color .15s ease,
                max-width .18s var(--ease), margin-left .18s var(--ease);
}
.account-row:hover .account-editable { color: var(--gold-bright); }
.account-row:hover .account-editable em {
    opacity: 1; color: var(--gold); max-width: 5em; margin-left: 8px;
}
.server-selection { display: flex; gap: 8px; }
.server-choice {
    padding: 5px 14px;
    cursor: var(--cur-point, pointer);
    font-family: var(--font-head);
    font-size: 14px;
    color: var(--ink-dim);
}
.server-choice.active {
    border-color: var(--gold);
    background: rgba(200,164,92,0.16);
    color: var(--gold-bright);
}
.email-form { display: flex; gap: 8px; align-items: center; margin-top: 8px; }
.email-form input { flex: 1; }

/* ---- unconfirmed address (phase 3) ----
   Same "unfinished business" register as the guest banner, one step quieter:
   an unconfirmed address costs nothing until the password is lost, so it must
   not compete with the guest warning for alarm. */
.verify-nudge {
    display: flex; gap: 10px; align-items: center; justify-content: space-between;
    margin-top: 10px; padding: 10px 12px;
    border: 1px solid rgba(200,164,92,0.22);
    border-radius: var(--radius);
    background: rgba(200,164,92,0.06);
    font-size: 14px; color: var(--ink-dim); line-height: 1.35;
}
.verify-nudge .btn { margin: 0; flex: none; font-size: 12px; padding: 8px 14px; }

/* "Forgot your password?" — a text link, not a third button competing with
   Log in / Create account. */
.link-line { margin: 10px 0 0; text-align: center; }
.link-line a { color: var(--ink-dim); text-decoration: underline; text-underline-offset: 3px; }
.link-line a:hover { color: var(--gold-bright); }

/* ---- guest play + claim (phase 4) ----
   Register: the guest state is a WARNING, not an error — oxblood would read
   as "something broke". Gold-on-dark with a faint amber wash says "unfinished
   business" in the same voice as the rest of the lobby. */
.guest-banner {
    padding: 12px 14px; margin-bottom: 14px;
    border: 1px solid var(--gold-deep);
    border-radius: var(--radius);
    background: rgba(200,164,92,0.10);
}
.gb-title {
    font-family: var(--font-head); font-size: 15px; letter-spacing: .05em;
    color: var(--gold-bright); margin-bottom: 5px;
}
.gb-sub { color: var(--ink-dim); font-size: 14px; line-height: 1.4; }

.claim-form { display: flex; flex-direction: column; gap: 8px; margin-top: 14px; }
.claim-form input { width: 100%; text-align: center; }
.claim-form .btn { margin-top: 4px; }

/* Chip under the account name, top-right. Reads as status, not as a button,
   until hovered. */
/* Callout hanging under a menu button (guest → Account). Mirrors the in-game
   first-run coach plate (#coach in painterly-hud.css): dark plate, gold title,
   rotated-square arrow into the button above it. No infinite animation. */
.menu_button { position: relative; }
.menu-cue {
    position: absolute; top: calc(100% + 9px); left: 0;
    min-width: 100%; width: max-content; max-width: 260px;
    padding: 9px 14px 8px;
    text-align: left;
    font-family: var(--font-body); font-weight: 400; letter-spacing: 0;
    background: linear-gradient(180deg, rgba(46,35,20,0.98), rgba(24,17,10,0.99));
    border: 1px solid var(--gold); border-radius: var(--radius);
    box-shadow: inset 0 1px 0 rgba(255,235,190,0.10),
                0 6px 18px rgba(0,0,0,0.6),
                0 0 18px rgba(200,164,92,0.18);
    animation: rise .5s var(--ease) both;
}
.menu-cue b {
    display: block;
    font-family: var(--font-head); font-weight: 600;
    font-size: 14px; letter-spacing: .04em;
    color: var(--gold-bright); white-space: nowrap;
}
.menu-cue span {
    display: block; margin-top: 3px;
    font-size: 12px; color: var(--ink-dim); white-space: nowrap;
}
.menu-cue:hover b { color: #fff3d4; }
.mc-arrow {
    position: absolute; top: -6px; left: 22px;
    width: 10px; height: 10px; margin-left: -5px;
    background: rgba(46,35,20,0.98);
    border-left: 1px solid var(--gold); border-top: 1px solid var(--gold);
    transform: rotate(45deg);
}


/* Guest CTA on the logged-out panel + the "or sign in" rule under it. */
.guest-cta { margin: 22px auto 0; max-width: 360px; }
.guest-cta .btn { width: 100%; }
.guest-cta .dim { margin-top: 10px; line-height: 1.4; }
.form-divider {
    display: flex; align-items: center; gap: 12px;
    margin: 22px 0 4px; color: var(--ink-faint);
    font-family: var(--font-head); font-size: 12px; letter-spacing: .12em;
    text-transform: uppercase;
}
.form-divider::before, .form-divider::after {
    content: ""; flex: 1; height: 1px; background: var(--panel-edge);
}

/* ---- character creation modal ---- */
.class-selection {
    display: flex; flex-wrap: wrap; gap: 14px; justify-content: center;
}
.class-card {
    padding: 26px 26px 22px;
    cursor: var(--cur-point, pointer);
    min-width: 170px;
    max-width: 225px;
    text-align: center;
    display: flex; flex-direction: column; align-items: center;
    transition: border-color .15s, box-shadow .2s, transform .12s var(--ease);
}
.class-card:hover { border-color: var(--gold); transform: translateY(-2px); box-shadow: 0 0 16px rgba(200,164,92,0.3); }
.class-card.active {
    border-color: var(--gold-bright);
    background: rgba(200,164,92,0.12);
    box-shadow: 0 0 20px rgba(236,202,128,0.4), inset 0 0 22px rgba(200,164,92,0.12);
}
.class-glyph { font-family: var(--font-head); font-size: 62px; line-height: 124px; margin: 0 auto 10px; color: var(--gold); }
.class-card.active .class-glyph { color: var(--gold-bright); text-shadow: 0 0 16px rgba(236,202,128,0.6); }

/* painted class sigils — replace the unicode fallback where art exists.
   Classes without a file keep their unicode glyph (color stays). */
.class-glyph[data-cls="mage"], .class-glyph[data-cls="wizard"],
.class-glyph[data-cls="musketeer"], .class-glyph[data-cls="guardian"],
.class-glyph[data-cls="witch"], .class-glyph[data-cls="alchemyst"],
.player-glyph[data-cls="mage"], .player-glyph[data-cls="wizard"],
.player-glyph[data-cls="musketeer"], .player-glyph[data-cls="guardian"],
.player-glyph[data-cls="witch"], .player-glyph[data-cls="alchemyst"],
.class-glyph[data-cls="berserker"], .player-glyph[data-cls="berserker"] {
    color: transparent;          /* hide the unicode fallback */
    background-repeat: no-repeat;
    background-position: center;
    background-size: contain;
    /* icons are now alpha-cut (transparent bg, object mode) — normal blend keeps
       the rich painterly gold/dark detail instead of washing it out. */
}
.class-glyph[data-cls="mage"],      .player-glyph[data-cls="mage"]      { background-image: url("/viewer/textures/class/mage.png?v=2"); }
.class-glyph[data-cls="wizard"],    .player-glyph[data-cls="wizard"]    { background-image: url("/viewer/textures/class/wizard.png?v=2"); }
.class-glyph[data-cls="musketeer"], .player-glyph[data-cls="musketeer"] { background-image: url("/viewer/textures/class/musketeer.png?v=2"); }
.class-glyph[data-cls="guardian"],  .player-glyph[data-cls="guardian"]  { background-image: url("/viewer/textures/class/guardian.png?v=2"); }
.class-glyph[data-cls="witch"],     .player-glyph[data-cls="witch"]     { background-image: url("/viewer/textures/class/witch.png?v=2"); }
.class-glyph[data-cls="alchemyst"], .player-glyph[data-cls="alchemyst"] { background-image: url("/viewer/textures/class/alchemyst.png?v=2"); }
.class-glyph[data-cls="berserker"], .player-glyph[data-cls="berserker"] { background-image: url("/viewer/textures/class/berserker.png?v=2"); }
/* keep the unicode fallback hidden on the active card too — the card's own
   border/glow signals selection; the painted icon carries the rest */
.class-card.active .class-glyph[data-cls] { color: transparent; text-shadow: none; }

/* size the sigil boxes (bg-image needs explicit dimensions) */
.class-glyph  { width: 124px; height: 124px; }
.player-glyph { width: 64px; height: 64px; }
.class-name { font-family: var(--font-head); font-size: 21px; color: var(--ink); letter-spacing: .06em; }
.class-hint {
    font-size: 16px; font-style: italic; line-height: 1.4;
    color: var(--ink-faint);
    margin-top: 8px;
}
.class-card.active .class-hint { color: var(--ink-dim); }
.character-input { display: flex; justify-content: center; }
.character-input input { min-width: 300px; text-align: center; }

/* ---- character creation: the undercroft takeover --------------------------
   The 3D diorama (charselect/undercroft.js) is a 16:9 cinematic, so this step
   takes the whole screen rather than sitting in a modal. .cs-frame is the
   letterbox: the SAME fit the module computes for its own stage, expressed in
   CSS so the chrome (plate, strip, form) is anchored to the frame and not to
   the window — at a wide window the picture is a band in the middle and a
   window-anchored plate would float in the black.
   .cs-stage is the module's territory; everything in .cs-ui is Vue's. Nothing
   reaches across. */
#charselect {
    position: fixed; inset: 0; z-index: 30;
    background: #07080a;
    display: flex; align-items: center; justify-content: center;
    animation: fade .18s ease both;   /* the room should feel opened, not faded up */
}
/* The backdrop is painted HERE, in CSS, not by the module — and that is the
   whole trick to opening fast. undercroft.js can only append its stage after
   the dynamic import has fetched and parsed three (~650 K), so anything the
   module draws is gated behind that parse. This rule paints the room on the
   same frame the takeover appears, straight from the HTTP cache (the head
   prefetches the same unstamped URL). The module's own backdrop layer then
   fades in the identical image over it — invisibly. */
.cs-frame {
    position: relative;
    width: min(100vw, calc(100vh * 16 / 9));
    height: min(100vh, calc(100vw * 9 / 16));
    background: #07080a url("/viewer/charselect/undercroft.jpg") center / cover no-repeat;
}
.cs-stage { position: absolute; inset: 0; }
.cs-ui {
    position: absolute; left: 0; right: 0; bottom: 0;
    padding: 0 24px 2.6%;
    display: flex; flex-direction: column; align-items: center; gap: 14px;
    /* The picture must stay clickable — figures are picked by raycast on the
       canvas underneath. Only the controls themselves take the pointer. */
    pointer-events: none;
    background: linear-gradient(to top, rgba(6,7,9,.86) 30%, rgba(6,7,9,0) 100%);
}
.cs-ui > * { pointer-events: auto; }

.cs-plate { text-align: center; pointer-events: none; text-shadow: 0 2px 12px #000, 0 0 30px #000; }
.cs-name {
    font-family: var(--font-head); font-size: clamp(26px, 3.2vw, 42px);
    letter-spacing: .2em; text-transform: uppercase; color: var(--gold-bright);
    line-height: 1.15;
}
.cs-role {
    font-size: clamp(13px, 1.2vw, 17px); font-style: italic;
    letter-spacing: .1em; color: var(--ink-dim);
}

/* Class names as a thin strip: the roster stays scannable without a card grid
   competing with seven lit characters. */
.cs-strip { display: flex; flex-wrap: wrap; gap: 4px; justify-content: center; }
.cs-tab {
    padding: 5px 14px;
    font-family: var(--font-head); font-size: 14px; letter-spacing: .1em;
    color: var(--ink-faint); cursor: var(--cur-point, pointer);
    border-bottom: 1px solid transparent;
    transition: color .18s var(--ease), border-color .18s var(--ease);
}
/* .hover is driven from the 3D layer too: pointing at a FIGURE lights its
   name here, and pointing at a name lights the figure. One control, two
   halves. */
.cs-tab:hover, .cs-tab.hover { color: var(--ink); }
.cs-tab.active { color: var(--gold-bright); border-bottom-color: var(--gold); }

.cs-form { display: flex; gap: 10px; align-items: center; }
.cs-form input { min-width: 300px; text-align: center; }

/* No WebGL: the flat class grid this step used to be, centred in the frame. */
.cs-flat {
    position: absolute; inset: 0;
    display: flex; align-items: center; justify-content: center;
    padding: 0 24px 22vh;
    overflow-y: auto;
}

/* ---- game mode toggle ---- */
.mode-selection { display: flex; gap: 10px; margin-top: 4px; }
.mode-option {
    flex: 1;
    padding: 12px 14px;
    text-align: center;
    cursor: var(--cur-point, pointer);
    font-family: var(--font-head);
    font-size: 14px;
    color: var(--ink-dim);
    letter-spacing: .05em;
}
.mode-option.active {
    border-color: var(--gold);
    background: rgba(200,164,92,0.12);
    color: var(--gold-bright);
}

/* ---- reconnect banner ---- */
#reconnect-banner {
    position: fixed; top: 16px; left: 50%; transform: translateX(-50%);
    z-index: 300;
    padding: 8px 20px;
    font-size: 15px;
    color: var(--gold-bright);
    background: rgba(12, 10, 6, 0.88);
    border: 1px solid var(--gold);
    animation: rise .3s var(--ease) both;
}

/* ---- alerts ---- */
/* Top-centre, NOT bottom: the bottom-centre slot belongs to #hero_plate and
   its Play button — a toast there covered the primary CTA. */
#alerts {
    position: fixed; top: 16px; left: 50%; transform: translateX(-50%);
    display: flex; flex-direction: column; align-items: center; gap: 8px; z-index: 200;
}
/* the reconnect banner owns top:16px while it's up */
#alerts.shifted { top: 60px; }
.alert {
    padding: 10px 18px;
    font-size: 16px;
    display: flex; align-items: center; gap: 10px;
    border-left-width: 3px;
    animation: rise .3s var(--ease) both;
}
.alert-error   { border-left-color: var(--danger); }
.alert-warning { border-left-color: var(--gold); }
.alert-info    { border-left-color: var(--moss); }
.alert-icon { display: none; } /* drop emoji; the colored edge carries meaning */

/* ---- ladder ---- */
.ladder-table { display: flex; flex-direction: column; gap: 2px; min-width: 580px; }
.ladder-header, .ladder-row {
    display: grid;
    grid-template-columns: 48px 40px 1fr 1fr 70px 110px;
    column-gap: 10px;
    align-items: center;
    padding: 6px 12px;
    font-size: 16px;
}
.ladder-header {
    color: var(--ink-faint); font-family: var(--font-head);
    border-bottom: 1px solid var(--panel-edge); font-size: 13px; letter-spacing: .06em;
}
.ladder-row:nth-child(even) { background: rgba(200,164,92,0.05); }
.ladder-row > div:first-child { color: var(--gold); font-family: var(--font-head); }
.ladder-row .player-glyph { width: 30px; height: 30px; font-size: 20px; line-height: 30px; }
/* the logged-in account's own characters — fill + glow, no accent bar */
.ladder-row.mine {
    background: rgba(200,164,92,0.14);
    box-shadow: inset 0 0 14px rgba(200,164,92,0.18);
    border-radius: var(--radius);
}
.ladder-row.mine > div { color: var(--gold-bright); }
/* the account is context for the character name, not a peer of it */
.ladder-account { color: var(--ink-faint); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.ladder-name { display: flex; align-items: center; gap: 7px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
/* in-game right now. Static — an idle lobby must not paint on a timer. */
.ladder-live {
    flex: none; width: 7px; height: 7px; border-radius: 50%;
    background: var(--moss); box-shadow: 0 0 7px rgba(124,148,102,0.85);
}

/* ---- footer hints ---- */
#controls {
    display: none;
    position: fixed; bottom: 16px; left: 16px;
    flex-direction: column; gap: 3px;
    color: var(--ink-faint); font-size: 14px; font-style: italic; z-index: 5;
}

/* ---- launching button (Play click → /play redirect wait) ---- */
.btn.launching {
    pointer-events: none;
    animation: launch-pulse 1.1s ease-in-out infinite;
}
.btn.launching::before {
    content: "";
    width: 14px; height: 14px; flex: none;
    border-radius: 50%;
    border: 2px solid rgba(26,19,10,0.25);
    border-top-color: #1a130a;
    animation: launch-spin .8s linear infinite;
}
@keyframes launch-spin { to { transform: rotate(360deg); } }
@keyframes launch-pulse {
    0%, 100% { box-shadow: 0 0 10px rgba(236,202,128,0.30), inset 0 1px 0 rgba(255,250,230,0.5); }
    50%      { box-shadow: 0 0 30px rgba(236,202,128,0.75), inset 0 1px 0 rgba(255,250,230,0.5); }
}

/* ============================================================================
   RESPONSIVE — the lobby is desktop-first (fixed corner chrome, min-widths in
   px). Mobile can't PLAY (see mobile_guard.js) but it can still land here:
   the guard's "continue anyway" escape hatch, a shared link on a tablet, a
   narrow desktop window. These blocks keep the page readable instead of
   letting #connection sit on top of #top_left_menu.

   NB the viewport meta uses initial-scale=0.9, so a 390 px phone lays out at
   ~433 CSS px — breakpoints are set above the raw device widths for that.
   ============================================================================ */

/* ---- ≤900px: unpin the corner chrome into the flow ---- */
@media (max-width: 900px) {
    #ui { padding: 14px; }

    /* Both were position:fixed in opposite corners and collide once the
       viewport is narrower than their combined width. In the flow they
       become two centered rows at the top; `order` fixes their sequence
       (menu above the account chip) without touching the markup. */
    #top_left_menu, #connection {
        position: static;
        width: 100%;
        max-width: 520px;
    }
    #top_left_menu {
        order: -2;
        justify-content: center; flex-wrap: wrap;
        margin-bottom: 8px;
    }
    #connection {
        order: -1;
        flex-direction: row; flex-wrap: wrap;
        justify-content: center; gap: 10px;
        padding: 10px 14px;
        min-width: 0;
        margin-bottom: 4px;
    }

    /* The menu is in the flow here — an absolute callout under a button would
       land on top of #connection. The Account modal's guest banner says it. */
    .menu-cue { display: none; }

    #title { margin: 22px 0 20px; }
    #title img { width: clamp(220px, 68vw, 420px); }

    #description { margin: 18px auto; padding: 22px 20px; max-width: 100%; }
    #description h2 { font-size: 23px; }
    #description p { font-size: 16px; }
    .login-panel { padding: 24px 20px 20px; }

    /* px min-widths are what actually overflow a phone — clear them all */
    .player-card { min-width: 0; width: 100%; max-width: 360px; }
    .btn.hero { min-width: 0; width: 100%; max-width: 340px; padding: 16px 24px; font-size: 19px; }

    /* rail + plate unpin into the flow (the stage stays behind as decor) */
    .lobby-rail {
        position: static; transform: none; max-height: none;
        width: 100%; max-width: 420px;
        margin: 12px auto 0;
    }
    #hero_plate {
        position: static; transform: none;
        margin: 18px auto 6px;
    }
    #hero_plate .btn { width: 100%; max-width: 340px; }

    .modal-content, .large-modal, .small-modal {
        min-width: 0;
        width: 94vw; max-width: 94vw;
        padding: 24px 18px;
    }
    .modal-actions { flex-wrap: wrap; justify-content: center; }
    .character-input input { min-width: 0; width: 100%; }
    .class-card { min-width: 0; flex: 1 1 140px; max-width: 200px; padding: 18px 14px 14px; }
    .class-glyph { width: 88px; height: 88px; font-size: 46px; line-height: 88px; }

    /* the ladder grid can't shrink below its column sum — let it scroll
       inside the modal rather than push the page sideways */
    .modal-body { overflow-x: auto; }
    .ladder-table { min-width: 520px; }
}

/* ---- ≤560px: one column, tightest spacing ---- */
@media (max-width: 560px) {
    #ui { padding: 10px; }
    .menu_button { padding: 9px 14px; font-size: 13px; }
    #connection { gap: 8px; padding: 9px 10px; }
    #region_toggle { gap: 4px; }

    .login-actions { flex-direction: column; }
    .login-actions .btn { width: 100%; }
    /* the nudge stacks rather than squeezing the button off the edge */
    .rail-row { flex-wrap: wrap; }
    .rail-row .btn { flex: 1 1 44%; }

    .ladder-header, .ladder-row { grid-template-columns: 34px 30px 1fr 1fr 48px 84px; font-size: 14px; }
    .ladder-table { min-width: 420px; }
    .account-row { flex-wrap: wrap; }
    .email-form { flex-wrap: wrap; }
    .email-form input { flex: 1 1 100%; }
}
