  /* ---- Metric-matched fallback fonts ----
     Both webfonts load with `display: swap`, which means text paints
     immediately in a fallback and then RE-paints in the real font
     once it arrives. If the fallback's letters occupy a different
     amount of space than the real font's, that re-paint reflows the
     text and shifts everything below it — a layout shift that gets
     worse the longer the font takes to arrive, so it hits players
     furthest from the CDN hardest.

     These two @font-face blocks define local-only fallbacks whose
     metrics are adjusted to closely match the real fonts, so the
     pre-swap and post-swap text occupy nearly the same space and the
     swap barely moves anything. They download nothing (`local()`
     only, resolving to a font already on the system), so they cost
     no extra requests.

     The size-adjust/ascent/descent/line-gap numbers are derived from
     comparing Arial's metrics to Nunito's and Fredoka's; they are
     approximations that shrink the shift substantially rather than
     eliminating it perfectly. */
  @font-face {
    font-family: 'Nunito Fallback';
    src: local('Arial'), local('Helvetica'), local('Liberation Sans');
    size-adjust: 101.5%;
    ascent-override: 101%;
    descent-override: 35%;
    line-gap-override: 0%;
  }
  @font-face {
    font-family: 'Fredoka Fallback';
    src: local('Arial'), local('Helvetica'), local('Liberation Sans');
    size-adjust: 104%;
    ascent-override: 96%;
    descent-override: 30%;
    line-gap-override: 0%;
  }

  :root {
    /* Career buttons live in a fixed 4-across grid with nowrap
       labels, so they cannot absorb the full text multiplier without
       running past the card edge. --fs-tight is a damped version:
       it moves with the setting, just less far. Applied only where
       horizontal space is genuinely fixed. */
    --fs-tight: calc(1 + (var(--fs) - 1) * 0.35); /* 0.45 still pushed a 360px phone over the edge at the largest tier */
    /* ---- Text size ----
       One multiplier every font-size in this stylesheet is expressed
       against, so the Text Size setting can scale ALL text with a
       single write instead of maintaining parallel size tables.

       Deliberately not rem: rem scales from the browser's root font
       size, which this layout's fixed pixel geometry (the 400px
       phone card, career button heights, the avatar ring) would not
       follow — text would inflate while its containers didn't, and
       overflow. A local multiplier keeps the choice in the player's
       hands and inside geometry we control and can test. */
    --fs: 1;
    --cream: #FFF6EC;
    --card: #FFFFFF;
    --coral: #FF8FA3;
    --coral-dark: #F26D85;
    --mint: #7FD9C4;
    --mint-dark: #4FBBA3;
    --lavender: #C9B6E4;
    --lavender-dark: #7A64A8;
    --sun: #FFD166;
    --sun-dark: #A8791E;
    --plum: #4A3B52;
    --plum-soft: #8B7D93;
    --line: #F0E4D8;
    --shadow: 0 8px 24px rgba(74, 59, 82, 0.10);
    --shadow-sm: 0 3px 10px rgba(74, 59, 82, 0.08);
    --scrim: rgba(74, 59, 82, 0.45);
    --tag-bg: rgba(127,217,196,0.18);
    --bg-glow-1: rgba(255,143,163,0.14);
    --bg-glow-2: rgba(127,217,196,0.16);
    --bg-glow-3: rgba(201,182,228,0.14);
    --option-selected-bg: #EBDFCF;
  }

  /* ---- Chill: cool blues and teals, calm and unhurried ---- */
  body[data-theme="chill"] {
    --cream: #EAF3F7;
    --card: #FFFFFF;
    --coral: #5AA9E6;
    --coral-dark: #3E8FCB;
    --mint: #7FD9D0;
    --mint-dark: #3FAFA4;
    --lavender: #A8B8E0;
    --lavender-dark: #5470B0;
    --sun: #A8D8E8;
    --sun-dark: #3E7C91;
    --plum: #2E4756;
    --plum-soft: #6E8A99;
    --line: #D7E8EE;
    --shadow: 0 8px 24px rgba(46,71,86,0.12);
    --shadow-sm: 0 3px 10px rgba(46,71,86,0.09);
    --scrim: rgba(30,50,60,0.45);
    --tag-bg: rgba(127,217,208,0.20);
    --bg-glow-1: rgba(90,169,230,0.14);
    --bg-glow-2: rgba(127,217,208,0.16);
    --bg-glow-3: rgba(168,184,224,0.14);
    --option-selected-bg: #D3E0E6;
  }

  /* ---- Cold-Blooded: gothic and dark, crimson and antique gold ---- */
  body[data-theme="cold"] {
    --cream: #1B1620;
    --card: #221B28;
    --coral: #A8264A;
    --coral-dark: #D65073;
    --mint: #3F5245;
    --mint-dark: #7FA88E;
    --lavender: #7B5A9E;
    --lavender-dark: #A87ED1;
    --sun: #B08D57;
    --sun-dark: #D4B06E;
    --plum: #EDE3EE;
    --plum-soft: #B29DBE;
    --line: #3A2F42;
    --shadow: 0 8px 28px rgba(0,0,0,0.5);
    --shadow-sm: 0 3px 14px rgba(0,0,0,0.4);
    --scrim: rgba(0,0,0,0.7);
    --tag-bg: rgba(176,141,87,0.22);
    --bg-glow-1: rgba(168,38,74,0.18);
    --bg-glow-2: rgba(63,82,69,0.16);
    --bg-glow-3: rgba(123,90,158,0.18);
    --option-selected-bg: #0F0C14;
  }

  * { box-sizing: border-box; }

  body {
    margin: 0;
    min-height: 100vh;
    background: var(--cream);
    background-image:
      radial-gradient(circle at 12% 18%, var(--bg-glow-1) 0, transparent 40%),
      radial-gradient(circle at 88% 82%, var(--bg-glow-2) 0, transparent 42%),
      radial-gradient(circle at 85% 12%, var(--bg-glow-3) 0, transparent 38%);
    font-family: 'Nunito', 'Nunito Fallback', sans-serif;
    color: var(--plum);
    padding: 24px 12px;
    transition: background-color 0.5s ease;
  }

  h1, h2, h3, .display {
    font-family: 'Fredoka', 'Fredoka Fallback', sans-serif;
    font-weight: 600;
    margin: 0;
  }

  /* ---- Page shell ----
     Holds the game card centered, with room on either side for it
     to breathe on a real monitor. The game card itself also scales
     up at the same breakpoints (see the #app rules below) so
     widescreen desktop users get a genuinely bigger, more legible
     game rather than a small phone-shaped card floating in a sea of
     empty background. */
  .page-shell {
    display: flex;
    align-items: flex-start;
    justify-content: center;
    gap: 24px;
    width: 100%;
    max-width: 1800px;
    margin: 0 auto;
  }

  /* ---- Phone frame ----
     Scales up progressively on wider viewports via `zoom` (widely
     supported since 2024/Firefox 126) so the whole game — text,
     buttons, spacing, modals — grows together rather than just
     stretching into more whitespace. `zoom` degrades safely: a
     browser that ignores it still gets the wider max-width below,
     which alone is a big improvement over the fixed 400px card. */
  #app {
    width: 100%;
    max-width: 400px;
    background: var(--card);
    border-radius: 32px;
    box-shadow: var(--shadow);
    overflow: hidden;
    border: 1px solid var(--line);
    position: relative;
    transition: filter 0.6s ease;
  }

  /* max-height here is intentionally (100vh - Npx) DIVIDED by that
     breakpoint's own zoom factor, not just (100vh - Npx). #app is
     the zoomed element itself, and zoom scales its own rendered box
     — including max-height — as a unit, the same way it scales
     width/max-width above. Viewport units always measure the true,
     unzoomed viewport, so computing the raw vh-based budget and then
     dividing by zoom gives a PRE-zoom height that, once zoom scales
     it back up for rendering, lands almost exactly on the real
     available space — capping the card to the viewport without
     shrinking its width. In the rare case content still doesn't fit
     (a very short browser window), it scrolls internally within the
     card instead of growing the whole page and triggering a
     page-level scrollbar. */
  @media (min-width: 1024px) {
    #app {
      max-width: 460px;
      zoom: 1.03;
      max-height: calc((100vh - 24px) / 1.03);
      overflow-y: auto;
      scrollbar-width: thin; /* Firefox — a slim bar reads as intentional, not broken */
    }
    #app::-webkit-scrollbar { width: 6px; }
    #app::-webkit-scrollbar-track { background: transparent; }
    #app::-webkit-scrollbar-thumb { background: var(--line); border-radius: 999px; }
  }
  @media (min-width: 1280px) {
    #app { max-width: 500px; zoom: 1.14; max-height: calc((100vh - 24px) / 1.14); }
  }
  @media (min-width: 1600px) {
    #app { max-width: 560px; zoom: 1.25; max-height: calc((100vh - 24px) / 1.25); }
  }

  /* Tablet portrait (768–1023px) was showing the 400px phone card in
     the middle of a 768px screen — 368px of empty margin. Widen the
     card so it uses the room it has without triggering the desktop
     multi-column layout, which needs landscape to make sense. */
  @media (min-width: 700px) and (max-width: 1023px) and (orientation: portrait) {
    #app { max-width: 620px; zoom: 1.18; max-height: calc((100vh - 24px) / 1.18); }
  }
  @media (min-width: 1920px) {
    #app { max-width: 620px; zoom: 1.34; max-height: calc((100vh - 24px) / 1.34); }
  }

  /* Trims the outer vertical breathing room at desktop widths — the
     scaled-up card doesn't need as much surrounding padding as the
     mobile layout does, and on shorter monitor viewports the full
     24px top+bottom was just enough extra height to trigger a page
     scrollbar. */
  @media (min-width: 1024px) {
    body { padding: 12px 12px; }
  }

  #app.mood-low {
    filter: grayscale(0.6) saturate(0.45) brightness(0.97);
  }

  .screen { display: none; flex-direction: column; }
  .screen.active { display: flex; }

  /* ---- Header ---- */
  .topbar {
    padding: 18px 20px 14px;
    background: linear-gradient(135deg, var(--coral) 0%, var(--sun) 130%);
    color: white;
    display: flex;
    align-items: center;
    gap: 12px;
  }

  .avatar-ring {
    position: relative;
    width: 54px;
    height: 54px;
    flex-shrink: 0;
  }
  .avatar-ring > svg { transform: rotate(-90deg); }
  .avatar-ring .emoji {
    position: absolute;
    inset: 0;
    display: flex;
    align-items: center;
    justify-content: center;
    font-size: calc(24px * var(--fs));
  }
  /* renderAll() now asks avatarHtml for 256px so the character
     PORTRAIT decodes at full source resolution instead of being
     upscaled from a 34px decode (which is what made it blurry).
     But avatarHtml derives the emoji fallback's font-size from that
     same number as an INLINE style, which would render a ~205px
     emoji. These two rules constrain both children to the ring
     itself; !important is needed specifically to beat that inline
     style, which no ordinary selector can outrank. */
  .avatar-ring .emoji > span { font-size: 24px !important; line-height: 1 !important; }
  .avatar-ring .emoji img { width: 100% !important; height: 100% !important; }
  .ring-bg { fill: none; stroke: rgba(255,255,255,0.35); stroke-width: 5; }

  /* ---- Hovering the portrait ----
     The ring is the affordance, not the picture — the picture is the
     player's face and should not light up. Hovering brightens the
     track and lifts the whole thing a hair, so a desktop player can
     see it is a target without anything moving under the cursor.
     Guarded on hover:hover so a touch device never gets a state it
     cannot leave. */
  @media (hover: hover) {
    .avatar-ring { transition: transform 0.14s ease; }
    .avatar-ring:has(#topAvatar:hover),
    .avatar-ring:hover { transform: translateY(-1px); }
    .avatar-ring:hover .ring-bg {
      stroke: rgba(255,255,255,0.72);
      transition: stroke 0.14s ease;
    }
    .avatar-ring:hover .ring-fill {
      filter: drop-shadow(0 0 4px rgba(255,255,255,0.55));
      transition: filter 0.14s ease;
    }
  }
  .ring-fg { fill: none; stroke: white; stroke-width: 5; stroke-linecap: round; transition: stroke-dashoffset 0.5s ease; }


  .id-block { line-height: 1.25;     /* Lets the name/age block shrink instead of forcing the topbar
       wider than the card. Without min-width:0 a flex item refuses
       to shrink below its content, which is what pushed the menu
       buttons off the right edge with a long name at large text. */
    min-width: 0;
    overflow: hidden;
  }

  .id-name { font-family: 'Fredoka', 'Fredoka Fallback', sans-serif; font-size: calc(17px * var(--fs)); }
  .id-age { font-size: calc(14px * var(--fs)); opacity: 0.9; }

  .menu-btn {
    flex-shrink: 0;
    width: 34px;
    height: 34px;
    background: rgba(255,255,255,0.22);
    border: none;
    border-radius: 10px;
    color: white;
    font-size: calc(16px * var(--fs-tight));
    cursor: pointer;
    display: flex;
    align-items: center;
    justify-content: center;
  }
  .menu-btn:hover { background: rgba(255,255,255,0.38); }
  #leaderboardBtn { margin-left: auto; } /* must stay on the FIRST button of the trio — it's what pushes the whole group right. Putting a button before it strands that button back by the character name, since only elements after the auto-margin get carried right. */

  /* ---- Stats ---- */
  .stats {
    display: grid;
    grid-template-columns: 1fr 1fr;
    gap: 10px;
    padding: 16px 20px;
  }

  .stat { display: flex; flex-direction: column; gap: 5px; }
  .stat-label {
    font-size: calc(12.5px * var(--fs));
    font-weight: 800;
    text-transform: uppercase;
    letter-spacing: 0.04em;
    color: var(--plum-soft);
    display: flex;
    align-items: center;
    gap: 5px;
  }
  .bar-track {
    height: 8px;
    border-radius: 6px;
    background: var(--line);
    overflow: hidden;
  }
  .bar-fill {
    height: 100%;
    border-radius: 6px;
    transition: width 0.5s ease;
  }
  .bar-fill.health { background: var(--coral); }
  .bar-fill.happiness { background: var(--sun); }
  .bar-fill.wits { background: var(--lavender); }
  .bar-fill.looks { background: var(--mint); }

  /* ---- Feed ---- */
  /* ---- Stat cards and the career panel ----
     Both were flat rectangles in --card with a hairline border: two
     large, evenly-weighted blocks with nothing distinguishing them,
     which made the sidebar read as filler rather than as the two
     different things it actually contains.

     Each stat now carries a tint of its OWN bar color, mixed
     against the card so it stays a wash rather than a block. That
     costs nothing in legibility and means the four rows are
     scannable by color before you read a word. All of it is
     color-mix against the theme variables, so every palette gets its
     own version rather than one hardcoded set. */
  .stat {
    background: color-mix(in srgb, var(--card) 92%, var(--stat-tint, var(--lavender)) 8%);
    border: 1px solid color-mix(in srgb, var(--line) 70%, var(--stat-tint, var(--lavender)) 30%);
    border-radius: 12px;
    padding: 9px 11px 11px;
    box-shadow: 0 1px 2px rgba(0,0,0,0.045);
  }
  .stat-health    { --stat-tint: var(--coral); }
  .stat-happiness { --stat-tint: var(--sun); }
  .stat-wits      { --stat-tint: var(--lavender, var(--plum-soft)); }
  .stat-looks     { --stat-tint: var(--mint); }

  /* The career panel gets a gentle vertical wash so it reads as a
     panel rather than a plain box, and a slightly stronger shadow
     than the stat cards so the two tiers sit at different depths. */
  .career-bar {
    background:
      linear-gradient(180deg,
        color-mix(in srgb, var(--card) 88%, var(--mint) 12%) 0%,
        var(--card) 62%);
    box-shadow: 0 2px 6px rgba(0,0,0,0.07);
  }

  /* ---- The four action buttons ----
     A very shallow extrusion: a solid one-pixel edge in the button's
     own color, plus a soft ambient shadow. Enough to read as
     pressable without tipping into the glossy beveled look. The
     press state removes the edge and nudges the button down by the
     same pixel, so it physically depresses. */
  .career-btn {
    box-shadow:
      0 1.5px 0 color-mix(in srgb, var(--lavender) 55%, transparent),
      0 2px 5px rgba(0,0,0,0.09);
    transition: transform 0.06s ease, box-shadow 0.06s ease, background 0.12s ease;
  }
  .career-btn:active {
    transform: translateY(1.5px);
    box-shadow: 0 0 0 transparent, 0 1px 2px rgba(0,0,0,0.10);
  }
  /* Each button already has its own accent color; the extruded edge
     should match it rather than all four sharing the lavender. */
  .career-btn.health-btn {
    box-shadow: 0 1.5px 0 color-mix(in srgb, var(--mint-dark) 55%, transparent), 0 2px 5px rgba(0,0,0,0.09);
  }
  .career-btn.activities-btn {
    box-shadow: 0 1.5px 0 color-mix(in srgb, var(--coral-dark) 50%, transparent), 0 2px 5px rgba(0,0,0,0.09);
  }
  .career-btn.services-btn {
    box-shadow: 0 1.5px 0 color-mix(in srgb, var(--sun-dark) 45%, transparent), 0 2px 5px rgba(0,0,0,0.09);
  }
  .career-btn.health-btn:active,
  .career-btn.activities-btn:active,
  .career-btn.services-btn:active {
    transform: translateY(1.5px);
    box-shadow: 0 0 0 transparent, 0 1px 2px rgba(0,0,0,0.10);
  }

  /* ---- The narrative feed ----
     This was plain white, and on a desktop monitor it is by far the
     largest surface in the game — a big empty field with a few small
     cards floating in it. A soft wash built from the theme's own
     variables gives it something to be without competing with the
     cards, and because it is color-mixed against --coral and
     --mint it re-tints itself for every palette rather than needing
     a hardcoded value per theme.

     Kept very low-saturation on purpose: the entry cards sit on top
     of it and have to stay legible, so this reads as a tint rather
     than a background image. */
  .feed {
    flex: 1;
    background:
      radial-gradient(115% 75% at 6% 4%,
        color-mix(in srgb, var(--coral) 24%, transparent) 0%, transparent 60%),
      radial-gradient(105% 80% at 98% 46%,
        color-mix(in srgb, var(--mint) 26%, transparent) 0%, transparent 64%),
      radial-gradient(90% 60% at 55% 100%,
        color-mix(in srgb, var(--lavender, var(--coral)) 16%, transparent) 0%, transparent 70%),
      linear-gradient(180deg,
        color-mix(in srgb, var(--sun) 12%, transparent) 0%,
        transparent 58%);
    padding: 4px 20px 12px;
    overflow-y: auto;
    max-height: min(260px, 34vh); /* the midpoint between the original 320px and the tighter 200px — caps here on tall screens, shrinks further on short ones so the topbar/footer never scroll off */
    display: flex;
    flex-direction: column-reverse;
    gap: 10px;
  }
  .feed::-webkit-scrollbar { width: 5px; }
  .feed::-webkit-scrollbar-thumb { background: var(--line); border-radius: 4px; }

  .entry {
    /* Slightly opaque rather than flat --cream, so the wash behind
       shows through a little and the cards feel like they are
       sitting ON something instead of punched out of it. */
    background: color-mix(in srgb, var(--cream) 88%, transparent);
    backdrop-filter: blur(2px);
    border-radius: 14px;
    padding: 11px 14px;
    font-size: calc(15.5px * var(--fs));
    line-height: 1.4;
    border: 1px solid var(--line);
  }
  .entry .entry-age {
    font-family: 'Fredoka', 'Fredoka Fallback', sans-serif;
    font-size: calc(11px * var(--fs));
    color: var(--coral-dark);
    font-weight: 600;
    display: block;
    margin-bottom: 2px;
  }

  .choices {
    display: flex;
    flex-direction: column;
    gap: 8px;
    margin-top: 8px;
  }
  .choice-btn {
    font-family: 'Nunito', 'Nunito Fallback', sans-serif;
    font-weight: 700;
    font-size: calc(13.5px * var(--fs));
    padding: 10px 14px;
    border-radius: 12px;
    border: 1.5px solid var(--coral);
    background: var(--card);
    color: var(--coral-dark);
    cursor: pointer;
    text-align: left;
    transition: transform 0.12s ease, background 0.12s ease;
  }
  .choice-btn:hover { background: var(--coral); color: white; transform: translateY(-1px); }

  /* ---- Footer / actions ---- */
  .actions {
    padding: 14px 20px 20px;
    border-top: 1px solid var(--line);
    display: flex;
    align-items: stretch;
    gap: 8px;
  }

  .action-icon-btn {
    flex-shrink: 0;
    width: 46px;
    background: var(--cream);
    border: 1.5px solid var(--line);
    border-radius: 16px;
    color: var(--plum);
    font-size: calc(18px * var(--fs));
    cursor: pointer;
    display: flex;
    align-items: center;
    justify-content: center;
    transition: transform 0.12s ease, background 0.14s ease, border-color 0.14s ease, color 0.14s ease;
  }
  /* ---- Hovering the action bar ----
     Matches how the Career/Health/Go Out/Buy buttons behave: fill
     with the accent color on hover. Each button takes the color of
     what it does — the dice its own warm tone, Repeat the mint of
     the time controls — so the row reads as four distinct things
     rather than three identical gray squares.

     hover:hover only. On a phone these would stick after a tap,
     leaving a button that looks permanently pressed. */
  @media (hover: hover) {
    .action-icon-btn:hover {
      background: var(--lavender);
      border-color: var(--lavender);
      color: white;
    }
    #diceBtn:hover {
      background: var(--sun);
      border-color: var(--sun-dark);
      color: var(--plum);
    }
    #repeatBtn:not(:disabled):hover {
      background: var(--mint);
      border-color: var(--mint-dark);
      color: white;
    }
    /* A disabled Repeat must not respond — it is the one button here
       that is frequently unavailable. */
    .action-icon-btn:disabled:hover {
      background: var(--cream);
      border-color: var(--line);
      color: var(--plum);
    }
    .btn-primary:hover { filter: brightness(1.07); }
    .btn-primary:active { filter: brightness(0.96); }
  }
  /* .ai-label carries the text half of an icon button ("Stats",
     "Social") — hidden here on the narrow card, where there's no
     room for it beside the two full-width advance-time buttons, and
     revealed at widescreen (see the widescreen block near the end of
     this stylesheet). aria-label stays on the button either way, so
     the accessible name is the full description regardless of what's
     visually shown. */
  .ai-label { display: none; }
  .action-icon-btn:active { transform: scale(0.94); }
  .menu-btn:disabled, .career-btn:disabled, .action-icon-btn:disabled {
    opacity: 0.4;
    cursor: not-allowed;
    pointer-events: none;
  }

  .btn {
    font-family: 'Fredoka', 'Fredoka Fallback', sans-serif;
    font-weight: 600;
    font-size: calc(14.5px * var(--fs));
    border: none;
    border-radius: 16px;
    padding: 13px 16px;
    cursor: pointer;
    flex: 1;
    transition: transform 0.12s ease, box-shadow 0.12s ease;
  }
  .btn:active { transform: scale(0.97); }
  .btn:disabled { opacity: 0.45; cursor: not-allowed; }
  .btn:disabled:active { transform: none; }
  .btn-primary {
    background: linear-gradient(135deg, var(--mint) 0%, var(--mint-dark) 100%);
    color: white;
    box-shadow: var(--shadow-sm);
  }
  .btn-secondary {
    background: var(--cream);
    color: var(--plum);
    border: 1.5px solid var(--line);
  }

  /* ---- Character creation screen ---- */
  .create-screen { padding: 28px 24px 24px; text-align: center; gap: 18px; position: relative; }

  /* A light-background variant of .menu-btn — the create screen sits
     on the plain cream card background, not the coral/sun gradient
     topbar, so a white icon-on-white-overlay button would be nearly
     invisible there. Positioned absolutely in the corner since the
     create screen has no topbar row to anchor it inline. */
  .menu-btn-light {
    position: absolute;
    top: 16px;
    right: 16px;
    width: 34px;
    height: 34px;
    background: var(--cream);
    border: 1.5px solid var(--line);
    border-radius: 10px;
    color: var(--plum);
    font-size: calc(16px * var(--fs));
    cursor: pointer;
    display: flex;
    align-items: center;
    justify-content: center;
  }
  .menu-btn-light:hover { border-color: var(--coral); }
  .create-screen .display { font-size: calc(26px * var(--fs)); color: var(--coral-dark); }
  .create-sub { font-size: calc(14px * var(--fs)); color: var(--plum-soft); margin-top: -8px; }

  /* A wrapper purely for badge positioning — kept separate from
     #createAvatarPreview itself, since that element's innerHTML gets
     fully replaced every time the avatar changes (see
     selectCharacterDesign). Nesting the button inside the element
     that gets overwritten would wipe the button out on every single
     click or randomize. */
  .avatar-badge-wrap {
    position: relative;
    width: 64px;
    height: 64px;
    margin: 6px auto 2px;
  }
  .create-avatar {
    width: 64px;
    height: 64px;
    display: flex;
    align-items: center;
    justify-content: center;
  }

  /* The randomize button as a badge anchored to the avatar circle's
     own bottom-right corner — not a separate element floating in its
     own row nearby. Bottom-right specifically because most players
     are on a phone, holding it one-handed, thumb naturally resting
     lower-right of center; anchoring it directly to the avatar (not
     just "somewhere near it") also makes the relationship between
     the two obvious at a glance. */
  .randomize-btn {
    position: absolute;
    bottom: -6px;
    right: -6px;
    width: 30px;
    height: 30px;
    background: var(--cream);
    border: 1.5px solid var(--line);
    border-radius: 10px;
    color: var(--plum);
    font-size: calc(14px * var(--fs));
    cursor: pointer;
    display: flex;
    align-items: center;
    justify-content: center;
    box-shadow: var(--shadow-sm);
    transition: border-color 0.12s ease, transform 0.12s ease;
  }
  .randomize-btn:hover { border-color: var(--coral); }
  .randomize-btn:active { transform: rotate(-14deg); }

  .field-optional { font-weight: 400; opacity: 0.75; text-transform: none; letter-spacing: normal; }

  /* Legacy Name: a plain text field (styled exactly like Name right
     above it) plus one trailing word picker, styled with the same
     mad-libs treatment as .story-select — a deliberately playful,
     coral-accented native select rather than a browser-default one,
     matching the one other place in the game a dropdown reads as
     part of a sentence instead of a form control. */
  /* Name field + its randomise button, matching .legacy-name-row's
     pattern directly below so the two text fields on this screen
     line up and behave identically. */
  .name-input-row {
    display: flex;
    gap: 6px;
    align-items: center;
  }
  .name-input-row input {
    flex: 1;
    min-width: 0; /* lets the input actually shrink beside the button rather than forcing the row wider than the panel */
    margin-top: 0; /* .field input carries its own top margin under a label; here the row handles spacing instead */
  }
  /* Square, same height as the input beside it. Distinct from
     .randomize-btn, which is the absolutely-positioned badge stuck
     to the avatar circle — this one sits inline in a flex row. */
  .randomize-inline-btn {
    flex-shrink: 0;
    width: 42px;
    height: 42px;
    background: var(--cream);
    border: 1.5px solid var(--line);
    border-radius: 12px;
    color: var(--plum);
    font-size: calc(16px * var(--fs));
    cursor: pointer;
    display: flex;
    align-items: center;
    justify-content: center;
    transition: border-color 0.12s ease, transform 0.12s ease;
  }
  .randomize-inline-btn:hover { border-color: var(--coral); }
  .randomize-inline-btn:active { transform: rotate(-14deg); }

  .legacy-name-row {
    display: flex;
    gap: 6px;
    align-items: center;
    flex-wrap: wrap;
  }
  .legacy-name-row input {
    flex: 1;
    min-width: 130px;
    margin-top: 0; /* .field input normally carries its own top margin under a label; here it sits beside the select instead */
  }
  .legacy-trailing-select {
    font: inherit;
    font-weight: 700;
    font-size: calc(14px * var(--fs));
    color: var(--coral-dark);
    background: var(--tag-bg);
    border: none;
    border-bottom: 2px solid var(--coral);
    border-radius: 6px;
    padding: 10px 10px;
    cursor: pointer;
    flex-shrink: 0;
    min-height: 43px; /* the <option>s are added by JS after load, so an empty <select> renders shorter and then grows — reserving its measured populated height keeps the row stable */
  }
  .legacy-trailing-select:focus { outline: 2px solid var(--coral); outline-offset: 1px; }
  .legacy-name-preview {
    font-family: 'Fredoka', 'Fredoka Fallback', sans-serif;
    font-size: calc(14.5px * var(--fs));
    color: var(--plum);
    margin-top: 8px;
    min-height: 19px; /* this starts EMPTY and is filled by updateLegacyNamePreview() after load — reserving its measured height stops the legacy-name block growing and pushing the page down */
  }

  .field { text-align: left; }
  /* Wraps a related group of create-screen fields for widescreen
     container styling (see the widescreen block near the end of
     this stylesheet). display: contents here means it generates NO
     box of its own at this base/narrow size — its children flow
     directly into the normal single-column stack exactly as if this
     wrapper didn't exist, so the mobile layout is completely
     unaffected by its presence. */
  .create-panel { display: contents; }
  /* Hidden until the widescreen breakpoint reveals it — there's no
     room for a dedicated promotional banner on the narrow card, and
     it isn't asked for there. */
  .steam-wishlist { display: none; }
  .field label {
    font-size: calc(12px * var(--fs));
    font-weight: 800;
    text-transform: uppercase;
    letter-spacing: 0.04em;
    color: var(--plum-soft);
  }
  .field input {
    width: 100%;
    margin-top: 6px;
    padding: 11px 14px;
    border-radius: 12px;
    border: 1.5px solid var(--line);
    font-family: 'Nunito', 'Nunito Fallback', sans-serif;
    font-size: calc(14px * var(--fs));
    background: var(--cream);
    color: var(--plum);
  }
  .field input:focus { outline: 2px solid var(--mint); border-color: transparent; }

  /* min-height reserves the space these containers will occupy once
     JavaScript populates them. Both start EMPTY in the HTML and are
     filled after first paint (renderAvatarPlaceholders, the gender/
     preference chip builders), so without a reserved height they
     render at 0px, then jump to full height a few hundred ms later
     and shove everything below them down the page — measured at
     120-180px of movement on mobile, which is the dominant cause of
     this page's Cumulative Layout Shift.

     Values are the real measured populated heights (33px for a
     single row of chips; the avatar grid wraps to 2 rows at narrow
     widths, hence 88px there and 40px at widescreen where it sits
     in one row). min-height rather than height so nothing is capped
     if the content ever grows beyond expectation. */
  .avatar-picker { display: grid; grid-template-columns: repeat(3, 1fr); gap: 8px; max-width: 220px; margin: 0 auto; min-height: 88px; }
  .avatar-opt {
    padding: 6px;
    border-radius: 12px;
    border: 1.5px solid var(--line);
    background: var(--card);
    cursor: pointer;
    display: flex;
    align-items: center;
    justify-content: center;
  }
  .avatar-opt.selected { border-color: var(--coral); background: var(--tag-bg); }
  .avatar-opt.locked { cursor: not-allowed; opacity: 0.4; }
  .avatar-opt.locked:hover { border-color: var(--line); }
  .chip-picker { display: flex; gap: 8px; justify-content: center; flex-wrap: wrap; min-height: 33px; }

  /* ---- Location & family screens ---- */
  .location-list { display: flex; flex-direction: column; gap: 6px; max-height: 50vh; overflow-y: auto; padding: 2px; }

  /* ---- Story-driven character creation ----
     Inline <select> elements styled to read as part of the sentence
     rather than a form field — a well-established "mad libs" pattern
     that also gets the OS's native picker for free on mobile
     (genuinely good UX there), rather than a custom dropdown that
     would need to be rebuilt for touch. */
  .story-text {
    font-size: calc(16px * var(--fs));
    line-height: 1.7;
    color: var(--plum);
    text-align: left;
    margin: 18px 0 8px;
  }
  .story-text p { margin: 0 0 16px; }
  .story-select {
    font: inherit;
    font-weight: 700;
    color: var(--coral-dark);
    background: var(--tag-bg);
    border: none;
    border-bottom: 2px solid var(--coral);
    border-radius: 6px;
    padding: 2px 8px;
    margin: 0 2px;
    cursor: pointer;
  }
  .story-select:focus { outline: 2px solid var(--coral); outline-offset: 1px; }

  .location-row {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 8px;
    background: var(--cream);
    border-radius: 14px;
    padding: 9px 12px;
  }
  .location-row .state-name {
    font-family: 'Fredoka', 'Fredoka Fallback', sans-serif;
    font-size: calc(13px * var(--fs));
    color: var(--plum);
    flex: 1;
    text-align: left;
  }
  .location-pills { display: flex; gap: 6px; flex-shrink: 0; }
  .location-pill {
    font-family: 'Nunito', 'Nunito Fallback', sans-serif;
    font-size: calc(11px * var(--fs));
    font-weight: 700;
    padding: 6px 10px;
    border-radius: 999px;
    border: 1.5px solid var(--line);
    background: var(--card);
    color: var(--plum);
    cursor: pointer;
    white-space: nowrap;
  }
  .location-pill.selected { border-color: var(--coral); background: var(--tag-bg); color: var(--coral-dark); }
  .chip-opt {
    font-family: 'Nunito', 'Nunito Fallback', sans-serif;
    font-size: calc(13px * var(--fs));
    font-weight: 700;
    padding: 8px 14px;
    border-radius: 12px;
    border: 1.5px solid var(--line);
    background: var(--card);
    color: var(--plum);
    cursor: pointer;
  }
  .chip-opt.selected { border-color: var(--coral); background: var(--tag-bg); color: var(--coral-dark); }
  .modal-input {
    width: 100%;
    padding: 11px 14px;
    border-radius: 12px;
    border: 1.5px solid var(--line);
    font-family: 'Nunito', 'Nunito Fallback', sans-serif;
    font-size: calc(14px * var(--fs));
    background: var(--cream);
    color: var(--plum);
    margin-bottom: 14px;
    box-sizing: border-box;
  }
  .footnote {
    font-size: calc(11px * var(--fs));
    color: var(--plum-soft);
    text-align: center;
    padding: 10px 20px 16px;
  }

  /* ---- Career bar ---- */
  .career-bar {
    margin: 0 20px 14px;
    padding: 10px 14px 12px;
    background: var(--cream);
    border: 1px solid var(--line);
    border-radius: 14px;
    font-size: calc(13.5px * var(--fs));
  }
  .career-bar .cb-top {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 10px;
    margin-bottom: 9px;
  }
  .career-bar .cb-job {
    display: flex;
    flex-direction: column;
    line-height: 1.3;
  }
  .career-bar .cb-job-title {
    font-family: 'Fredoka', 'Fredoka Fallback', sans-serif;
    font-size: calc(15px * var(--fs));
    color: var(--plum);
  }
  .career-bar .cb-job-sub {
    color: var(--plum-soft);
    font-size: calc(12.5px * var(--fs));
  }
  .career-bar .cb-money {
    text-align: right;
    line-height: 1.3;
    flex-shrink: 0;
  }
  .career-bar .cb-debt {
    color: var(--coral-dark);
    font-weight: 800;
    background: none;
    border: none;
    padding: 0;
    font-family: inherit;
    font-size: inherit;
    cursor: pointer;
    text-decoration: underline;
    text-decoration-style: dotted;
  }
  .career-bar .cb-savings { color: var(--mint-dark); font-weight: 800; }
  .career-btn {
    font-family: 'Fredoka', 'Fredoka Fallback', sans-serif;
    /* Scales with the CARD's width, not just the text setting, so the
       longest label ("Activities") always fits on one line however
       narrow the phone is. 2.4vw is ~7.7px at 320px and ~9.4px at
       390px; the text-size ceiling stops it inflating on tablets.

       This replaces an earlier `white-space: normal` +
       `overflow-wrap: anywhere` fix. That stopped the buttons
       overflowing at large text sizes, but did it by breaking words
       mid-syllable — "SERVICE / S", "ACTIVIT / IES" — which is worse
       than the problem it solved. Shrinking to fit is the right
       trade: the label stays whole and readable, just smaller. */
    font-size: min(calc(11px * var(--fs-tight)), max(7.4px, 2.4vw));
    font-weight: 600;
    background: var(--card);
    border: 1.5px solid var(--lavender);
    color: var(--lavender-dark);
    padding: 7px 2px;  /* every pixel counts at 320px — "Activities" is the constraint */
    border-radius: 10px;
    cursor: pointer;
    text-align: center;
    white-space: nowrap;   /* never break a label across lines */
    overflow: hidden;      /* belt and braces if a future label is far longer */
    text-overflow: ellipsis;
    line-height: 1.15;
  }
  .career-btn:hover { background: var(--lavender); color: white; }
  /* .cbtn-icon/.cbtn-label replace what used to be plain button text
     ("💼 Career" as one text node) so the widescreen tier can restack
     them icon-above-label for a squarer look. At this base/narrow
     size they stay inline side by side — margin-right stands in for
     the literal space character that used to sit between the emoji
     and the word. */
  .cbtn-icon { margin-right: 4px; }
  .career-buttons {
    display: grid;
    grid-template-columns: repeat(4, 1fr);
    gap: 6px;
  }
  .career-btn.health-btn {
    border-color: var(--mint-dark);
    color: var(--mint-dark);
  }
  .career-btn.health-btn:hover { background: var(--mint-dark); color: white; }
  .career-btn.activities-btn {
    border-color: var(--coral);
    color: var(--coral-dark);
  }
  .career-btn.activities-btn:hover { background: var(--coral); color: white; }
  .career-btn.services-btn {
    border-color: var(--sun);
    color: var(--sun-dark);
  }
  .career-btn.services-btn:hover { background: var(--sun); color: white; }

  /* ---- Modal ---- */
  .modal-overlay {
    display: none;
    position: absolute;
    inset: 0;
    background: var(--scrim);
    align-items: flex-end;
    justify-content: center;
    z-index: 10;
    border-radius: 32px;
  }
  .modal-overlay.active { display: flex; }
  .modal-card {
    width: 100%;
    max-height: 88%;
    background: var(--card);
    border-radius: 24px 24px 0 0;
    padding: 22px 20px 24px;
    overflow-y: auto;
    box-shadow: 0 -6px 24px rgba(74,59,82,0.2);
  }
  .modal-card h2 {
    font-size: calc(19px * var(--fs));
    color: var(--coral-dark);
    margin-bottom: 4px;
  }
  .modal-sub {
    font-size: calc(14.5px * var(--fs));
    color: var(--plum-soft);
    margin-bottom: 14px;
  }
  .option-list {
    display: flex;
    flex-direction: column;
    gap: 8px;
  }
  .option-card {
    border: 1.5px solid var(--line);
    background: var(--cream);
    border-radius: 14px;
    padding: 11px 14px;
    cursor: pointer;
    text-align: left;
    font-family: 'Nunito', 'Nunito Fallback', sans-serif;
    transition: border-color 0.12s ease, transform 0.12s ease;
  }
  .option-card:hover { border-color: var(--coral); transform: translateY(-1px); }
  /* Selected state — a real, visible indicator across all three
     themes at once. A single color-mix()/filter darken doesn't work
     here: those are multiplicative, so they barely move an
     already-dark color like the Cold-Blooded theme's near-black
     --cream (verified numerically — an 18% mix toward black only
     shifted its luminance by ~5 out of 255, essentially invisible).
     --option-selected-bg is instead a hand-picked, per-theme value
     for each of the three theme blocks below, each one calibrated to
     the same real, consistent luminance drop regardless of how light
     or dark that theme's base color already is — "a slightly darker
     shade of its original color" that actually looks that way in
     every theme, not just the light ones. Paired with the same
     border-color change already used for hover and for every other
     .selected pattern in this file (.avatar-opt.selected,
     .chip-opt.selected, etc.), so both the fill and the outline
     change, not just one. */
  .option-card.selected {
    background: var(--option-selected-bg);
    border-color: var(--coral);
  }
  /* Selected state — a real, visible indicator across all three
     themes at once: darkens the card's own background (whatever
     --cream currently is for the active theme) by a fixed amount via
     color-mix, so it's always "a slightly darker shade of its
     original color" rather than one hardcoded color that would look
     right in only one theme and wrong (or invisible) in the others.
     Paired with the same border-color change already used for hover
     and for every other .selected pattern in this file
     (.avatar-opt.selected, .chip-opt.selected, etc.), so both the
     fill and the outline change, not just one. */
  .option-card.selected {
    background: color-mix(in srgb, var(--cream) 82%, black);
    border-color: var(--coral);
  }
  .option-title {
    font-family: 'Fredoka', 'Fredoka Fallback', sans-serif;
    font-size: calc(16px * var(--fs));
    color: var(--plum);
    display: flex;
    justify-content: space-between;
    align-items: baseline;
  }
  /* Base tag styling, usable anywhere a small status/value pill is
     needed — not just inside an .option-title, which is the only
     place this used to be scoped to. */
  .tag {
    font-size: calc(11.5px * var(--fs));
    font-weight: 700;
    color: var(--mint-dark);
    background: var(--tag-bg);
    padding: 2px 7px;
    border-radius: 8px;
  }
  .option-detail {
    font-size: calc(13.5px * var(--fs));
    color: var(--plum-soft);
    margin-top: 3px;
  }

  /* Mirrors the player's portrait horizontally. Applied to the
     containing element rather than the <img> itself, so it works
     for both the real character art and the emoji fallback, and
     survives avatarHtml() rewriting the inner markup. */
  .avatar-flipped img, .avatar-flipped > span { transform: scaleX(-1); }

  /* The portrait + flip button row at the top of the player profile
     modal. Bigger than the old inline 30px thumbnail, since this is
     the one screen dedicated to looking at your own character. */
  .profile-portrait-row {
    display: flex;
    align-items: center;
    justify-content: center;
    gap: 14px;
    margin-bottom: 10px;
  }
  .profile-portrait {
    width: clamp(88px, 12vh, 150px);
    height: clamp(88px, 12vh, 150px);
    border-radius: 50%;
    overflow: hidden;
    background: var(--tag-bg);
    display: flex;
    align-items: center;
    justify-content: center;
    flex-shrink: 0;
  }
  .profile-portrait img { width: 100% !important; height: 100% !important; object-fit: cover; }
  .profile-portrait > span { font-size: clamp(48px, 7vh, 84px) !important; line-height: 1 !important; }
  .flip-btn {
    flex-shrink: 0;
    width: clamp(38px, 4.6vh, 54px);
    height: clamp(38px, 4.6vh, 54px);
    background: var(--cream);
    border: 1.5px solid var(--line);
    border-radius: 12px;
    color: var(--plum);
    font-size: clamp(calc(17px * var(--fs)), calc(2.2vh * var(--fs)), calc(24px * var(--fs)));
    cursor: pointer;
    display: flex;
    align-items: center;
    justify-content: center;
    transition: border-color 0.12s ease, transform 0.12s ease;
  }
  .flip-btn:hover { border-color: var(--coral); }
  .flip-btn:active { transform: scale(0.94); }

  /* A feed card with room to breathe. Deliberately NOT a different
     color or border — it should read as the same feed, just a
     longer entry, so it doesn't fragment the column visually. */
  .entry-rich { padding: calc(14px * var(--fs)) calc(16px * var(--fs)); }
  .entry-rich-title {
    font-family: 'Fredoka', 'Fredoka Fallback', sans-serif;
    font-size: calc(15px * var(--fs));
    color: var(--plum);
    margin-bottom: 5px;
  }
  .entry-rich-body { line-height: 1.5; }
  .entry-rich-lines {
    margin-top: 7px;
    padding-top: 7px;
    border-top: 1px solid var(--line);
    font-size: calc(13.5px * var(--fs));
    color: var(--plum-soft);
    line-height: 1.55;
  }

  /* ---- Stock listings ----
     Row = text on the left, sparkline pinned right at a fixed width
     so 25 rows of varying name length still line their charts up. */
  .stock-row { display: flex; align-items: center; gap: 10px; width: 100%; }
  .stock-meta { flex: 1; min-width: 0; }
  .stock-chart { flex-shrink: 0; width: 108px; opacity: 0.9; }
  .stock-chart-large { margin: 10px 0 4px; display: flex; justify-content: center; }
  .stock-chart-large svg { width: 100%; height: auto; max-width: 300px; }
  /* color-mix against the theme tokens rather than hardcoded green/red,
     so these stay legible in every theme (see the earlier tint bug) */
  .stock-up { color: var(--mint-dark); font-weight: 700; }
  .stock-down { color: var(--coral-dark); font-weight: 700; }

  /* Five controls do not fit on a 320px phone at the default
     spacing — measured 319px of content into 294px of room. Tighten
     the gap and the icon padding just at that width rather than
     shrinking the buttons everywhere, since 360px and up have the
     space and larger tap targets are better where they fit. */
  @media (max-width: 340px) {
    #screen-game .actions { gap: 4px; }
    #screen-game .actions .action-icon-btn { padding: 1px 3px; }
    #screen-game .actions .btn-primary { padding: 13px 10px; }
  }

  /* ---- The wordmark ----
     A flat 600-weight heading in the body font read as a form label
     rather than a title — no weight, no color, nothing to look at.
     This is built from what the game already loads (Fredoka) and its
     own palette, so it costs nothing extra and cannot fail to
     arrive.

     Three things do the work: a heavy weight at a genuinely large
     size, a warm gradient lifted from the stat colours, and a soft
     drop shadow that gives it a little physical presence without
     tipping into the plasticky bevelled-logo look. */
  .game-title {
    font-family: 'Fredoka', 'Fredoka Fallback', sans-serif;
    font-weight: 700;
    /* Height-aware as well as width-aware. Sizing on vw alone meant
       a 1366x768 laptop — wide but short — got the full 70px title,
       and those 100px (76 + margin) were exactly what pushed the
       creation screen past the fold and put a scrollbar on it. The
       vh term makes the title yield on short screens, which is where
       the vertical budget is actually tight. */
    font-size: clamp(34px, min(13vw, 8vh), 70px);
    line-height: 1.02;
    letter-spacing: -0.015em;
    text-align: center;
    margin: 24px auto 2px;
    /* At the widescreen breakpoint the creation screen becomes a
       grid, and the title was being placed as an ordinary
       shrink-to-fit item in the first column — which is why it sat
       noticeably left of center. Spanning every column and taking
       full width makes text-align:center mean what it looks like it
       means. This was also what pushed the banner off-center. */
    width: 100%;
    grid-column: 1 / -1;
    /* The gradient is painted on the text itself. background-clip is
       supported everywhere that matters; the plain color underneath
       is what shows if it is not. */
    color: var(--coral-dark);
  }

  /* The gradient lives on the SPANS, not the h1. Two Chromium
     behaviours forced this and both cost a render to find:
       - a `filter` on an element using background-clip:text stops
         the gradient painting entirely, and
       - a `transform` on a child creates its own stacking context,
         so it paints outside the parent's clip and comes out blank.
     Putting the gradient and the clip on the same element as the
     transform sidesteps both. */
  .game-title span {
    display: inline-block;
    position: relative;
    background: linear-gradient(140deg, var(--coral-dark) 0%, var(--coral) 30%, var(--sun) 74%, var(--mint-dark) 100%);
    -webkit-background-clip: text;
    background-clip: text;
    -webkit-text-fill-color: transparent;
    color: transparent;
  }
  /* Depth. A pseudo-element copy ghosted at the glyph edges, so
     instead this is a plain text-shadow — which still paints against
     a transparent text-fill because it shadows the glyph SHAPE, not
     the fill. Cleaner, and one less element. */
  .game-title span {
    text-shadow: 0 1px 0 rgba(255,255,255,0.3), 0 3px 12px rgba(0,0,0,0.13);
  }
  /* "Lives" sits a touch lower and larger, so the pair reads as a
     designed mark rather than two words in a row. */
  /* Straight across. The words were tilted in opposite directions
     to read as a designed mark; it looked like a mistake instead. */
  .game-title span:last-child,
  .game-title span:first-child { transform: none; }

  /* The line under it. It was the same muted gray as every other
     caption in the game; giving it a little space and weight makes
     the pair read as a title block. */
  .create-sub {
    margin-top: 10px !important;
    font-weight: 600;
    width: 100%;
    grid-column: 1 / -1;
  }

  /* 520, not 600 — at 600 this was catching ordinary 568-tall
     phones and shrinking the title to 30px on a screen with room
     for far more. This is for genuinely short viewports: a phone
     held sideways. */
  /* Laptop-height desktops (720-820px) are the tight case: wide
     enough for the two-column layout, short enough that the default
     rhythm overflows. Trims the gaps rather than the content. */
  @media (min-width: 1000px) and (max-height: 830px) and (orientation: landscape) {
    .game-title { margin-top: 10px; margin-bottom: 0; }
    .create-sub { margin-top: 6px !important; margin-bottom: 4px; }
    #screen-create.active { padding-top: 14px; padding-bottom: 12px; row-gap: 10px; }
  }
  /* 720p laptops are the tightest common case and were still 16px
     over. A slightly harder squeeze on the title and the footnote
     closes it without touching anything the player interacts with. */
  @media (min-width: 1000px) and (max-height: 745px) and (orientation: landscape) {
    .game-title { font-size: clamp(30px, 6.6vh, 52px); margin-top: 6px; }
    .create-sub { margin-top: 4px !important; margin-bottom: 2px; font-size: calc(13px * var(--fs)); }
    #screen-create.active { padding-top: 10px; padding-bottom: 8px; row-gap: 8px; }
    #screen-create.active .footnote { margin-top: 0; margin-bottom: 4px; }
  }

  @media (max-height: 520px) {
    /* margin-top clears the ☰ and 🎵 buttons. At 10px the title ran
       under them on a landscape phone, where the buttons sit at the
       same y but the title is wide enough to reach them. */
    .game-title { font-size: clamp(28px, 7vw, 40px); margin-top: 26px; }
  }

  /* The action bar's profile button carries the player's own
     portrait rather than a generic face — it is the one button that
     is about them specifically, and a miniature of their character
     reads faster than any icon could. Falls back to the emoji if the
     art never loads. */
  .ai-portrait {
    /* The label is display:none on phones (widescreen only), so this
       button is icon-only there — the portrait is sized to actually
       fill it rather than float in the middle of a 46px target. */
    width: 27px; height: 27px;
    border-radius: 50%;
    object-fit: cover;
    display: block;
    margin: 0 auto;
  }
  .ai-portrait.avatar-flipped { transform: scaleX(-1); }

  /* The permanently unreachable portion of the health bar — years of
     professional sport past the point a body can take it. Faded red
     rather than empty, so the player can see the ceiling they lost
     rather than just noticing health stops climbing. */
  .bar-track { position: relative; }
  .bar-lost {
    position: absolute;
    top: 0; right: 0; bottom: 0;
    width: 0;
    background: repeating-linear-gradient(
      -45deg,
      color-mix(in srgb, var(--coral-dark) 38%, transparent) 0 4px,
      color-mix(in srgb, var(--coral-dark) 22%, transparent) 4px 8px);
    border-radius: 0 999px 999px 0;
    pointer-events: none;
  }

  /* ---- Profile headers ----
     Opening someone's profile should actually SHOW you the character
     art you paid to have drawn. These headers used to render the
     portrait inline in an <h2> at 30px, which is a thumbnail beside
     text rather than a portrait. This stacks it: a large circular
     image with the name beneath, scaled the same way the player's own
     topbar portrait is. */
  .profile-header {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 8px;
    margin-bottom: 6px;
  }
  .profile-header-art {
    width: clamp(96px, 13vh, 168px);
    height: clamp(96px, 13vh, 168px);
    border-radius: 50%;
    overflow: hidden;
    background: var(--tag-bg);
    display: flex;
    align-items: center;
    justify-content: center;
    flex-shrink: 0;
    cursor: pointer;
    box-shadow: var(--shadow-sm);
  }
  /* !important for the same reason the topbar portrait needs it —
     avatarHtml writes width/height ATTRIBUTES on the <img>, which an
     ordinary selector can't override. */
  .profile-header-art img { width: 100% !important; height: 100% !important; object-fit: cover; }
  .profile-header-art > span { font-size: clamp(52px, 7.5vh, 92px) !important; line-height: 1 !important; }
  .profile-header-name {
    font-family: 'Fredoka', 'Fredoka Fallback', sans-serif;
    font-size: clamp(calc(19px * var(--fs)), calc(2.4vh * var(--fs)), calc(28px * var(--fs)));
    color: var(--plum);
    text-align: center;
  }

  /* ---- Person rows (social menu) ----
     Portrait on the left, spanning the full height of the card, with
     name and closeness stacked beside it. Previously the portrait sat
     INLINE inside .option-title with the closeness line below it, so
     its size was capped by the title's line-height — it could never
     be more than about 22px without pushing the text around. Pulling
     it out into its own column removes that ceiling entirely, which
     is what lets the portrait scale up to something actually
     recognisable. */
  .person-row {
    display: flex;
    align-items: center;
    gap: 12px;
    width: 100%;
  }
  .person-portrait {
    flex-shrink: 0;
    width: clamp(44px, 5.4vh, 72px);
    height: clamp(44px, 5.4vh, 72px);
    border-radius: 50%;
    overflow: hidden;
    background: var(--tag-bg);
    display: flex;
    align-items: center;
    justify-content: center;
  }
  .person-portrait img { width: 100% !important; height: 100% !important; object-fit: cover; }
  .person-portrait > span { font-size: clamp(24px, 3vh, 40px) !important; line-height: 1 !important; }
  .person-info { flex: 1; min-width: 0; }
  .person-name {
    font-family: 'Fredoka', 'Fredoka Fallback', sans-serif;
    font-size: clamp(calc(16px * var(--fs)), calc(2.0vh * var(--fs)), calc(24px * var(--fs)));
    color: var(--plum);
    display: flex;
    justify-content: space-between;
    align-items: baseline;
    gap: 8px;
  }
  .person-detail {
    font-size: clamp(calc(13.5px * var(--fs)), calc(1.7vh * var(--fs)), calc(19px * var(--fs)));
    color: var(--plum-soft);
    margin-top: 3px;
  }
  .modal-empty {
    font-size: calc(14px * var(--fs));
    color: var(--plum-soft);
    text-align: center;
    padding: 20px 10px;
  }
  .modal-close {
    margin-top: 14px;
    width: 100%;
    background: var(--cream);
    border: 1.5px solid var(--line);
    color: var(--plum);
    font-family: 'Fredoka', 'Fredoka Fallback', sans-serif;
    font-weight: 600;
    font-size: calc(13.5px * var(--fs));
    padding: 10px;
    border-radius: 12px;
    cursor: pointer;
  }

  @media (prefers-reduced-motion: reduce) {
    * { transition: none !important; }
  }

  /* =========================================================
     WIDESCREEN LAYOUT — monitor & tablet landscape
     -----------------------------------------------------------
     Adapted from the desktop/Steam build's widescreen.css. The base
     rules above already scale the phone-card up via `zoom` at wider
     viewports (see the #app breakpoints), which helps but still caps
     the game at a fixed-width card with the event feed showing only
     ~5 entries regardless of how much monitor is available. This
     block reflows the SAME markup into a two-column layout instead,
     once there's genuinely enough width and the device is in
     landscape — sidebar (identity/stats/career) on the left, the
     event feed filling the right column and the full window height.

     No DOM or JS changes needed for the grid itself: .topbar,
     .stats, .career-bar, .feed and .actions are already direct
     siblings inside #screen-game, so they can be placed on a CSS
     Grid directly. The one real markup change was adding
     .ai-icon/.ai-label spans inside the two icon buttons above, so
     their text labels ("Stats", "Social") have somewhere to live
     when there's room to show them.

     Gate: min-width AND landscape. A tall narrow window keeps the
     original card, so nothing regresses for a player who snaps the
     browser to half a screen or is on a portrait tablet.

     Worth checking on the live site specifically: this assumes
     index.html effectively IS the whole page at these widths (the
     `html, body { overflow: hidden }` rule below expects nothing
     else — no surrounding header/footer template — needs to scroll
     independently). If tiny-lives.com wraps this file in additional
     page chrome, that rule may need loosening. */
  @media (min-width: 1000px) and (orientation: landscape) {

    html, body {
      height: 100%;
      overflow: hidden; /* the feed scrolls internally; the page itself never should */
    }
    body { padding: 0; }

    .page-shell {
      height: 100%;
      max-width: none;
      gap: 0;
      padding: 0;
    }

    #app {
      zoom: 1;
      width: 100%;
      max-width: none;
      height: 100%;
      max-height: none;
      margin: 0;
      border: none;
      border-radius: 0;
      box-shadow: none;
      overflow: hidden;
      /* #app is now full-bleed and opaque, covering the entire
         viewport — which means it's ALSO now hiding body's own
         ambient glow background (the soft radial gradients that
         give the narrow card's surroundings some color instead of
         flat white). Since nothing of body is visible behind a
         full-bleed #app anymore, the glow needs to move here
         instead, or the whole screen just reads as a blank page —
         reusing the same theme-aware --bg-glow-* tokens body already
         uses, so this adapts correctly across Chill/Cozy/Cold-
         Blooded with no new color decisions. */
      background-image:
        radial-gradient(circle at 8% 12%, var(--bg-glow-1) 0, transparent 45%),
        radial-gradient(circle at 92% 88%, var(--bg-glow-2) 0, transparent 48%),
        radial-gradient(circle at 90% 8%, var(--bg-glow-3) 0, transparent 40%);
    }

    /* Character creation / the story screen can run taller than the
       viewport, and needs to scroll — but scrolling THAT element
       directly (width-capped to 720px and centered) puts the
       scrollbar at the edge of that narrow centered column, floating
       in the middle of a wide screen rather than sitting at the true
       edge of the window. Scrolling #app instead — which is already
       full width here — keeps the scrollbar exactly where it's
       expected: hugging the actual right edge of the browser.
       Existing #app::-webkit-scrollbar styling above already covers
       the cosmetic treatment; nothing new needed there.

       Gated with :has() so this only kicks in for these two screens
       — #screen-game manages its own internal scroll through .feed
       and #app must stay non-scrolling while it's active, which is
       exactly what the plain #app rule two rules up still does. */
    #app:has(#screen-create.active),
    #app:has(#screen-story.active) {
      overflow-y: auto;
    }

    #screen-game.active {
      display: grid;
      height: 100%;
      grid-template-columns: 440px 1fr;
      /* Rows stay `auto`. A previous attempt made row 3
         minmax(0, auto) to stop a tall sidebar pushing the action
         bar off the bottom — but row 4 is 1fr, which greedily takes
         every spare pixel, so row 3 collapsed to nothing and the
         career panel rendered as a sliver with its own scrollbar.
         The overflow case is handled on .career-bar instead, where
         it belongs. */
      grid-template-rows: auto auto auto 1fr auto;
      align-content: stretch;
      /* Safety net: rows 1/2/3/5 are all auto (sized to content,
         never compressible), so if their combined natural height
         ever exceeds what's actually available — exactly what
         happened here, once .career-bar's buttons grew — row 4
         alone absorbing the difference isn't enough on its own, and
         without this the excess has nowhere to go but visually
         overlapping whatever's nearby. With this, the worst case is
         the whole grid scrolls (topbar included) rather than
         breaking into overlapping content — a real trade-off, but a
         far smaller one than what this was actually doing. */
      /* Was overflow-y:auto as a safety net. That net is what the
         player was actually seeing: when the sidebar's natural
         height exceeded the row it was given — which happens with
         the real webfonts loaded, since their metrics differ from
         the fallbacks — the WHOLE grid scrolled, topbar and action
         bar included, and produced a page-edge scrollbar plus dead
         space under the action bar.

         Hidden here, and the sidebar column scrolls inside itself
         instead (see the .stats / .career-bar rules below). The
         chrome stays put and only the part with too much in it
         moves, which is the behaviour anybody would expect. */
      overflow: hidden;
      /* A 1px column-boundary rule painted as a gradient on the grid
         itself, since the sidebar isn't a wrapper element — .stats
         and .career-bar are separate grid children with nothing to
         put a border on without adding markup. Fixed column width
         (not minmax) specifically so this line has a position to
         pin to. */
      background-image: linear-gradient(to right,
        transparent 0 439px, var(--line) 439px 440px, transparent 440px);
    }

    /* The player's portrait is the one substantial piece of art on
       this screen, so it gets real prominence at widescreen rather
       than the 34px thumbnail that made sense on a phone card.
       Scaled via CSS only — the SVG's viewBox handles the ring
       geometry, so cx/cy/r and the hardcoded 144.5 circumference in
       renderAll() stay correct without touching a single number.

       Capped at 132px: the source art is roughly 256x256, and the
       ring's own padding means the image inside sits well under the
       220px ceiling where upscaling would start to show. */
    #screen-game.active > .topbar {
      grid-column: 1 / -1;
      grid-row: 1;
      padding: clamp(10px, 1.8vh, 22px) 28px clamp(9px, 1.6vh, 20px);
      gap: 18px;
    }
    #screen-game.active .avatar-ring {
      width: clamp(72px, 9.5vh, 132px);
      height: clamp(72px, 9.5vh, 132px);
    }
    #screen-game.active .avatar-ring > svg {
      width: 100%;
      height: 100%;
    }
    /* The <img> inside is written by renderAll() with explicit
       width/height attributes (34), so it needs an override here —
       otherwise the ring grows and the portrait stays tiny inside
       it. Percentage-based so it tracks the clamp above rather than
       needing its own matching numbers. */
    #screen-game.active .avatar-ring .emoji img {
      width: 78% !important;  /* !important to match the base rule's specificity war with avatarHtml's inline width/height attributes */
      height: 78% !important;
    }
    #screen-game.active .avatar-ring .emoji > span {
      font-size: clamp(38px, 5vh, 58px) !important; /* the emoji fallback path, for saves with no character art */
    }
    /* The two topbar buttons scale with the bar instead of staying a
       fixed 34px. Because the bar's height is now driven by the
       avatar ring (which scales with vh), a fixed button size meant
       the ratio decayed as the window grew — measured at 37% of the
       bar on a short window down to 20% on a tall one, which is why
       they read as shrinking into the corner.

       6.45vh is derived rather than guessed: the bar is ring +
       padding, and solving 0.5 * bar for the vh-driven range gives
       ~6.45vh across everything from 800px to 1080px tall. The
       clamp bounds keep it a usable target on very short windows
       and stop it ballooning on very tall ones. */
    /* Bumped from ~50% of the bar to ~65% — at 50% they still read as
       small relative to the enlarged portrait and text beside them.
       8.4vh is derived, not guessed: the bar is avatar-ring +
       padding, so 0.65 * bar solves to ~8.4vh through the 800-1080px
       range where most players are. Verified to stay INSIDE the
       bar's own padding at every size, so a taller button can never
       spill past the topbar edge. */
    #screen-game.active .menu-btn {
      width: clamp(44px, 8.4vh, 110px);
      height: clamp(44px, 8.4vh, 110px);
      border-radius: clamp(12px, 1.8vh, 24px);
      font-size: clamp(calc(20px * var(--fs)), calc(3.9vh * var(--fs)), calc(46px * var(--fs)));
    }
    #screen-game.active .id-name { font-size: clamp(calc(19px * var(--fs)), calc(2.4vh * var(--fs)), calc(26px * var(--fs))); }
    #screen-game.active .id-age { font-size: clamp(calc(13px * var(--fs)), calc(1.6vh * var(--fs)), calc(16px * var(--fs))); }

    /* ---- Compact stats on desktop ----
       Four progress bars were taking 305px here and closer to 415px
       in a real browser, where the webfonts are taller than the
       fallbacks this was tuned against. That difference is exactly
       what pushed the career panel past its row and put a scrollbar
       on it at 1080p.

       Trimmed hard: the cards keep their tint and border, they just
       stop being generously padded boxes around a 8px bar. Nothing
       here changes on phones, where the two-column layout means the
       stats block was never the constraint. */
    #screen-game.active > .stats {
      grid-column: 1;
      grid-row: 2;
      align-self: start;
      padding: 12px 20px 8px 28px;
      gap: 6px;
    }
    #screen-game.active > .stats .stat {
      padding: 5px 10px 7px;
      gap: 3px;
    }
    #screen-game.active > .stats .stat-label {
      font-size: calc(11px * var(--fs));
      letter-spacing: 0.03em;
    }
    #screen-game.active > .stats .bar-track { height: 7px; }


    /* .career-bar's CARD stretches through the row that used to sit
       empty below it (the same row .feed already fills on the
       right) — but the BUTTON GRID inside stays at its fixed,
       known-good size (see .career-buttons below) rather than
       growing to match. Three earlier attempts at making the
       buttons themselves dynamically fill available space each
       broke in a different way, most recently causing this exact
       row to overflow its budget and overlap the action bar below
       it. Letting the outer card stretch while its content stays a
       fixed size is a much smaller, safer claim: worst case is
       visible empty space at the bottom of a taller card, never
       overlap or clipped content. */
    /* Ends at row 5 (the action bar's row), NOT `-1`. Spanning into
       row 5 made the sidebar card share a track with the action
       bar, and since row 5 is `auto` it then sized to whichever of
       the two was taller — so a tall career card forced row 5 taller
       than the bar needed, leaving dead space under the bar. The
       card is `align-self: start` and sizes to its own content
       anyway, so it has no need to reach into row 5. */
    /* Trimmed alongside the stats, for margin rather than necessity.
       The panel needed 324px against roughly 318px available in a
       real browser at 1080p — close enough that a slightly different
       font, a longer job title or a browser's own rounding tipped it
       into scrolling. This buys back enough that it is not a
       coin-flip any more. */
    #screen-game.active > .career-bar .career-buttons { gap: 8px; }
    #screen-game.active > .career-bar .career-btn { padding: 10px 4px; }
    #screen-game.active > .career-bar .cb-top { margin-bottom: 7px; }

    #screen-game.active > .career-bar {
      grid-column: 1;
      /* Row 3 only, and NO height constraint at all.
         Every previous attempt tried to make this panel fit inside a
         computed space — max-height with overflow-y, then a
         compressible row — and every one of them produced the same
         scrollbar in a real browser, because the space was being
         computed smaller than the content needed.

         So it is no longer constrained. The panel is always its
         natural height and physically cannot scroll inside itself.
         The FEED yields instead: it is the 1fr row with min-height 0,
         it is the thing on screen that can lose 40px without anybody
         noticing, and it already scrolls internally by design. */
      grid-row: 3;
      align-self: start;
      padding: 12px 14px;
      margin: 0 20px 20px 28px;
      padding: 16px;
      display: flex;
      flex-direction: column;
      min-height: 0;
    }

    /* The feed spans every row between the topbar and the action
       bar, growing with the window instead of being capped — this is
       the single biggest win of the whole layout, taking the event
       log from ~5 visible entries to 15-20 at 1080p. */
    #screen-game.active > .feed {
      grid-column: 2;
      grid-row: 2 / 5;
      max-height: none;
      height: 100%;
      min-height: 0; /* lets the flex child actually scroll inside the grid track */
      padding: 20px 28px 20px 20px;
      gap: 12px;
    }

    /* Caps the reading measure — the feed column is ~1100px at
       1080p and 2000px+ on an ultrawide, well past a comfortable
       line length otherwise. Left-aligned rather than centred, so
       the log still reads as a column of records against the
       sidebar edge. */
    #screen-game.active > .feed > * {
      max-width: 1040px;
    }

    /* Three fixes over the narrow-card action bar:
       1. Opaque background — the column divider is a gradient
          painted across the whole grid, so a transparent bar lets it
          run straight through as a stray vertical tick.
       2. Centred rather than left-packed — .btn keeps flex:1 above,
          so without a cap here the two advance-time buttons absorb
          all free space and shove the icon buttons to the left edge.
       3. Even gaps, so all four controls read as one cluster. */
    /* Column 2 only — no longer spanning the full width. This is
       both what makes the bar sit beside the sidebar instead of
       running underneath it, AND the actual fix for the overlap
       bug: .career-bar spans rows 3/5 in column 1, so a full-width
       .actions in row 5 was occupying the same cell as the bottom
       of the career panel and painting its opaque background over
       the career buttons. Scoping it to column 2 removes the
       conflict entirely rather than trying to size around it.

       align-items: center replaces the base rule's `stretch` — with
       the bar no longer full-width there's less room, so the
       buttons sit at their natural height rather than stretching to
       fill the whole track. */
    /* align-items: center is deliberately NOT set here — with the
       row-5 track being `auto` (content-sized), the bar's height is
       just its padding plus its tallest button. Centring inside a
       taller track was what made it read as a big empty band with
       small buttons floating in the middle. Symmetric 12px padding
       top and bottom keeps it tight to the buttons. */
    #screen-game.active > .actions {
      grid-column: 2;
      grid-row: 5;
      padding: 12px 28px 12px 20px;
      background: var(--card);
      justify-content: center;
      align-items: center;
      gap: 10px;
    }

    #screen-game.active .ai-label {
      display: inline;
      font-family: 'Fredoka', 'Fredoka Fallback', sans-serif;
      font-weight: 600;
      font-size: calc(13.5px * var(--fs));
    }

    /* Resized for the narrower column-2-only bar — the previous
       96px minimum and 300px primaries were sized assuming the full
       window width, which would now crowd or wrap. */
    #screen-game.active .action-icon-btn {
      width: auto;
      min-width: 84px;
      min-height: 44px;
      padding: 0 14px;
      gap: 7px;
      flex: 0 0 auto;
    }

    #screen-game.active > .actions .btn-primary {
      flex: 1 1 0;
      min-width: 0; /* lets these actually shrink to fit rather than forcing the row wider than its column */
      max-width: 240px;
      min-height: 44px;
      padding: 11px 14px;
    }

    /* Fixed row heights, not 1fr/minmax — a known, comfortable size
       that always shows all four buttons with visible labels,
       rather than something that grows or shrinks based on
       available space (which is exactly what kept going wrong; see
       the note above .career-bar). */
    /* Viewport-relative rather than a fixed 92px, so these scale
       with the window instead of staying stubbornly the same size
       at every zoom level and resolution.

       clamp() is the key difference from the earlier attempts that
       kept breaking: those tied button height to LEFTOVER space
       (flex: 1 / 1fr rows), which can legitimately go to zero and
       take the labels with it. This is tied to viewport height
       instead — a value that always exists and is never zero — and
       is bounded at both ends, so it can't collapse below a legible
       68px or grow past 104px no matter how extreme the window
       gets. Predictable at every size, which the fixed value wasn't
       and the leftover-space version definitely wasn't. */
    #screen-game.active .career-buttons {
      grid-template-columns: 1fr 1fr;
      grid-auto-rows: clamp(68px, 10.5vh, 104px);
      gap: clamp(8px, 1.1vh, 12px);
    }
    #screen-game.active .career-btn {
      display: flex;
      flex-direction: column;
      align-items: center;
      justify-content: center;
      font-size: clamp(calc(11.5px * var(--fs-tight)), calc(1.35vh * var(--fs-tight)), calc(13.5px * var(--fs-tight)));
      gap: clamp(3px, 0.5vh, 6px);
      padding: 4px;
      border-radius: 14px;
      border-width: 2px;
      min-height: 0;
    }
    #screen-game.active .cbtn-icon {
      font-size: clamp(calc(17px * var(--fs)), calc(2.4vh * var(--fs)), calc(23px * var(--fs))); /* scales alongside the button height above, so the icon stays proportionate instead of looking lost in a taller button or crowding a shorter one */
      margin-right: 0; /* the column layout uses the flex gap above instead of the inline margin */
      line-height: 1;
    }

    /* Full-width stat bars read more precisely than half-width ones,
       and there's vertical room for it now. Also gives the whole
       sidebar real visual presence — at the size this stretches to
       once it's a genuine two-column layout, floating stat text
       directly on #app's flat background reads as empty rather than
       calm. A bordered card plus a light per-stat tint (reusing each
       stat's own bar color — coral/sun/lavender/mint, already
       established below) gives the section real structure and color
       without introducing a single new hue to the palette. */
    #screen-game.active .stats {
      grid-template-columns: 1fr;
      gap: 10px;
      background: var(--card);
      border: 1.5px solid var(--line);
      border-radius: 20px;
      box-shadow: var(--shadow-sm);
    }
    #screen-game.active .stat {
      padding: 10px 14px;
      border-radius: 12px;
    }
    /* color-mix() against the actual theme variables, not hardcoded
       RGB matching only the Cozy palette — same bug and same fix as
       the character-creation panels above; these tints would have
       rendered nearly identically regardless of which theme was
       active otherwise. Strength bumped up to match. */
    #screen-game.active .stat:nth-child(1) { background: color-mix(in srgb, var(--coral) 18%, var(--card)); } /* health */
    #screen-game.active .stat:nth-child(2) { background: color-mix(in srgb, var(--sun) 22%, var(--card)); } /* happiness */
    #screen-game.active .stat:nth-child(3) { background: color-mix(in srgb, var(--lavender) 24%, var(--card)); } /* wits */
    #screen-game.active .stat:nth-child(4) { background: color-mix(in srgb, var(--mint) 20%, var(--card)); } /* looks */

    /* Same card treatment for the career panel, plus a soft tinted
       header behind the job title/money line so it reads as a
       genuine banner rather than plain text floating on white. Kept
       deliberately light (the same translucent-tint approach as
       --tag-bg elsewhere) rather than a saturated block, so the
       existing debt/savings text colours — meaningful red/green
       color-coding — stay legible against it untouched. */
    #screen-game.active > .career-bar {
      background: var(--card);
      border: 1.5px solid var(--line);
      border-radius: 20px;
      box-shadow: var(--shadow-sm);
    }
    #screen-game.active .cb-top {
      background: linear-gradient(120deg, color-mix(in srgb, var(--lavender) 24%, var(--card)) 0%, color-mix(in srgb, var(--coral) 22%, var(--card)) 100%);
      padding: 12px 14px;
      border-radius: 14px;
      margin: 0 0 12px 0; /* matches the card's own 16px padding closely enough to read as even spacing all round, rather than a wider gap under the banner than beside it */
    }

    /* Scales with the window instead of sitting at a fixed pixel
       size. The narrow breakpoints below 1000px still scale text via
       the #app `zoom` property, but widescreen sets zoom back to 1 —
       so without this, feed text stayed frozen at ~14.5px whether
       the window was 1280px or 4K, which is what made it look
       progressively smaller as the layout grew around it.

       1.8vh is chosen to stay at the 14px floor on short windows
       (where vertical space is already tight), grow meaningfully
       through the common 900-1080px range, and cap at 22px before
       it starts to look oversized on very tall displays. Padding
       and radius scale alongside so the bubbles stay proportioned
       rather than becoming thin text in fat boxes. */
    #screen-game.active .entry {
      font-size: clamp(calc(15.5px * var(--fs)), calc(2.0vh * var(--fs)), calc(24px * var(--fs)));
      padding: clamp(11px, 1.35vh, 18px) clamp(14px, 1.7vh, 24px);
      border-radius: clamp(14px, 1.7vh, 20px);
      line-height: 1.45;
    }

    /* Modals: the narrow-card rules pin them to the bottom and round
       the scrim to match the card's own corners — both wrong once
       the app fills the window, where a bottom-pinned sheet strands
       against a square window edge. Centred, width-capped, and
       square-cornered here instead; the width cap also keeps option
       lists from stretching into rows too wide to scan. */
    .modal-overlay {
      align-items: center;
      border-radius: 0;
      padding: 24px;
    }
    /* Modal text was the worst case of the fixed-size problem: unlike
       feed entries it had NO widescreen override at all, so
       .option-title / .option-detail / .modal-sub rendered at exactly
       14.5 / 12 / 13px identically on a phone and on a 4K monitor.
       These scale on the same 1.8vh curve as the feed so the whole
       interface reads as one consistent size family, and the card
       itself widens to match rather than becoming a narrow column of
       large text. */
    .modal-card {
      max-width: clamp(620px, 46vw, 860px);
      max-height: min(88%, 760px);
      border-radius: 20px;
      padding: clamp(22px, 2.6vh, 34px) clamp(20px, 2.4vh, 32px) clamp(24px, 2.8vh, 36px);
    }
    .modal-card h2 { font-size: clamp(calc(20px * var(--fs)), calc(2.6vh * var(--fs)), calc(30px * var(--fs))); }
    .modal-sub { font-size: clamp(calc(14.5px * var(--fs)), calc(1.85vh * var(--fs)), calc(21px * var(--fs))); }
    .option-title { font-size: clamp(calc(16px * var(--fs)), calc(2.0vh * var(--fs)), calc(24px * var(--fs))); }
    .option-detail { font-size: clamp(calc(13.5px * var(--fs)), calc(1.7vh * var(--fs)), calc(19px * var(--fs))); }
    .option-card { padding: clamp(12px, 1.5vh, 20px) clamp(14px, 1.7vh, 22px); }
    .option-title .tag, .tag { font-size: clamp(calc(10.5px * var(--fs)), calc(1.25vh * var(--fs)), calc(14px * var(--fs))); }
    .btn { font-size: clamp(calc(16.5px * var(--fs)), calc(2.05vh * var(--fs)), calc(23px * var(--fs))); }

    #screen-story.active {
      max-width: 720px;
      margin: 0 auto;
      width: 100%;
      min-height: 100%;
      justify-content: center;
      padding: 28px 0;
    }

    /* Character creation gets its own two-column layout rather than
       just a wider single column. The visual choices (gender,
       avatar) and the textual details (name, preferences, legacy
       name) are naturally independent halves of the same decision,
       so splitting them left/right actually uses the width rather
       than padding a single column out wider with more empty margin
       on either side.

       Every child gets an explicit grid-column/grid-row instead of
       relying on grid-auto-flow: column (which needs a fixed
       row-track count to know when to wrap into a new column, and
       mixing that with full-width spanning rows for the title and
       the Begin Life button is exactly the kind of interaction
       that's easy to get subtly wrong without being able to see it
       render). Explicit placement means every position here can be
       verified by reading the rule, not by guessing how
       auto-placement resolves — deliberately the more verbose
       choice given how much of this whole layer has had to be
       corrected after the fact once real browsers disagreed with
       what looked right on paper. */
    #screen-create.active {
      display: grid;
      grid-template-columns: 1fr 1fr;
      column-gap: 40px;
      row-gap: 16px;
      max-width: 900px;
      margin: 0 auto;
      width: 100%;
      min-height: 100%;
      align-content: center;
      padding: 28px 0;
    }

    #screen-create.active > h1.display { grid-column: 1 / -1; grid-row: 1; }
    /* Pulls the menu button out of its fixed top-right corner and
       onto the grid alongside the title. The title is vertically
       centred within the grid, but the button was pinned at a fixed
       top: 16px — so the gap between them grew with window height
       (measured at 88px on a short window and 228px on a tall one),
       which is why it read as floating unanchored. Placing it in the
       title's own row and centring it vertically keeps them aligned
       at every size. justify-self keeps it hard right; position:
       static undoes the base absolute positioning so grid placement
       actually takes effect. */
    /* The music toggle sits immediately left of the menu button in
       the same grid cell. Both are .menu-btn-light, which is
       absolutely positioned at top-right by default — so without
       placing them explicitly here they'd stack directly on top of
       each other. */
    #screen-create.active > #createMusicToggleBtn {
      position: relative;
      top: auto;
      right: auto;
      z-index: 1;
      grid-column: 2;
      grid-row: 1;
      justify-self: end;
      align-self: center;
      margin-right: 44px;
    }
    #screen-create.active > #createMenuBtn {
      position: relative; /* not static — this establishes a stacking context so z-index below actually applies */
      top: auto;          /* the base .menu-btn-light rule sets top:16px for its absolute positioning; left as-is it offsets this by 16px once position is relative */
      right: auto;
      z-index: 1;         /* the h1 spans 1/-1 and overlaps this cell; without this it paints on top and swallows every click (verified: elementFromPoint returned the h1, not the button) */
      grid-column: 2;
      grid-row: 1;
      justify-self: end;
      align-self: center;
    }
    #screen-create.active > .create-sub { grid-column: 1 / -1; grid-row: 2; margin-bottom: 8px; }

    /* Columns flipped: the look/avatar panel now sits on the RIGHT,
       details (name/preferences/legacy) on the LEFT. Class names are
       deliberately content-based (-look / -details) rather than
       -left / -right, so they stay accurate through exactly this
       kind of change instead of becoming actively misleading.

       Column 2 stacks look panel → Begin Life → wishlist as three
       separate grid rows, which fills what used to be empty space
       under the avatar picker and keeps Begin Life reliably on
       screen rather than stranded at the bottom of a long page. */
    /* The title and its tagline are pinned to the first two rows.
       Without this they auto-placed AFTER the panels — which sit on
       grid-row 3 — and the title rendered at the bottom of the
       screen on desktop while looking correct on every phone. */
    #screen-create.active .game-title { grid-column: 1 / -1; grid-row: 1; }
    #screen-create.active .create-sub  { grid-column: 1 / -1; grid-row: 2; }

    #screen-create.active .create-panel-look {
      grid-column: 2;
      grid-row: 3;
      align-self: start;
    }
    #screen-create.active .create-panel-details {
      grid-column: 1;
      grid-row: 3 / 6;
      align-self: start;
    }

    /* The label above Gender was drifting out of alignment with the
       centred chip row below it once this field stretched to fill a
       wide grid column — .field's base rule is text-align: left
       (fine at the narrow card width, where it barely matters), but
       .chip-picker centres its own contents regardless. Centring
       the label here keeps both aligned to the same reference. */
    #screen-create.active #genderField {
      text-align: center;
    }

    /* Real containers now, not just contents pass-throughs — each
       panel gets a soft tinted card so the two columns read as
       distinct sections instead of floating unbounded on plain
       background. Kept as a light translucent wash (the same
       approach used for the in-game sidebar) rather than a
       saturated block, so none of the existing text colours inside
       need to change. */
    #screen-create.active .create-panel {
      display: flex;
      flex-direction: column;
      align-items: center;
      gap: 18px;
      width: 100%;
      background: var(--card);
      border: 1.5px solid var(--line);
      border-radius: 20px;
      padding: 26px 24px;
      box-shadow: var(--shadow-sm);
    }
    /* color-mix() blends against the theme's ACTUAL --coral/
       --lavender variable rather than a hardcoded RGB approximation
       — the previous version used fixed rgba() values matching only
       the Cozy theme's colours, so Chill and Cold-Blooded rendered
       nearly the same pale pink regardless of their own palette
       being blue or crimson. This adapts correctly across all three.
       Strength bumped well past the original 10% too — that was
       barely perceptible, which was the core of "looks like a blank
       page" in the first place. */
    #screen-create.active .create-panel-look {
      background: linear-gradient(160deg, color-mix(in srgb, var(--coral) 26%, var(--card)) 0%, var(--card) 60%);
    }
    #screen-create.active .create-panel-details {
      background: linear-gradient(160deg, color-mix(in srgb, var(--lavender) 26%, var(--card)) 0%, var(--card) 60%);
    }
    /* Excludes .avatar-badge-wrap specifically — it was never meant
       to be full width, only its 64px natural size, centred. This
       is the actual root cause of two things that looked like
       separate bugs: the avatar sitting pinned to the left edge
       instead of centred, AND the randomize-dice badge appearing
       disconnected out near the panel's right edge. Both come from
       the SAME mistake — stretching this element to 100% width also
       stretched the box the dice button's absolute position:
       bottom/right is anchored to, so "hug this box's corner"
       stopped meaning "hug the avatar's corner" the moment the box
       became much wider than the avatar itself. */
    #screen-create.active .create-panel > *:not(.avatar-badge-wrap) { width: 100%; }
    #screen-create.active .avatar-badge-wrap { align-self: center; }

    /* TODO: once the Steam store page exists, point this at it —
       window.open(URL, '_blank') is enough. See steamWishlistBtn's
       click listener near the character-creation setup code. A real
       button rather than an <a href="#"> on purpose, so it can't
       misbehave before that link exists. */
    #screen-create.active .steam-wishlist {
      display: flex;
      grid-column: 2;
      grid-row: 5;
      align-self: start;
      align-items: center;
      gap: 14px;
      width: 100%;
      padding: 16px 20px;
      border: none;
      border-radius: 18px;
      background: linear-gradient(120deg, var(--lavender-dark) 0%, var(--coral-dark) 100%);
      color: white;
      cursor: pointer;
      text-align: left;
      box-shadow: var(--shadow-sm);
      transition: transform 0.12s ease;
    }
    #screen-create.active .steam-wishlist:active { transform: scale(0.98); }
    #screen-create.active .steam-wishlist-icon { font-size: calc(26px * var(--fs)); flex-shrink: 0; }
    #screen-create.active .steam-wishlist-text { display: flex; flex-direction: column; gap: 2px; }
    #screen-create.active .steam-wishlist-title {
      font-family: 'Fredoka', 'Fredoka Fallback', sans-serif;
      font-weight: 700;
      font-size: calc(15px * var(--fs));
    }
    #screen-create.active .steam-wishlist-sub {
      font-size: calc(12px * var(--fs));
      opacity: 0.9;
    }

    /* nameField/preferenceField/legacyNameField no longer need
       individual grid placement — they're nested inside
       .create-panel-details now (which IS the direct grid child, row
       3), and simply flow as normal flex children within it. */

    /* Begin Life moves visually into the look column, directly above
       the wishlist button — filling the empty space under the avatar
       picker and, more importantly, keeping it reliably on screen
       instead of stranded at the bottom of a long page.

       Done purely with grid placement rather than by moving the DOM
       node: at the narrow/mobile breakpoint this same element still
       needs to sit in its natural document position at the end of
       the form, and relocating it in the markup would break that. */
    #screen-create.active > .actions {
      grid-column: 2;
      grid-row: 4;
      margin-top: 0;
    }
    #screen-create.active > p.footnote { grid-column: 1 / -1; grid-row: 6; margin-top: 4px; }

    /* Safety net for browsers without :has() support (a shrinking
       minority, but not zero) — without this, those browsers would
       silently match neither the #app:has(...) rule above nor get
       any scroll path at all, reintroducing the exact lockout this
       whole scroll treatment exists to fix. @supports selector() has
       been supported since long before :has() itself existed, so
       gating on it here is safe. Falls back to scrolling the screen
       element directly — the scrollbar sits at the edge of the
       720px column rather than the true window edge, but a working
       scrollbar in an imperfect spot beats no scrollbar at all. */
    @supports not selector(:has(a)) {
      #screen-create.active,
      #screen-story.active {
        max-height: 100%;
        overflow-y: auto;
        scrollbar-width: thin;
      }
      #screen-create.active::-webkit-scrollbar,
      #screen-story.active::-webkit-scrollbar { width: 6px; }
      #screen-create.active::-webkit-scrollbar-track,
      #screen-story.active::-webkit-scrollbar-track { background: transparent; }
      #screen-create.active::-webkit-scrollbar-thumb,
      #screen-story.active::-webkit-scrollbar-thumb { background: var(--line); border-radius: 999px; }
    }

    /* Centred flex rather than a fixed column count, so it stays
       centred for any number of avatar choices — CHARACTER_CHOICES_SHOWN
       changing shouldn't ever require touching CSS. */
    .avatar-picker {
      display: flex;
      flex-wrap: wrap;
      justify-content: center;
      align-items: center;
      max-width: 560px;
      gap: 10px;
      min-height: 40px; /* single row here (vs 2 wrapped rows at narrow widths), so less reserved space is needed than the base 88px — measured, not estimated */
    }
    .avatar-opt { flex: 0 0 auto; }
  }

  /* Past ~1500px the shell stops growing, so widen the sidebar and
     the reading column rather than adding more empty margin. */
    /* ---- Desktop landscape typography ----
     The landscape layout sets zoom: 1 and goes full-bleed, so unlike
     the portrait path it never scaled with the screen: a 2560px
     monitor rendered everything at the same size a 1000px laptop
     did, which is the "it all stays uniformly small" problem.

     Zoom rather than a font-size sweep, because it scales the whole
     system together — text, portraits, bars, padding, tap targets —
     and it is what the portrait breakpoints already use, so the two
     paths behave consistently. It also keeps line length in
     CHARACTERS roughly constant as the screen grows, which is the
     part that actually governs readability.

     height is re-derived at each step because zoom divides the
     coordinate space; without it the layout overflows vertically. */
  /* 1250 catches the common 1280×800 laptop, which was sitting just
     below the first step and running ~107 characters a line. */
  /* ---- Desktop zoom ladder ----
     Dialled back from 1.06/1.12/1.26/1.44. Those were tuned in a
     headless viewport, which has no scrollbar and no browser
     chrome; in a real window the same numbers rendered noticeably
     too large and pushed a scrollbar onto the page. A player at 80%
     browser zoom — an effective scale near 1.15 at 1080p — reported
     that as the size it should have been, so the ladder now tops
     out there instead of at 1.44.

     dvh rather than vh: on a real desktop window vh can resolve
     against a viewport that ignores the scrollbar gutter, which is
     exactly the few pixels that turn "just fits" into "scrolls". */
  @media (min-width: 1250px) and (orientation: landscape) {
    #app { zoom: 1.02; height: calc(100dvh / 1.02); max-height: calc(100dvh / 1.02); }
  }
  @media (min-width: 1400px) and (orientation: landscape) {
    #app { zoom: 1.06; height: calc(100dvh / 1.06); max-height: calc(100dvh / 1.06); }
  }
  @media (min-width: 1700px) and (orientation: landscape) {
    #app { zoom: 1.12; height: calc(100dvh / 1.12); max-height: calc(100dvh / 1.12); }
  }
  @media (min-width: 2100px) and (orientation: landscape) {
    #app { zoom: 1.2; height: calc(100dvh / 1.2); max-height: calc(100dvh / 1.2); }
  }
  @media (min-width: 2800px) and (orientation: landscape) {
    #app { zoom: 1.32; height: calc(100dvh / 1.32); max-height: calc(100dvh / 1.32); }
  }


    /* ---- Reading width ----
     Zoom fixed the size but not the LINE LENGTH: at 2560 the feed
     ran to 244 characters a line, roughly three times the point
     where the eye starts losing its place returning to the left
     margin. Typographic guidance puts the comfortable range around
     50–90.

     Capping the text column rather than the feed container, so the
     cards keep filling their column and only the prose reflows. */
  @media (min-width: 1200px) and (orientation: landscape) {
    #feed .entry, #feed .entry-rich { max-width: 78ch; }
  }

  @media (min-width: 1600px) and (orientation: landscape) {
    #screen-game.active {
      grid-template-columns: 420px 1fr;
      background-image: linear-gradient(to right,
        transparent 0 419px, var(--line) 419px 420px, transparent 420px);
    }
    /* entry font-size intentionally omitted here — the clamp() in the
       main widescreen block scales continuously, so a fixed override
       at this tier would flatten it back out. */
  }

  /* Compact landscape devices — a tablet held sideways, a Steam Deck,
     a small laptop window. The pixel count reads desktop-ish but the
     physical size doesn't, so this is the one tier that needs LARGER
     touch targets rather than smaller ones; everything here is about
     finger accuracy, not fitting more on screen. Keyed off max-height
     specifically so a merely-narrow desktop window doesn't inherit
     it. */
  @media (min-width: 1000px) and (max-width: 1360px) and (max-height: 860px) and (orientation: landscape) {
    #screen-game.active {
      grid-template-columns: 340px 1fr;
      background-image: linear-gradient(to right,
        transparent 0 339px, var(--line) 339px 340px, transparent 340px);
    }
    /* Keeps the clamp()-based scaling from the main widescreen tier
       rather than reverting to fixed values — just shifts the range
       slightly larger at the bottom end, since this tier is about
       finger accuracy on a physically small screen. Overriding with
       a flat font-size/padding here would undo the scaling entirely
       for exactly the devices that most need a sensible size. */
    #screen-game.active .career-buttons {
      grid-auto-rows: clamp(64px, 11vh, 92px);
    }
    #screen-game.active .career-btn {
      font-size: clamp(calc(11px * var(--fs)), calc(1.5vh * var(--fs)), calc(13px * var(--fs)));
    }
    #screen-game.active > .actions {
      padding: 12px 20px;
    }
    #screen-game.active > .actions .btn {
      min-height: 48px; /* comfortably above the ~44px minimum touch target */
    }
    #screen-game.active .action-icon-btn {
      min-width: 92px;
      min-height: 48px;
    }
    /* Scoped to genuine touch devices only. This rule insets the app
       by 12px with rounded corners — intended for a 7-9" tablet or
       Deck-class screen, where a little breathing room reads better
       than an edge-to-edge rectangle. But the surrounding media
       query matches on width/height alone, so ANY desktop browser
       window under 1360x860 was also getting it: that 12px bottom
       margin is exactly the persistent gap under the action bar,
       measured at 1000x600 through 1280x800 and absent at every
       larger size. `pointer: coarse` restricts it to devices whose
       primary input is a finger, which is what the rule was always
       actually about. */
    @media (pointer: coarse) {
      #app {
        height: calc(100vh - 24px);
        margin: 12px auto;
        border-radius: 18px;
      }
    }
  }

  /* ---- Resentment meter (partner screen) ----
     A thin bar above the line, filled to --resent in --resent-color.
     Both set inline from the theme variables (invariant 11). */
  .resent-line { line-height: 1.5; }
  .resent-meter {
    display: block;
    height: 6px;
    margin: 2px auto 6px;
    max-width: 220px;
    border-radius: 999px;
    background: color-mix(in srgb, var(--plum-soft) 18%, var(--card));
    overflow: hidden;
  }
  .resent-meter > span {
    display: block;
    height: 100%;
    width: var(--resent, 0%);
    background: var(--resent-color, var(--coral-dark));
    border-radius: inherit;
  }

  /* ---- Relationship, wrapped ----
     The card a partner leaves you with. It is a ranking, so the
     numbers are real; the quote is the loudest thing on it. */
  .wrapped-card {
    border-radius: 20px;
    padding: 18px 16px 14px;
    margin-bottom: 12px;
    color: var(--plum);
    background:
      radial-gradient(120% 80% at 0% 0%, color-mix(in srgb, var(--coral) 34%, transparent), transparent 60%),
      radial-gradient(120% 80% at 100% 100%, color-mix(in srgb, var(--lavender) 40%, transparent), transparent 60%),
      color-mix(in srgb, var(--coral) 8%, var(--card));
    border: 1px solid color-mix(in srgb, var(--coral) 30%, var(--line));
  }
  .wrapped-head { text-align: center; }
  .wrapped-portrait {
    width: 76px; height: 76px;
    margin: 0 auto 8px;
    border-radius: 50%;
    overflow: hidden;
    filter: grayscale(0.55);
    border: 3px solid var(--card);
    box-shadow: var(--shadow-sm);
    display: flex; align-items: center; justify-content: center;
  }
  .wrapped-portrait img { width: 100% !important; height: 100% !important; object-fit: cover; }
  .wrapped-portrait > span { font-size: 44px !important; line-height: 1 !important; }
  .wrapped-title {
    font-family: 'Fredoka', 'Fredoka Fallback', sans-serif;
    font-size: calc(22px * var(--fs-tight));
    font-weight: 600;
    color: var(--coral-dark);
  }
  .wrapped-sub { font-size: calc(14px * var(--fs-tight)); color: var(--plum-soft); margin-top: 4px; line-height: 1.45; }
  .wrapped-stats {
    display: flex; flex-wrap: wrap; justify-content: center; gap: 6px;
    margin: 12px 0 4px;
    font-size: calc(12.5px * var(--fs-tight));
  }
  .wrapped-stats > span {
    padding: 4px 10px;
    border-radius: 999px;
    background: color-mix(in srgb, var(--card) 75%, transparent);
    color: var(--plum-soft);
  }
  .wrapped-stats strong { color: var(--plum); }
  .wrapped-label {
    margin: 14px 0 6px;
    font-family: 'Fredoka', 'Fredoka Fallback', sans-serif;
    font-size: calc(14px * var(--fs-tight));
    color: var(--plum-soft);
    text-align: left;
  }
  .wrapped-list { list-style: none; margin: 0; padding: 0; display: grid; gap: 8px; }
  .wrapped-item {
    display: grid;
    grid-template-columns: auto auto minmax(0, 1fr);
    align-items: center;
    gap: 10px;
    padding: 10px 12px;
    border-radius: 14px;
    background: color-mix(in srgb, var(--card) 82%, transparent);
    text-align: left;
  }
  .wrapped-rank {
    font-family: 'Fredoka', 'Fredoka Fallback', sans-serif;
    font-size: calc(20px * var(--fs-tight));
    font-weight: 700;
    color: var(--coral-dark);
    min-width: 1.6em;
  }
  .wrapped-item:first-child .wrapped-rank { font-size: calc(26px * var(--fs-tight)); }
  .wrapped-icon { font-size: 22px; line-height: 1; }
  .wrapped-text { display: flex; flex-direction: column; min-width: 0; }
  .wrapped-quote {
    font-family: 'Fredoka', 'Fredoka Fallback', sans-serif;
    font-size: calc(15.5px * var(--fs-tight));
    color: var(--plum);
    line-height: 1.3;
  }
  .wrapped-item:first-child .wrapped-quote { font-size: calc(17.5px * var(--fs-tight)); }
  .wrapped-stat { font-size: calc(12.5px * var(--fs-tight)); color: var(--plum-soft); margin-top: 2px; }
  .wrapped-foot {
    margin-top: 10px;
    font-size: calc(13px * var(--fs-tight));
    color: var(--plum-soft);
    font-style: italic;
    text-align: center;
  }

  /* Where an ex stands with you, at the foot of the wrapped card. */
  .wrapped-stance {
    margin-top: 12px;
    padding: 10px 12px;
    border-radius: 14px;
    background: color-mix(in srgb, var(--mint) 18%, var(--card));
    color: var(--plum);
    font-size: calc(13.5px * var(--fs-tight));
    line-height: 1.45;
    text-align: left;
  }
  .wrapped-stance span { color: var(--plum-soft); }

  /* ---- Relocation picker ----
     Two dependent dropdowns (state, then place) instead of 340 pills. */
  .reloc-picker {
    margin: 10px 0 4px;
    padding: 12px;
    border-radius: 16px;
    background: color-mix(in srgb, var(--lavender) 14%, var(--card));
    border: 1px solid var(--line);
  }
  .reloc-label {
    display: block;
    font-family: 'Fredoka', 'Fredoka Fallback', sans-serif;
    font-size: calc(12.5px * var(--fs-tight));
    color: var(--plum-soft);
    margin: 0 0 4px;
  }
  .reloc-label + .reloc-select { margin-bottom: 10px; }
  .reloc-select {
    width: 100%;
    padding: 10px 12px;
    border-radius: 12px;
    border: 1px solid var(--line);
    background: var(--card);
    color: var(--plum);
    font-family: inherit;
    font-size: calc(15px * var(--fs-tight));
    /* Long place labels must not push the control off a narrow phone. */
    max-width: 100%;
    text-overflow: ellipsis;
  }
  .reloc-note {
    margin-top: 2px;
    font-size: calc(13px * var(--fs-tight));
    color: var(--plum-soft);
    line-height: 1.45;
  }
