/* Extracted from app.html 2026-07-29. Everything was inline, so the browser
   re-downloaded 166KB of CSS on every single load and could not cache any of
   it separately. Order of the three original <style> blocks is preserved. */
html.gate-precleared #gate-overlay{display:none!important}

    /* ====================================================
       DESIGN TOKENS
    ==================================================== */
    :root {
      /* THE GROUND FAMILY. --ground-hi and --ground-lo are the lighter and
         darker stops every full-page green gradient in this file was writing
         by hand. They exist so the theme picker has ONE place to move the
         ground from; before this, the modal backdrop, --void and the
         onboarding grounds all carried their own literal #5A8B4F/#416839 and
         simply ignored --midnight. */
      /* GROUND DROPPED L37 -> L24, 2026-08-11. Shane on the dashboard: "i thought
         this is overkill" - and the fault was never the green, it was that ground,
         rail, composer and post cards all sat within ten points of each other, so
         nothing separated and the screen read as one wash.
         HUE AND SATURATION ARE UNCHANGED (112 / 28). Only lightness moved, so this
         is the same colour under less light - the brand survives. Deriving every
         surface from the same h/s is also why the cards below still lift correctly.
         THE ENTRY PAGE (index.html) DELIBERATELY STAYS LIGHT. Outside is a shop
         window, inside is a room you sit in; getting dimmer as you go deeper is
         the product's own promise, not an inconsistency. */
      --midnight:#314E2C; --ground-hi:#3B5F35; --ground-lo:#243B21;
      --crimson:#7a2318; --crimson-lt:#9a3626;
      /* --gold NO LONGER MEANS GOLD. It is the NEUTRAL hairline/decoration
         ink for the charcoal palette. The one saturated colour is --neon, and
         it is rationed: brand wordmark and primary action only. Renaming it
         across ~200 uses was judged churn, not cleanliness. */
      --gold:#DCE7D6; --gold-dim:#B6C7AE;
      /* THEME ACCENT. 2026-08-11: every hardcoded gold in this file was
         repointed at these three tokens - 82 lines, previously written as
         rgba(255,215,0,x), rgba(212,175,55,x), rgba(247,200,56,x), #FFD700,
         #F7C838, #d4af37 and #D4A32A. They were NOT light-mode aware, so
         light mode painted bright gold on a pale green ground and read as
         sand. Use rgba(var(--neon-rgb),x) for any new gold - never a literal.
         NOT CONVERTED, and must never be: rgba(232,194,90,x) / #e8c25a is the
         GOLD TIER signal, not a UI accent, and rgba(246,201,76,x) / #f6c94c
         is gabi-mode's own lamp gold inside gabi rules. */
      --neon:#F7C838; --neon-dim:#D4A32A; --neon-rgb:247,200,56;
      /* PLUS IS SILVER, 2026-08-09 (replaces #e05a4d red). Metal ladder:
         nothing -> silver -> gold, which reads as a ladder without being
         explained; red never sat anywhere on one. --silver-ramp is the only
         thing that actually reads as METAL rather than a grey border, so use
         it on any ring big enough to show it (>=40px). At 30px there is not
         enough arc to see a ramp, so the feed ring is flat silver + the same
         STATIC halo the red ring had. Static, never animated - the pulse is
         still the only thing Gold buys. */
      --silver:#C7CCD4; --silver-dim:#8F97A3; --silver-soft:rgba(199,204,212,.26); --silver-ink:#23262B;
      /* Whole box-shadow value, not a colour: the JS that writes the ring inline
         reads this token straight into el.style.boxShadow. */
      --silver-glow:0 0 10px 1px rgba(199,204,212,.40);
      /* LINEAR, NOT CONIC, 2026-08-09. A conic ramp sweeps around the circle, so its
         two dark stops (#A9AFB9 and #8F97A3) landed as visible dark smudges on the
         left and right of the ring - Shane called it a dark fade. A vertical linear
         ramp reads as ONE light source from above, which is what metal actually does:
         bright top, soft mid, no dark arc anywhere. Darkest stop is now #B9C0C9, a
         light grey, not a near-charcoal. */
      --silver-ramp:linear-gradient(180deg,#F7F9FB 0%,#DCE1E7 55%,#B9C0C9 100%);
      --card-bg:#3E6039; --surface:#45693F; --surface-2:#4E7548;
      --border:rgba(236,243,232,0.22); --border-gold:rgba(236,243,232,0.30);
      --text:#F7FBF4; --text-muted:#D2E0CB; --text-soft:#E8F0E3;
      --glow-red:0 0 18px rgba(122,35,24,0.6); --glow-gold:0 0 22px rgba(var(--neon-rgb),.45);
      --glow-neon:0 0 22px rgba(var(--neon-rgb),.42);
      /* PANEL COLOUR — every modal, dropdown, pill and button gradient in this
         file is built from these two RGB triplets. Change these two lines and
         the whole app's container colour follows. Do not hardcode the numbers
         again: that is what made this a 69-place edit the first time. */
      --panel-hi-rgb:69,105,63; --panel-lo-rgb:58,90,53;
      /* AVATAR CIRCLE INTERIOR — the muted gold behind every face. */
      --avatar-bg:#3E5B38;
      --radius:10px; --radius-sm:7px;
      --font-serif:'Playfair Display',Georgia,serif;
      /* THE BRAND FACE, and only the brand. Kept separate from --font-serif so
         the wordmark can be Georgia (matching index.html and hqmask.html) while
         nicknames, confessions and headings stay Playfair. */
      --font-mark:Georgia,'Times New Roman',serif;
      --font-ui:'Inter',system-ui,sans-serif;
      --chrome-bg:rgba(var(--panel-lo-rgb),.94);
    }
    /* LIGHT MODE — neutral gray theme. Gabi Mode (below) always overrides this,
       since late-night mode should stay dark no matter what. */
    body.light-mode {
      --midnight:#A9CE99; --ground-hi:#B8D8A9; --ground-lo:#93BC82;
      --crimson:#6B1F1F; --crimson-lt:#8F3A3A;
      --gold:#4A5C42; --gold-dim:#3F5537;
      --neon:#8F6F1C; --neon-dim:#6E5414; --neon-rgb:143,111,28;
      /* Light mode needs a DARKER silver - #C7CCD4 on a pale green card is
         pale-on-pale and the ring disappears. Same reason --neon darkens here. */
      --silver:#6E767F; --silver-dim:#4C535B; --silver-soft:rgba(110,118,127,.20); --silver-ink:#F4F6F9;
      --silver-glow:0 0 10px 1px rgba(110,118,127,.35);
      --silver-ramp:linear-gradient(180deg,#8E959D 0%,#6E767F 55%,#575E66 100%);
      --card-bg:#C6E0B7; --surface:#BBD8AB; --surface-2:#AFD09E;
      --border:rgba(30,58,24,0.16); --border-gold:rgba(46,74,38,0.30);
      --text:#15250F; --text-muted:#4A5C42; --text-soft:#24371C;
      --glow-red:0 0 0 rgba(0,0,0,0); --glow-gold:0 0 0 rgba(0,0,0,0);
      --panel-hi-rgb:198,224,183; --panel-lo-rgb:175,208,158;
      --avatar-bg:#8FB87E;
      --chrome-bg:rgba(169,206,153,.96);
    }
    body.light-mode::before { background: radial-gradient(ellipse 80% 60% at 50% -10%,rgba(0,0,0,0.02) 0%,transparent 70%),radial-gradient(ellipse 40% 30% at 90% 80%,rgba(0,0,0,0.015) 0%,transparent 60%); }
    /* GABI MODE */
    body.gabi-mode:not(.light-mode) {
      --midnight:#1B331A; --ground-hi:#244423; --ground-lo:#122009;
      --card-bg:#244423; --surface:#2B4F29;
      --crimson:#551818; --text:#E8F0E3; --text-muted:#AEC7A2;
      --panel-hi-rgb:43,79,41; --panel-lo-rgb:32,60,30;
      --chrome-bg:rgba(20,38,16,.96);
    }
    body.gabi-mode:not(.light-mode)::before {
      background:
        radial-gradient(ellipse 100% 80% at 50% -5%,rgba(65,104,57,0.34) 0%,transparent 70%),
        radial-gradient(ellipse 50% 40% at 10% 90%,rgba(var(--neon-rgb),0.06) 0%,transparent 60%);
    }
    /* RESET */
    *,*::before,*::after{box-sizing:border-box;margin:0;padding:0}
    /* THE SIDEWAYS SHAKE WHEN A SHEET OPENS — fixed 2026-08-11. Shane: clicking Go
       Gold and landing on the avatar made the page "bump or shake to right and
       left". Opening a modal locks page scroll, the scrollbar vanishes, the
       viewport gets ~15px wider and every centred element slides — then slides
       back on close. It reads as the sheet animating; it is the whole page
       re-centring. Reserving the track permanently means the width never changes.
       SAME FIX ALREADY IN confession.html. Keep the two in step.
       DO NOT remove these to get rid of the empty strip — the strip IS the fix.
       Neither costs anything on mobile, where scrollbars float over content. */
    html{font-size:17px;-webkit-text-size-adjust:100%;overflow-x:hidden;overflow-y:scroll;scrollbar-gutter:stable}
    html.gate-locked{overflow:hidden;height:100%}
    body{background:radial-gradient(ellipse 650px 550px at 12% 15%,rgba(255,214,110,.16),transparent 60%),radial-gradient(ellipse 550px 450px at 90% 85%,rgba(28,55,22,.42),transparent 60%),var(--midnight);color:var(--text);font-family:var(--font-ui);min-height:100vh;overflow-x:hidden;transition:background 1s,color .5s}
    @media (min-width:900px){body{background-attachment:fixed}}
    /* --font-ui, NOT --font-serif, for UI text. fonts/fonts.css self-hosts
       Playfair in ITALIC ONLY - there is no upright face in the folder - so
       setting the serif on body or buttons puts the entire interface in slanted
       display type. Serif belongs on display text (nicknames, confessions,
       headings), never on chrome. */
    button{cursor:pointer;border:none;background:none;font-family:var(--font-ui)}
    textarea,input{font-family:var(--font-ui)}
    a{color:inherit;text-decoration:none}
    ::-webkit-scrollbar{width:8px}
    ::-webkit-scrollbar-track{background:transparent}
    ::-webkit-scrollbar-thumb{background:rgba(255,255,255,.15);border-radius:4px;border:2px solid transparent;background-clip:padding-box}
    ::-webkit-scrollbar-thumb:hover{background:rgba(255,255,255,.28);background-clip:padding-box}
    body.light-mode ::-webkit-scrollbar-thumb{background:rgba(0,0,0,.18);background-clip:padding-box}
    body.light-mode ::-webkit-scrollbar-thumb:hover{background:rgba(0,0,0,.3);background-clip:padding-box}
    body::before{content:'';position:fixed;inset:0;background:radial-gradient(ellipse 80% 60% at 50% -10%,rgba(236,243,232,0.10) 0%,transparent 70%),radial-gradient(ellipse 40% 30% at 90% 80%,rgba(236,243,232,0.05) 0%,transparent 60%);pointer-events:none;z-index:0;transition:background 1s}
    .bg-ghost{position:fixed;inset:0;display:flex;align-items:center;justify-content:center;font-family:var(--font-serif);font-style:italic;font-weight:700;color:rgba(var(--neon-rgb),.03);font-size:min(30vw,22rem,28vh);white-space:nowrap;pointer-events:none;z-index:0;overflow:hidden;letter-spacing:.02em}
    body.light-mode .bg-ghost{color:rgba(139,0,0,.03)}
    @keyframes pulse-dot{0%,100%{opacity:1}50%{opacity:.35}}
    @keyframes spin{to{transform:rotate(360deg)}}
    @keyframes card-in{from{opacity:0;transform:translateY(12px)}to{opacity:1;transform:translateY(0)}}
    @keyframes sheet-up{from{transform:translateY(100%)}to{transform:translateY(0)}}
    @keyframes shimmer{0%{background-position:-200% center}100%{background-position:200% center}}
    @keyframes pulse-btn{0%,100%{opacity:1}50%{opacity:.85}}
    @keyframes glowPulse{0%,100%{text-shadow:0 0 22px rgba(var(--neon-rgb),.5)}50%{text-shadow:0 0 38px rgba(var(--neon-rgb),.85),0 0 60px rgba(var(--neon-rgb),.3)}}

    /* ====================================================
       GABI BAR
    ==================================================== */
    /* Recolored from violet (#9B59B6) to gold — gabi-mode's full background is
       now a warm gold lamp-glow room, so a purple banner/logo/pulse dot read as
       a mismatched leftover "spot" instead of belonging to the same room. */
    .gabi-bar{display:none;background:linear-gradient(90deg,#244423,#1B331A,#244423);border-bottom:1px solid rgba(246,201,76,.25);padding:6px 16px;text-align:center;font-size:.68rem;color:#f6c94c;letter-spacing:.08em;position:sticky;top:0;z-index:101;align-items:center;justify-content:center;gap:8px}
    body.gabi-mode .gabi-bar{display:flex}
    .gabi-pulse{width:6px;height:6px;border-radius:50%;background:#f6c94c;box-shadow:0 0 8px #f6c94c;animation:pulse-dot 1.5s infinite}
    /* COLOUR AND TEXT-SHADOW BOTH DROPPED 2026-08-11: .logo-name now paints its
       gradient through a transparent fill, so a later `color` does nothing and a
       text-shadow draws behind letters that have no solid fill. Gabi keeps its
       stronger lamp glow, as a filter. */
    body.gabi-mode .logo-name{filter:drop-shadow(0 0 12px rgba(246,201,76,.5))}

    /* ====================================================
       HEADER
    ==================================================== */
    .app-header{position:sticky;top:0;z-index:100;background:var(--chrome-bg);backdrop-filter:blur(16px);padding:10px 16px}
    .header-inner{max-width:480px;margin:0 auto;display:flex;align-items:center;justify-content:space-between;position:relative}
    .logo{display:flex;flex-direction:column;line-height:1}
    /* THE WORDMARK IS GEORGIA, NOT PLAYFAIR — 2026-08-11. Shane: "the maskara
       inside dashboards is not playfield its georgia ... in users app dashboards
       desktops and mobiles."
       index.html settled on Georgia on 2026-08-08 ("change the index into old
       MASKARA WORD LOGO GEORGIA") after Playfair lasted about an hour there.
       The app kept `--font-serif` — which IS Playfair — so the entry page and
       the app have been showing two different wordmarks for three days.
       `--font-mark` is a NEW token so the change reaches ONLY the brand. Do not
       repoint --font-serif for this: that face is display text everywhere in the
       app — nicknames, confessions, headings — and moving it would restyle the
       whole product to fix a logo.
       This is the same token name hqmask.html uses, for the same reason. Three
       files, one face, all system-font: nothing to download, so no flash.
       COST, already accepted on index.html: Georgia is not installed on Android,
       so much of the PH market sees Noto Serif instead. Gelasio (open, metric-
       compatible) or an SVG wordmark is the fix if that ever matters. */
    /* GOLD, NOT WHITE — 2026-08-11. This rule said color:var(--gold), and
       --gold has not meant gold since the charcoal repaint: it is the NEUTRAL
       hairline ink (#DCE7D6, a pale green-white). So the header wordmark - the
       ONLY wordmark on a phone, since .logo-name is hidden above 860px where
       the rail takes over - was rendering near-white with a faint gold glow
       behind it. Now carries the SAME metal ramp as .desk-rail-brand so both
       wordmarks agree; see the long note above that rule for why no stop may
       open at cream. text-shadow cannot work here (the fill is transparent so
       the gradient can show through the letters), so the glow is a
       drop-shadow filter instead. */
    .logo-name{font-family:var(--font-mark);font-style:italic;font-size:1.5rem;font-weight:700;letter-spacing:.06em;
      background-image:linear-gradient(180deg,color-mix(in srgb,var(--neon) 72%,#fff) 0%,var(--neon) 24%,var(--neon) 54%,var(--neon-dim) 80%,color-mix(in srgb,var(--neon-dim) 82%,#000) 100%);
      -webkit-background-clip:text;background-clip:text;color:transparent;
      -webkit-text-fill-color:transparent;filter:drop-shadow(0 0 10px rgba(var(--neon-rgb),.35))}
    /* LINE-HEIGHT AND PADDING-BOTTOM ARE THE DESCENDER FIX - do not remove.
       overflow:hidden is here for the long-name ellipsis, but it clips
       VERTICALLY too, and with no line-height the box is exactly one default
       line tall. The nickname font is an ITALIC SERIF with deep descenders, so
       the tail of g/y/p and almost all of an UNDERSCORE (which sits below the
       baseline) fall outside the box and get cut. That is why "gold_stripe"
       reads as "gold stripe". */
    /* PADDING-LEFT IS 10px AND THAT IS THE SETTLED VALUE - do not trim it.
       The avatar ring's GLOW spreads roughly 7px past the circle itself, so
       the letters have to clear the GLOW, not the border. It went 2px (letters
       touching the glow) -> 7px (still touching) -> 14px (Shane: too much,
       "a little beside to the glow but not literally beside") -> 10px. */
    /* overflow is VISIBLE. It used to be hidden with an ellipsis, which cost two
       things at once: it clipped the descenders and the underscore, and - the
       reason it matters here - a flex item whose overflow is not visible does
       not expose a text baseline, so align-items:baseline on the row above had
       nothing real to align the Maskara badge to. Visible overflow gives the
       row a true baseline AND shows the whole nickname, which Shane asked for
       separately. Nicknames are generated ADJ_NOUN pairs, so they are short. */
    .logo-tag{font-size:.95rem;color:var(--text);font-weight:600;letter-spacing:.01em;line-height:1.5;padding-left:10px;padding-bottom:2px;display:block;max-width:100%;overflow:visible;white-space:nowrap}
    .profile-chip{display:flex;align-items:center;gap:12px;margin-top:8px;cursor:pointer;transition:opacity .15s;min-width:0}
    .profile-chip:active{opacity:.7}
    .profile-chip>div{min-width:0;overflow:hidden}
    /* background: var(--avatar-bg) matches "Choose your mask"'s avatar
       panel (--onb-panel-2, scoped only inside .onb-skin so referenced here
       as the same literal value) instead of the app's near-black --surface —
       any avatar with transparent edges was showing black behind it before. */
    /* No box-shadow ring here on purpose — matches .profile-avatar-big's clean
       look in My Profile. It used to bake in a static "0 0 0 3px var(--midnight)"
       dark ring outside the border; that read fine while the (buggy, always-on)
       gold glow animation was masking it, but once the ring is a plain static
       tier color (blue/red for Free/Illustrated — see applyAvatarTierRing),
       that dark ring shows up as a stray black outline around it. */
    /* Emoji glyphs do NOT fill their em box - a colour emoji typically draws at
       around 80% of its font-size, with transparent padding around it. So a
       font-size chosen to give "55% of the circle" on paper renders closer to
       45% on screen, which is why these kept looking lost inside their rings
       even after being set explicitly. Sized up to compensate. */
    /* MATCHED TO THE PREVIEW, 2026-08-11. Glyph 36 -> 38px (~73% of the disc) and
       the ring 1.5px -> 3px, so the sidebar chip and the "Choose your mask"
       preview look like the same object at two sizes.
       ONLY THE NEUTRAL RING THICKENS. --border-gold is the hairline ink, not a
       tier signal — the Free / Gold TIER rings are applied on top of this by
       applyAvatarTierRing and are deliberately left at their locked widths, so
       nothing here can blur what a tier looks like. */
    .profile-chip-avatar{width:52px;height:52px;border-radius:50%;background:var(--avatar-bg);display:flex;align-items:center;justify-content:center;color:var(--gold);font-size:38px;line-height:1;border:3px solid var(--border-gold);flex-shrink:0;overflow:hidden}
    .header-stats{max-width:480px;margin:8px auto 0;display:flex;gap:8px;align-items:center;flex-wrap:wrap}
    .stat-pill{display:flex;align-items:center;gap:5px;background:var(--card-bg);border:1px solid var(--border);border-radius:20px;padding:4px 10px;font-size:.72rem;color:var(--text-muted)}
    .stat-dot{width:6px;height:6px;border-radius:50%;background:#00C853;box-shadow:0 0 6px #00C853;animation:pulse-dot 2s infinite}
    /* Was --card-bg (near-black), which read as an empty hole against the warm
       page wash. Now the same purple family as every other control surface -
       .post-dest-btn, .avatar-tier-btn, .avatar-tile all use this exact fill. */
/* NO CIRCLE, 2026-08-01. Shane: "profile has a circle now, its overkill".
   Three circles sat in a row and only ONE of them was a person - the avatar
   stopped reading as a face and became a third button. Stripping the ring and
   the fill off these two leaves the avatar as the only circle on the row,
   which is what makes it read as someone rather than something.
   THE 42px BOX STAYS. Only the border and background are gone, not the button:
   an icon you can hit only dead-centre is worse than an ugly one, and 42 is
   already at the accessible minimum for a touch target.
   Icon size goes UP to compensate - without a ring around it a small glyph
   looks lost, and the border was doing part of the work of saying "tappable".
   #call-vb-header-btn keeps its green ring below: that one is a STATE (a Vent
   Buddy is live right now), not a control, and the ring is how it says so. */
    /* WIDTH 36, HEIGHT 42. Shane: "make it beside with each other, not too much
       spacing". The gap between them was already only 8px - the air came from
       the BOXES: a 25px glyph centred in a 42px square leaves 8px of padding on
       each side, so two neighbours sat ~25px apart before the gap was counted.
       Narrowing the box closes that without touching the gap.
       HEIGHT STAYS 42 and the row keeps its 8px gap, so the vertical tap target
       is untouched and the two are still separated enough that a thumb aiming
       at the bell cannot open the menu. Going narrower than 36 starts costing
       real mis-taps on a phone. */
    .settings-btn{width:36px;height:42px;border-radius:8px;background:none;border:0;font-size:1.55rem;color:var(--gold);display:flex;align-items:center;justify-content:center;transition:transform .15s;position:relative}
    .settings-btn:active{transform:scale(0.9)}
    /* Restores the ring this one still needs - see the note above. */
    /* This one keeps its ring, so it needs its circle back too. */
    #call-vb-header-btn{width:42px;border-radius:50%;border:1.5px solid #39ff88;background:linear-gradient(180deg,rgba(var(--panel-hi-rgb),0.55) 0%,rgba(var(--panel-lo-rgb),0.45) 100%);box-shadow:0 0 8px rgba(57,255,136,.55),0 0 16px rgba(57,255,136,.22)}
    #call-vb-header-btn::before{content:'live';position:absolute;bottom:-26px;left:50%;transform:translateX(-50%) scale(0.85);transform-origin:center bottom;background:#39ff88;color:#04331b;font-size:11px;font-weight:700;padding:2px 8px;border-radius:999px;white-space:nowrap;animation:liveBlink 1.2s ease-in-out infinite;line-height:1.3}
    @keyframes liveBlink{0%,100%{opacity:1}50%{opacity:.25}}
    .settings-btn[data-tooltip]::after{content:attr(data-tooltip);position:absolute;top:120%;left:50%;transform:translateX(-50%);background:rgba(20,16,10,.95);color:#fff;padding:4px 10px;border-radius:6px;font-size:.7rem;white-space:nowrap;opacity:0;pointer-events:none;transition:opacity .15s ease;border:1px solid var(--border-gold)}
    .settings-btn[data-tooltip]:hover::after{opacity:1}
    @media (hover:none){.settings-btn[data-tooltip]::after{display:none}}
    .icon-svg{display:inline-block;vertical-align:-3px}
    .tab-btn .icon-svg{width:24px;height:24px;margin-right:0}
    .nav-icon .icon-svg{width:22px;height:22px;vertical-align:middle}
    .nav-post-btn .icon-svg{width:20px;height:20px}
    .settings-btn .icon-svg{width:16px;height:16px}
    .settings-btn:active{transform:scale(.9)}
    /* Same purple glass container the rest of the app uses (login/recover/
       profile/mask-picker sheets, room cards, the compose box). These two
       dropdowns were falling back to --card-bg, a near-black #141319, so the
       menus read as a different app to everything they sit on top of.
       Both the Settings panel and the hamburger menu use this class. */
/* =========================================================================
   LOCKED - COMPOSER TOOLBAR AND SUB-PAGE HEADER. Signed off 2026-08-05.
   Do not change any value below without Shane explicitly asking.

   THE TOOLBAR IS ONE ROW: emoji, four pills, then the count and Post.
   It fits by about ten pixels. Every padding and gap listed here was trimmed
   to make it fit and each one is load-bearing - raising any of them puts the
   row back over the card's width and it wraps again:
     .post-dest-btn    padding 7px 10px, gap 5
     .compose-pill-row gap 5
     .mood-picker-row  gap 6
     .compose-send     gap 10
     .post-btn         padding 8px 16px
   The count and Post MUST stay inside .compose-send. Splitting them, or
   putting margin-left:auto back on .char-count, is the original bug.
   flex-wrap stays ON as the fallback. Do NOT replace it with a nowrap
   breakpoint - that was tried, it overflows the card below the breakpoint,
   and it only ever looked right at 80% browser zoom.

   THE SUB-PAGE HEADER is .page-header on Saved, Notifications, My posts and
   Blocked: bare chevron, no label, gold italic serif title, count right,
   hairline divider. Chosen by Shane from four mockups.
   Back returns to the PREVIOUS tab (SUBPAGE_TABS / backTab in app.html),
   never a hardcoded Wall. The 32px circle with a 44px ::after hit area is
   deliberate - do not enlarge the circle to enlarge the target.
   Do NOT re-add #myposts-back-btn / #bookmarks-back-btn / #blocked-back-btn /
   #notif-back-btn to the .myposts-tab rules below; an ID outranks a class and
   would draw a pill around the chevron.
   ========================================================================= */
/* MY POSTS filter pills. .action-btn already carries the purple glass and the
   pill radius, so these only add the SELECTED state - a gold tint, matching how
   a chosen option reads everywhere else in the app. Without it all four look
   identical and nothing says which list you are on. */
/* The MY POSTS filter pills need their OWN surface. .action-btn is transparent
   by default - it only gets a fill on hover - so on this page they rendered as
   bare text and only the selected one looked like a control. Same purple glass
   as the header buttons and the settings panel.
   .28/.22, not .55/.45 - matched to the post cards, the action-row pills and
   the notification rows, 2026-08-02. Those were the HEADER button value, which
   is a heavier purple because a header sits on the page wash. Down here they
   sit among cards.
   THE FOUR BACK BUTTONS WERE DROPPED FROM THESE SELECTORS 2026-08-05. They are
   no longer purple-glass pills reading "back to Wall" - they are bare chevrons
   inside .page-header below, and the ID rules here outrank any class rule, so
   leaving them listed would have kept painting a pill border around a chevron.
   Do not add them back. */
.myposts-tab{
  background:linear-gradient(180deg,rgba(var(--panel-hi-rgb),.28),rgba(var(--panel-lo-rgb),.22));
  border:1px solid rgba(255,255,255,.16)}
.myposts-tab:hover{
  background:linear-gradient(180deg,rgba(65,104,57,.38),rgba(34,64,28,.30));border-color:var(--border-gold)}
/* PRESSED STATE, so tapping does something visible. .action-btn is transparent
   by default and only fills on HOVER - which does not exist on a phone, so on
   the surface most people use these gave no feedback at all when touched.
   .28, matching the feed tabs exactly - was .18, which is the same value the
   SELECTED state uses elsewhere and so read as barely a change at all. A press
   should be the strongest gold on screen for the moment it lasts, or it does
   not register as feedback.
   !important on purpose: .action-btn:active sets a white tint further down this
   file, and without it that later rule wins on order. */
.myposts-tab:active{
  background:rgba(232,194,90,.28)!important;border-color:rgba(232,194,90,.6);color:var(--gold)}
/* PAGE HEADER (top app bar) for the four PUSHED sub-pages - Saved,
   Notifications, My posts, Blocked. Reached from the hamburger and never from
   the bottom nav, so each needs a title saying where you are and a back control
   saying how to leave. Tab ROOTS (Wall, Rooms, Call, Diary, People) get
   neither - they are destinations, not pushed screens.
   Back is an ARROW WITH NO LABEL. It used to read "back to Wall" on all four,
   which is a destination, not a back: opening Notifications from Rooms and
   tapping it dropped you on a screen you had never been on. It now returns to
   the tab you came from, so no fixed label could be correct.
   The divider uses --border, the same faint gold hairline as every other rule
   in the app, and flips with light mode on its own. */
.page-header{display:flex;align-items:center;gap:8px;margin:4px 0 12px;padding-bottom:10px;border-bottom:1px solid var(--border)}
.page-header-back{position:relative;display:flex;align-items:center;justify-content:center;width:32px;height:32px;flex:0 0 32px;margin-left:-6px;padding:0;background:none;border:0;border-radius:50%;color:var(--text-muted);cursor:pointer;transition:color .18s,background .18s,transform .1s}
/* The VISIBLE circle stays 32px so it does not shout beside a 1.05rem
   title, but the TAPPABLE area is 44px - the smallest target Apple and
   Material both consider reliable with a thumb. Growing the button itself
   would have grown the hover circle with it; an invisible pseudo-element
   grows only the hit box. Do not delete this thinking it is decorative. */
.page-header-back::after{content:'';position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);width:44px;height:44px;border-radius:50%}
.page-header-back:hover{color:var(--text);background:var(--surface-2)}
/* Gold flash, not just the scale - matching the filter pills above. A phone has
   no hover, so this is the ONLY press feedback the chevron gets. */
.page-header-back:active{background:rgba(232,194,90,.28);color:var(--gold);transform:scale(.94)}
.page-header-title{font-family:var(--font-serif);font-style:italic;font-size:1.05rem;color:var(--gold);line-height:1.5;padding-bottom:2px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.page-header-count{margin-left:auto;flex:0 0 auto;font-size:.7rem;color:var(--text-muted)}
/* NOTIFICATIONS. Unread is marked by a TINT and a dot, not by colour alone -
   a single dot is easy to miss on a phone in daylight, and tint alone is
   invisible to anyone who cannot pick it out against the page. */
/* EVERY ROW GETS A CONTAINER, 2026-08-01. Read notifications had no surface at
   all, so the page was a list of floating text under one gold row - the only
   screen in the app where content sat on the page rather than on a card.
   Same purple glass as a post card, so a notification reads as the same kind of
   object as everything else.
   UNREAD keeps the gold tint AND the left stripe. It now has to stand out from
   a container rather than from nothing, so the tint is stronger than the .07 it
   was - at that value against a surface it would have been invisible. */
.notif-row{display:flex;gap:10px;padding:12px;border-radius:10px;margin-bottom:8px;align-items:flex-start;
  background:linear-gradient(180deg,rgba(var(--panel-hi-rgb),.28) 0%,rgba(var(--panel-lo-rgb),.22) 100%);
  border:1px solid rgba(255,255,255,.08)}
/* Unread is the TINT only now. The left stripe belongs to the actor's tier -
   the same job it does on a post card - so unread cannot borrow it without the
   two meaning different things on different rows. */
.notif-row.unread{background:rgba(232,194,90,.12);border-color:rgba(232,194,90,.28)}
.notif-text{font-size:.82rem;color:var(--text);line-height:1.5}
.notif-excerpt{font-size:.72rem;color:var(--text-muted);margin-top:3px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.notif-time{font-size:.68rem;color:var(--text-muted);margin-top:3px}
.notif-dot{width:7px;height:7px;border-radius:50%;background:var(--gold);flex:none;margin-top:7px}

/* THE REPORT DIALOG. Two fixes, 2026-08-02.
   The note field was a bare <textarea>, which browsers paint WHITE - a white
   box in the middle of a purple panel, the only light surface in the app.
   The reason options are .action-btn, which is transparent until HOVER, and a
   phone has no hover - so on the surface most people use they rendered as bare
   text with a circle beside them, not as things to tap. Same fault the back
   buttons and the feed tabs had. */
/* A LIFT, NOT A DARK WASH — same correction made in confession.html tonight.
   rgba(20,15,42,.6) is darker than the sheet it sits on, so the box you type
   in read as a hole. White at 7% adds light instead of removing it, and works
   on any future background without being re-tuned. */
#report-modal textarea{background:rgba(255,255,255,.07);border:1px solid rgba(255,255,255,.14);
  border-radius:10px;color:var(--text);padding:10px 12px;resize:vertical}
#report-modal textarea::placeholder{color:var(--text-muted)}
.report-reason{background:linear-gradient(180deg,rgba(var(--panel-hi-rgb),.28),rgba(var(--panel-lo-rgb),.22));
  border:1px solid rgba(255,255,255,.16);gap:10px}
.report-reason:hover{border-color:var(--border-gold)}

.myposts-tab.selected{background:rgba(232,194,90,.18);border-color:rgba(232,194,90,.45);color:var(--gold)}

    .settings-panel{display:none;position:absolute;top:100%;right:16px;margin-top:6px;z-index:150;
      background:linear-gradient(180deg,rgba(var(--panel-hi-rgb),0.97) 0%,rgba(var(--panel-lo-rgb),0.96) 100%);
      backdrop-filter:blur(16px);-webkit-backdrop-filter:blur(16px);
      border:1px solid rgba(255,255,255,0.10);border-radius:14px;padding:12px;width:260px;max-width:calc(100vw - 32px);box-sizing:border-box;
      box-shadow:0 20px 50px rgba(0,0,0,0.5),inset 0 1px 0 rgba(255,255,255,0.06);word-wrap:break-word;
      /* SCROLLS, 2026-07-31. The panel is anchored to the header and grows
         downward, so once it had Theme, Text size, Language, the Account and
         GOLD tabs and the page links, it ran past the bottom of a laptop
         window at 100% zoom. There was no overflow rule, so everything below
         the fold - Saved posts, Blocked people, My posts - simply could not
         be reached. Not hidden by a bug; just unreachable.
         190px, not 120px: the first attempt only allowed for the header this
         hangs from and forgot the BOTTOM NAV BAR, so the panel scrolled but
         its last item still sat underneath Call / Diary / People / Rooms and
         could not be tapped. 190 covers the header, the nav and a gap.
         dvh is repeated after vh so mobile browsers use the DYNAMIC viewport
         height - with plain vh, Safari and Chrome on a phone measure the
         window as if the address bar were hidden, which puts the bottom of
         the panel behind the address bar. Browsers without dvh keep the vh
         line above and are no worse off than before. overscroll-behavior:contain stops a
         scroll that runs off the end of the panel from carrying on into the
         feed behind it, which reads as the page jumping on its own. */
      max-height:calc(100vh - 190px);max-height:calc(100dvh - 190px);
      overflow-y:auto;overscroll-behavior:contain;padding-bottom:14px}
    /* Light mode keeps its own flat surface - the purple only belongs to the
       dark theme. */
    body.light-mode .settings-panel{background:var(--card-bg);border-color:var(--border);backdrop-filter:none;-webkit-backdrop-filter:none}
    .settings-panel.open{display:block}
    /* Settings is a flat list of buttons where a harmless one (Log out) sits a
       few pixels from an irreversible one (Delete account). Grouping by
       CONSEQUENCE, each group under its own heading, is what stops someone
       reading straight down the list and tapping one row too far. */
    .settings-group{display:flex;flex-direction:column;gap:4px;padding:8px;margin-top:10px;border:1px solid rgba(255,255,255,.08);border-radius:12px}
    .settings-group-label{font-size:.6rem;letter-spacing:.06em;color:var(--text-muted);padding:0 2px 2px}
    .settings-group.danger{border-color:rgba(226,75,74,.28)}
    .settings-group.danger .settings-group-label{color:#E24B4A}
    body.light-mode .settings-group{border-color:var(--border)}
    .main-lang-pill{position:relative}
    .main-lang-list{display:none;position:absolute;top:calc(100% + 6px);right:0;margin:0;padding:6px;list-style:none;
      min-width:170px;max-height:min(280px,50vh);overflow-y:auto;background:var(--midnight,#0a0c1c);
      border:1px solid rgba(var(--neon-rgb),0.18);border-radius:12px;box-shadow:0 12px 30px rgba(0,0,0,.5);z-index:200}
    .main-lang-list .main-lang-option{margin-bottom:2px !important}
    .settings-row{display:flex;align-items:center;justify-content:space-between;gap:10px;padding:6px 0}
    .settings-label{font-size:.72rem;color:var(--text-muted)}
    .settings-toggle{display:flex;gap:4px;background:var(--surface);border-radius:8px;padding:2px}
    .settings-tab-switch{display:flex;gap:4px;margin:10px 0 8px;border-bottom:1px solid var(--border);padding-bottom:2px}
    /* MY MASK SHEET, 2026-08-05. The PLATE is one gold hairline frame holding
       avatar, nickname and bio, so the sheet opens on a single object rather
       than a column of controls. Deliberately a HAIRLINE, not a filled card -
       a second filled surface inside the purple glass sheet is the
       card-within-a-card the share card was rejected for. */
    /* THE PLATE HUGS ITS CONTENT — width capped 2026-08-11. Shane, looking at
       My mask on a wide desktop: "do you thiknk this is overkill".
       He picked this plate over a frameless version BECAUSE it hugs — but it is
       a block, so it took the full width of the sheet, and on a wide screen the
       modal is wide. The avatar, name and bio stayed centred in the middle
       third, leaving a large gold box drawn around empty green: a frame around
       air, which is the exact complaint that resized this plate the first time.
       `width:max-content` sizes it to its widest child (the nickname, normally)
       and the auto margins re-centre it. max-width:100% so a long nickname
       still wraps inside the sheet instead of pushing past it.
       COLOURS DELIBERATELY UNTOUCHED. He asked to copy hqmask's login-card fill
       in here and then questioned it himself — "maybe its not the same with
       other containers?" — and he was right: every container in this app is one
       shared value that took several rounds to agree, and hqmask has its own
       :root. This is a SIZING fix only. */
    /* NO FRAME, 2026-08-11. The gold hairline is gone and the avatar is the
       biggest thing on the sheet. The plate existed to stop a 58px avatar
       looking lost in a wide sheet; at 104px it holds the space on its own,
       and a box drawn around your own face was the "frame around air" note
       twice over. Element and class KEPT so applyStaffProfileLock's
       .staff-view hook and every existing selector still resolve. */
    .mask-plate{border:0;border-radius:0;padding:2px 0 4px;text-align:center;margin:0 0 4px;width:auto;max-width:100%}
    /* The plate HUGS its contents. .profile-avatar-big is 84px and
       .profile-name 1.3rem - both sized for a full-width sheet, which left the
       gold frame around mostly air. Sized down HERE only, so those elements
       keep their normal size everywhere else they are used. */
    .mask-plate .profile-avatar-wrap{width:104px;margin:0 auto 11px}
    /* RING 1.5px -> 3px AND EMOJI 64 -> 76px, 2026-08-11. Scoped to .mask-plate
       so ONLY this sheet changes — .profile-avatar-big is also used at 84px
       elsewhere, where a 3px ring would be heavy.
       THE TIER RINGS ARE NOT AFFECTED and must not be: Free, PLUS and Gold are
       all locked at 1.5px across both files (see the locked-design banner), and
       the pulse is what Gold buys. This is the neutral hairline on the profile
       sheet's own disc, not a tier signal. */
    .mask-plate .profile-avatar-big{width:104px;height:104px;font-size:76px;border-width:3px}
    /* NICKNAME 1.2rem -> 1.7rem, 2026-08-11. The sheet is about WHO YOU ARE, so
       your name should outrank the heading, which is only the label on the
       door. .mask-plate.staff-view overrides this at 1.35rem and was lifted in
       the same pass - any change here has to check that rule too. */
    /* THE PENCIL MUST NOT COUNT TOWARD CENTRING — 2026-08-11. .profile-name is
       text-align:center, and the pencil is an inline 28px button with a 4px
       left margin sitting INSIDE it, so the browser was centring "name +
       32px of button" and the NAME landed ~16px left of true centre. Obvious
       at 1.7rem, invisible at the old 1.2rem, which is why it surfaced now.
       A negative right margin equal to the button's own footprint takes it
       back out of the measurement; the pencil then overhangs to the right and
       the name sits dead centre. Scoped to .mask-plate so the same button
       elsewhere is untouched.
       COST: on a very long nickname the pencil can overhang the plate edge.
       .mask-plate is max-width:100% so it clips rather than pushing the sheet
       wider, which is the right failure of the two. */
    .mask-plate .profile-name-edit{margin-right:-32px}
    .mask-plate .profile-name{font-size:1.7rem;line-height:1.25;margin-bottom:5px}
    .mask-plate .mask-bio-line{font-size:.78rem}

    /* ==========================================================================
       THE STAFF VIEW OF "MY MASK" — 2026-08-11.
       An agent cannot change her face, her name or her bio, so applyStaffProfileLock
       hides both pencils and the Face/Font/Colour row. That leaves the plate ALONE
       in the sheet, and `width:max-content` then sized it to the 58px avatar and
       CLIPPED HER NICKNAME. The class is added by applyStaffProfileLock.

       FULL WIDTH, not max-content: with nothing beneath it, a shrink-wrapped box
       floating in an empty sheet is the "frame around air" problem in its purest
       form. Here the plate IS the sheet's content, so it fills it.

       THE MASK IS 96px, not 58. It is the only thing on this screen — and it is
       her UNIFORM, the thing people recognise before they read the badge. Sizing
       it like a settings-row avatar undersells the one element that matters.

       NO GOLD BORDER. Gold means a PAID TIER everywhere else in this app; her
       mask is issued, not bought. Same reason the staff badge is green. A neutral
       hairline instead, with the green ring on the avatar doing the identifying.

       NOT CLICKABLE, and it must LOOK unclickable: applyStaffProfileLock already
       sets pointer-events:none, but the disc still had `cursor:pointer` from the
       base rule, so it read as a button that ignored you. cursor:default here.
       ========================================================================== */
    .mask-plate.staff-view{
      width:auto;
      border-color:rgba(236,243,232,.22);
      padding:22px 18px 20px;
    }
    .mask-plate.staff-view .profile-avatar-wrap{width:104px;margin:0 auto 14px}
    .mask-plate.staff-view .profile-avatar-big{width:104px;height:104px;font-size:76px;border-width:3px;cursor:default}
    .mask-plate.staff-view .profile-name{font-size:1.7rem;line-height:1.25;margin-bottom:6px}
    .mask-plate.staff-view .mask-bio-line{font-size:.82rem}
    .mask-bio-line{font-size:.7rem;color:var(--text-soft);line-height:1.4}
    .mask-bio-line #profile-bio-display.empty{color:var(--text-muted);font-style:italic}
    /* Face / Font / Colour. Equal thirds, so none of them reads as the primary.
       When Colour is hidden for a non-Gold account the row becomes two equal
       halves - nothing is left hanging. There is deliberately NO label above
       this row; it was "WARDROBE" and Shane cut it twice. */
    /* CHIPS AGAIN, 2026-08-11 — Shane picked "I" from the mock, sized to hug
       the word. This REPLACES the underline tabs described in the comment
       block below, which are kept as history because this row has now had
       three treatments and the reasons matter. Underlines lost because the
       nickname above them grew to 1.7rem: a 2px hairline is too weak a signal
       under something that heavy. */
    /* ONE CONTAINER, 2026-08-12 — Shane picked "D" from the mock: the three
       chips are now a single segmented control with hairline dividers instead
       of three separate pills. THE ROW OWNS THE BORDER AND THE RADIUS NOW, not
       the buttons. overflow:hidden is load-bearing: it clips each segment's
       square corners to the container's 12px radius, so the first and last
       segments round themselves without any :first-child/:last-child radius
       rules. flex-wrap must stay OFF - a wrapped segment would carry a divider
       into the middle of nowhere. */
    /* NO CONTAINER AT ALL — option 10, Shane 2026-08-12. The row was one bordered
       box with dividers; it is now THREE TINTED BLOCKS with a gap between them and
       no outer frame. Every corner-clipping bug this row has had came from a child
       drawing a square edge inside a rounded, overflow-hidden parent — with no
       parent box and no dividers, that whole class of fault cannot happen.
       `overflow:hidden` IS DELIBERATELY GONE with the border. Do not put it back
       to "tidy" the corners; each block rounds itself now.
       flex-wrap stays NOWRAP: three blocks that wrap would strand one on its own
       line, and for staff `#wardrobe-face-btn` is hidden so the row is only two. */
    .wardrobe-row{padding:0;gap:6px;margin:0 0 15px;display:flex;flex-wrap:nowrap;border:0;border-radius:0}
    /* Written as .settings-tab-btn.wardrobe-pill on purpose. .settings-tab-btn
       is a TAB STRIP rule further down this file carrying
       border-bottom:2px solid transparent, and being later it beat a plain
       .wardrobe-pill border - which is what sliced the bottom edge off every
       pill and made the row look cut. Two classes beat one whatever the
       order. */
    /* HISTORY, underline tabs 2026-08-11 (were 20px chips before that). Three
       chips in a row read as three buttons you could press in any order; an
       underline says one of these is open right now. Superseded above. */
    /* display:inline-flex IS REQUIRED HERE, 2026-08-11. The base .settings-tab-btn
       is a plain button with no display set, so gap and justify-content did
       nothing and the icon rendered hard against its label with no space. */
    /* flex:1 IS DELIBERATELY GONE. It made each chip a third of the row; Shane
       asked for "small container enough to cover the word", so they size to
       their own label and the row centres. border-bottom is written to 0 here
       to beat .settings-tab-btn's transparent 2px, which would otherwise sit
       inside the new full border and make the chip look bottom-heavy. */
    /* flex:1 IS BACK, 2026-08-12, and it is required by the container above:
       segments of different widths inside one box read as a broken table, not
       as a control. This reverses the 2026-08-11 "hug the word" sizing, which
       only made sense while these were three loose chips.
       border:0 FIRST, then the divider - .settings-tab-btn's transparent 2px
       bottom border must be cleared in one shorthand or it reappears inside
       the segment. The divider is on the RIGHT of every segment and removed
       from the last one, so hiding #wardrobe-face-btn for staff leaves two
       clean halves with no stray edge. */
    .settings-tab-btn.wardrobe-pill{flex:1;display:inline-flex;align-items:center;justify-content:center;gap:7px;padding:11px 6px;line-height:1.2;border:0;border-radius:10px;background:rgba(255,255,255,.05);color:var(--text-muted);font-size:.75rem;font-weight:500;transition:color .18s,background .18s,box-shadow .18s}
    .settings-tab-btn.wardrobe-pill .wp-ico{width:16px;height:16px;flex-shrink:0}
    .settings-tab-btn.wardrobe-pill:hover{color:var(--text);background:rgba(255,255,255,.09)}
    /* ACTIVE IS BRAND GREEN, AND IT DOES NOT FOLLOW THE ACCENT — Shane, 2026-08-11:
       "make it in the green original mask colors. gray is the one if people will
       change the theme". Gold is what Gold BUYS, so the tab row must never wear
       it. These are LITERALS on purpose: the whole point is that this chip stays
       Maskara green no matter what --neon or --midnight are set to.
       KNOWN FRAGILITY, told him: on the default theme this is green on a green
       sheet, so the lighter border is what makes it read. If the panel colour
       ever moves closer to #3E6039, check this still separates. Lifted from #4E7A45
       to #3E6039 when the ground dropped to L24, so the chip stays a step ABOVE
       the sheet rather than matching the old, lighter page. */
    /* ACTIVE IS A SOLID BRIGHT BLOCK WITH DARK INK — option 10. There is no ring
       and no border now: with no container to sit inside, the fill alone carries
       the state, and it reads from further away than an outline did.
       #17300F ink, not white: #A8E693 is a LIGHT green, and near-white text on it
       fails contrast badly. The dark ink is the same deep green the entry page
       uses on its gold CTA, for the same reason.
       The glow is a DROP shadow tinted to the fill, so the chosen block lifts off
       the sheet instead of being outlined against it. */
    .settings-tab-btn.wardrobe-pill.active{background:#A8E693;color:#17300F;font-weight:700;
      box-shadow:0 6px 16px -8px rgba(168,230,147,.65)}
    .settings-tab-btn.wardrobe-pill.active .wp-ico{color:#17300F}
    /* THE PRESS STATE WAS BLANKING THE BLOCK — FIXED 2026-08-12. Shane: "if a
       user will click the face, there no filled color it will just go to gold and
       free categories emojis".
       TWO THINGS, and only one of them is a bug.
       NOT A BUG: Face never keeps the filled state, because it is not a TAB. Font
       and Colour carry data-profile-tab and open a pane BELOW; Face has its own
       handler and opens the mask-picker modal instead. A filled block on this row
       means "this pane is open underneath" — Face has no pane, so keeping it lit
       while a different sheet covers the screen would be a lie.
       THE BUG: `:active` set `background:none`, so PRESSING any of the three
       REMOVED its tint. On a phone there is no hover, so tapping Face went blank
       and then a modal appeared — no acknowledgement at all. It now brightens on
       press, which is the only feedback a touch device gets. */
    .settings-tab-btn.wardrobe-pill:active{background:rgba(168,230,147,.30);color:var(--text)}
    .settings-tab-btn.wardrobe-pill.active:active{background:#A8E693;color:#17300F}
    /* THE FILL MUST SURVIVE HOVER AND FOCUS TOO — 2026-08-12. Shane: "when u
       click its just like a slight filled not the original filled of font".
       `:hover`, `:focus` and `.active` all weigh the SAME (three class-level
       selectors), so whichever is written LAST wins. After a click the pointer is
       still on the button and it also holds focus, so any equal-weight rule
       declared later repaints it with its weak tint. Naming them here, after
       everything else, ends the argument.
       ID-LEVEL RULE FOR FACE: an id outranks every class combination, so this one
       cannot be beaten by anything added later either. Face is the button that
       reported the fault; the class rules above still cover Font and Colour. */
    .settings-tab-btn.wardrobe-pill.active:hover,
    .settings-tab-btn.wardrobe-pill.active:focus,
    .settings-tab-btn.wardrobe-pill.active:focus-visible{background:#A8E693;color:#17300F}
    #wardrobe-face-btn.active,#wardrobe-face-btn.active:hover,#wardrobe-face-btn.active:focus,
    #wardrobe-face-btn.active:active{background:#A8E693;color:#17300F;font-weight:700}
    #wardrobe-face-btn.active .wp-ico{color:#17300F}

    /* ====================================================
       MAKE YOUR OWN FACE — the Leonardo block above the Gold grid, 2026-08-12.
       Markup and behaviour in app.html (renderAvatarGen / agenGenerate).
       IT IS DELIBERATELY QUIET. This sits above a grid of faces and must not
       out-shout them: one hairline card, one gold accent on the button, no
       gradient panel. The loud thing on this sheet should be the faces.
    ==================================================== */
    #avatar-gen{margin-bottom:12px}
    #avatar-gen-locked,#avatar-gen-form{border:1px solid rgba(var(--neon-rgb),.30);
      border-radius:13px;padding:12px;background:rgba(var(--neon-rgb),.05)}
    .agen-lockrow{display:flex;align-items:center;gap:8px;font-size:.82rem;font-weight:700;
      color:var(--text)}
    .agen-lock{width:14px;height:14px;flex:0 0 auto;color:var(--neon)}
    .agen-go{display:block;width:100%;margin-top:10px;border:0;border-radius:11px;
      padding:10px;cursor:pointer;font-family:var(--font-ui);font-size:.76rem;font-weight:700;
      color:color-mix(in srgb,var(--neon) 26%,#000);
      background:linear-gradient(180deg,color-mix(in srgb,var(--neon) 82%,#fff),var(--neon))}
    .agen-head{font-size:.82rem;font-weight:700;color:var(--text);margin-bottom:9px}
    /* THE INPUT IS A REAL FIELD, not a styled div: it needs a caret, a maxlength
       and the phone keyboard. font-size 16px MINIMUM — iOS Safari zooms the whole
       page in on focus for anything smaller, and the sheet never zooms back out. */
    /* THE PROMPT INPUT IS GONE — 2026-08-12. Its rules are deleted with it rather
       than left behind: a styled text box waiting in the stylesheet is an
       invitation to put one back, and putting one back is what breaks the
       guarantee that a real person's name cannot be asked for. */

    /* ===== THE STAGE, WITH THE REAL GOLD RING =====
       Same values a Gold member's own avatar already uses: the amber #e8c25a
       border and the onbAvatarRingGlow pulse at 2.6s. The pulse is the one thing
       Gold buys that is always visible, so putting it around the face being made
       is the honest version of "this is how you will look".
       The result image lands INSIDE this circle — a square preview would hide
       exactly the edge cropping that makes a generated face look wrong on a post. */
    .agen-stage{text-align:center;padding:2px 0 14px}
    .agen-disc{width:118px;height:118px;border-radius:50%;margin:0 auto;background:var(--surface-2);
      border:3px solid #e8c25a;display:flex;align-items:center;justify-content:center;overflow:hidden;
      animation:onbAvatarRingGlow 2.6s ease-in-out infinite}
    .agen-disc img{width:100%;height:100%;object-fit:cover}
    .agen-ph{font-size:.62rem;color:var(--text-muted);padding:0 16px;line-height:1.5}
    /* The nickname sits between the ring and the caption, in the app's own
       nickname face — white italic serif everywhere else, so it must not look
       like a different person here. The badge is a child node sized in em. */
    /* CENTRED AT ANY LENGTH: flex-wrap lets a long nickname wrap instead of
       overflowing the row, and text-align centres each wrapped line — a nowrap
       flex row pushes the overflow out one side, which is what made long names
       look off-centre against the ring. Side padding keeps a wrapped line off
       the sheet walls. */
    .agen-name{font-family:var(--font-serif);font-style:italic;font-weight:700;
      font-size:1.15rem;color:var(--text);margin-top:11px;display:flex;align-items:center;
      justify-content:center;flex-wrap:wrap;text-align:center;gap:6px;padding:0 14px;
      max-width:100%;line-height:1.25;overflow-wrap:anywhere}
    .agen-name img{height:1.15em;width:auto;flex-shrink:0}
    /* See the comment in renderAvatarGen: the badge is positioned off the name
       rather than sitting beside it, so it can never shift the name off centre.
       left:100% is the right edge of the name itself, so it follows a short or
       long nickname without any measurement. */
    .agen-name-txt{position:relative;display:inline-block}
    .agen-name-badge{position:absolute;left:100%;top:50%;transform:translateY(-50%);
      margin-left:7px;line-height:0;white-space:nowrap}
    .agen-cap{font-size:.64rem;color:var(--text-muted);margin-top:5px}
    .agen-cap b{color:var(--neon);font-weight:600}

    /* ===== ONE ROW PER THING: label left, choices right =====
       A heading above every group meant eleven headings and the sheet read as a
       wall. A label column makes it a list you scan down in one movement. */
    .agen-row{display:flex;align-items:center;gap:11px;padding:8px 0}
    .agen-row+.agen-row{border-top:1px solid rgba(236,243,232,.07)}
    .agen-lbl{flex:0 0 68px;font-size:.74rem;font-weight:700;color:var(--text-muted)}
    /* ONE LINE, SCROLLED SIDEWAYS, NOT WRAPPED. Wrapping gives every row a
       different height and the label stops lining up with its own choices. */
    /* align-items:center IS LOAD-BEARING SINCE THE PICTURE TILES LANDED. A flex
       row defaults to `stretch`, so once one item (a 79px tile) is taller than
       the rest, every word chip stretched to match — and with border-radius:999px
       a stretched pill draws as a tall OVAL. Centre them instead.
       GENERAL: adding one taller item to a flex row resizes every other item in
       it. Check the short ones, not just the new one. */
    .agen-opts{flex:1;min-width:0;display:flex;align-items:center;gap:6px;overflow-x:auto;scrollbar-width:none;padding:2px 0}
    .agen-opts::-webkit-scrollbar{display:none}
    /* TILE ROWS WRAP INTO A BLOCK instead of scrolling off the edge. The label
       moves ABOVE the tiles here — a 68px label column beside three wrapped rows
       of pictures leaves a tall empty gutter, and the row is no longer one line
       for it to sit level with. */
    .agen-row-tiles{display:block;padding:11px 0}
    .agen-row-tiles .agen-lbl{display:block;margin-bottom:9px}
    /* GARMENT ROWS ARE A GRID, NOT A WRAPPED FLEX LINE — 2026-08-12. A fixed
       tile width left dead space at the end of every row and capped how big the
       art could be; auto-fill lets each tile take a third of whatever width the
       sheet has, so the garment is as large as the screen allows. The tile
       overrides its own fixed width to 100% inside here. */
    .agen-opts-wrap{display:grid;grid-template-columns:repeat(auto-fill,minmax(96px,1fr));
      gap:12px 10px;overflow-x:visible}
    .agen-opts-wrap .agen-chip.agen-tile{width:100%}
    .agen-opts-wrap .agen-tile-art{width:100%;height:auto;aspect-ratio:1}
    .agen-chip{flex:0 0 auto;padding:8px 13px;border-radius:999px;border:0;
      background:rgba(255,255,255,.06);color:var(--text-muted);font-family:var(--font-ui);
      font-size:.78rem;font-weight:600;cursor:pointer;white-space:nowrap;
      display:inline-flex;align-items:center;gap:6px}
    .agen-chip:hover{background:rgba(255,255,255,.10);color:var(--text)}
    /* THE BLACK/WHITE DOUBLE RING ON A TAPPED CHIP IS CHROME'S OWN FOCUS RING,
       not a colour in this file — same fault already fixed on .avatar-tier-btn
       and .onb-price-card. box-shadow, never outline: outline boxes the corners
       of a 999px pill. Keyboard users keep a visible ring via :focus-visible. */
    .agen-chip:focus{outline:none}
    .agen-chip:focus-visible{box-shadow:0 0 0 2px rgba(var(--neon-rgb),.55)}
    .agen-chip.agen-tile:focus-visible{box-shadow:none}
    .agen-chip.agen-tile:focus-visible .agen-tile-art{box-shadow:0 0 0 2px rgba(var(--neon-rgb),.55)}
    .agen-chip.on{background:#A8E693;color:#17300F;font-weight:700}
    .agen-sw{width:12px;height:12px;border-radius:50%;border:1px solid rgba(0,0,0,.3);flex:0 0 auto}

    /* ===== COLOUR ROWS ARE CIRCLES, THE SAME SHAPE AS EVERY OTHER SWATCH IN THE
       APP (.theme-sw, .name-color-btn). Bare colour, no label — the pill and its
       word doubled the width of every colour row for no information.
       THE LIT RING IS DRAWN OUTSIDE THE CIRCLE with an inset:-4px ::before, not
       a border: a border would shrink the colour itself as you select it, and a
       32px swatch cannot afford to lose 2px of the thing being judged. Copied
       from .theme-sw so the two never look like different controls. */
    .agen-chip.agen-dotbtn{padding:0;background:none;border-radius:50%;position:relative;
      width:34px;height:34px;justify-content:center;transition:transform .12s}
    .agen-chip.agen-dotbtn:hover{background:none;transform:scale(1.08)}
    .agen-chip.agen-dotbtn:active{transform:scale(.94)}
    /* THE SAME TWO SHADOW LAYERS AS .theme-sw, copied deliberately rather than
       re-invented — an outer drop shadow to lift the circle off the panel and a
       1px inset highlight for the glass curve, with the highlight kept LOW so
       the colour you tap is the colour that renders. Any change to the swatch
       treatment must be made in both places or the two rows drift apart.
       NO BORDER on the circle: a border eats into the fill, and 34px cannot
       spare 2px of the thing being judged. */
    .agen-chip.agen-dotbtn .agen-sw{width:34px;height:34px;border:0;
      box-shadow:0 3px 8px rgba(0,0,0,.35),inset 0 1px 1px rgba(255,255,255,.25)}
    .agen-chip.agen-dotbtn.on::before{content:'';position:absolute;inset:-4px;
      border:1.5px solid var(--neon);border-radius:50%;pointer-events:none}
    .agen-chip.agen-dotbtn.on{background:none}
    .agen-chip.agen-dotbtn:focus-visible{box-shadow:none}
    .agen-chip.agen-dotbtn:focus-visible .agen-sw{box-shadow:0 0 0 2px rgba(var(--neon-rgb),.55)}

    /* ===== PICTURE TILES =====
       Same button as a word chip (.agen-chip keeps every handler); this class
       only reshapes it. The garment art is a transparent PNG in avatars/tiles/,
       drawn in GREY on purpose — colour is its own row, so a red tile beside a
       blue colour dot would contradict the choice sitting next to it.
       The tile labels a CATEGORY. It is not a preview of the face that comes
       back: every generation is a fresh image, so the seams will differ.
       border-radius/padding/background are all reset from the pill above, or
       the 999px pill shape fights the square art inside it. */
    .agen-chip.agen-tile{width:96px;padding:0;border-radius:0;background:none;
      display:flex;flex-direction:column;align-items:center;gap:5px;flex:0 0 auto}
    .agen-chip.agen-tile:hover{background:none}
    /* 78 -> 96px, 2026-08-12: "make the hoodie bigger so i can see it clearly in
       the app". The garments carry fabric folds now and those vanish at 78. The
       rows wrap, so a taller tile costs rows, not visibility. */
    .agen-tile-art{width:96px;height:96px;border-radius:14px;background:rgba(255,255,255,.06);
      border:2px solid transparent;display:flex;align-items:center;justify-content:center;
      overflow:hidden;transition:border-color .15s,background .15s}
    /* ===== HOW THE GARMENT TAKES A COLOUR =====
       COLOUR UNDERNEATH, ARTWORK ON TOP IN MULTIPLY. The PNG in avatars/tiles/
       is a SHADING LAYER: its garment body is pure WHITE, which is the neutral
       value for multiply, so whatever colour sits beneath comes through exactly.
       Only the outlines, seams and folds (the dark pixels) survive the blend.
       THIS IS WHY WHITE NOW WORKS. The first version painted colour ON TOP in
       multiply, and multiply can only ever darken — white and cream came out
       grey. Reversing the stack removes that limit entirely.
       isolation:isolate is REQUIRED: without it the img would multiply against
       the sheet behind the tile instead of against its own colour layer.
       IF A TILE IS EVER REDRAWN, its body must be pushed to pure white before
       it ships, or that garment alone will render dull in every colour. */
    .agen-tile-art{position:relative;isolation:isolate}
    .agen-tile-art img{width:94%;height:94%;object-fit:contain;display:block;
      position:relative;z-index:1;mix-blend-mode:multiply}
    /* Sits exactly where the img sits (88% centred = 6% inset) and is masked by
       the same PNG, so the colour lands on the garment and nowhere else.
       The default is a mid grey, not transparent — a tile with no colour picked
       yet would otherwise render as a pure white cut-out. */
    /* DEFAULT IS WHITE, NOT GREY. The PNG's body is pure white, so with a grey
       layer under it an untouched tile rendered as a grey garment and read as
       "the colour picker is not working" — which is exactly how it was reported.
       White is multiply-neutral: the tile shows the artwork as drawn until a
       colour is actually chosen. */
    .agen-tile-tint{position:absolute;inset:3%;z-index:0;background:var(--agen-tint,#FFFFFF);
      pointer-events:none;
      -webkit-mask-image:var(--tile-img);mask-image:var(--tile-img);
      -webkit-mask-size:contain;mask-size:contain;
      -webkit-mask-position:center;mask-position:center;
      -webkit-mask-repeat:no-repeat;mask-repeat:no-repeat}
    .agen-tile-cap{font-size:.68rem;line-height:1.2;color:var(--text-muted)}
    .agen-chip.agen-tile:hover .agen-tile-art{background:rgba(255,255,255,.10)}
    /* THE LIT STATE IS ON THE ART BOX, NOT THE BUTTON. `.agen-chip.on` fills
       itself #A8E693, which behind a transparent PNG would flood the garment's
       own cut-out with bright green. Neutralised here and drawn as a ring
       instead — inset-free box-shadow so it never nudges its neighbours. */
    .agen-chip.agen-tile.on{background:none;color:inherit}
    .agen-chip.agen-tile.on .agen-tile-art{border-color:#A8E693;box-shadow:0 0 0 2px rgba(168,230,147,.25)}
    .agen-chip.agen-tile.on .agen-tile-cap{color:var(--text);font-weight:700}
    /* The five trim rows stay closed until asked for. */
    /* ===== TABS — built 2026-08-12 to Shane's reference design =====
       A segmented track, the same shape as the wardrobe row's active block:
       bright fill, DEEP GREEN INK not white (#A8E693 is a light green and
       near-white on it fails contrast — the same call already made on the entry
       page CTA and the wardrobe segments).
       flex, not grid: four labels of different widths sharing the row evenly. */
    .agen-tabs{display:flex;gap:5px;background:rgba(0,0,0,.18);border-radius:12px;
      padding:4px;margin:2px 0 13px}
    .agen-tab{flex:1;border:0;background:none;color:var(--text-muted);
      font-family:var(--font-ui);font-size:.74rem;font-weight:700;padding:9px 0;
      border-radius:9px;cursor:pointer}
    .agen-tab:hover{color:var(--text)}
    .agen-tab.on{background:#A8E693;color:#17300F}
    .agen-tab:focus{outline:none}
    .agen-tab:focus-visible{box-shadow:0 0 0 2px rgba(var(--neon-rgb),.55)}
    /* EVERY PANE IS BUILT ON FIRST OPEN AND ONLY HIDDEN, never re-rendered —
       a rebuild on tab switch would wipe choices already made in that pane. */
    .agen-pane{display:none}
    .agen-pane.on{display:block}
    /* UNUSED SINCE THE TABS LANDED. Left in place: an unmatched selector costs
       nothing, and its markup may still exist in a cached copy of app.html. */
    .agen-more{display:block;width:100%;margin-top:11px;padding:9px;border:1px solid var(--border);
      border-radius:10px;background:none;color:var(--text-muted);cursor:pointer;
      font-family:var(--font-ui);font-size:.7rem;font-weight:700}
    .agen-more:hover{background:rgba(255,255,255,.05);color:var(--text)}
    #agen-rows-more{display:none}
    #agen-rows-more.open{display:block}
    .agen-btn{display:block;width:100%;margin-top:9px;border:0;border-radius:11px;padding:11px;
      cursor:pointer;font-family:var(--font-ui);font-size:.78rem;font-weight:700;
      color:color-mix(in srgb,var(--neon) 26%,#000);
      background:linear-gradient(180deg,color-mix(in srgb,var(--neon) 82%,#fff),var(--neon))}
    .agen-btn:disabled{opacity:.55;cursor:default}
    .agen-note{font-size:.64rem;line-height:1.45;color:var(--text-muted);margin-top:8px}
    /* Refusals and failures print HERE, under the button, and stay until the next
       attempt. Not a toast: the difference between "you are out this month" and
       "that failed and cost nothing" is something people need to be able to
       re-read. */
    .agen-msg{font-size:.7rem;line-height:1.45;margin-top:9px;padding:8px 10px;border-radius:9px;
      background:rgba(255,255,255,.06);color:var(--text-soft)}
    .agen-msg.warn{background:rgba(var(--neon-rgb),.12);color:var(--text)}
    /* .agen-result is UNUSED since the result moved into .agen-disc. Left in
       place — an unmatched selector costs nothing and deleting it is a separate
       decision from this one. */
    .agen-result{margin-top:11px;text-align:center}
    /* Shown in a CIRCLE because that is the only shape this face is ever drawn
       in — a square preview would hide exactly the edge cropping that makes a
       generated face look wrong on a post card. */
    .agen-result img{width:120px;height:120px;border-radius:50%;object-fit:cover;
      border:2px solid rgba(var(--neon-rgb),.45);background:var(--surface-2)}
    .agen-use{display:block;width:100%;margin-top:10px;border:1px solid var(--border);
      border-radius:11px;padding:10px;cursor:pointer;background:rgba(255,255,255,.06);
      color:var(--text);font-family:var(--font-ui);font-size:.76rem;font-weight:700}
    .agen-use:hover{background:rgba(255,255,255,.10)}
    body.has-custom-theme #wardrobe-face-btn.active,
    body.has-custom-theme #wardrobe-face-btn.active:hover{background:rgba(255,255,255,.88);color:#1A1A1A}
    /* ON A CUSTOM THEME THE ACTIVE CHIP GOES NEUTRAL. body.has-custom-theme is
       written by MKTheme.apply in app.html. Brand green on a purple or navy
       ground would read as a stray leftover rather than as branding, and it is
       the one colour that cannot follow the theme by design. Grey reads as
       "selected" against every ground. */
    body.has-custom-theme .settings-tab-btn.wardrobe-pill.active{background:rgba(255,255,255,.88);color:#1A1A1A;
      box-shadow:0 6px 16px -8px rgba(255,255,255,.5)}
    body.has-custom-theme .settings-tab-btn.wardrobe-pill.active .wp-ico{color:#1A1A1A}
    /* ONE tier line, not three price cards. Same height for every tier, so the
       sheet does not change shape depending on who is looking at it. */
    /* THEME PICKER, 2026-08-11. Lives in the Colour tab of My mask.
       DEVICE-ONLY: everything here writes inline custom properties onto <body>
       and a localStorage key. Nothing is sent to the server and nobody else
       sees it, which is why it is safe for it to be free-choice.
       IT DOES NOT TOUCH TIER COLOUR. The Free blue, PLUS silver, Gold amber and
       the reserved Vent Buddy green all read their own tokens and must keep
       meaning the same thing on everybody's screen whatever theme is picked. */
    .theme-label{font-size:.7rem;color:var(--text-muted);margin:2px 0 8px;letter-spacing:.04em}
    /* GRID, NOT FLEX-WRAP — 2026-08-11. Eleven swatches wrapped 7 + 4, so the
       second row was ragged and the gaps between circles did not line up with
       the row above. A 7-column grid gives two full rows of equal spacing and
       stays even whatever the sheet width. Swatch count must stay a MULTIPLE OF
       SEVEN (currently 14 incl. the default) or the last row goes ragged again. */
    .theme-row{display:grid;grid-template-columns:repeat(7,1fr);gap:10px 8px;justify-items:center;margin-bottom:14px}
    /* ===== SHARED SWATCH TREATMENT — 2026-08-11 =====
       ONE rule set for BOTH rows: the ground swatches and the accent swatches,
       which already carry .theme-sw alongside .accent-sw. Anything added here
       reaches every swatch in the app, which is the point — two rows sitting
       one above the other cannot be allowed to drift apart.
       TWO SHADOW LAYERS: an outer drop shadow lifts the circle off the panel,
       and a 1px inset highlight along the top gives it a glass curve. The
       highlight is kept at .25 DELIBERATELY LOW — a stronger one lightens the
       fill and then the colour you tap is not the colour the page renders,
       which matters here more than anywhere because these swatches ARE the
       promise of what the app will look like. */
    .theme-sw{width:34px;height:34px;border-radius:50%;border:0;cursor:pointer;padding:0;display:flex;align-items:center;justify-content:center;position:relative;flex-shrink:0;transition:transform .12s;
      box-shadow:0 3px 8px rgba(0,0,0,.35),inset 0 1px 1px rgba(255,255,255,.25)}
    .theme-sw:active{transform:scale(.94)}
    /* THE RING FLOATS OUTSIDE THE CIRCLE, it does not eat into it. This used to
       be `border:2px solid` on the swatch itself, which meant selecting a colour
       SHRANK its fill by 4px and the row visibly jumped. inset:-4px draws the
       ring in the gap instead, so nothing moves.
       ::before, NOT ::after — ::after is the padlock badge further down, and a
       locked swatch can also be the selected one. */
    .theme-sw.active::before{content:'';position:absolute;inset:-4px;border:1.5px solid var(--neon);border-radius:50%;pointer-events:none}
    /* THE TICK IS WHITE WITH A DARK SHADOW, on every swatch, and that is not a
       style choice. CSS cannot see the fill (it is an inline background written
       per swatch), so a fixed dark tick would vanish on Black and Charcoal —
       the two most likely picks on a dark app — and a fixed light one would
       vanish on White and Sand. White plus a drop shadow reads on both ends,
       the same universal-legibility trick already used for nicknames. */
    .theme-sw .theme-tick{display:none;width:16px;height:16px;color:#fff;filter:drop-shadow(0 1px 1.5px rgba(0,0,0,.75))}
    .theme-sw.active .theme-tick{display:block}
    /* The default swatch shows the BRAND green and its tick is gold, so the
       row reads "Maskara, then six alternatives". It was previously left with
       no fill at all, and the global `button{background:none}` rule made it an
       invisible circle - the row looked like six swatches with a gap. */
    /* The default swatch tick used to be pinned gold so it read against the
       brand green. It now inherits the shared white-plus-shadow tick above,
       which works on that green as well as on all thirteen others — one tick
       treatment, no per-swatch exceptions to maintain. */
    .theme-slider-label{font-size:.7rem;color:var(--text-muted);margin-bottom:7px}
    /* ACCENT ROW. Same 7-column grid as the ground swatches so the two rows line
       up column for column - they sit one above the other in the same panel and
       any mismatch reads as a mistake. Tighter margin because the slider it
       belongs to sits directly under it. */
    /* The rail's emoji sit in the same 20px slot the cloned SVG used, so the
       label column does not shift. font-size, not width, is what sizes them. */
    .desk-rail-item .rail-emoji{font-size:1.05rem;line-height:1;display:flex;align-items:center;justify-content:center}
    .accent-row{margin-bottom:10px}
    /* GOLD PREVIEW BAR. Sits above the mobile bottom nav (z-index 400 clears it)
       and uses safe-area-inset so it is not under the iPhone home indicator.
       It is the ONLY thing on screen saying the current colours are temporary —
       an unexplained reversible state is indistinguishable from a bug. */
    #gold-preview-bar{position:fixed;left:0;right:0;bottom:0;z-index:400;display:none;align-items:center;gap:10px;justify-content:center;flex-wrap:wrap;
      padding:10px 14px calc(10px + env(safe-area-inset-bottom));background:rgb(var(--panel-lo-rgb));border-top:1px solid var(--border);font-size:.74rem;color:var(--text-soft)}
    #gold-preview-bar button{font-size:.74rem;font-weight:600;padding:6px 14px;border-radius:16px;cursor:pointer}
    #gold-preview-go{background:var(--neon);color:color-mix(in srgb,var(--neon) 26%,#000);border:0}
    #gold-preview-undo{background:none;border:1px solid var(--border);color:var(--text-soft)}
    /* READ-ONLY WHILE PREVIEWING. Controls are disabled rather than the page
       being covered, so the feed can still be SCROLLED - a full-screen overlay
       eats touch scrolling, and scrolling is how you actually judge a theme.
       The two bar buttons are excluded by id; everything else that takes a tap
       is listed. Cursor goes to default so a desktop pointer says so too. */
    body.gold-preview button:not(#gold-preview-go):not(#gold-preview-undo),
    body.gold-preview a,
    body.gold-preview input,
    body.gold-preview textarea,
    body.gold-preview select,
    body.gold-preview [role="button"],
    body.gold-preview [contenteditable],
    body.gold-preview .nav-item,
    body.gold-preview .post-card{pointer-events:none!important}
    body.gold-preview .post-card,body.gold-preview .compose-card{cursor:default}
    /* THE PADLOCK IS A CORNER BADGE, NOT A SCRIM — 2026-08-11. It used to be a
       45% black wash over the whole circle, which made the GOLD swatch read as
       the same muddy brown as the Brown swatch beside it: Shane asked "i think
       that and the lock is one?". The thing being sold has to look like itself.
       A badge marks the swatch as locked without touching the colour it is
       advertising. pointer-events:none so it can never swallow the tap. */
    .theme-sw[data-theme-lock],.accent-sw[data-accent-lock]{position:relative}
    body:not(.is-gold) .theme-sw[data-theme-lock]::after,
    body:not(.is-gold) .accent-sw[data-accent-lock]::after{content:'\1F512';position:absolute;right:-3px;bottom:-3px;width:16px;height:16px;border-radius:50%;z-index:2;
      background:rgba(0,0,0,.72);display:flex;align-items:center;justify-content:center;font-size:9px;line-height:1;pointer-events:none}
    /* The padlock on the Gold accent for a free account. A ::after rather than a
       child node so the swatch markup stays one tag, and pointer-events:none so
       it can never swallow the tap that is supposed to explain the lock. */
    /* Superseded by the shared corner-badge rule below. */
    /* The track carries its own gradient, written from script, so the control
       previews the range instead of describing it. -webkit-appearance:none is
       required or iOS paints its own blue capsule over the gradient. */
    .theme-range{-webkit-appearance:none;appearance:none;width:100%;height:22px;border-radius:11px;background:var(--surface);outline:none;margin-bottom:14px;border:1px solid var(--border)}
    .theme-range::-webkit-slider-thumb{-webkit-appearance:none;appearance:none;width:20px;height:20px;border-radius:50%;background:transparent;border:2px solid #fff;cursor:pointer}
    .theme-range::-moz-range-thumb{width:20px;height:20px;border-radius:50%;background:transparent;border:2px solid #fff;cursor:pointer}
    .theme-reset{background:none;border:0;padding:0;font-size:.7rem;color:var(--neon);cursor:pointer;text-decoration:underline}
        .mask-tier-line{display:flex;align-items:center;justify-content:space-between;gap:10px;margin-top:0;padding-top:10px;border-top:1px solid rgba(255,255,255,.08);font-size:.7rem;color:var(--text-soft)}
    .mask-tier-line button{background:none;border:0;padding:0;font-size:.7rem;color:var(--gold);white-space:nowrap;cursor:pointer}
    .mask-tier-line button:hover{text-decoration:underline}
    .mask-tier-gold{color:var(--gold)}
    /* Muted and small on purpose - a disclosure, not a warning. Sized to be
       read once and then ignored, which is what an honest label should do. */
    /* .68rem and 1.4 to match the identical line on the onboarding picker. */
    .avatar-ai-note{margin-top:10px;font-size:.68rem;line-height:1.4;color:var(--text-muted);text-align:center}
    .settings-tab-btn{flex:1;padding:8px 6px;font-size:.75rem;font-weight:600;color:var(--text-muted);border-bottom:2px solid transparent;transition:color .2s,border-color .2s}
    .settings-tab-btn.active{color:var(--gold);border-bottom-color:var(--gold)}
    .settings-opt{padding:5px 9px;border-radius:6px;font-size:.72rem;color:var(--text-muted);transition:background .2s,color .2s}
    .settings-opt.active{background:var(--crimson);color:#fff}
    /* Real slide toggle (auto-renew) — a text button that says "tap to turn
       off" reads ambiguous about its current state; a switch shows on/off
       at a glance and can't be mis-tapped the way a full-width text button
       sometimes gets tapped by accident while scrolling. */
    .toggle-row{display:flex;align-items:center;justify-content:space-between;gap:10px;padding:9px 2px}
    .toggle-row-label{font-size:.8rem;color:var(--text)}
    .toggle-switch{position:relative;display:inline-block;width:44px;height:24px;flex-shrink:0}
    .toggle-switch input{opacity:0;width:0;height:0}
    .toggle-slider{position:absolute;inset:0;cursor:pointer;background:rgba(255,255,255,.15);transition:background .2s;border-radius:24px}
    .toggle-slider::before{content:'';position:absolute;height:18px;width:18px;left:3px;bottom:3px;background:#fff;transition:transform .2s;border-radius:50%;box-shadow:0 1px 3px rgba(0,0,0,.3)}
    .toggle-switch input:checked+.toggle-slider{background:var(--gold)}
    .toggle-switch input:checked+.toggle-slider::before{transform:translateX(20px)}
    .toggle-switch input:disabled+.toggle-slider{opacity:.5;cursor:default}
    .toggle-switch input:focus-visible+.toggle-slider{box-shadow:0 0 0 2px rgba(var(--neon-rgb),.5)}

    /* ====================================================
       TABS
    ==================================================== */
    /* Background/blur/mask live on ::before now, not on .tabs-bar itself — a
       mask-image on the bar was fading its own children (the active tab pill)
       out at the bottom along with the backdrop tint, making the pill look
       dim near its bottom edge. .tabs-inner sits above ::before so tab
       content always renders at full opacity regardless of any background fade. */
    .tabs-bar{position:sticky;top:56px;z-index:99;border-bottom:1px solid var(--border)}
    .tabs-bar::before{content:'';position:absolute;inset:0;z-index:0;background:var(--chrome-bg);backdrop-filter:blur(16px);pointer-events:none}
    .tabs-bar::-webkit-scrollbar{display:none}
    .tabs-inner{position:relative;z-index:1;max-width:480px;margin:0 auto;display:flex;justify-content:space-between;overflow-x:auto;scrollbar-width:none;-ms-overflow-style:none}
    .tabs-inner::-webkit-scrollbar{display:none}
    /* Emoji + one space + title, centered, on one row, on every breakpoint —
       used to be a column-stacked SVG with the title hidden entirely unless
       the tab was active, which read as uneven/indented next to each other. */
    .tab-btn{flex:1;padding:12px 6px;font-size:.8rem;font-weight:500;color:var(--text-muted);border-bottom:2px solid transparent;transition:color .2s,border-color .2s,background .2s;white-space:nowrap;position:relative;outline:none;display:flex;flex-direction:row;align-items:center;justify-content:center;gap:0}
    .tab-emoji{font-size:1.05rem;line-height:1;display:inline-block}
    .tab-label{display:inline;font-size:.72rem;line-height:1}
    /* CONTAINERS ON THE ACTIVE TAB ONLY, 2026-08-01. Boxing all five was tried
       and reverted: five filled pills directly above the gold Maskara card put
       three competing containers at the top of the screen before any content.
       An unselected pill carries no information - one filled tab already
       answers "where am I" at a glance, which is why iOS, Twitter and
       Instagram all mark only the current tab.
       The PRESS flash below stays: that was the real gap, since these tabs
       only ever responded to hover and a phone has none. */
    /* The 232,194,90 here is a UI ACCENT on a tab pill, NOT the Gold TIER signal,
       so unlike every other instance of that triplet it DOES follow the theme. */
    .tab-btn.active{color:var(--neon);background:rgba(var(--neon-rgb),.18);border-color:rgba(var(--neon-rgb),.45)}
    .tab-btn.active::after{content:'';position:absolute;left:10px;right:10px;bottom:0;height:2px;background:var(--gold);border-radius:1px}
    /* PRESSED STATE, 2026-08-01. These tabs only ever changed on HOVER, which
       does not exist on a phone - so on the surface most of this app's users
       are on, tapping Night or Following gave no feedback at all until the feed
       finished redrawing. On a slow connection that is a long silence, and the
       usual reading of silence is that the tap missed.
       A FULL GOLD CONTAINER on press, at Shane's instruction - .28 alpha, not a
       solid fill. Solid gold would need dark text to stay readable, so the
       label would flip colour for a fraction of a second and read as a glitch
       rather than a press. At .28 the gold reads unmistakably as a filled
       container while the label stays gold and legible.
       It cannot be confused with the ACTIVE tab: that one is marked by a gold
       underline and gold text on no background, so pressed and selected still
       look like different things.
       The bottom nav already had its own :active (scale + gold) - this brings
       the top row in line with it. */
    .tab-btn:active{color:var(--neon);background:rgba(var(--neon-rgb),.28);border-radius:999px}
    .tab-btn:focus-visible{box-shadow:inset 0 0 0 1px var(--gold)}

    /* THE FOURTH TAB WAS OFF THE SCREEN ON A PHONE — FIXED 2026-08-11.
       Shane: "in mobile phone now trending navs etc following are not showed
       whole."
       CAUSE: every .tab-btn is flex:1 with white-space:nowrap and no min-width
       override, so none of them can shrink below the width of their own emoji
       plus label. Four of those are wider than a 360px phone, `.tabs-inner` is
       overflow-x:auto, and its scrollbar is deliberately hidden — so the row
       silently scrolled and Following simply was not there. A nav that hides a
       destination with no sign it is hiding one is worse than a cramped nav.
       THE ACTIVE TAB MADE IT WORSE: the rule at ~3180 adds padding:6px 10px to
       whichever tab is selected, so the row got ~20px wider the moment anyone
       tapped something.
       FIX IS TO FIT, NOT TO SCROLL. Padding, label and emoji all come down a
       step, and min-width:0 lets the flex items actually give way. Measured for
       a 360px screen: four labels at .66rem plus four emoji plus padding lands
       around 270px inside roughly 336px of usable width, so there is room to
       spare even before the labels compress.
       IF IT IS STILL CLIPPED on a narrower device, the next thing to remove is
       the EMOJI, not more type size — they cost ~17px each and carry nothing
       the label does not already say. */
    @media (max-width:480px){
      .tab-btn{padding:11px 2px;min-width:0;gap:3px}
      .tab-emoji{font-size:.9rem}
      .tab-label{font-size:.66rem}
      /* Removed with the desktop one above — same jump, same reason. The mobile
         block only needs to size the tabs; selecting one must not resize it. */
      .tabs-inner{justify-content:space-around}
    }

    /* ====================================================
       EMOTIONAL WAVE
    ==================================================== */
    .wave-bar{padding:8px 2px 4px;margin:6px 0 0}
    .wave-label{font-size:.78rem;color:var(--text-muted);display:flex;align-items:center}
    .wave-label span{color:var(--gold-dim);font-style:italic;letter-spacing:.02em}
    .wave-bars{display:flex;gap:5px;align-items:flex-end;height:44px}
    .wave-segment{flex:1;border-radius:3px 3px 0 0;min-height:4px;transition:height 1s ease,opacity .3s;cursor:default;position:relative}
    .wave-segment:hover::after{content:attr(data-label);position:absolute;bottom:110%;left:50%;transform:translateX(-50%);background:var(--surface-2);border:1px solid var(--border);border-radius:6px;padding:3px 8px;font-size:.65rem;color:var(--text);white-space:nowrap;pointer-events:none;z-index:10}
    .wave-mood-labels{display:flex;gap:5px;margin-top:8px}
    .wave-mood-lbl{flex:1;text-align:center;font-size:1.2rem;line-height:1;color:var(--text-muted)}

    /* ====================================================
       HUGOT TAGS ROW
    ==================================================== */
    .hugot-tags-row{display:flex;gap:6px;overflow-x:auto;padding:8px 0 4px;scrollbar-width:none}
    .hugot-tags-row::-webkit-scrollbar{display:none}
    .hugot-tag{flex-shrink:0;padding:5px 12px;border-radius:20px;background:rgba(139,0,0,.12);border:1px solid rgba(139,0,0,.3);font-size:.73rem;color:#FF6B6B;white-space:nowrap;transition:all .18s}
    .hugot-tag.active,.hugot-tag:active{background:rgba(139,0,0,.3);border-color:var(--crimson-lt);color:var(--text)}
    .hugot-tag-count{font-size:.62rem;color:var(--text-muted);margin-left:3px}

    /* ====================================================
       MAIN CONTENT
    ==================================================== */
    .main-content{max-width:480px;margin:0 auto;padding:0 16px 100px;position:relative;z-index:1}
    @media (min-width:600px){
      .header-inner,.header-stats,.tabs-inner,.main-content,.bottom-nav-inner{max-width:560px}
      .main-content{padding:0 20px 100px}
      .tabs-inner{justify-content:center}
      .tab-btn{flex:none;padding:12px 14px}
      .tab-btn.active::after{left:14px;right:6px;bottom:-1px}
    }
    @media (min-width:1024px){
      .header-inner,.header-stats,.tabs-inner,.main-content,.bottom-nav-inner{max-width:680px}
      .main-content{padding:0 24px 100px}
    }

    /* TRUE DESKTOP/LAPTOP — treat this as a mobile app viewed on a wide screen.
       Leave the full-width bars alone (safer — no position/transform tricks that
       can throw off page width). Just frame the already-centered inner content
       columns so the app reads as a distinct card against a flatter void. */
    @media (min-width:1024px){
      .header-inner,.header-stats,.tabs-inner,.main-content,.bottom-nav-inner{
        max-width:680px;
        padding-left:16px;padding-right:16px;box-sizing:border-box
      }
    }

    /* Tablet-specific breakpoint (iPad portrait ~768px, most Android tablets) —
       between phone and the 1024px desktop breakpoint, so those devices get
       an intermediate column width instead of jumping straight to mobile sizing. */
    @media (min-width:768px) and (max-width:1023px){
      .header-inner,.header-stats,.tabs-inner,.main-content,.bottom-nav-inner{max-width:620px}
    }

    /* ====================================================
       MASKARA OF THE DAY
    ==================================================== */
    .motd-wrapper{padding:14px 0 2px}
    .motd-label{font-size:.65rem;text-transform:uppercase;letter-spacing:.14em;color:var(--gold-dim);margin-bottom:8px;display:flex;align-items:center;gap:6px}
    .motd-label::before{content:'';display:block;width:16px;height:1px;background:var(--gold-dim)}
    .motd-card{background:linear-gradient(135deg,var(--surface-2) 0%,var(--card-bg) 100%);border:1px solid var(--border-gold);border-radius:var(--radius);padding:18px 16px 14px;box-shadow:var(--glow-gold),inset 0 1px 0 rgba(var(--neon-rgb),.1);position:relative;overflow:hidden}
    .motd-card::before{content:'';position:absolute;top:0;left:0;right:0;height:3px;background:linear-gradient(90deg,var(--gold),#FFF8DC,var(--gold));background-size:200%;animation:shimmer 3s infinite}
    .motd-crown{font-size:.85rem;margin-bottom:8px}
    /* The gold Maskara badge used as the Gold tier card "face", replacing the
       old 👑 emoji. Sized in em so it scales with whatever font-size
       .onb-card-avatar already sets, exactly like the emoji it replaced. */
    /* Width-driven for the same reason as the nickname badge: the mask is wider
       than it is tall, so height:1em left it looking half-size. */
    .maskara-badge-face{width:1.15em;height:auto;display:block}
    .motd-text{font-family:var(--font-serif);font-style:italic;font-size:1.05rem;line-height:1.55;color:var(--text);margin-bottom:10px}
    .motd-meta{display:flex;align-items:center;justify-content:space-between}
    .motd-name{font-size:.72rem;color:var(--gold-dim)}
    .motd-share-btn{font-size:.72rem;color:var(--gold);background:rgba(var(--neon-rgb),.1);border:1px solid rgba(var(--neon-rgb),.25);border-radius:20px;padding:4px 12px;transition:background .2s}
    .motd-share-btn:active{background:rgba(var(--neon-rgb),.2)}

    /* ====================================================
       COMPOSE
    ==================================================== */
    .compose-wrapper{padding:14px 0 8px}
    .compose-card{background:var(--card-bg);border:1px solid var(--border);border-radius:var(--radius);padding:14px;position:relative;overflow:hidden}
    /* overflow:hidden above clips the rounded corners and the inset mood glow,
       but it was ALSO cutting the Post-to and Vanish dropdowns off at the card
       edge - the last option (Regrets) was rendered and simply invisible.
       Lifted only while a dropdown is actually open, so the clipping still
       does its job the rest of the time. Set by setupPostDestPicker /
       setupVanishPicker rather than :has(), so it does not depend on selector
       support. */
    .compose-card.dropdown-open{overflow:visible}
    /* The list can now run past the card, so it needs to sit above the
       composer's own stacking context and anything below it in the feed. */
    .compose-card.dropdown-open .post-dest-list{z-index:120}
    /* HIDDEN 2026-08-05. The avatar, "Posting as" and the nickname were a
       SECOND copy of the identity row that sits in the page header a few
       hundred pixels above - the same face and the same name twice on one
       screen, which is what made the composer read as a card about you rather
       than a place to type. Facebook and X both show identity once per screen.
       The nodes STAY IN THE DOM on purpose: #my-avatar, #my-anon-name,
       #my-anon-name-gold-badge and #my-bio-preview are written by
       refreshMyNameDisplays, setAvatarEmoji, applyAvatarTierRing and half a
       dozen other call sites. Deleting them would mean guarding every one.
       Hidden costs nothing and keeps all of that working untouched.
       The header name still previews the chosen font and colour, so nothing
       is lost for a Gold member - that was the one real argument for keeping
       a name in the composer. */
    .compose-header{display:none}
    .compose-header.show{display:flex;align-items:center;gap:10px;margin-bottom:10px}
    /* Same fix as .profile-chip-avatar above — dropped the static
       "0 0 0 2px var(--card-bg)" box-shadow so this matches the clean,
       ring-only look of the avatar in My Profile instead of showing a dark
       outline around the tier-colored border. */
    /* 1.5px, not 1px. Every other avatar in this file (.profile-chip-avatar,
       .profile-avatar-big) is 1.5px, so the composer's ring rendered visibly
       lighter than the identical avatar sitting in the header right above it. */
    .compose-avatar{width:52px;height:52px;border-radius:50%;background:var(--avatar-bg);display:flex;align-items:center;justify-content:center;color:var(--gold);font-size:36px;line-height:1;flex-shrink:0;border:1.5px solid var(--border-gold);cursor:pointer;transition:transform .15s;overflow:hidden}
    .compose-avatar:active{transform:scale(.92)}
    .profile-avatar-big{width:84px;height:84px;border-radius:50%;background:var(--avatar-bg);display:flex;align-items:center;justify-content:center;color:var(--gold);margin:0 auto 14px;border:1.5px solid var(--border-gold);overflow:hidden;font-size:56px;line-height:1}
    /* Tapping the big avatar in My Profile is the standard "change your
       picture" gesture (same as Instagram/X/Discord) — it opens the mask
       picker directly, defaulted to whichever tier the current avatar
       belongs to. The wrapper (not #profile-avatar-preview itself) carries
       the click handler and the edit badge, since #profile-avatar-preview's
       innerHTML gets fully replaced on every avatar change — a badge placed
       inside it would get wiped out the next time the avatar updates. */
    .profile-avatar-wrap{position:relative;width:84px;margin:0 auto 14px;cursor:pointer}
    .profile-avatar-wrap .profile-avatar-big{margin:0}
    .profile-avatar-wrap:active .profile-avatar-big{transform:scale(.96)}
    /* Just the pencil itself — no circular gold badge / dark border ring
       behind it. A drop-shadow keeps it visible against any avatar
       underneath without needing a solid background shape. Sits at the
       right side of the avatar, vertically centered, with a small gap
       (left:100% lands exactly at the ring's right edge since the wrap's
       width matches the ring's diameter; margin-left adds the actual gap)
       instead of tucked into the bottom-right corner. */
    .profile-avatar-edit-badge{position:absolute;left:100%;top:50%;transform:translateY(-50%);margin-left:8px;width:22px;height:22px;display:flex;align-items:center;justify-content:center;background:none;border:none;pointer-events:none;font-size:17px;line-height:1;filter:drop-shadow(0 1px 2px rgba(0,0,0,.7))}
    .profile-name{text-align:center;font-family:var(--font-serif);font-style:italic;font-size:1.3rem;color:var(--text);margin-bottom:4px}
    .profile-sub{text-align:center;font-size:.72rem;color:var(--text-muted);margin-bottom:20px}
    .emoji-input-row{display:flex;gap:8px;margin-bottom:12px}
    /* Pencil beside your own nickname in the "Your mask" sheet. Muted until
       hovered - it is an affordance on a line of text, not a button competing
       with the name it sits beside. 28px box on a 13px icon so it is still a
       reliable thumb target. */
    .profile-name-edit{display:inline-flex;align-items:center;justify-content:center;width:28px;height:28px;margin-left:4px;padding:0;background:none;border:0;border-radius:50%;color:var(--text-muted);cursor:pointer;vertical-align:middle;transition:color .18s,background .18s}
    .profile-name-edit:hover{color:var(--text);background:rgba(255,255,255,.07)}
    .profile-name-edit:active{background:rgba(232,194,90,.28);color:var(--gold)}
    .emoji-input{flex:1;min-width:0;background:var(--surface);border:1px solid var(--border);border-radius:var(--radius-sm);padding:10px 12px;font-size:1.2rem;color:var(--text);text-align:center}
    .emoji-input:focus{outline:none;border-color:var(--border-gold)}
    @keyframes flipY{0%{transform:rotateY(0)}100%{transform:rotateY(360deg)}}
    .emoji-apply-btn{background:var(--crimson);color:var(--gold);border-radius:var(--radius-sm);padding:0 16px;font-size:.78rem;font-weight:600;white-space:nowrap;border:1px solid transparent;transition:box-shadow .2s,transform .15s}
    .emoji-apply-btn.flipping{animation:flipY .5s ease}
    .emoji-apply-btn:active{transform:scale(.95)}
    .emoji-apply-btn.confirmed{box-shadow:var(--glow-gold);border-color:var(--gold)}
    /* Was a gold-tinted box (rgba(var(--neon-rgb),.07) + gold border) — out of step
       with the rest of My Profile's containers, which use this same purple
       nested-gradient treatment (#profile-modal .settings-tab-content,
       .avatar-cat-btn). Covers both places this class is used: the pencil/
       reroll button next to the nickname field, and the picker's "Back to
       categories" button. */
    .emoji-dice-btn{width:44px;flex-shrink:0;background:linear-gradient(180deg,rgba(var(--panel-hi-rgb),0.28) 0%,rgba(var(--panel-lo-rgb),0.22) 100%);border:1px solid rgba(255,255,255,0.10);border-radius:var(--radius-sm);color:var(--gold);font-size:1.15rem;display:flex;align-items:center;justify-content:center;transition:background .2s;overflow:hidden;cursor:pointer}
    .emoji-dice-btn:hover{background:linear-gradient(180deg,rgba(var(--panel-hi-rgb),0.4) 0%,rgba(var(--panel-lo-rgb),0.34) 100%)}
    .emoji-dice-btn:active{transform:scale(.95)}
    .emoji-dice-btn.spinning .dice-face{animation:diceRollMove .75s cubic-bezier(.4,0,.6,1)}
    /* Label and count on one baseline. The label keeps its own class and id so
       renderAvatarGrid's existing textContent write is untouched; the row is a
       wrapper around it, not a replacement. */
    .emoji-quick-head{display:flex;align-items:baseline;justify-content:space-between;gap:10px;margin-bottom:8px}
    .emoji-quick-head .emoji-quick-label{margin-bottom:0}
    .emoji-quick-count{font-family:var(--font-mono,'Courier New'),monospace;font-size:.6rem;color:var(--text-muted);flex-shrink:0}
    /* Raised with the buttons above — this prints the CATEGORY NAME once you are
       inside one, so if the list was too small to read this was too. Kept a step
       BELOW the buttons: it is a heading for something already on screen, not a
       thing to choose between. */
    .emoji-quick-label{font-size:.76rem;color:var(--text-muted);margin-bottom:8px;text-transform:uppercase;letter-spacing:.08em}
    /* FOUR COLUMNS, NOT SIX — 2026-08-11. At six, a 16-icon category wrapped
       4 + 4 + 4 + 4 by luck but a 9-icon one left three dangling on the last
       row. Four divides more of these categories evenly and gives each tile
       enough area for the emoji to read at a glance. */
    /* NOTE 2026-08-11: the MASK PICKER does NOT use this class — its grid is
       #profile-emoji-quick carrying .avatar-icon-grid, further down this file.
       This rule is kept for any other .emoji-quick-grid markup; check which
       class an element actually carries before tuning a grid here. */
    .emoji-quick-grid{display:grid;grid-template-columns:repeat(4,1fr);gap:10px}
    .emoji-quick-btn{aspect-ratio:1;display:flex;align-items:center;justify-content:center;font-size:1.7rem;background:var(--surface);border-radius:8px;border:1px solid transparent;transition:border-color .15s,box-shadow .2s}
    .emoji-quick-btn.confirmed{box-shadow:var(--glow-gold);border-color:var(--gold)}
    .name-font-grid{display:flex;flex-direction:column;gap:8px}
    /* ===== FONT SPECIMEN ROWS — 2026-08-11 =====
       Each row is a card showing the user's OWN nickname set in that font, with
       a caption naming the family and its feel. Replaces four identical bars
       reading Default/Elegant/Bold/Typewriter, which showed the words rather
       than the type and gave no way to compare.
       The row is a <button>, so text-align and width have to be set explicitly
       — a button does not inherit them. */
    .name-font-btn{display:flex;align-items:center;justify-content:space-between;gap:12px;width:100%;text-align:left;
      padding:13px 14px;border-radius:14px;border:1px solid transparent;background:rgba(255,255,255,.03);
      color:var(--text);cursor:pointer;transition:background .15s,border-color .15s,box-shadow .15s}
    .name-font-btn:hover{background:rgba(255,255,255,.06)}
    .nf-preview{display:flex;flex-direction:column;gap:4px;min-width:0;flex:1}
    /* 1.15rem, not the spec's fixed 19px, so the preview scales with the user's
       own browser text size — this sheet is where somebody with poor eyesight
       is most likely to be. Nickname can be long: clamp to two lines rather
       than letting one row grow taller than the other three. */
    .nf-name{font-size:1.15rem;line-height:1.25;color:var(--text);overflow-wrap:anywhere}
    .nf-label{font-family:var(--font-mono,'Courier New'),monospace;font-size:.62rem;letter-spacing:.06em;text-transform:uppercase;color:var(--text-muted)}
    /* ACTIVE USES --neon, NOT A LITERAL GOLD. On the default theme --neon IS the
       Maskara gold the mock specified, so this matches it exactly; on a picked
       theme it follows the accent instead of leaving a gold row stranded on a
       purple sheet. Same reasoning as the swatch ring. */
    .name-font-btn.active{background:linear-gradient(135deg,rgba(var(--neon-rgb),.12),rgba(var(--neon-rgb),.03));
      border-color:rgba(var(--neon-rgb),.35);box-shadow:0 4px 14px rgba(var(--neon-rgb),.08)}
    .nf-mark{width:22px;height:22px;border-radius:50%;flex-shrink:0;display:flex;align-items:center;justify-content:center;font-size:10px;line-height:1}
    .nf-check{border:1.5px solid rgba(255,255,255,.15)}
    .name-font-btn.active .nf-check{border:0;background:linear-gradient(180deg,color-mix(in srgb,var(--neon) 72%,#fff),var(--neon))}
    /* The tick is drawn only when active, and its colour is derived FROM the
       accent rather than pinned to the mock's #0d160d — on a light theme the
       accent darkens and a near-black tick on a dark fill would disappear. */
    .name-font-btn.active .nf-check::after{content:'\2713';color:color-mix(in srgb,var(--neon) 22%,#000);font-size:11px;font-weight:700}
    .nf-lock{background:rgba(255,255,255,.05);color:var(--text-muted)}
    /* A LOCKED ROW IS DIMMED, NOT HIDDEN AND NOT UNCLICKABLE. It still shows the
       user's own name in that font, because seeing your name in it is the whole
       sales pitch — hiding the preview would be advertising a thing nobody can
       picture. Tapping routes to the upgrade screen. */
    .name-font-btn.nf-locked .nf-name{opacity:.66}
    /* Group headings. Two words, so a hairline would be more furniture than the
       label deserves — the mono caps and the space above are the separation. */
    .nf-group{font-family:var(--font-mono,'Courier New'),monospace;font-size:.58rem;letter-spacing:.16em;text-transform:uppercase;color:var(--text-muted);margin:14px 4px 8px}
    .nf-group:first-child{margin-top:2px}
    .nf-goldtag{display:inline-block;margin-left:7px;padding:1px 6px;border-radius:6px;font-size:.52rem;font-weight:700;letter-spacing:.04em;
      color:var(--neon);background:rgba(var(--neon-rgb),.14)}
    /* SUPERSEDED 2026-08-12 — this used to read "THE LIST SCROLLS INSIDE
       ITSELF". It no longer does; see the note on .name-font-grid below for
       why the inner scroll box was removed. */
    /* THE INNER SCROLL BOX IS GONE — 2026-08-12. Shane, testing on a Vent Buddy
       account: "fonts in staff buddy cant show or cant scroll all fonts".
       THE FAULT WAS TWO SCROLLERS INSIDE EACH OTHER. This grid was
       `max-height:min(52vh,420px);overflow-y:auto` sitting inside
       `#profile-modal .modal-sheet`, which is capped at 85vh and ALSO scrolls.
       On a laptop window the header, the plate and the tab row already use most
       of that 85vh, so the grid's own 333px box STARTED near the bottom of the
       sheet and most of it hung below the fold. Reaching the bottom of the list
       then needed the SHEET to scroll — but the pointer was over the grid, so
       every wheel and drag scrolled the grid instead. The Gold group was
       genuinely unreachable, which is both halves of what he reported.
       IT SHOWED UP ON A STAFF ACCOUNT FIRST because `.mask-plate.staff-view`
       sets a 1.7rem nickname (against 1.2rem for a member), so her plate is
       taller and pushes the pane further down. The bug is not staff-specific.
       ONE SCROLLER, AND IT IS THE SHEET. The list now runs at its full height
       and the sheet scrolls through it, which is how every long settings sheet
       behaves.
       THE ORIGINAL REASON FOR THE CAP IS ACCEPTED AND OVERRULED: the comment
       here said 14 rows would push the tab row off screen on a phone. True —
       but the tab row comes back with one scroll up, and a list you cannot
       reach the end of is worse than a control you have to scroll back to.
       DO NOT reintroduce a max-height here. If the sheet ever needs a fixed
       header, that belongs on .modal-header (position:sticky, as
       #profile-modal .modal-header already does), NOT on this list. */
    .name-font-grid{padding-right:4px}
    /* Kept: these only style a scrollbar if this element ever scrolls again. */
    .name-font-grid::-webkit-scrollbar{width:4px}
    .name-font-grid::-webkit-scrollbar-track{background:transparent}
    .name-font-grid::-webkit-scrollbar-thumb{background:rgba(255,255,255,.14);border-radius:4px}

    /* ====================================================
       FONT PANE — TILES, 2026-08-12. Shane picked this out of five mocked
       layouts, then tightened it: "8 free 8 gold. in one screen",
       "1 nickname displyed only", "only jst go gold".
       The pane is now: a FREE grid of letter tiles, a framed GOLD grid, ONE
       nickname preview, and a Go Gold button that appears only while a locked
       font is previewed. Markup is built by renderNameFontGrid in app.html.
       THE OLD ROW STYLES ABOVE (.nf-name/.nf-label/.nf-mark/.nf-check/
       .nf-lock/.nf-goldtag) ARE NOW UNUSED but left in place — nothing costs
       anything for an unmatched selector, and deleting them is a separate
       decision from this one.
    ==================================================== */
    /* FOUR ACROSS. Eight in one row inside a 400px sheet gives ~36px tiles,
       too small to judge a letterform by — which is the only job this pane
       has. Four gives ~72px and still fits one screen. */
    .nf-grid{display:grid;grid-template-columns:repeat(4,1fr);gap:8px}
    /* THREE ACROSS WHILE THERE ARE SIX GOLD FONTS, so the block is two clean
       rows instead of 4+2 ragged. WHEN THE 7th AND 8th GOLD FONTS LAND, change
       this to repeat(4,1fr) and delete the rule — nothing else needs touching. */
    .nf-grid-gold{grid-template-columns:repeat(3,1fr)}
    /* TWO CLASSES, DELIBERATELY: `.name-font-btn` is kept on every tile so
       updateProfileFontActive() and the existing handlers still resolve, and
       that class is the old ROW style (flex, 13px padding). This rule has to
       outrank it, and two classes beat one whatever the source order — the
       same fix as .settings-tab-btn.wardrobe-pill. */
    .name-font-btn.nf-tile{
      display:flex;align-items:center;justify-content:center;
      /* FIXED HEIGHT, NOT aspect-ratio:1 — 2026-08-12. Shane: "mske the
         container a little smaller, and font bigger. too much space for the
         container". A square tile takes its height from its width, so the free
         grid (4 across, ~72px wide) and the gold grid (3 across, ~100px wide)
         came out at two DIFFERENT heights, and both were far taller than the
         letter needed. One fixed height makes every tile in both grids the same
         and takes the empty space out. Change this number, not the glyph, if the
         pane ever needs to be shorter still. */
      height:50px;width:auto;padding:0;gap:0;
      border-radius:12px;border:2px solid transparent;
      background:var(--surface-2);color:#DCE7D6;position:relative;overflow:hidden;
      transition:transform .12s,border-color .12s,background .12s}
    .name-font-btn.nf-tile:hover{transform:translateY(-2px);background:var(--surface-2)}
    .nf-glyph{font-size:38px;line-height:1;display:block}
    /* 40px inside a 56px tile: the letter is the point, the box is only
       the hit area. Do not push past ~44 — Homemade Apple and Caveat carry
       tall ascenders and start clipping against the tile's overflow. */
    /* SELECTED is a solid edge and a filled ground — the same brand green the
       wardrobe row uses, NOT gold. Gold on this pane means "costs money", and
       a free font lit in gold would say the wrong thing. */
    .name-font-btn.nf-tile.active{border-color:#A8E693;background:#2F4C2B;color:#FFF}

    /* The gold group is a FRAMED block, not just a heading — the split has to
       be a shape on screen, or it reads as one long list with a word in it. */
    .nf-goldbox{border:1px solid rgba(var(--neon-rgb),.30);border-radius:13px;
      padding:10px;background:rgba(var(--neon-rgb),.05);margin-top:12px}
    .nf-goldbox .nf-group{margin-top:2px}
    .nf-group-gold{color:var(--neon)}
    .nf-count{float:right;opacity:.75;letter-spacing:0}
    .nf-goldbox .name-font-btn.nf-tile{background:rgba(255,255,255,.05)}
    .nf-goldbox .name-font-btn.nf-tile.active{border-color:var(--neon);background:rgba(var(--neon-rgb),.16)}
    .nf-goldnote{font-size:.64rem;color:var(--text-muted);margin:9px 2px 0;line-height:1.45}
    .nf-goldnote b{color:var(--neon);font-weight:600}

    /* ===== THE PADLOCK =====
       A CORNER BADGE, NEVER A SCRIM OVER THE TILE. Learned on the gold theme
       swatch, 2026-08-11: a 45% black wash made that swatch read as the same
       brown as the Brown swatch and it could not be told apart. A LOCK MUST
       NOT DIM THE THING IT IS SELLING — the letterform stays at full strength.
       pointer-events:none so the badge can never swallow the tap that is
       supposed to explain it. */
    .name-font-btn.nf-tile.nf-locked::after{
      content:'';position:absolute;right:5px;bottom:5px;width:17px;height:17px;border-radius:50%;
      background:var(--neon);pointer-events:none;
      filter:drop-shadow(0 1px 2px rgba(0,0,0,.5));
      -webkit-mask:url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'><path fill='black' d='M12 2a5 5 0 0 0-5 5v3H6a2 2 0 0 0-2 2v8a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2v-8a2 2 0 0 0-2-2h-1V7a5 5 0 0 0-5-5zm-3 5a3 3 0 0 1 6 0v3H9V7z'/></svg>") center/13px no-repeat;
              mask:url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'><path fill='black' d='M12 2a5 5 0 0 0-5 5v3H6a2 2 0 0 0-2 2v8a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2v-8a2 2 0 0 0-2-2h-1V7a5 5 0 0 0-5-5zm-3 5a3 3 0 0 1 6 0v3H9V7z'/></svg>") center/13px no-repeat}
    /* PREVIEWING gets a DASHED edge; a real selection gets a SOLID one.
       "You are looking at this" is not "this is yours", and nothing has been
       saved — see the click handler in app.html. */
    .name-font-btn.nf-tile.previewing{border:2px dashed var(--neon);background:rgba(var(--neon-rgb),.12)}

    /* ===== THE ONE NICKNAME =====
       min-height is LOAD-BEARING: a script face is far taller than a text
       serif, so without a floor the pane jumps on every tap. */
    /* TIGHTER, AND THE NAME IS BIGGER — 2026-08-12. Shane, on a screenshot of
       the preview: "make it the font bigger and the space will not be wide".
       min-height still exists and is still load-bearing: a script face is far
       taller than a text serif, and without a floor the pane jumps on every
       tap. It is now 84 rather than 92 because the padding above and below
       came down with it — that is where the empty space was, not the floor. */
    .nf-showcase{margin-top:10px;padding:10px 10px 8px;border-top:1px solid var(--border);
      text-align:center;min-height:92px;display:flex;flex-direction:column;justify-content:center}
    /* 2.6rem = ~44px at the app's 17px root. IT WENT 1.8 -> 2.2 -> 2.6 because
       Shane kept reporting it small, and THE RULE WAS NEVER THE PROBLEM —
       checked 2026-08-12: no other selector in this file sets a font-size that
       can reach this element, and this block is at top level, not inside a
       media query. The cause is TYPOGRAPHIC. Cormorant Garamond and Italiana
       have a very small x-height, so at an identical font-size they render
       roughly a quarter smaller than Georgia or Abril Fatface. One number
       cannot look the same across fourteen faces.
       IF THEY MUST LOOK OPTICALLY EQUAL, the tool is `font-size-adjust` on
       this rule — but it would blow up Caveat and Homemade Apple, whose script
       x-heights are tiny, so it needs a per-font value in NAME_FONTS rather
       than one blanket number. Not built; say so before promising it. */
    .nf-showname{font-size:2.6rem;line-height:1.2;color:var(--text);overflow-wrap:anywhere}
    .nf-showlabel{font-family:var(--font-mono,'Courier New'),monospace;font-size:.56rem;
      letter-spacing:.1em;text-transform:uppercase;color:var(--text-muted);margin-top:6px}

    /* Hidden until a locked font is previewed. Just the button, by his
       instruction — every explanatory line was cut. */
    .nf-golock{display:none;margin-top:11px}
    .nf-golock.show{display:block}
    .nf-golock button{display:block;width:100%;border:0;border-radius:11px;padding:11px 13px;
      cursor:pointer;font-family:var(--font-ui);font-size:.78rem;font-weight:700;
      color:color-mix(in srgb,var(--neon) 26%,#000);
      background:linear-gradient(180deg,color-mix(in srgb,var(--neon) 82%,#fff),var(--neon));
      box-shadow:0 8px 20px -10px rgba(var(--neon-rgb),.5)}
    .nf-golock button:hover{filter:brightness(1.06)}
    .name-color-grid{display:grid;grid-template-columns:repeat(7,1fr);gap:8px}
    .name-color-btn{aspect-ratio:1;border-radius:50%;border:2px solid transparent;transition:border-color .15s,transform .15s;cursor:pointer}
    .name-color-btn.active{border-color:var(--text);transform:scale(1.12)}
    .emoji-quick-btn:active{border-color:var(--border-gold)}
    .compose-identity{display:flex;flex-direction:column;gap:1px;min-width:0}
    .compose-label{font-size:.6rem;text-transform:uppercase;letter-spacing:.08em;color:var(--text-muted)}
    /* Same descender fix as .logo-tag above. */
    /* Same 10px as .logo-tag - kept in step even though this row is hidden. */
    .compose-name{font-size:1.1rem;font-weight:600;letter-spacing:.01em;color:var(--text);line-height:1.45;padding-left:10px;padding-bottom:2px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
    .compose-name span{color:var(--text);font-weight:700}
    .compose-textarea{width:100%;background:transparent;border:none;outline:none;color:var(--text);font-family:var(--font-ui);font-style:normal;font-size:1rem;line-height:1.6;resize:none;min-height:72px}
    .compose-textarea::placeholder{color:var(--text-muted);font-family:var(--font-ui);font-style:normal}
    .hugot-detected{font-size:.72rem;color:#FF6B6B;margin-top:4px;min-height:18px}
    /* Toolbar */
    .compose-toolbar{display:flex;gap:4px;margin:8px 0 6px;border-top:1px solid rgba(255,255,255,.05);padding-top:8px}
    .toolbar-btn{padding:5px 8px;border-radius:8px;font-size:.8rem;color:var(--text-muted);border:1px solid transparent;transition:all .15s}
    .toolbar-btn.active{background:rgba(139,0,0,.18);border-color:var(--border);color:var(--text-soft)}
    /* Quote context */
    .compose-quote-ctx{margin-bottom:8px}
    .quote-ctx-label{font-size:.7rem;color:var(--text-muted);margin-bottom:4px}
    .quote-source-preview{background:rgba(0,0,0,.3);border-left:3px solid var(--crimson);border-radius:0 8px 8px 0;padding:10px 12px;font-family:var(--font-serif);font-style:italic;font-size:.88rem;color:var(--text-soft);line-height:1.5}
    /* Thread */
    .thread-parts{margin-top:8px;display:none}
    .thread-parts.visible{display:block}
    .thread-part{display:flex;gap:8px;margin-top:8px}
    .thread-line{width:2px;background:var(--border);border-radius:2px;margin:0 14px}
    .thread-part-input{flex:1;background:var(--surface);border:1px solid var(--border);border-radius:10px;color:var(--text);font-family:var(--font-serif);font-style:italic;font-size:.92rem;padding:8px 12px;outline:none;resize:none}
    .thread-part-input::placeholder{color:var(--text-muted)}
    .add-thread-btn{font-size:.75rem;color:var(--crimson-lt);padding:4px 10px;margin-top:6px;border:1px dashed rgba(139,0,0,.4);border-radius:8px;transition:background .18s}
    .add-thread-btn:active{background:rgba(139,0,0,.1)}
    /* Poll */
    .poll-section{display:none;margin-top:8px}
    .poll-section.visible{display:block}
    .poll-option-row{display:flex;gap:6px;margin-bottom:6px;align-items:center}
    .poll-option-input{flex:1;background:var(--surface);border:1px solid var(--border);border-radius:8px;color:var(--text);font-size:.85rem;padding:7px 10px;outline:none}
    .poll-option-input::placeholder{color:var(--text-muted)}
    .remove-option-btn{color:var(--text-muted);font-size:1rem;padding:4px 6px}
    /* OPM */
    .opm-attach-bar{display:none;align-items:center;gap:8px;background:rgba(var(--neon-rgb),.07);border:1px solid rgba(var(--neon-rgb),.16);border-radius:10px;padding:7px 10px;margin-top:8px}
    .opm-attach-bar.visible{display:flex}
    .opm-song-input{flex:1;background:transparent;border:none;outline:none;color:var(--gold);font-size:.82rem}
    .opm-song-input::placeholder{color:var(--gold-dim)}
    .opm-remove-btn{color:var(--text-muted);font-size:.9rem}
    /* Post destination (Wall vs a confession room) — compact dropdown, not a pill row,
       so it never overflows/clips on narrow screens the way a scrolling pill row can. */
    .post-dest-row{margin:10px 0 4px}
    /* Post-to and Vanish sit on one line. Stacking them would push the
       textarea and Post button further down on every phone. */
    /* SCROLLS SIDEWAYS INSTEAD OF WRAPPING, 2026-08-02. flex-wrap dropped the
       fourth pill onto a line of its own with a wide empty gap beside it. The
       feed tabs already solve this the same way, and a row that scrolls reads
       as "there is more" where a wrapped one reads as broken.
       gap 14 -> 6 and align-items centre, since the labels above each pill are
       gone and there is nothing left to bottom-align to. */
    /* WRAPS, does NOT scroll. overflow-x:auto was tried and it CLIPPED the
       dropdown menus - each pill opens a list positioned inside this row, and a
       scrolling container cuts off anything that overflows it. That is why the
       pickers stopped opening.
       Wrapping was the original problem, but only because each pill carried a
       tall label above it. Without those the pills are short enough that a
       second line looks deliberate rather than broken. */
    .compose-pill-row{display:flex;flex-wrap:wrap;gap:5px;align-items:center}
    .vanish-pill.armed .post-dest-btn{border-color:#BA7517;background:linear-gradient(180deg,rgba(186,117,23,.22) 0%,rgba(186,117,23,.14) 100%)}
    .post-vanish-tag{display:inline-flex;align-items:center;gap:4px;font-size:.62rem;padding:2px 8px;border-radius:20px;background:rgba(186,117,23,.16);border:1px solid rgba(186,117,23,.4);color:var(--gold);white-space:nowrap}
    .post-dest-label{font-size:.68rem;color:var(--text-muted);text-transform:uppercase;letter-spacing:.05em;margin-bottom:6px}
    .post-dest-pill{position:relative;display:inline-block}
    /* Post-destination pill + its dropdown ("Wall ▾" / Love Confessions /
       Family / etc.) used to be gold-tinted (pill) and near-black (dropdown)
       — different from every other container in the app, which reads as
       "this costs something" even though picking Wall vs a Confession Room
       is free. Now uses the same nested-gradient container look as the rest
       of the app (avatar-cat-btn, emoji-dice-btn, subs-disclaimer, etc). */
    .post-dest-btn{display:flex;align-items:center;gap:5px;padding:7px 10px;border-radius:20px;border:1px solid rgba(255,255,255,0.10);background:linear-gradient(180deg,rgba(var(--panel-hi-rgb),0.28) 0%,rgba(var(--panel-lo-rgb),0.22) 100%);color:var(--gold);font-size:.8rem;font-weight:600}
    .post-dest-btn .dest-caret{font-size:.6rem;opacity:.65;margin-left:1px;transition:transform .15s}
    .post-dest-pill.open .post-dest-btn .dest-caret{transform:rotate(180deg)}
    .post-dest-list{display:none;position:absolute;top:calc(100% + 6px);left:0;margin:0;padding:6px;list-style:none;min-width:200px;max-height:min(320px,50vh);overflow-y:auto;background:linear-gradient(180deg,rgba(var(--panel-hi-rgb),0.97) 0%,rgba(var(--panel-lo-rgb),0.96) 100%);border:1px solid rgba(255,255,255,0.10);border-radius:12px;box-shadow:0 12px 30px rgba(0,0,0,.5);z-index:60}
    .post-dest-list::-webkit-scrollbar{display:none}
    .post-dest-pill.open .post-dest-list{display:block}
    .post-dest-option{display:flex;align-items:center;gap:8px;width:100%;text-align:left;padding:8px 10px;border-radius:8px;font-size:.82rem;color:var(--text-soft);margin-bottom:2px;white-space:nowrap}
    .post-dest-option:hover{background:rgba(255,255,255,.06)}
    .post-dest-option.selected{background:rgba(var(--neon-rgb),.12);color:var(--gold)}
    body.light-mode .post-dest-option.selected{background:rgba(178,34,34,.1)}
    /* Mood — a single small trigger in the toolbar instead of a permanent row of six chips, so
       tagging a mood reads as an optional flourish (tap only if you want to) rather than a step
       everyone has to get past before posting. Opens a compact emoji-first grid on tap. */
    /* The pickers and the emoji button share this row now - see the note in
       app.html. min-width:0 on the row so the scrolling pill row inside can
       actually shrink rather than forcing the whole composer wider. */
    .mood-picker-row{display:flex;flex-wrap:wrap;align-items:center;gap:6px;min-width:0;margin:0 0 6px}
    .mood-picker-wrap{position:relative;display:inline-flex}
    .mood-picker-pop{display:none;position:absolute;bottom:calc(100% + 8px);right:0;z-index:70;
      grid-template-columns:repeat(3,1fr);gap:6px;min-width:220px;
      background:var(--midnight,#0a0c1c);border:1px solid rgba(var(--neon-rgb),.18);border-radius:14px;
      box-shadow:0 12px 30px rgba(0,0,0,.5);padding:10px}
    .mood-picker-wrap.open .mood-picker-pop{display:grid}
    .mood-picker-opt{display:flex;flex-direction:column;align-items:center;gap:3px;padding:10px 6px;border-radius:10px;background:rgba(255,255,255,.03);border:1px solid transparent;transition:all .15s}
    .mood-picker-opt:hover{background:rgba(255,255,255,.08)}
    .mood-picker-opt .mp-emoji{font-size:1.5rem;line-height:1}
    .mood-picker-opt .mp-label{font-size:.62rem;color:var(--text-muted);white-space:nowrap}
    .mood-picker-opt.selected{border-color:var(--c);background:color-mix(in srgb,var(--c) 18%,transparent);box-shadow:0 0 16px -6px var(--c)}
    .mood-picker-opt.selected .mp-label{color:var(--text)}
    .mood-picker-opt[data-mood="sawi"]{--c:#E91E63}
    .mood-picker-opt[data-mood="galit"]{--c:#FF5722}
    .mood-picker-opt[data-mood="sawa"]{--c:#607D8B}
    .mood-picker-opt[data-mood="pagasa"]{--c:var(--neon)}
    .mood-picker-opt[data-mood="malalim"]{--c:#9C27B0}
    .mood-picker-opt[data-mood="nakakatawa"]{--c:#4CAF50}
    #tb-mood.active,#qm-tb-mood.active{background:rgba(178,34,34,.22)}
    body.light-mode #tb-mood.active,body.light-mode #qm-tb-mood.active{background:rgba(178,34,34,.14)}
    .compose-footer{display:flex;align-items:center;justify-content:space-between;margin-top:10px}
    /* The count and the Post button are ONE flex item, so the row cannot break
       between them. margin-left:auto lives HERE, on the pair, not on the count -
       an auto margin on a wrapping flex row consumes all remaining space on the
       line, which is what pushed Post onto a second row on its own. 2026-08-05. */
    .compose-send{display:flex;align-items:center;gap:10px;margin-left:auto;flex:0 0 auto}

    /* ==========================================================================
       COMPOSER TOOLBAR ON A PHONE — 2026-08-11. Shane: "in the user post
       container emoticons wall punlic off ddefault, 0/500 post ... right now its
       a mess indention and spacing", and then "desktop right now is ok. im
       concern in mobile" — so this is a MOBILE-ONLY block. The desktop row and
       every number in the LOCKED banner at the top of this file are untouched.

       THE CAUSE, and the banner says it out loud: the one-row layout "fits by
       about ten pixels". That is on a DESKTOP. On a phone it does not fit, so
       `flex-wrap` — which is deliberately kept as the fallback — breaks the row
       wherever it happens to run out. Where that lands depends on the phone
       width, the language and the room name, so it looks different on every
       device and after every change. That is the "mess", and it is not a
       spacing value anyone can tune: a wrap point nobody chose cannot be tidy.

       SO: TWO ROWS ON PURPOSE, below 480px.
         row 1  emoji + the four pickers, sharing the width equally
         row 2  the count and Post
       A chosen break is stable. Nothing moves when a label changes, because the
       pills no longer size to their text.

       THE PILLS SHARE THE WIDTH EQUALLY (`flex:1 1 0` + `min-width:0`) and clip
       with an ellipsis instead of pushing their neighbours. `min-width:0` is
       load-bearing: without it a flex item refuses to shrink below its content
       and the row overflows again — the same fault as the desk-rail nickname
       and the feed tabs, three times in one day.

       A VENT BUDDY SEES TWO PILLS, not four (Who can see and Vanish are hidden
       for staff). flex:1 handles that by construction — two pills split the
       same width. That is exactly why this is flex and not a fixed grid.

       POST GETS A REAL TAP TARGET back on its own row. It is the one control on
       this card people actually press, and on a phone it was being squeezed by
       whatever happened to wrap beside it.

       COST, accepted: the card is ~30px taller on a phone.
       ========================================================================== */
    @media (max-width:480px){
      /* WRAP, NOT NOWRAP — corrected 2026-08-11 after my first attempt made it
         worse. `flex-wrap:nowrap` on .mood-picker-row forced the emoji, the
         pills AND .compose-send onto ONE line, so Post was pushed off the right
         edge of the card. The break has to be CHOSEN, not forbidden: wrap stays
         on, and .compose-send is given `flex-basis:100%` so it always lands on
         its own row. A flex item that cannot fit beside its siblings wraps by
         itself — that is a deterministic two-row layout, not a guess. */
      .mood-picker-row{flex-wrap:wrap;gap:6px;margin:0 0 8px}

      /* EMOJI MOVES DOWN TO THE SEND ROW — Shane picked option B, 2026-08-11,
         MOBILE ONLY ("so u will wire it just in mobile? because dekstop wdth is
         ok?" — yes: desktop keeps the one-row order emoji, pills, count, Post,
         and every number in the LOCKED banner at the top of this file).
         WHY IT HAD TO MOVE: the four pills now share the full width, so the
         emoji was the odd item out on that row and wrapped ABOVE them — a
         button on a line of its own with nothing beside it.
         DONE WITH `order`, NOT BY MOVING THE MARKUP. The button is
         #tb-emoji inside .mood-picker-row and its click handler is bound by id;
         relocating the element would mean touching app.html and risking the
         handler. order:2 puts it after .compose-pill-row (order:1) in the flex
         flow, which lands it at the start of the wrapped second row beside the
         count and Post. Source order is untouched, so the tab order still runs
         write -> emoji -> decide -> send, and a screen reader reads it in the
         order it is written.
         I RECOMMENDED A (emoji inside the text box, where it types) and said so;
         he chose B. Do not re-pitch A. */
      #tb-emoji{flex:0 0 auto;order:2;align-self:center}
      .compose-pill-row{flex:1 1 100%;order:1;flex-wrap:nowrap;gap:6px;min-width:0;overflow:visible}

      /* .post-dest-ROW is the flex child; .post-dest-PILL inside it is
         `display:inline-block`, which was my second mistake: an inline-block
         keeps its own content width, so when I shrank the row the pill spilled
         straight out of it and the four buttons drew ON TOP OF each other.
         Both levels have to be told to fill: the row shares the width, the pill
         fills the row, the button fills the pill.
         Its margin goes too — `.post-dest-row{margin:10px 0 4px}` was adding a
         10px lift to every pill inside a row that already has its own spacing. */
      .compose-pill-row > .post-dest-row{flex:1 1 0;min-width:0;margin:0}
      .compose-pill-row .post-dest-pill{display:block;width:100%}
      .post-dest-btn{
        width:100%;
        justify-content:center;
        padding:7px 6px;
        font-size:.74rem;
        gap:4px;
        white-space:nowrap;
        overflow:hidden;
        text-overflow:ellipsis;
      }
      /* The dropdown must NOT inherit the pill's new narrow width — it is
         absolutely positioned, so `min-width:200px` still applies, but on a
         narrow pill it would open off the right edge. Anchored to the right
         instead for the last two pills is overkill; letting it start at the
         pill's left and cap at the card width is enough. */
      .compose-pill-row .post-dest-list{max-width:min(240px,72vw)}

      /* Count and Post on their own full-width row. flex-basis:100% is what
         forces the wrap; margin-left:auto is cancelled because with the row to
         itself there is nothing to push against. */
      /* order:3 keeps it last. `flex:1 1 auto` (not 100%) so it SHARES the second
         row with the emoji instead of forcing itself onto a third one, and
         margin-left:0 because the emoji is now the thing on its left. */
      .compose-send{flex:1 1 auto;order:3;width:auto;margin-left:0;justify-content:space-between;gap:10px}
      .post-btn{padding:9px 24px;font-size:.85rem}
    }
/* ON A WIDE SCREEN THE ROW DOES NOT WRAP AT ALL. Grouping the pair stopped the
   count and the button from being split from each other, but the pair still
   dropped to a second line, because emoji + four pills + the pair added up to
   fractionally MORE than the card is wide - they very nearly fitted, which is
   why it looked arbitrary.
   TWO WRONG FIXES BEFORE THIS ONE, recorded so neither is retried:
   (a) flex:1 1 auto + min-width:0 on the pill row - it then shrank below
       its content, so DEFAULT dropped a line instead of the pair. One wrap
       traded for another.
   (b) a nowrap media query - it only held at wide viewports, and anywhere
       narrower it would push content past the card edge instead of wrapping.
   THE ROW WAS ONLY OVERFLOWING BY ROUGHLY TEN PIXELS. Measured off a
   screenshot: emoji 36 + four pills ~400 + the pair ~136 + gaps came to a
   hair MORE than the card's inner width, which is why one line looked so
   nearly right. So nothing is forced onto one line - about 25px is taken
   out of the padding and gaps instead, which is enough to fit honestly.
   flex-wrap STAYS on, as the fallback: a longer label in another language,
   or a larger system font, will wrap rather than overflow. Do not swap this
   back for a breakpoint. */
    .char-count{font-size:.72rem;color:var(--text-muted);white-space:nowrap}
    .char-count.warning{color:#FF6B6B}
    .post-btn{background:var(--crimson);color:var(--gold);font-size:.82rem;font-weight:600;letter-spacing:.04em;padding:8px 16px;border-radius:20px;border:1px solid var(--border-gold);transition:all .2s}
    .post-btn:active{transform:scale(.97)}
    .post-btn:disabled{opacity:.4;cursor:not-allowed;transform:none}

    /* ====================================================
       PROGRESSIVE REVEAL
    ==================================================== */
    .reveal-section{opacity:0;transform:translateY(16px);transition:opacity .6s ease,transform .6s ease}
    /* `transform:none`, NOT `translateY(0)` — 2026-08-10, and this is why the
       composer's dropdowns rendered UNDERNEATH the post cards on both mobile and
       desktop no matter what z-index they carried.
       ANY transform value other than `none` creates a STACKING CONTEXT. The
       composer sits in `#compose-wrapper.reveal-section`, so once the reveal
       finished, translateY(0) left that wrapper as a sealed context — and a
       child cannot paint above something OUTSIDE its own context however high
       its z-index. `.post-dest-list` at z-index 120 was competing only with its
       own siblings inside the composer, never with the feed below it.
       translateY(0) and none are visually IDENTICAL, and the transition still
       animates because browsers treat `none` as the identity matrix. So the
       context simply stops existing once the section has settled.
       THIS FIXES EVERY OVERLAY INSIDE ANY .reveal-section, not just this one —
       the emoji picker and the post menu were on the same fault.
       If a dropdown ever hides behind content again, check the ANCESTORS for
       transform / filter / opacity below 1 / backdrop-filter before touching
       another z-index. */
    .reveal-section.visible{opacity:1;transform:none}
    .feed-label{font-size:.65rem;text-transform:uppercase;letter-spacing:.14em;color:var(--text-muted);padding:12px 0 6px;display:flex;align-items:center;gap:6px}
    .feed-label::after{content:'';flex:1;height:1px;background:var(--border)}
    #feed-refresh-btn svg{transition:transform .5s ease}
    #feed-refresh-btn.spinning svg{transform:rotate(360deg)}
    #pull-refresh{position:fixed;top:0;left:0;right:0;display:flex;align-items:center;justify-content:center;height:0;overflow:hidden;color:var(--gold);font-size:1.1rem;z-index:120;pointer-events:none;transition:height .18s}

    /* ====================================================
       POST CARD
    ==================================================== */
    .post-card{background:var(--card-bg);border:1px solid var(--border);border-radius:var(--radius);margin-bottom:12px;position:relative;overflow:hidden;animation:card-in .4s ease both;transition:border-color .25s,box-shadow .25s}
    .post-card::before{content:'';position:absolute;top:0;left:0;right:0;height:2px;transition:opacity .3s}
    .post-card[data-mood="sawi"]::before{background:linear-gradient(90deg,#E91E63,transparent)}
    .post-card[data-mood="galit"]::before{background:linear-gradient(90deg,#FF5722,transparent)}
    .post-card[data-mood="sawa"]::before{background:linear-gradient(90deg,#607D8B,transparent)}
    .post-card[data-mood="pagasa"]::before{background:linear-gradient(90deg,var(--gold),transparent)}
    .post-card[data-mood="malalim"]::before{background:linear-gradient(90deg,#9C27B0,transparent)}
    .post-card[data-mood="nakakatawa"]::before{background:linear-gradient(90deg,#4CAF50,transparent)}
    .post-card:hover{border-color:rgba(139,0,0,.6);box-shadow:var(--glow-red)}
    /* MASKARA FLIP */
    .post-card.masked .post-content,.post-card.masked .post-mood-badge{filter:blur(6px);user-select:none;pointer-events:none;transition:filter .4s}
    .mask-reveal-btn{position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);background:rgba(139,0,0,.85);border:1px solid rgba(var(--neon-rgb),.35);border-radius:20px;padding:8px 18px;font-size:.8rem;color:var(--gold);font-weight:600;z-index:4;box-shadow:var(--glow-red);animation:pulse-btn 2s infinite}
    .post-card.revealed .mask-reveal-btn{display:none}
    .post-card.revealed .post-content,.post-card.revealed .post-mood-badge{filter:none}
    .post-inner{padding:14px 14px 10px}
    .post-meta{display:flex;align-items:center;justify-content:space-between;margin-bottom:10px}
    .post-author{display:flex;align-items:center;gap:8px}
    /* 30px -> 40px, 2026-08-11. On this app the mask IS the person - no photo,
       no real name, nothing else identifying - so it carries more weight than a
       standard social avatar and 30px was losing the detail in the illustrated
       faces. 40 AND NOT MORE for a measured reason: the nickname + timestamp
       stack beside it is already about 40px tall, so at this size the avatar is
       not what sets the row height and the card does not grow. Past ~44px it
       starts driving the height and EVERY card in the feed gets taller, which
       on a long scroll is a real cost. Font-size 21 -> 27 keeps an emoji face
       filling the same proportion of the disc.
       .reply-avatar deliberately STAYS 30px (see its override further down) -
       a reply is nested under a post and should read one step smaller. */
    .author-avatar{width:40px;height:40px;border-radius:50%;display:flex;align-items:center;justify-content:center;font-size:27px;line-height:1;flex-shrink:0;border:1.5px solid var(--border)}
    /* 1.5px and the same three colours as everywhere else - locked. Free flat
       blue, PLUS static SILVER glow, Gold the animated pulse (.avatar-gold-ring),
       staff green (.avatar-agent-ring). Free and PLUS never animate: the pulse
       is what Gold buys. */
    .author-avatar.author-ring-free{border-color:var(--silver);box-shadow:none}
    .author-avatar.author-ring-plus{border-color:var(--silver);box-shadow:0 0 10px 1px rgba(199,204,212,.40)}
    /* The shared .avatar-gold-ring forces 3px, which is right on a 96px profile
       avatar and far too heavy on a 40px one. 1.5px is the locked ring width
       everywhere and does not change with the disc size. */
    .author-avatar.avatar-gold-ring{border-width:1.5px!important}
    /* STAFF IS THE ONE EXCEPTION TO THE 1.5px LOCK, 2026-08-13. At 1.5px the
       green ring read as a hairline and Shane could not tell a Vent Buddy's
       post from anyone else's at a glance - "the ring is not bold". Staff
       outranks every paid tier in this app's own ordering, so it is the one
       mark allowed to be heavier. 3px, with the halo widened to match, on the
       40px feed avatar only. */
    .author-avatar.avatar-agent-ring{border-width:3px!important;box-shadow:0 0 14px 2px rgba(63,206,122,.55)!important}
    .author-name{font-size:.78rem;font-weight:500;color:var(--text-soft)}
    .author-bio{display:inline-block;margin-left:6px;padding:2px 9px;border-radius:11px;background:var(--surface);border:1px solid var(--border);font-size:.68rem;font-style:italic;color:var(--text-muted);max-width:180px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;vertical-align:middle}
    .post-time{font-size:.68rem;color:var(--text-muted)}
    .post-meta .translate-btn{width:26px;height:26px;padding:0;flex-shrink:0;justify-content:center;
      border-radius:50%;border:1px solid rgba(255,255,255,.16);
      background:linear-gradient(180deg,rgba(var(--panel-hi-rgb),.28) 0%,rgba(var(--panel-lo-rgb),.22) 100%);
      font-size:.8rem;line-height:1}
    .post-meta .translate-btn:hover{border-color:var(--border-gold)}
    /* Showing a translation. The only way an unlabelled icon can say "tap me to
       go back". */
    .post-meta .translate-btn.showing-translation{border-color:var(--gold);box-shadow:0 0 8px rgba(var(--neon-rgb),.25)}
    .post-meta .translate-btn.translating{opacity:.5;animation:globeSpin 1s linear infinite}
    @keyframes globeSpin{to{transform:rotate(360deg)}}
    /* Night mode dims the whole feed, so the button needs its own edge or it
       disappears into the card. */
    body.gabi-mode .post-meta .translate-btn{border-color:rgba(246,201,76,.28)}
    .post-mood-badge{font-size:.68rem;padding:2px 8px;border-radius:10px;background:rgba(255,255,255,.06);color:var(--text-muted)}
    /* ITALIC REMOVED ENTIRELY, 2026-07-31. Shane: "other people doesnt like
       italic" - real reader feedback, and it beats an aesthetic preference for
       body copy. It was italic for EVERY post on every tier and nobody chose it.
       Serif stays: the literary voice is in the typeface, not the slant.
       A length-based version of this shipped earlier the same day (italic kept
       for short posts) and was replaced by this. Do not reintroduce a .long
       class - the answer turned out to be simpler than the rule.
       This also settles a parity gap: confession.html's .c-quote has always been
       upright serif, so the same confession rendered italic on the Wall and
       upright in its room. The two now match exactly.
       Italic survives where it is a brand mark rather than reading matter - the
       MASKARA wordmark, the share card and the Gold name default. */
    /* BODY FONT, NOT --font-serif. 2026-07-31, second attempt at this.
       Removing `font-style:italic` from this rule did NOTHING, and the reason
       is worth recording: EVERY Playfair Display @font-face in fonts/fonts.css
       is declared font-style:italic - six of them, no upright face exists. A
       browser given one face for a family uses it for everything, so asking
       for upright Playfair still returns the italic file.
       So --font-serif cannot produce upright text at all. Post bodies now use
       --font-ui (Inter), which has real normal faces at 300-600.
       Playfair is still reachable, as a deliberate CHOICE, via the composer's
       Style picker - which is where an italic serif belongs. */
    .post-content{font-family:var(--font-ui);font-size:1.02rem;line-height:1.65;color:var(--text);margin-bottom:12px;word-break:break-word}
    .post-hugot-tag{color:#FF6B6B;font-style:normal;background:rgba(139,0,0,.12);border-radius:4px;padding:0 2px;cursor:pointer}
    /* OPM card */
    .post-opm-card{display:flex;align-items:center;gap:8px;background:rgba(var(--neon-rgb),.07);border:1px solid rgba(var(--neon-rgb),.18);border-radius:10px;padding:7px 10px;margin-bottom:10px;font-size:.78rem;color:var(--gold-dim)}
    /* Quote preview inside post */
    .quote-preview{border:1px solid var(--border);border-radius:10px;padding:10px 12px;margin-bottom:10px;background:rgba(0,0,0,.25);position:relative}
    .quote-preview::before{content:'';position:absolute;top:0;left:0;bottom:0;width:3px;background:var(--crimson);border-radius:3px 0 0 3px}
    .quote-author{font-size:.7rem;color:var(--text-muted);margin-bottom:4px}
    .quote-text{font-family:var(--font-serif);font-style:italic;font-size:.88rem;color:var(--text-soft);line-height:1.5}
    /* Thread indicator */
    .thread-indicator{display:flex;align-items:center;gap:6px;font-size:.68rem;color:var(--text-muted);padding:0 0 6px}
    .thread-dot{width:3px;height:3px;border-radius:50%;background:var(--text-muted)}
    /* Poll */
    .post-poll{border:1px solid var(--border);border-radius:10px;overflow:hidden;margin-bottom:10px}
    .poll-opt-btn{width:100%;text-align:left;padding:10px 14px;border-bottom:1px solid var(--border);font-size:.85rem;color:var(--text-soft);position:relative;overflow:hidden;transition:background .2s}
    .poll-opt-btn:last-child{border-bottom:none}
    .poll-opt-fill{position:absolute;top:0;left:0;bottom:0;background:rgba(139,0,0,.18);transition:width .6s ease}
    .poll-opt-label{position:relative;z-index:1;display:flex;justify-content:space-between}
    .poll-opt-pct{font-size:.72rem;color:var(--text-muted)}
    .poll-meta{padding:6px 14px;font-size:.68rem;color:var(--text-muted)}
    /* Reaction bar */
    .reaction-bar{display:flex;gap:4px;flex-wrap:wrap;border-top:1px solid rgba(255,255,255,.05);padding-top:10px;margin-top:4px}
    .reaction-btn{display:flex;align-items:center;gap:4px;padding:5px 9px;border-radius:20px;border:1px solid rgba(255,255,255,.08);background:transparent;color:var(--text-muted);font-size:.72rem;transition:all .18s}
    .reaction-btn:active{transform:scale(.93)}
    .reaction-btn.reacted{border-color:var(--crimson-lt);background:rgba(139,0,0,.15);color:var(--text-soft)}
    .reaction-emoji{font-size:.9rem}
    .reaction-count{font-size:.7rem;min-width:12px}
    /* currentColor on the icon means the saved state colours icon and label
       together, and a filled shape reads as saved even to someone who cannot
       distinguish the gold. */
    .bookmark-btn.saved{color:var(--gold)}
    .bookmark-btn.saved .act-ico{fill:var(--gold)}
    /* Post actions */
    .reaction-bar:empty{display:none}
    /* Gold, at Shane's call. It is the brand colour and it makes React the one
       thing the eye lands on first in that row, which matches it being the
       lightest-weight way to respond to a confession. */
    .action-btn.rxn-open-btn{color:var(--text-muted)}
    /* Emoji cannot be recoloured - the OS draws them in fixed colours and CSS
       `color` does nothing. An inline SVG is the only way to control the hue,
       so the envelope is now a stroked icon in the Free-tier blue #4a7cf6. */
    .act-ico{flex-shrink:0}
    /* The + is a taller glyph than the label text, so it sat low against it.
       Nudged up a pixel to sit on the same optical line as "React". */
    .rxn-plus{color:var(--gold);font-size:1.05rem;line-height:1;font-weight:600;position:relative;top:-1px}
    .post-actions{display:flex;gap:6px;padding:0 14px 12px;align-items:center;flex-wrap:wrap}
    /* Floats ABOVE the dots, sized to its own contents. .post-card carries
       overflow:hidden, so .post-card.menu-open lifts the clipping only while a
       menu is open - the same trick .compose-card.dropdown-open uses for the
       Vanish list. Opens UPWARD so the last card in the feed does not push its
       menu off the bottom of the page. */
    .post-more-wrap{position:relative;margin-left:auto;display:flex}
    .post-more-menu{display:none;position:absolute;right:0;bottom:calc(100% + 8px);z-index:120;
      min-width:0;width:max-content;max-width:210px;padding:4px;border-radius:10px;
      border:1px solid rgba(255,255,255,.12);
      background:linear-gradient(180deg,rgba(var(--panel-hi-rgb),.97) 0%,rgba(var(--panel-lo-rgb),.96) 100%);
      box-shadow:0 12px 30px rgba(0,0,0,.5)}
    .post-more-menu.open{display:block}
    .post-card.menu-open{overflow:visible}
    /* Plain rows inside the panel - a pill inside a bordered box is two
       containers doing one job. */
    /* Needs .post-actions in the selector: the menu now lives INSIDE the action
       row, so the pill rule above was winning and every row got its own outline
       inside an already-outlined panel. */
    .post-actions .post-more-menu .action-btn{width:100%;justify-content:flex-start;background:none;border-color:transparent;border-radius:8px;padding:7px 9px;font-size:.7rem;white-space:nowrap}
    .post-actions .post-more-menu .action-btn:hover{background:rgba(255,255,255,.06);border-color:transparent}
    .post-actions .post-more-menu .motd-btn.voted{color:var(--gold)}
    .post-more-sep{height:1px;background:rgba(255,255,255,.08);margin:3px 6px}
    .post-actions .post-more-btn[aria-expanded="true"]{color:var(--gold);border-color:var(--border-gold)}
    body.light-mode .post-more-menu{background:var(--card-bg);border-color:var(--border)}
    .action-btn{display:flex;align-items:center;gap:5px;font-size:.72rem;color:var(--text-muted);padding:5px 10px;border-radius:8px;border:1px solid transparent;transition:all .18s}
    .action-btn:active{background:rgba(255,255,255,.05)}
    /* Outlined pills, copied from confession.html's .trans-btn. The Wall's row
       was bare text on the card - the same four controls read as a solid group
       in a room and as loose words here. */
    .post-actions .action-btn{background:linear-gradient(180deg,rgba(var(--panel-hi-rgb),.28) 0%,rgba(var(--panel-lo-rgb),.22) 100%);border-color:rgba(255,255,255,.16);border-radius:20px;padding:5px 11px}
    .post-actions .action-btn:hover{border-color:var(--border-gold);color:var(--text)}
    .post-actions .post-more-btn{border-radius:50%;padding:5px 11px;font-size:.95rem;line-height:1}

    /* ==========================================================================
       THE ACTION ROW ON A PHONE — 2026-08-11. Shane: "in mobile the indention
       and spacing are messy yes fix".
       SAME SHAPE AS THE COMPOSER FAULT, one card lower: five items in a
       flex-wrap row that fits on a desktop and does not fit on a phone, so the
       break lands wherever it runs out — usually with ⋯ stranded alone on a
       second line, and further left or right depending on the reply count, the
       language, and the phone.
       The code comment in app.html already records this happening ONCE, when
       there were seven labelled actions and the last one "stranded itself on
       its own line". Moving three into the ⋯ menu bought room on a desktop; it
       did not buy enough for 360px.

       FIT, DON'T WRAP. React / Reply / Share / Save share the row equally and ⋯
       keeps its own fixed width, so the row is one line at every phone size and
       nothing moves when a reply count appears.

       LABELS STAY. Cutting to icons was the alternative and it is worse: this
       app has an unlabelled-icon bug in its own history — Save used to sit in
       the card corner as a bare icon and Shane could not tell what it did.
       The labels shrink instead.

       min-width:0 AGAIN — the third place today. A flex item will not shrink
       below its content unless told, and that is what turns "share the row"
       into "overflow the row". */
    @media (max-width:480px){
      .post-actions{gap:5px;padding:0 12px 12px;flex-wrap:nowrap}
      .post-actions .action-btn{
        flex:1 1 0;
        min-width:0;
        justify-content:center;
        padding:6px 4px;
        font-size:.68rem;
        gap:3px;
        white-space:nowrap;
        overflow:hidden;
        text-overflow:ellipsis;
      }
      /* ⋯ is an icon, not a label — it must not stretch with the others, and it
         needs to stay a circle. A 34px target is the floor for a control that
         holds Delete behind it. */
      .post-actions .post-more-wrap{flex:0 0 auto}
      .post-actions .post-more-btn{flex:0 0 auto;width:34px;min-width:34px;padding:5px 0;justify-content:center}
      /* The menu inside ⋯ is a dropdown, not part of the row: its buttons must
         keep full width and left alignment, which the flex:1 rule above would
         otherwise take away. */
      .post-actions .post-more-menu .action-btn{
        flex:1 1 auto;
        justify-content:flex-start;
        padding:8px 9px;
        font-size:.74rem;
        text-overflow:clip;
      }
      .act-ico{width:13px;height:13px;flex-shrink:0}
    }
    /* Was #FF6B6B. Red made Message the loudest thing in the row - and it leads
       to DMs, which are cut from v1. Every label in this row is now the same
       muted grey; the emoji carry the colour. */
    .action-btn.dm-btn{color:var(--text-muted)}
    .action-btn.report-btn{margin-left:auto;color:var(--text-muted)}
    .action-btn.report-btn:active{color:#FF5722}
    .action-btn.quote-action{color:#9B59B6}
    /* Hugot chips under post */
    .post-tags-row{display:flex;gap:4px;flex-wrap:wrap;padding:0 14px 10px}
    .post-tag-chip{font-size:.68rem;color:#FF6B6B;background:rgba(139,0,0,.1);border:1px solid rgba(139,0,0,.25);border-radius:8px;padding:2px 8px;cursor:pointer;transition:background .15s}
    .post-tag-chip:active{background:rgba(139,0,0,.25)}

    /* ====================================================
       REPLY THREAD
    ==================================================== */
    .replies-section{border-top:1px solid rgba(255,255,255,.05);background:rgba(0,0,0,.2);padding:0 14px;display:none}
    .replies-section.open{display:block}
    .reply-card{display:flex;align-items:flex-start;gap:10px;padding:8px 0}
    .reply-body{display:flex;flex-direction:column;gap:2px;min-width:0;flex:1}
    .reply-del-btn{flex-shrink:0;background:none;border:0;color:var(--text-muted);font-size:.8rem;padding:0 2px;cursor:pointer}
    .reply-del-btn:hover{color:#E24B4A}
    .reply-card:last-child{border-bottom:none}
    .reply-meta{display:flex;align-items:center;gap:6px;margin-bottom:5px}
    .reply-avatar{width:22px;height:22px;border-radius:50%;display:flex;align-items:center;justify-content:center;font-size:15px;line-height:1;border:1px solid var(--border)}
    .reply-author{font-size:.72rem;color:var(--text-muted)}
    .reply-time{font-size:.65rem;color:var(--text-muted);flex-shrink:0}
    /* No left padding now - the text sits under the name inside .reply-body
       rather than being indented to clear an avatar on the line above. */
    .reply-text{font-family:var(--font-ui);font-style:normal;font-size:.9rem;line-height:1.55;color:var(--text-soft)}
    .reply-compose{display:flex;gap:8px;padding:10px 0;align-items:flex-start}
    /* Same box confession.html uses: dark translucent fill, white 16% border,
       14px radius - matching the composer rather than the crimson-tinted
       --border and --surface this used before. */
    .reply-input{flex:1;background:linear-gradient(180deg,rgba(var(--panel-hi-rgb),.28) 0%,rgba(var(--panel-lo-rgb),.22) 100%);border:1px solid rgba(255,255,255,.16);border-radius:14px;color:var(--text);font-family:var(--font-ui);font-style:normal;font-size:.88rem;padding:8px 12px;outline:none;resize:none;min-height:40px}
    .reply-input::placeholder{color:var(--text-muted)}
    .reply-input::placeholder{color:var(--text-muted)}
    .reply-send-btn{background:linear-gradient(180deg,rgba(var(--panel-hi-rgb),.28) 0%,rgba(var(--panel-lo-rgb),.22) 100%);border:1px solid rgba(255,255,255,.16);color:var(--text-soft);border-radius:20px;padding:7px 14px;font-size:.75rem;font-weight:500;flex-shrink:0;align-self:flex-end;transition:.15s}
    .reply-send-btn:hover{border-color:var(--border-gold);color:var(--text)}
    /* Small round emoji-trigger buttons that sit next to a text box (reply,
       DM, group chat) — the toolbar version (main composer, quote post)
       reuses .toolbar-btn instead, see .compose-toolbar. */
    .emoji-trigger-btn{flex-shrink:0;width:36px;height:36px;border-radius:50%;background:rgba(var(--neon-rgb),.08);border:1px solid var(--border-gold);font-size:1.05rem;display:flex;align-items:center;justify-content:center;cursor:pointer;transition:background .15s;align-self:flex-end}
    .emoji-trigger-btn:hover{background:rgba(var(--neon-rgb),.16)}
    .reply-emoji-btn{width:32px;height:32px;font-size:.95rem}
    /* Emoji keyboard panel — a phone-style emoji picker shared by every text
       box in the app. position:fixed + JS-computed top/left so it can anchor
       next to whichever trigger button opened it, anywhere on the page. */
    /* Same nested-container gradient as the rest of the app instead of a
       flat near-black panel — this picker is shared by Wall, Post, and
       (via the identical CSS in confession.html) every Confession Room. */
    .emoji-picker-panel{position:fixed;z-index:2000;width:300px;max-width:92vw;height:320px;background:linear-gradient(180deg,rgba(var(--panel-hi-rgb),0.97) 0%,rgba(var(--panel-lo-rgb),0.96) 100%);border:1px solid rgba(255,255,255,0.10);border-radius:14px;box-shadow:0 20px 50px rgba(0,0,0,.6);display:none;flex-direction:column;overflow:hidden}
    .emoji-picker-panel.show{display:flex}
    .emoji-picker-cats{display:flex;gap:2px;padding:8px 8px 6px;border-bottom:1px solid var(--border);overflow-x:auto;flex-shrink:0}
    .emoji-picker-cat{flex-shrink:0;padding:6px 9px;font-size:1.05rem;border-radius:8px;background:transparent;border:none;cursor:pointer;opacity:.5;transition:opacity .15s,background .15s;line-height:1}
    .emoji-picker-cat.active{opacity:1;background:rgba(var(--neon-rgb),.14)}
    .emoji-picker-grid{flex:1;overflow-y:auto;display:grid;grid-template-columns:repeat(7,1fr);gap:2px;padding:8px}
    .emoji-picker-grid button{font-size:1.35rem;background:transparent;border:none;border-radius:8px;padding:5px 0;cursor:pointer;transition:background .15s;line-height:1}
    .emoji-picker-grid button:hover{background:rgba(255,255,255,.09)}
    .reply-send-btn:active{opacity:.7}

    /* ====================================================
       HUGOT DUEL
    ==================================================== */
    .duel-card{background:linear-gradient(135deg,#150010,#0A0A0F);border:1px solid rgba(160,0,200,.35);border-radius:var(--radius);margin-bottom:12px;overflow:hidden;position:relative}
    .duel-card::before{content:'';position:absolute;top:0;left:0;right:0;height:2px;background:linear-gradient(90deg,#9B59B6,#FF6B6B,#9B59B6)}
    .duel-header{padding:10px 14px 6px;display:flex;align-items:center;gap:6px;font-size:.7rem;color:#9B59B6;text-transform:uppercase;letter-spacing:.1em}
    .duel-vs{padding:0 14px 12px;display:flex;gap:10px}
    .duel-side{flex:1;background:rgba(0,0,0,.3);border-radius:10px;padding:12px;border:1px solid rgba(255,255,255,.06);display:flex;flex-direction:column;gap:8px;transition:border-color .2s}
    .duel-side.winning{border-color:rgba(160,0,200,.4)}
    .duel-text{font-family:var(--font-serif);font-style:italic;font-size:.88rem;line-height:1.5;color:var(--text)}
    .duel-vote-btn{width:100%;padding:7px;border-radius:8px;border:1px solid rgba(160,0,200,.3);background:transparent;font-size:.72rem;color:#9B59B6;font-weight:600;transition:all .2s}
    .duel-vote-btn.voted{background:rgba(160,0,200,.2);color:var(--text);border-color:#9B59B6}
    .duel-vote-count{font-size:.7rem;color:var(--text-muted);text-align:center}
    .duel-prog-bar{height:3px;background:rgba(255,255,255,.06);border-radius:2px;margin:0 14px 10px;overflow:hidden}
    .duel-prog-fill{height:100%;background:linear-gradient(90deg,#9B59B6,#FF6B6B);border-radius:2px;transition:width .6s ease}

    /* ====================================================
       MODALS
    ==================================================== */
    /* Tinted with the app's own midnight purple instead of flat black. Pure
   rgba(0,0,0,.75) reads as a browser dialog dimming a page - it drops the
   whole product out of view rather than pushing it back. Shared by all 28
   overlays, so they can't drift apart. */
    /* THE BACKDROP IS GREEN NOW — 2026-08-10. rgba(16,12,34,.82) is the old
       purple-black: a near-opaque dark wash left from the retired palette, so
       every modal in a green app opened onto a black screen. It survived the
       repaint because a backdrop is only visible while something is open, and
       nobody screenshots the thing behind the thing they are looking at.
       DERIVED FROM .bg's DARKEST STOP (#416839) rather than a new colour, so
       the dimmed page still reads as this app rather than as a different one.
       .82 KEPT: the job of a backdrop is to take the page out of play, and a
       lighter wash would leave the feed competing with a report form. */
    /* THE BACKDROP WAS A GREEN LITERAL — fixed 2026-08-11, and it is the reason
       three separate "the theme is not following" reports all looked different.
       `rgba(45,72,38,.82)` is a DARK GREEN, so with any sheet open, most of the
       screen was green no matter what theme was picked: on a BLACK theme the
       sheet went black correctly and sat on a green page, which reads as the
       sheet being wrong rather than the backdrop.
       `--panel-lo-rgb` is written by applyTheme, so the wash now follows. The
       .82 alpha and the blur are unchanged. THIS IS THE FIFTH FULL-SCREEN
       SURFACE found painting from a literal — see the list in the theme notes;
       any rule covering the whole viewport must read a token. */
    .modal-overlay{position:fixed;inset:0;background:rgba(var(--panel-lo-rgb),.82);backdrop-filter:blur(8px);-webkit-backdrop-filter:blur(8px);z-index:200;display:none;align-items:flex-end;justify-content:center}
    /* these can be opened while the onboarding screen (z-index 999) is still showing, so they need to render above it */
    #account-login-modal,#account-recover-modal,#show-login-code-modal,#gold-purchase-success-modal{z-index:1001}
    /* Login + recovery + Your mask mirror the REAL app backdrop (.bg): violet base gradient
       with a gold glow from the top-center and a purple glow from the bottom-center, plus
       vignette — same background onboarding/confession/the dashboard already use, instead
       of the flat black the generic .modal-overlay falls back to. */
    #account-login-modal,#account-recover-modal,#profile-modal{background:radial-gradient(120% 115% at 50% 42%,transparent 52%,rgba(0,0,0,.5) 100%),radial-gradient(120% 85% at 50% -8%,rgba(236,243,232,.20) 0%,rgba(236,243,232,.07) 22%,transparent 52%),radial-gradient(105% 72% at 50% 118%,rgba(var(--panel-lo-rgb),.50) 0%,rgba(var(--panel-lo-rgb),.22) 44%,transparent 72%),linear-gradient(178deg,var(--ground-hi) 0%,var(--midnight) 55%,var(--ground-lo) 100%);backdrop-filter:blur(6px)}
    .modal-overlay.open{display:flex}
    /* Purple glass is now the DEFAULT for every modal. Five sheets had it
       individually overridden (login, recover, profile, mask picker, call) and
       every other one - Manage my codes, recovery code, redeem, signup, save
       code, user preview, Barkada, group manage, wave, create group, DM, group
       chat, quote - fell through to the near-black --card-bg. So which modal
       you opened decided which colour scheme you got. The five specific
       overrides further down are now redundant but harmless; they set the same
       values. */
    .modal-sheet{
      background:linear-gradient(180deg,rgba(var(--panel-hi-rgb),0.97) 0%,rgba(var(--panel-lo-rgb),0.96) 100%);
      backdrop-filter:blur(16px);-webkit-backdrop-filter:blur(16px);
      border:1px solid rgba(255,255,255,0.10);border-radius:20px 20px 0 0;width:100%;max-width:480px;max-height:85vh;
      display:flex;flex-direction:column;overflow-y:auto;scrollbar-gutter:stable;
      box-shadow:0 20px 50px rgba(0,0,0,0.5),inset 0 1px 0 rgba(255,255,255,0.06);
      animation:sheet-up .28s cubic-bezier(.22,.61,.36,1)}
    /* SAME SPRING REMOVED HERE — 2026-08-11. This is the BOTTOM-anchored sheet
       animation (every modal that is not .modal-centered). With a >1 control
       point the sheet slides up PAST its resting position and drops back, which
       is the same bump under a different name. Fixed together so the two open
       animations cannot disagree. */
    body.light-mode .modal-sheet{background:var(--card-bg);border-color:var(--border);backdrop-filter:none;-webkit-backdrop-filter:none}
    #dm-modal .modal-sheet{max-width:680px;max-height:92vh}
    #dm-modal .modal-title{font-size:1.05rem}
    #dm-modal .dm-send-btn{width:46px;height:46px;font-size:1.1rem}
    #dm-modal .dm-info-bar{font-size:.8rem;padding:12px 18px}
/* CENTRED VERTICALLY, BUT SCROLLED — 2026-08-11. Pure align-items:center makes
       the sheet's position depend on its HEIGHT, so anything that renders after the
       sheet opens (the face grid, the colour grid, the subscription card) grows it
       and the whole panel JUMPS as it re-centres. Shane reported it twice as a
       "bump" on opening My mask.
       overflow-y:auto + `margin:auto 0` on the sheet centres it while it is short
       and simply scrolls once it is tall, so growth extends the sheet DOWNWARD
       instead of moving it. scrollbar-gutter keeps the overlay's own track
       reserved so nothing shifts sideways either. */
    .modal-overlay.modal-centered{align-items:flex-start;overflow-y:auto;scrollbar-gutter:stable}
    /* A one-row list must not sit in a full-height sheet. height:auto lets the
       sheet shrink to its content; the cap only kicks in once the list is long.
       vh first, then dvh for browsers that support it - on mobile, vh is the
       viewport with the address bar HIDDEN, so a vh-capped sheet can extend
       under the browser chrome and lose its bottom rows. dvh follows the
       visible area instead. Older browsers ignore the second line and keep vh. */
    #reactors-modal .modal-sheet{height:auto;max-height:80vh;max-height:80dvh}
    /* `margin:auto 20px` — the AUTO IS THE CENTRING. This rule sits later than the
       .modal-centered block above and a flat `margin:0 20px` here would silently
       wipe it, leaving the sheet pinned to the top of the overlay. The vertical
       auto is what lets a short sheet sit centred while a tall one scrolls. */
    /* THE BUMP WAS THIS EASING — found 2026-08-11 after two wrong guesses.
       cubic-bezier(.34,1.56,.64,1) has a control point ABOVE 1, which is a spring:
       the sheet scales PAST its final size and snaps back. On a tall sheet that
       overshoot is a few pixels at the top and bottom edges, which reads exactly
       as Shane described — a bump at the bottom of My mask a moment after it
       opens. It is the animation, not the layout, which is why reserving the
       scrollbar and fixing the centring both failed to stop it.
       Now ease-out: it decelerates into place and never passes it.
       DO NOT put a >1 control point back on a sheet this size. Overshoot is fine
       on a small popover; on a full panel it looks like a rendering fault. */
    .modal-overlay.modal-centered .modal-sheet{border-radius:20px;margin:auto 20px;animation:modal-pop .22s cubic-bezier(.22,.61,.36,1)}
    .modal-overlay.modal-fullscreen{align-items:stretch;justify-content:stretch}
    .modal-overlay.modal-fullscreen .modal-sheet{max-width:100%;width:100%;height:100vh;max-height:100vh;border-radius:0;margin:0;animation:none}
    /* .96, not .92. A smaller starting scale needs a bigger journey and makes any
       imprecision at the end more visible; this is a panel appearing, not a
       button popping. */
    @keyframes modal-pop{from{opacity:0;transform:scale(.96)}to{opacity:1;transform:scale(1)}}
    .modal-header{padding:14px 16px 12px;border-bottom:1px solid var(--border);display:flex;align-items:center;justify-content:space-between;flex-shrink:0}
    .modal-title{font-size:.9rem;font-weight:600;color:var(--text)}
    /* Title + one line of subtitle, 2026-08-11. Scoped to #profile-modal so no
       other sheet's header changes shape. .modal-header stays space-between -
       the stack is one flex child, the close button is the other. */
    .modal-title-stack{display:flex;flex-direction:column;gap:1px;min-width:0}
    .modal-subtitle{font-size:.7rem;color:var(--text-muted);line-height:1.3}
    /* THIS SHEET ONLY, 2026-08-11. The shared .modal-title is .9rem/600 with a
       hairline under the header - right for a utility dialog, but My mask opens
       on WHO YOU ARE, so the title carries weight and the rule under it goes.
       The only hairline left on the sheet is the one under the tabs, which
       means the divider now marks the one real division on the screen. */
    /* STICKY, 2026-08-11. The Colour tab made this sheet tall enough to
       scroll, and the title scrolled up under the sheet's own rounded top edge
       and got sliced through the middle. The header carries the close button,
       so it has to stay reachable at any scroll position. */
    #profile-modal .modal-header{border-bottom:0;padding:20px 18px 4px;align-items:flex-start;position:sticky;top:0;z-index:3;background:linear-gradient(180deg,rgba(var(--panel-hi-rgb),0.99) 0%,rgba(var(--panel-hi-rgb),0.97) 78%,rgba(var(--panel-hi-rgb),0) 100%);padding-bottom:12px}
    /* MATCHES `.page-header-title` — 2026-08-11, Shane: "i want it like other
       style and font. and BOLD". Every other page title in this app is italic
       serif; this one was plain sans, so My mask was the only screen whose
       heading did not look like the app. Same family, same italic, same --gold
       ink, just larger and at weight 700 because he asked for bold and because
       a sheet title outranks a sub-page title.
       NOTE --font-serif ships ITALIC ONLY in fonts/fonts.css, so the italic is
       not decoration — an upright request would be served the italic file
       anyway. Do not "fix" it to normal. */
    /* GEORGIA, UPRIGHT, NOT --font-serif — Shane's pick 2026-08-11 ("F but not
       italic"). Deliberately NOT var(--font-serif): that token is Playfair and
       fonts.css ships it ITALIC ONLY, so an upright request there silently
       serves the italic file. Georgia is a system face with a real upright
       bold, so this needs no webfont and cannot flash.
       IT ALSO MATCHES THE ENTRY PAGE, which reverted to Georgia on 2026-08-08 —
       app.html is still Playfair everywhere else, so this heading is currently
       the ONE place the two files agree. If Playfair is ever dropped app-wide,
       start here.
       ANDROID HAS NO GEORGIA and falls back to Noto Serif — close enough in
       colour and weight, but it is not identical. Gelasio is the metric-
       compatible fix if that ever matters. */
    #profile-modal .modal-title{font-family:Georgia,'Times New Roman',serif;font-style:normal;font-weight:700;font-size:1.4rem;letter-spacing:-.01em;line-height:1.15;color:var(--text)}
    #profile-modal .modal-subtitle{font-size:.8rem;margin-top:2px}
    /* CAMERA BADGE, 2026-08-11. Shane asked for it three times, so it is built.
       ON THE RECORD: it is the third route to the same face picker (the disc
       itself and the Face tab are the other two) and was deleted on 2026-08-05
       for exactly that. It sits BOTTOM-RIGHT ON the disc rather than floating
       beside it, so it reads as part of the avatar and not as a stray control.
       pointer-events:none - the wrapper's own click handler catches the tap, so
       the badge never needs its own listener and cannot go out of step with it.
       applyStaffProfileLock must hide this: a Vent Buddy's mask is issued. */
    .profile-avatar-cam{position:absolute;right:2px;bottom:2px;width:30px;height:30px;border-radius:50%;display:flex;align-items:center;justify-content:center;background:rgba(var(--panel-lo-rgb),.96);border:1.5px solid rgba(236,243,232,.30);color:var(--text);pointer-events:none;box-shadow:0 2px 6px rgba(0,0,0,.35)}
    .profile-avatar-cam svg{width:15px;height:15px}
    .mask-plate.staff-view .profile-avatar-cam{display:none}
    .modal-close{width:28px;height:28px;display:flex;align-items:center;justify-content:center;color:var(--text-muted);font-size:1.2rem;border-radius:50%;transition:background .15s}
    .modal-close:active{background:rgba(255,255,255,.08)}
    .dm-info-bar{padding:10px 16px;background:rgba(139,0,0,.1);border-bottom:1px solid var(--border);font-size:.72rem;color:var(--text-muted);text-align:center;flex-shrink:0}

    /* ====================================================
       LOGIN MODAL — gold seam / mask-panel treatment
       (mirrors the standalone login-page design; uses the
       app's own tokens + background, no new bg introduced)
    ==================================================== */
    /* Synced with .gate-card: flat dark bg + thin gold border + var(--radius), matching
       the age gate and the onboarding cards instead of the old purple glass look. */
    #account-login-modal .modal-sheet{background:linear-gradient(180deg,rgba(var(--panel-hi-rgb),0.97) 0%,rgba(var(--panel-lo-rgb),0.96) 100%);backdrop-filter:blur(16px);-webkit-backdrop-filter:blur(16px);position:relative;overflow:hidden;border-radius:20px;box-shadow:0 20px 50px rgba(0,0,0,0.5),inset 0 1px 0 rgba(255,255,255,0.06)}
    /* My Profile: same big-container treatment as the other full modals (login, call, onboarding
       cards) instead of the flat default panel, so it doesn't read as a visually different screen. */
    /* overflow:hidden here used to clip the whole sheet — including anything
       below the fold — since it overrode the base .modal-sheet's overflow-y:auto.
       That's why the Gold card (third in the list) was unreachable no matter
       how far you tried to scroll. overflow-x:hidden still clips the rounded
       corners horizontally; overflow-y:auto restores real scrolling. */
    #profile-modal .modal-sheet{background:linear-gradient(180deg,rgba(var(--panel-hi-rgb),0.97) 0%,rgba(var(--panel-lo-rgb),0.96) 100%);backdrop-filter:blur(16px);-webkit-backdrop-filter:blur(16px);position:relative;overflow-x:hidden;overflow-y:auto;border-radius:20px;box-shadow:0 20px 50px rgba(0,0,0,0.5),inset 0 1px 0 rgba(255,255,255,0.06)}
    /* 400px, not the shared 480px. My mask holds one avatar, one name and one
       bio — a sheet about a single identity should not get wider just because
       the monitor is. The pills still span it, which is right: they are actions,
       and one full-width row under one hugging plate is what stops the two
       reading as loose boxes. Only this modal is narrowed; every other sheet
       keeps 480px. */
    /* min-height IS A BACKSTOP, not the fix. The real cause of the sheet shrinking
       after it opened is in updateProfileTierRow (see app.html). This stops any
       OTHER late render — a bio line, a badge image finishing, a font swapping —
       from visibly collapsing the sheet by a few pixels.
       Deliberately modest: tall enough to cover the normal contents, short
       enough that a staff sheet (which loses the whole tab row) is not a tall
       empty box. */
    #profile-modal .modal-sheet{max-width:400px;min-height:min(420px,72vh)}
    /* Nested "small containers" inside My Profile — same family gradient as the outer sheet,
       just lighter, so they read as boxes that belong to the same screen (matches how cvb-pack
       tiles nest inside the call modal). */
    #profile-modal .settings-tab-content{background:linear-gradient(180deg,rgba(var(--panel-hi-rgb),0.28) 0%,rgba(var(--panel-lo-rgb),0.22) 100%);border:1px solid rgba(255,255,255,0.10);border-radius:14px;padding:12px;margin-top:12px}
    /* Was falling back to the generic flat .modal-sheet{background:var(--card-bg)}
       — every other full-screen modal (onboarding, My Profile, login/recover)
       uses this same purple gradient as its "full background"; the picker had
       just never been given it, so it looked visually disconnected from the
       rest of the app despite being reached from inside those same screens. */
    #mask-picker-modal .modal-sheet{background:linear-gradient(180deg,rgba(var(--panel-hi-rgb),0.97) 0%,rgba(var(--panel-lo-rgb),0.96) 100%);backdrop-filter:blur(16px);-webkit-backdrop-filter:blur(16px);position:relative;overflow-x:hidden;overflow-y:auto;border-radius:20px;box-shadow:0 20px 50px rgba(0,0,0,0.5),inset 0 1px 0 rgba(255,255,255,0.06)}
    #account-login-modal .login-seam{position:absolute;top:0;left:8%;right:8%;height:1px;background:linear-gradient(90deg,transparent,var(--gold) 50%,transparent);box-shadow:0 0 14px 1px rgba(var(--neon-rgb),.45);animation:loginSeamPulse 4.5s ease-in-out infinite}
    @keyframes loginSeamPulse{0%,100%{opacity:.45}50%{opacity:1}}
    #account-login-modal .login-seam-bleed{position:absolute;top:0;left:0;right:0;height:70px;background:radial-gradient(ellipse 60% 100% at 50% 0%,rgba(var(--neon-rgb),.08),transparent 75%);pointer-events:none}
    #account-login-modal .modal-header{border-bottom:none;padding:24px 22px 6px}
    #account-login-modal .modal-title{font-family:var(--font-serif);font-style:italic;font-weight:700;font-size:1.4rem;color:var(--text)}
    #account-login-modal .login-divider{height:1px;background:var(--border);margin:14px 22px 0}
    #account-login-modal .login-field{position:relative;margin-bottom:16px}
    #account-login-modal .login-field label{position:absolute;left:14px;top:-8px;font-size:.62rem;letter-spacing:.14em;text-transform:uppercase;color:var(--gold-dim);background:rgba(var(--panel-hi-rgb),0.95);padding:0 6px;z-index:1;border-radius:4px}
    /* GLASS FIELDS, 2026-08-09. Both inputs were background:rgba(14,10,30,0.5) -
       a leftover from the PURPLE palette that the green repaint never caught, so
       they rendered as near-black slabs inside a green glass sheet. They now use
       the SAME glass as .onb-card so a field reads as the same material as every
       other container in the flow. If the panel colour ever changes, change this
       with it - do not go back to a hardcoded dark fill. */
    #account-login-modal .login-field .emoji-input{text-align:left;padding:16px 14px 15px;font-size:.95rem;width:100%;background:linear-gradient(160deg,rgba(255,255,255,.16) 0%,rgba(255,255,255,.07) 100%);border-color:rgba(255,255,255,0.16)}
    #account-login-modal .login-field.code .emoji-input{font-family:'Space Mono',monospace;letter-spacing:.1em;font-size:.9rem}
    /* PLACEHOLDER — added 2026-08-13. These two inputs had NO ::placeholder rule
       at all, so the browser default (roughly the text colour at ~40% opacity)
       painted the sample username and the code dots almost invisible against the
       lifted white-glass field. opacity:1 is required — Firefox applies its own
       opacity to ::placeholder on top of any colour. */
    #account-login-modal .login-field .emoji-input::placeholder,
    #account-recover-modal .login-field .emoji-input::placeholder{color:var(--text-muted);opacity:1}
    #account-login-modal .login-hint{display:block;margin-top:6px;font-size:.7rem;color:var(--text-muted);padding-left:2px}
    /* SAME GOLD AS NEXT, 2026-08-09. This was linear-gradient(180deg,#f0d980,
       var(--gold)) - and :root sets --gold to #DCE7D6, a pale green-white, so the
       button rendered as a cream slab with no gold in it at all. It now carries
       the EXACT ramp from .onb-step-btn.primary (140deg, saturated end to end)
       and the same #1E3A18 deep-green label, so the button that finishes login is
       the same button as NEXT. The gold border came off: an outline on a
       saturated gold fill only muddies the edge. If NEXT's ramp changes, change
       this with it. */
    #account-login-modal .login-cta{width:100%;margin-top:2px;padding:15px 16px;border:none;border-radius:var(--radius);background:linear-gradient(140deg,#FFE08A 0%,var(--neon) 42%,var(--neon-dim) 74%,#F5CF5F 100%);color:#1E3A18;font-family:var(--font-ui);font-weight:600;font-size:.9rem;letter-spacing:.03em;box-shadow:0 8px 26px -12px rgba(var(--neon-rgb),.55),inset 0 1px 0 rgba(255,255,255,.5);transition:transform .15s,filter .15s}
    #account-login-modal .login-cta:hover{filter:brightness(1.07);transform:translateY(-1px)}

    /* Recover-account modal shares the same gold-seam treatment */
    /* Recover-account modal — copper seam variant (distinct from login's gold seam) */
    /* Synced with .gate-card: flat dark bg + thin gold border + var(--radius), matching
       the age gate and the onboarding cards instead of the old purple glass look. */
    #account-recover-modal .modal-sheet{background:linear-gradient(180deg,rgba(var(--panel-hi-rgb),0.97) 0%,rgba(var(--panel-lo-rgb),0.96) 100%);backdrop-filter:blur(16px);-webkit-backdrop-filter:blur(16px);position:relative;overflow:hidden;border-radius:20px;box-shadow:0 20px 50px rgba(0,0,0,0.5),inset 0 1px 0 rgba(255,255,255,0.06)}
    #account-recover-modal .login-seam{position:absolute;top:0;left:8%;right:8%;height:1px;background:linear-gradient(90deg,transparent,var(--gold) 50%,transparent);box-shadow:0 0 14px 1px rgba(var(--neon-rgb),.45);animation:loginSeamPulse 4.5s ease-in-out infinite}
    #account-recover-modal .login-seam-bleed{position:absolute;top:0;left:0;right:0;height:70px;background:radial-gradient(ellipse 60% 100% at 50% 0%,rgba(var(--neon-rgb),.08),transparent 75%);pointer-events:none}
    #account-recover-modal .modal-header{border-bottom:none;padding:26px 22px 18px;align-items:flex-start}
    #account-recover-modal .recover-eyebrow{display:block;font-family:var(--font-ui);font-size:.62rem;letter-spacing:.22em;text-transform:uppercase;color:var(--gold-dim);margin-bottom:6px}
    #account-recover-modal .modal-title{font-family:var(--font-serif);font-style:italic;font-weight:700;font-size:1.4rem;color:var(--text);display:flex;align-items:center;gap:10px}
    #account-recover-modal .modal-title svg{flex-shrink:0;color:var(--gold)}
    #account-recover-modal .login-divider{height:1px;background:var(--border);margin:0 22px}
    #account-recover-modal .recover-lede{color:var(--text-muted);font-size:.9rem;line-height:1.55;margin:0 0 20px}
    #account-recover-modal .login-field{position:relative;margin-bottom:8px}
    #account-recover-modal .login-field label{position:absolute;left:14px;top:-8px;font-size:.62rem;letter-spacing:.14em;text-transform:uppercase;color:var(--gold-dim);background:rgba(var(--panel-hi-rgb),0.95);padding:0 6px;z-index:1;border-radius:4px}
    #account-recover-modal .login-field .emoji-input{text-align:left;padding:16px 14px 15px;font-size:.95rem;width:100%;background:linear-gradient(160deg,rgba(255,255,255,.16) 0%,rgba(255,255,255,.07) 100%);border-color:rgba(255,255,255,0.16)}
    #account-recover-modal .login-cta{width:100%;margin-top:16px;padding:15px 16px;border:none;border-radius:var(--radius);background:linear-gradient(140deg,#FFE08A 0%,var(--neon) 42%,var(--neon-dim) 74%,#F5CF5F 100%);color:#1E3A18;font-family:var(--font-ui);font-weight:600;font-size:.9rem;letter-spacing:.03em;box-shadow:0 8px 26px -12px rgba(var(--neon-rgb),.55),inset 0 1px 0 rgba(255,255,255,.5);transition:transform .15s,filter .15s}
    #account-recover-modal .login-cta:hover{filter:brightness(1.07);transform:translateY(-1px)}
    #account-recover-modal .login-cta:disabled{opacity:.45;cursor:not-allowed;filter:none;transform:none}
    #account-recover-modal .recover-foot{padding:16px 22px 24px;text-align:center}
    #account-recover-modal .recover-footnote{font-size:.72rem;color:var(--text-muted);letter-spacing:.02em}
    .dm-messages{flex:1;overflow-y:auto;padding:14px 16px;display:flex;flex-direction:column;gap:10px;background:rgba(255,255,255,.015)}
    .dm-msg{max-width:80%;padding:11px 15px;border-radius:14px;font-size:1.05rem;line-height:1.5}
    .dm-msg.mine{background:var(--crimson);color:#fff;align-self:flex-end;border-bottom-right-radius:4px}
    .dm-msg.theirs{background:var(--surface-2);color:var(--text);align-self:flex-start;border-bottom-left-radius:4px}
    .dm-compose{padding:10px 16px 24px;border-top:1px solid var(--border);display:flex;gap:8px;flex-shrink:0}
    .dm-input{flex:1;background:var(--surface);border:1px solid var(--border);border-radius:22px;color:var(--text);font-size:1rem;padding:11px 18px;outline:none}
    .dm-input::placeholder{color:var(--text-muted)}
    .dm-send-btn{width:40px;height:40px;background:var(--crimson);border-radius:50%;display:flex;align-items:center;justify-content:center;color:var(--gold);font-size:1rem;flex-shrink:0;transition:transform .15s}
    .dm-send-btn:active{transform:scale(.9)}

    /* ====================================================
       EXPLORE / GROUPS
    ==================================================== */
    .explore-section{padding-top:14px}
    .explore-heading{font-size:.65rem;text-transform:uppercase;letter-spacing:.14em;color:var(--text-muted);margin-bottom:10px;display:flex;align-items:center;gap:6px}
    .explore-heading::after{content:'';flex:1;height:1px;background:var(--border)}
    .group-card{background:var(--card-bg);border:1px solid var(--border);border-radius:var(--radius);padding:14px;margin-bottom:10px;display:flex;align-items:center;gap:12px;transition:border-color .2s}
    .group-card:active{border-color:var(--crimson-lt)}
    .group-icon{width:42px;height:42px;border-radius:10px;display:flex;align-items:center;justify-content:center;font-size:1.05rem;font-weight:600;font-family:var(--font-serif);color:var(--gold);flex-shrink:0;background:rgba(var(--neon-rgb),.10);border:1px solid var(--border)}
    .group-info{flex:1;min-width:0}
    .group-name{font-size:.9rem;font-weight:600;color:var(--text);margin-bottom:2px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
    .group-desc{font-size:.75rem;color:var(--text-muted)}
    .group-meta{display:flex;flex-direction:column;align-items:flex-end;gap:4px}
    .group-members{font-size:.7rem;color:var(--text-muted)}
    .join-btn{font-size:.72rem;font-weight:600;color:var(--gold);background:rgba(var(--neon-rgb),.1);border:1px solid rgba(var(--neon-rgb),.25);border-radius:12px;padding:4px 10px;transition:background .18s}
    .join-btn:active{background:rgba(var(--neon-rgb),.2)}
    .create-group-btn{width:100%;padding:13px;border-radius:var(--radius);border:1px dashed rgba(139,0,0,.5);color:var(--crimson-lt);font-size:.85rem;font-weight:500;text-align:center;margin-top:4px;transition:background .18s}
    .create-group-btn:active{background:rgba(139,0,0,.08)}

    /* ====================================================
       CREATE GROUP
    ==================================================== */
    .cg-form{padding:16px;display:flex;flex-direction:column;gap:12px}
    .cg-label{font-size:.78rem;color:var(--text-muted);margin-bottom:4px;display:block}
    .cg-input{width:100%;background:var(--surface);border:1px solid var(--border);border-radius:var(--radius-sm);color:var(--text);font-size:.9rem;padding:10px 12px;outline:none}
    .cg-input:focus{border-color:var(--crimson-lt)}
    .cg-category-grid{display:grid;grid-template-columns:1fr 1fr;gap:8px}
    .cg-cat-btn{padding:10px;border-radius:var(--radius-sm);border:1px solid var(--border);background:transparent;color:var(--text-muted);font-size:.8rem;text-align:center;transition:all .18s}
    .cg-cat-btn.selected{border-color:var(--crimson-lt);background:rgba(139,0,0,.15);color:var(--text)}
    .cg-submit{padding:13px;background:var(--crimson);border-radius:var(--radius-sm);color:var(--gold);font-weight:600;font-size:.9rem;border:1px solid var(--border-gold)}

    /* ====================================================
       QUOTE MODAL
    ==================================================== */
    .quote-compose-area{padding:16px;display:flex;flex-direction:column;gap:10px}
    .quote-compose-input{width:100%;background:var(--surface);border:1px solid var(--border);border-radius:10px;color:var(--text);font-family:var(--font-serif);font-style:italic;font-size:.95rem;padding:12px;outline:none;resize:none;min-height:80px}
    .quote-compose-input::placeholder{color:var(--text-muted)}
    .quote-submit-btn{padding:12px;background:linear-gradient(135deg,#9B59B6,#6C3483);border-radius:10px;color:#fff;font-weight:600;font-size:.9rem}

    /* ====================================================
       GC MODAL
    ==================================================== */
    .gc-modal .modal-header{flex-direction:column;align-items:flex-start;gap:2px}
    .gc-title{font-size:.9rem;font-weight:600}
    .gc-category{font-size:.7rem;color:var(--text-muted)}
    .gc-members-bar{padding:8px 16px;font-size:.72rem;color:var(--text-muted);border-bottom:1px solid var(--border);background:rgba(0,0,0,.2)}

    /* ====================================================
       BOOKMARKS
    ==================================================== */
    .bm-empty{text-align:center;padding:48px 24px}
    .bm-empty .ei{font-size:2.5rem;margin-bottom:12px}
    .bm-empty .et{font-family:var(--font-serif);font-style:italic;font-size:1.1rem;color:var(--text-soft);margin-bottom:6px}
    .bm-empty .es{font-size:.82rem;color:var(--text-muted)}

    /* ====================================================
       MISC
    ==================================================== */
    .feed-loader{display:flex;flex-direction:column;align-items:center;padding:32px;gap:12px;color:var(--text-muted);font-size:.82rem}
    /* ARC IS THE ACCENT, NOT --crimson — fixed 2026-08-11. Shane: "im seeing
       like a brown". It was border-top-color:var(--crimson), the deep red left
       over from the pre-green palette; against a green ground that mixes to a
       muddy brown, and it followed no theme at all.
       BEST PRACTICE and the reason for the choice: a spinner has two parts and
       they should be the two things already in the design system - the TRACK is
       the app's hairline (--border) and the MOVING ARC is the accent (--neon).
       That way it reads as "the app is working" in the app's own colour, and it
       follows every picked theme for free. Never give a loader a colour of its
       own; a spinner is the one element a user stares at, and a stray hue there
       reads as an error state. */
    .loader-ring{width:28px;height:28px;border:2px solid var(--border);border-top-color:var(--neon);border-radius:50%;animation:spin .8s linear infinite}
    .empty-state{text-align:center;padding:48px 24px}
    .empty-icon{font-size:2.5rem;margin-bottom:12px}
    .empty-title{font-family:var(--font-serif);font-style:italic;font-size:1.1rem;color:var(--text-soft);margin-bottom:6px}
    .empty-sub{font-size:.82rem;color:var(--text-muted)}
    .ad-slot{border:1px dashed rgba(255,255,255,.08);border-radius:var(--radius);padding:20px;text-align:center;color:var(--text-muted);font-size:.72rem;margin-bottom:12px;background:rgba(255,255,255,.02)}
    .ad-slot-label{font-size:.6rem;margin-bottom:8px;text-transform:uppercase;letter-spacing:.1em}
    .toast{position:fixed;bottom:80px;left:50%;transform:translateX(-50%) translateY(20px);background:var(--surface-2);border:1px solid var(--border);border-radius:20px;padding:10px 20px;font-size:.82rem;color:var(--text);z-index:1002;opacity:0;transition:all .3s cubic-bezier(.34,1.56,.64,1);pointer-events:none;white-space:nowrap}
    .toast.show{opacity:1;transform:translateX(-50%) translateY(0)}

    /* ====================================================
       BOTTOM NAV
    ==================================================== */
    .bottom-nav{position:fixed;bottom:0;left:0;right:0;z-index:100;background:var(--chrome-bg);backdrop-filter:blur(16px);border-top:1px solid var(--border)}
    /* Small gap + horizontal padding per item — previously zero gap and zero
       side padding meant all 6 items sat flush against each other edge to
       edge, reading as one crowded row instead of 6 distinct tap targets. */
    .bottom-nav-inner{max-width:480px;margin:0 auto;display:flex;gap:4px;padding:0 14px env(safe-area-inset-bottom,0)}
    /* LABELS BACK, 2026-08-06, Shane's call. Icon-only ran from 08-05 to 08-06.
       Nothing had to be re-added: the label span was only ever hidden, so
       deleting the display:none rule restored it in all 20 languages at once,
       and labelNavIcons() keeps writing title + aria-label as before.
       Why: three of the five icons do not stand alone. Wall and Diary are both
       paper-with-writing two slots apart, Rooms is a bar stack that reads as
       "menu", and headphones reads as listening rather than talking. Apple and
       Material both label tab bars by default; the apps that skip labels
       (Instagram, X, Discord) use the universal house/magnifier/person set and
       get opened ten times a day. MASKARA is new and its words are its own.
       TOP PADDING STAYS AT 16px - the live pill lives in it (see #bn-call-btn
       ::after below, top:4px). Only the BOTTOM padding was trimmed to make
       room for the label, so the bar grows ~5px, not ~20px.
       INACTIVE COLOUR 2026-08-06: #8A8A9A -> #B9B4CC. The old gray was neutral
       with no purple in it, so it read dirty against the purple bar. Pure
       white was mocked and rejected - at full white an inactive tab is as
       bright as the active one, leaving gold hue alone to say "you are here".
       #B9B4CC is brighter and cleaner but still a clear step below gold. */
    .nav-item{flex:1;display:flex;flex-direction:column;align-items:center;justify-content:center;padding:16px 4px 10px;color:#B9B4CC;font-size:.62rem;letter-spacing:.04em;gap:3px;transition:color .2s,transform .15s;position:relative}
    .nav-item.active{color:#E5C100}
    .nav-item:active{transform:scale(.92);color:#E5C100}
    /* Post is an action, not a destination - it flashes on tap and never keeps
       the highlight. Same look as .active, different meaning. */
    .nav-item.nav-flash{color:#E5C100}
    @media (hover:hover){.nav-item:hover{color:#E5C100}}
    .nav-item::before{content:'';position:absolute;top:7px;width:3px;height:3px;border-radius:50%;background:#E5C100;opacity:0;transition:opacity .2s}
    .nav-item.active::before{opacity:1}
    /* Back to 1.2rem/22px now the label carries half the identifying work. The
       26px svg override that sat here is deleted rather than edited - the base
       .nav-icon .icon-svg rule further up already says 22px, so two rules were
       fighting and the later one only happened to win. */
    .nav-icon{font-size:1.2rem;line-height:1;position:relative}
    .nav-badge{position:absolute;top:-2px;right:-7px;min-width:14px;height:14px;padding:0 3px;border-radius:8px;background:var(--crimson-lt);border:1.5px solid var(--midnight);font-size:.55rem;line-height:11px;text-align:center;color:#fff;font-weight:600;display:none}
    .nav-badge.show{display:block}
    .nav-post-btn{width:46px;height:46px;border-radius:50%;background:var(--crimson);border:2px solid var(--border-gold);display:flex;align-items:center;justify-content:center;font-size:1.4rem;color:var(--gold);margin-top:-16px;transition:transform .15s}
    .nav-item:active .nav-post-btn{transform:scale(.9)}
    #bn-call-btn{position:relative;overflow:visible}
    /* Green marks it as the live thing; the SIZE now comes from .nav-icon like
       every other item, so Call lines up with Wall, Post and People instead of
       being a shrunken circle among them. */
    #bn-call-btn .nav-icon{color:#39ff88;position:relative}
    #bn-call-btn span:not(.nav-icon){color:#39ff88}
    /* LIVE PILL MOVED ON TOP, 2026-08-05. It used to hang off the RIGHT of the
       headphones (left:100%), which pushed the Call icon off-centre in its
       slot - obvious now that the labels are gone and every other icon sits
       dead centre. Above and centred, the icon lines up with its neighbours
       and the pill reads as a banner over the tab.
       THE HEADPHONES ITSELF IS UNTOUCHED - this is the ::after only, so the
       icon keeps the size and position it shares with Wall, Post and People.
       Smaller too (0.72 -> 0.58): sitting above the icon it only has the
       item's 16px top padding to live in. */
    /* ANCHORED TO THE BUTTON, NOT THE ICON, 2026-08-05. Hanging it off
       .nav-icon with bottom:calc(100% + 2px) put it OUTSIDE the nav bar
       entirely - .nav-icon is an inline span, so the percentage resolved
       against its small line box and the pill floated up over the feed,
       landing on top of "No stories yet".
       #bn-call-btn is the whole 16px-padded button, so top:3px parks the pill
       INSIDE that padding, just above the icon and safely inside the bar.
       THE HEADPHONES ICON IS STILL UNTOUCHED - this only ever moves the
       ::after. */
    /* TOP RIGHT of the icon, Shane 2026-08-05 - not centred. left:50% is the
       button's midline, so starting 4px left of it and growing rightwards
       with transform-origin:left top parks the pill over the icon's
       top-right corner, the way a notification badge sits. */
    #bn-call-btn::after{content:'live';position:absolute;top:4px;left:50%;transform:translateX(10px) scale(0.58);transform-origin:left top;background:#39ff88;color:#04331b;font-size:11px;font-weight:700;padding:2px 8px;border-radius:999px;white-space:nowrap;animation:liveBlink 1.2s ease-in-out infinite;line-height:1.3;z-index:2;pointer-events:none}
    /* The active-tab dot sits centred at the top of every item, which is now
       exactly where this pill is. A gold dot behind a green pill reads as a
       rendering fault, so Call alone does without it. */
    #bn-call-btn::before{display:none}
    #share-canvas{display:none}

    /* ====================================================
       AGE / AGREEMENT GATE
    ==================================================== */
    /* align-items:flex-start, and the card uses margin:0 auto rather than
       margin:auto. Vertically centred, a height change moved the TOP edge up and
       the BOTTOM edge down at the same time - so growing the card bumped
       everything in both directions. Pinned to the top, only the bottom edge
       moves, and the animated height change reads as the card growing rather
       than the page lurching. padding-top gives it the same resting position it
       had when centred on a normal screen. */
    .gate-overlay{position:fixed;inset:0;z-index:1000;background:radial-gradient(ellipse 120% 90% at 50% 40%,transparent 55%,rgba(0,0,0,.35) 100%);display:flex;justify-content:center;align-items:flex-start;padding:24px;padding-top:max(24px,6vh);overflow-y:auto;scrollbar-width:none;-ms-overflow-style:none}
    .gate-overlay::-webkit-scrollbar{display:none}
    /* Removed: this pseudo-element duplicated an old body-level glow layer that
       predates the shared .bg spotlight, including a round warm blob parked at
       90% 85% (bottom-right corner) that only ever showed up on this gate screen.
       Disabling it lets the single .bg layer (same one used on onboarding and the
       dashboard) be the only background source here too, so all three stay in sync. */
    .gate-overlay::before{display:none}
    /* 26.25rem, not 420px. Same size at default settings (16px root), but a
       reader who has raised their browser's base font size gets a proportionally
       wider card instead of the same 420px box with bigger text crammed into it.
       This is unrelated to browser ZOOM, which scales rem too - see the note in
       the min-width:769px block below. */
    .gate-card{position:relative;max-width:26.25rem;width:100%;margin:0 auto;background:linear-gradient(180deg,rgba(var(--panel-hi-rgb),0.62) 0%,rgba(var(--panel-lo-rgb),0.5) 100%);backdrop-filter:blur(16px);-webkit-backdrop-filter:blur(16px);border:1px solid rgba(255,255,255,0.12);border-radius:20px;box-shadow:0 20px 50px rgba(0,0,0,0.5),inset 0 1px 0 rgba(255,255,255,0.06);padding:20px 20px}
    /* THE BUMP: the card renders with the English markup, then applyGateLang
       swaps in a translation of a different length and the card jumps. Hidden
       until the language has been applied, then faded in at its final size, so
       there is nothing to resize in view. .lang-ready is set by applyGateLang.
       Falls back to visible if the script never runs - a hidden age gate would
       be far worse than a bump. */
    /* THE BLINK: .gate-card used to ALSO carry `animation:card-in .4s ease`.
       card-in animates opacity 0 -> 1, but has no animation-fill-mode, so the
       instant it finished the card fell back to the opacity:0 declared right
       here. setupGate() (which adds .lang-ready) runs after an awaited network
       call in DOMContentLoaded, so on any slow response the card faded IN over
       .4s, snapped fully INVISIBLE at .4s, then faded in a second time when the
       network call returned. That double reveal is the blink. The animation is
       gone; .lang-ready is now the ONE and ONLY thing that reveals this card.
       Deliberately a fade with NO movement: gate-fit-script owns this element's
       transform for scale-to-fit, so an entrance that also used transform would
       be wiped the moment the fit ran. */
    .gate-card{opacity:0;transition:opacity .28s ease}
    .gate-card.lang-ready{opacity:1}
    /* DESKTOP ONLY - ONE PAGE, CENTRED. Shane 2026-07-30, with a screenshot:
       on reload the card started about a third of the way down and ran off the
       bottom of the window. Two causes, both here in the base rule above:
       align-items:flex-start pinned it to the top, and padding-top:max(24px,6vh)
       pushed it further down still - then Tagalog, which is SIX lines where
       English is three, made it taller than what was left. overflow-y:auto meant
       the overflow just became a scroll instead of an error, so nothing looked
       broken while the Enter button sat under the fold.
       Desktop now centres it and CANNOT scroll: overflow:hidden makes a card
       that does not fit a real failure, which is what forces gate-fit-script to
       do its job properly instead of leaning on a scrollbar. Symmetric 24px
       padding replaces the 6vh so the measured space matches what is really
       there. Deliberately scoped min-width:769px - mobile keeps flex-start,
       the 6vh top padding, scrolling, and no scaling, exactly as before. */
    @media (min-width:769px){
      .gate-overlay{align-items:center;padding:24px;overflow:hidden}
      /* ESCAPE HATCH, set by gate-fit-script only when the card cannot fit even
         at its minimum readable scale. Scrolling comes back and the card returns
         to top alignment so the reader starts at the beginning rather than the
         middle. Normal loads never get this class. */
      .gate-overlay.gate-scrolls{align-items:flex-start;overflow-y:auto}
    }
    /* LARGE VIEWPORTS. Shane 2026-07-30, screenshot at 50% browser zoom: the
       overlay background fills the window but the fixed card does not, so on a
       lot of screen the card reads as marooned rather than composed. Zooming out
       makes the window behave like a much bigger monitor, so this fires there
       too, which is the point.
       WIDTH ONLY - deliberately no type scale. Sizing text off vh was built and
       reverted once already, and it is an accessibility failure besides: vh
       shrinks at exactly the rate browser zoom grows, so a low-vision reader who
       zooms to 200% would get no size increase at all. Widening is safe because
       the text simply reflows shorter, and the desktop overflow:hidden means a
       shorter card is strictly better.
       Gated on height as well as width so this only applies when there is real
       room, never on a short letterboxed window. */
    @media (min-width:1600px) and (min-height:900px){
      .gate-card{max-width:32rem}
    }
    /* Reserving a min-height did not work: English is 3 lines and Tagalog is 6,
       so either the card still jumped or English carried a hole. The height is
       ANIMATED instead - applyGateLang measures before and after and transitions
       between the two, so the card grows or shrinks smoothly rather than
       snapping. overflow:hidden stops the text spilling mid-animation. */
    .gate-card{overflow:hidden}
    .gate-card.animating-height{transition:height .28s cubic-bezier(.4,0,.2,1)}
    .gate-tag{display:flex;align-items:center;justify-content:center}
    .gate-logo{font-family:var(--font-serif);font-style:italic;font-weight:700;font-size:2rem;letter-spacing:.06em;color:var(--neon);text-shadow:var(--glow-neon);text-align:center;margin-bottom:4px;animation:glowPulse 3.6s ease-in-out infinite}
    .gate-tag{text-align:center;font-size:.7rem;color:var(--text-muted);letter-spacing:.1em;text-transform:uppercase;margin-bottom:12px}
    .gate-section{margin-bottom:10px}
    .gate-section h4{font-size:.78rem;color:var(--gold-dim);letter-spacing:.05em;text-transform:uppercase;margin-bottom:6px}
    .gate-section p{font-size:.84rem;line-height:1.4;color:var(--text-soft);text-align:justify;text-align-last:left}
    .gate-divider{height:1px;background:var(--border);margin:10px 0}
    .gate-check-row{display:flex;align-items:flex-start;gap:10px;margin-bottom:8px;cursor:pointer}
    .gate-check-row input{margin-top:3px;width:16px;height:16px;accent-color:var(--neon);flex-shrink:0}
    .gate-check-row span{font-size:.8rem;color:var(--text-soft);line-height:1.45;text-align:justify;text-align-last:left}
    .gate-btn{width:100%;padding:14px;border-radius:var(--radius-sm);background:linear-gradient(135deg,#FFE9A8,var(--neon) 42%,var(--neon-dim) 72%,#FFDF7A);color:#1E3A18;font-size:.88rem;font-weight:600;letter-spacing:.04em;text-transform:uppercase;opacity:.4;pointer-events:none;transition:opacity .2s,box-shadow .2s}
    .gate-btn.ready{opacity:1;pointer-events:auto;box-shadow:0 8px 26px -12px rgba(var(--neon-rgb),.42)}
    .gate-btn.ready:hover{filter:brightness(1.06)}
    .gate-exit{display:block;text-align:center;margin-top:8px;font-size:.72rem;color:var(--text-muted);text-decoration:underline;cursor:pointer}
    /* PINNED TO THE VIEWPORT'S TOP RIGHT, 2026-08-09 (Shane: same as index.html).
       It used to sit centred inside the card under the tagline. z-index sits above
       the card so the open dropdown is not clipped by it. The markup MUST stay
       outside .gate-card - see the comment in app.html. */
    .gate-lang{position:fixed;top:12px;right:12px;z-index:1001;display:flex;align-items:center}
    /* COPIED VALUE-FOR-VALUE FROM index.html's .lang-toggle, 2026-08-09, so the
       chip a visitor saw on the entry page is the same object here. 7px corners,
       NOT a 20px pill - index deliberately uses a soft rectangle. Transparent
       fill with the border doing the work; hover is the only fill. */
    .gate-lang-pill{position:relative;display:inline-flex;align-items:center;background:transparent;border:1px solid rgba(236,243,232,.4);border-radius:7px;padding:2px 7px;transition:border-color .2s,background .2s}
    .gate-lang-pill:hover{border-color:rgba(236,243,232,.65);background:rgba(236,243,232,.07)}
    .gate-lang-icon{font-size:.75rem;opacity:.85}
    /* SERIF ITALIC IS EXPLICIT HERE, not inherited. index.html sets its whole body
       to the serif italic so `font-family:inherit` lands there; app.css's body is
       the UI sans, so inheriting would have quietly given a different face.
       GEORGIA IS NAMED OUTRIGHT, NOT var(--font-serif), 2026-08-09. THE TWO FILES
       DISAGREE ON WHAT THAT TOKEN MEANS: index.html holds Georgia (Playfair was
       stripped from that page so it downloads no webfont), app.css holds
       'Playfair Display'. Using the token here rendered the chip in Playfair
       italic beside an index chip in Georgia italic - same instruction, two
       different faces. Do not "tidy" this back into the token. Muted
       grey, never gold: gold means brand and action, and a utility control must
       not compete with either. */
    .gate-lang-btn{background:none;border:none;display:flex;align-items:center;gap:4px;color:var(--text-muted);font-size:.46rem;font-weight:600;font-family:Georgia,'Times New Roman',serif;font-style:italic;letter-spacing:.12em;padding:0;line-height:1.2;cursor:pointer}
    .gate-lang-btn-label{max-width:88px;overflow:hidden;white-space:nowrap;text-overflow:ellipsis}
    .gate-lang-chevron{flex-shrink:0;transition:transform .2s ease}
    .gate-lang-pill.open .gate-lang-chevron{transform:rotate(180deg)}
    /* Opens flush with the chip's RIGHT edge. The old left:50%/translateX(-50%) was
       correct while the chip was centred in the card; from the right corner it hangs
       the 170px menu off the side of the screen. */
    .gate-lang-list{position:absolute;top:calc(100% + 8px);right:0;left:auto;margin:0;padding:6px;list-style:none;min-width:170px;max-height:min(280px,calc(100vh - 100px));overflow-y:auto;background:var(--midnight);border:1px solid rgba(236,243,232,0.18);border-radius:12px;box-shadow:0 12px 30px rgba(14,30,10,.42);display:none;z-index:10}
    .gate-lang-pill.open .gate-lang-list{display:block}
    .gate-lang-option{padding:8px 12px;border-radius:8px;font-size:.78rem;color:var(--text);cursor:pointer;white-space:nowrap;font-family:Georgia,'Times New Roman',serif;font-style:italic}
    .gate-lang-option:hover,.gate-lang-option.active{background:rgba(236,243,232,.14);color:var(--text)}
    .gate-card[dir="rtl"]{direction:rtl}
    .gate-card[dir="rtl"] .gate-check-row{flex-direction:row-reverse}
    /* NICKNAME SCREEN */
    .nick-overlay{position:fixed;inset:0;z-index:999;background:radial-gradient(ellipse 120% 90% at 50% 40%,transparent 55%,rgba(0,0,0,.35) 100%);display:none;align-items:flex-start;justify-content:center;padding:24px;overflow-y:hidden}
    .nick-overlay::before{content:'';position:fixed;inset:0;pointer-events:none}
    .nick-overlay.show{display:flex}
    .nick-card{position:relative;max-width:560px;width:100%;min-width:0;background:var(--card-bg);border:1px solid var(--border-gold);border-radius:var(--radius);padding:32px 26px;animation:card-in .4s ease;overflow:hidden;transition:transform .55s cubic-bezier(.55,0,.1,1),opacity .55s ease}
    .nick-card[dir="rtl"]{direction:rtl}
    .nick-card[dir="rtl"] .nick-input-row{flex-direction:row-reverse}
    .nick-card.leaving{transform:translateY(-120%);opacity:0}
    .nick-overlay.leaving{background:transparent}
    .nick-overlay.leaving::before{opacity:0;transition:opacity .55s ease}
    /* WATERMARK MATCHES THE LOGO — 2026-08-10, Shane: "make the watermark same the
       logo font style".
       It ALREADY used var(--font-serif) + italic + 700, which is the same stack
       .gate-logo uses. The one property it was missing is letter-spacing:.06em,
       and at 7rem that gap is what made it read as a different setting of the
       word rather than a bigger copy of the logo — tracking is far more visible
       at display size than at 2rem.
       NOTE for anyone matching this again: Playfair Display ships ITALIC-ONLY in
       fonts/fonts.css (all six @font-face blocks), so font-style:italic here is
       documentation rather than a switch — the slant comes from the file. Do not
       "fix" it by removing the property.
       KNOWN INCONSISTENCY, left alone: .desk-rail-brand uses .02em. Three
       wordmarks, two trackings. Worth unifying if he asks. */
    /* WATERMARK MOVED OFF .nick-card ONTO THE RECOVERY STEP — 2026-08-10.
       Shane: "the watermark here in choose ur mask delete here. but in save
       recovery codes page should have a watermark there".
       It hung on .nick-card, which is the SHARED wrapper around every
       onboarding step, so one rule painted it on all of them. Retargeting it at
       #nick-step-2 is what scopes it — no display toggling, no :has, no JS: the
       element only exists on that screen, so the watermark can only appear
       there. #nick-step-2 needs position:relative for the absolute centring to
       resolve against it instead of the card.
       It matches the .gate-logo type treatment on purpose (serif, italic, 700,
       letter-spacing .06em) — see the earlier note about tracking mattering far
       more at 7rem than at 2rem. Playfair ships italic-only, so font-style here
       is documentation, not a switch. */
    #nick-step-2{position:relative}
    #nick-step-2::before{content:'MASKARA';position:absolute;font-family:var(--font-serif);font-style:italic;font-weight:700;letter-spacing:.06em;font-size:7rem;color:rgba(var(--neon-rgb),.03);top:50%;left:50%;transform:translate(-50%,-50%);pointer-events:none;white-space:nowrap;line-height:1;z-index:0}
    .nick-logo{font-family:var(--font-serif);font-style:italic;font-weight:700;font-size:2rem;color:var(--gold);text-shadow:var(--glow-gold);text-align:center;margin-bottom:4px;animation:glowPulse 3.6s ease-in-out infinite}
    .nick-sub{text-align:center;font-size:.7rem;color:var(--text-muted);letter-spacing:.1em;text-transform:uppercase;margin-bottom:28px}
    .nick-label{font-family:var(--font-serif);font-style:italic;font-size:.95rem;color:var(--text-soft);margin-bottom:10px}
    .nick-input-row{display:flex;gap:0;margin-bottom:14px;align-items:stretch;border:1px solid var(--border-gold);border-radius:var(--radius-sm);overflow:hidden}
    .nick-input{flex:1;background:var(--surface);border:none;color:var(--text);font-family:var(--font-serif);font-style:italic;font-size:1rem;padding:13px 14px;outline:none;transition:opacity .3s}
    .nick-input.rolling{opacity:0}
    .nick-input::placeholder{color:var(--text-muted);font-style:italic}
    .nick-roll{width:54px;flex-shrink:0;background:rgba(var(--neon-rgb),.06);border:none;border-left:1px solid var(--border-gold);color:var(--gold);font-size:1.6rem;display:flex;align-items:center;justify-content:center;transition:background .2s;overflow:hidden;cursor:pointer}
    .nick-roll:hover{background:rgba(var(--neon-rgb),.12)}
    .dice-face{display:inline-block;transform-origin:center}
    .nick-roll.spinning .dice-face{animation:diceRollMove .75s cubic-bezier(.4,0,.6,1)}
    @keyframes diceRollMove{
      0%  {transform:translateY(0)     rotate(0deg)    scale(1)}
      15% {transform:translateY(-14px) rotate(-90deg)  scale(.85)}
      30% {transform:translateY(0)     rotate(-180deg) scale(1)}
      45% {transform:translateY(-10px) rotate(-270deg) scale(.85)}
      60% {transform:translateY(0)     rotate(-360deg) scale(1)}
      75% {transform:translateY(-6px)  rotate(-420deg) scale(.9)}
      90% {transform:translateY(0)     rotate(-450deg) scale(1)}
      100%{transform:translateY(0)     rotate(-540deg) scale(1)}
    }
    @keyframes nameReveal{from{opacity:0;letter-spacing:.2em}to{opacity:1;letter-spacing:normal}}
    .nick-input.revealed{animation:nameReveal .5s ease forwards}
    .nick-hint{font-size:.76rem;color:var(--text-muted);margin-bottom:22px;font-style:italic}
    .nick-slide-track{position:relative;height:52px;border-radius:26px;border:1px solid var(--border-gold);background:var(--surface);overflow:hidden;clip-path:inset(0 round 26px);user-select:none;touch-action:none;cursor:grab}
    .nick-slide-track.locked{opacity:.4;pointer-events:none;cursor:not-allowed}
    .nick-slide-track.dragging{cursor:grabbing}
    .nick-slide-fill{position:absolute;top:0;left:0;bottom:0;width:52px;background:linear-gradient(135deg,var(--crimson),var(--crimson-lt));border-radius:26px;box-shadow:var(--glow-red)}
    .nick-slide-label{position:absolute;inset:0;display:flex;align-items:center;justify-content:center;font-size:.82rem;font-weight:600;letter-spacing:.04em;text-transform:uppercase;color:var(--text);pointer-events:none;transition:opacity .15s ease;white-space:nowrap}
    .nick-slide-arrow{margin-left:6px}
    .nick-slide-thumb{position:absolute;top:2px;left:2px;width:46px;height:46px;border-radius:50%;background:radial-gradient(circle at 35% 30%,#fff2c2,var(--gold) 60%,var(--gold-dim) 100%);box-shadow:0 2px 8px rgba(0,0,0,.5),var(--glow-gold);display:flex;align-items:center;justify-content:center;will-change:transform}
    .nick-slide-thumb svg{width:18px;height:18px}
    .nick-slide-track.complete{border-color:var(--gold)}
    .nick-slide-track.complete .nick-slide-label{color:var(--gold)}
    .nick-slide-track.shake{animation:nickShake .4s ease}
    @keyframes nickShake{0%,100%{transform:translateX(0)}25%{transform:translateX(-6px)}75%{transform:translateX(6px)}}
    body.gate-locked{overflow:hidden}
    body.gate-locked .app-header,body.gate-locked .tabs-bar,body.gate-locked .main-content,body.gate-locked .gabi-bar,body.gate-locked .bottom-nav{visibility:hidden}
    /* THE RAIL BELONGS IN THAT LIST TOO — 2026-08-10. It showed straight through
       the age/consent gate. The list above predates the sidebar by months, which
       is the whole bug: a new fixed-position element has to be added to every
       "hide the app" rule that already exists, and there are two of them (this
       one and the .nick-overlay rule further down). display, not visibility,
       because the rail also drives body's padding-left. */
    body.gate-locked .desk-rail{display:none !important}
    /* CHECKOUT RETURN — hold the onboarding screen back until the purchase
       popup is ready. Set on <html> by the head script the moment the page
       loads with ?goldTopup / ?iconTopup, cleared when the popup is dismissed,
       when confirmation fails, or by that script's 9s safety timeout.
       visibility, not display: the screen keeps its layout and its scripts
       keep measuring real sizes, so nothing reflows or jumps when it appears.
       The success popup is a separate .modal-overlay outside #nick-overlay,
       so it still draws normally over the empty background. */
    html.checkout-returning #nick-overlay{visibility:hidden}
    /* ONBOARDING: username field + step swap + recovery step */
    .nick-username-row{margin-bottom:14px}
    .nick-username-input{width:100%;background:var(--surface);border:1px solid var(--border-gold);border-radius:var(--radius-sm);color:var(--text);font-size:.92rem;padding:12px 14px;outline:none;font-family:inherit}
    /* NOT CONVERTED TO --neon ON PURPOSE, 2026-08-11: this modal pins its own
       DARK GREEN grounds (#3A6033/#44703B/#4E7A45) which do not follow the theme,
       so its accent must stay bright gold. Light mode's dark gold #8F6F1C on a
       dark green card would be invisible. If the diary ever starts following the
       theme grounds, convert these four values with it. */
    #diary-modal{--gold:#FFD700;--gold-dim:#B8960C;--border-gold:rgba(255,215,0,.30);--card-bg:#3A6033;--surface:#44703B;--surface-2:#4E7A45;--border:rgba(255,215,0,0.20);--text:#F7FBF4;--text-muted:#AEC7A2}
    #diary-modal .settings-opt[style*="background:var(--gold)"]{color:#1A1509}
    .diary-keypad-btn{background:var(--surface);border:1px solid var(--border-gold);border-radius:var(--radius-sm);color:var(--text);font-size:1.05rem;font-weight:600;padding:12px 0;font-family:inherit;cursor:pointer;transition:transform .1s,background .15s}
    .diary-keypad-btn:active{transform:scale(.93);background:rgba(var(--neon-rgb),.12)}
    .diary-pin-dot{width:14px;height:14px;border-radius:50%;border:1.5px solid var(--border-gold);background:transparent;transition:background .18s,transform .18s,border-color .18s}
    .diary-pin-dot.filled{background:var(--gold);border-color:var(--gold);transform:scale(1.15)}
    @keyframes diaryShake{10%,90%{transform:translateX(-1px)}20%,80%{transform:translateX(2px)}30%,50%,70%{transform:translateX(-6px)}40%,60%{transform:translateX(6px)}}
    .diary-shake{animation:diaryShake .5s}
    .diary-shake .diary-pin-dot.filled{background:#e05a5a;border-color:#e05a5a}
    .diary-state-fade{animation:diaryStateFade .28s ease}
    @keyframes diaryStateFade{from{opacity:0;transform:scale(.97)}to{opacity:1;transform:scale(1)}}
    @keyframes diaryEntryIn{from{opacity:0;transform:translateY(6px)}to{opacity:1;transform:translateY(0)}}
    .diary-entry-card{animation:diaryEntryIn .3s ease both}
    .nick-username-input::placeholder{color:var(--text-muted)}
    .nick-username-input.error{border-color:#E24B4A}
    .nick-field-err{font-size:.7rem;color:#E24B4A;min-height:14px;margin:8px 0 10px 2px;display:flex;align-items:center;gap:5px}
.nick-field-err .nfe-icon{display:inline-flex;align-items:center;justify-content:center;width:16px;flex-shrink:0;font-size:.85rem}
    .nick-field-err.state-checking{color:var(--text-muted)}
    .nick-field-err.state-ok{color:#3ddc84}
    .nick-field-err.state-ok .nfe-icon{color:#2ecc71;font-weight:800;font-size:1rem;text-shadow:0 0 .5px currentColor,0 0 .5px currentColor}
    .nick-field-err.state-taken{color:#E24B4A}
    .nick-username-input.bump{animation:nickFieldBump .35s ease}
@keyframes nickFieldBump{0%,100%{transform:translateX(0)}25%{transform:translateX(-5px)}50%{transform:translateX(4px)}75%{transform:translateX(-2px)}}
    .nick-step{transition:opacity .35s ease}
    .nick-step.fading-out{opacity:0}
    .nick-step.fading-in{opacity:0;animation:nickStepIn .4s ease forwards}
    @keyframes nickStepIn{from{opacity:0;transform:translateY(6px)}to{opacity:1;transform:translateY(0)}}
    .nick-code-display{font-family:monospace;font-size:1rem;letter-spacing:.04em;text-align:center;color:var(--gold);background:var(--surface);border:1px solid var(--border-gold);border-radius:var(--radius-sm);padding:14px 10px;margin-bottom:14px;word-break:break-all}
    .nick-select{width:100%;background:var(--surface);border:1px solid var(--border-gold);border-radius:var(--radius-sm);color:var(--text);font-size:.88rem;padding:11px 12px;outline:none;font-family:inherit;margin-bottom:10px;appearance:none;-webkit-appearance:none;background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='14' height='14' viewBox='0 0 24 24' fill='none' stroke='%23c9a94a' stroke-width='2'%3E%3Cpath d='M6 9l6 6 6-6'/%3E%3C/svg%3E");background-repeat:no-repeat;background-position:right 12px center}
    .nick-recovery-hint{font-size:.72rem;color:var(--text-muted);line-height:1.5;margin-bottom:14px}
    .nick-skip-link{display:block;text-align:center;font-size:.74rem;color:var(--text-muted);margin-top:12px;text-decoration:underline;cursor:pointer}
    /* "Lost your code?" — Shane picked flat silver (option J), 2026-08-10.
       IT WAS `color:#4d7dff` TYPED INTO THE MARKUP, a leftover from the retired
       purple palette and nearly the Free-tier ring blue. Being an inline style
       is exactly why it survived the whole green repaint — a stylesheet cannot
       reach into a style attribute. Moved here so the next palette change does
       reach it.
       Metallic gradients (background-clip:text) were mocked and rejected for
       this: at .74rem the dark band of the gradient lands inside a single
       letter stroke and the word turns to mush on a phone. Metal needs a large
       filled shape, like the CTA button.
       NOTE, not changed: --silver is also the PLUS tier colour (rings, badge),
       and `#diary-forgot-pin-link` is the same kind of link but still inherits
       the muted default. Flagged to Shane. */
    #open-recover-link{color:var(--silver)}
    /* AVATAR TIERS: free / illustrated / gold picker inside profile "Icon" tab */
    .avatar-tier-row{display:flex;gap:8px;margin-top:10px}
    /* Base state used to be var(--surface), which is a near-black #1A1922 —
       read as a plain black box, not a themed container. Now uses the same
       nested-gradient container as everywhere else, PLUS a faint tier-tinted
       border (blue/red/gold) even at rest — same idea as the onboarding
       price-tier cards (.onb-price-card), which this row is meant to echo. */
    .avatar-tier-btn{flex:1;text-align:center;padding:9px 6px;border-radius:var(--radius-sm);background:linear-gradient(180deg,rgba(var(--panel-hi-rgb),0.28) 0%,rgba(var(--panel-lo-rgb),0.22) 100%);border:1px solid rgba(255,255,255,0.10);font-size:.72rem;font-weight:700;color:var(--text-muted);cursor:pointer;transition:all .15s ease;font-family:inherit}
    /* THE FREE TAB'S BLUE IS GONE — Shane 2026-08-12: "fix the bug ... free
       blue". It was rgba(74,124,246,.3), the Free-tier blue, drawn on the tab at
       REST. A resting tab is not a tier badge — it is one of two things you can
       tap — and a saturated blue hairline on a green sheet was the loudest edge
       in the modal while marking nothing. It now carries the same neutral
       hairline as the base rule, so the only coloured border in this row is the
       gold one, which does mean something. The ACTIVE Free state keeps --silver
       below; that one marks a real state. */
    .avatar-tier-btn[data-tier="free"]{border-color:rgba(255,255,255,0.10)}
    .avatar-tier-btn[data-tier="gold"]{border-color:rgba(var(--neon-rgb),.35)}
    /* THE BLACK RING WAS THE BROWSER'S — Shane 2026-08-12: "fix the bug black in
       gold line". Nothing here drew it. Chrome's default focus ring is a DARK
       ring with a white outer ring for contrast, and it lands on the Gold tab
       the moment it is clicked, so the selected tier looked like it had a thick
       black outline.
       :focus-visible KEEPS A RING, in gold — removing the outline outright
       would leave keyboard users with no idea where they are. box-shadow rather
       than outline so it follows the border-radius instead of boxing the corners.
       .onb-price-card already does the same thing a few hundred lines down; this
       row is the same control in a different sheet. */
    .avatar-tier-btn:focus{outline:none}
    .avatar-tier-btn:focus-visible{outline:none;box-shadow:0 0 0 2px rgba(var(--neon-rgb),.55)}
    /* ACTIVE IS --neon, NOT --gold. --gold HAS NOT MEANT GOLD since the charcoal
       repaint — it is the neutral decoration ink #DCE7D6, and --border-gold is a
       plain white hairline. So with the browser's black ring removed, the
       selected Gold tab would have had NO gold on it at all and would have
       looked identical to the resting one. Same fault, and the same fix, as the
       rail wordmark and .logo-name. */
    .avatar-tier-btn.active{color:var(--neon);border-color:rgba(var(--neon-rgb),.55);background:rgba(var(--neon-rgb),.10)}
    /* Tier colors match the onboarding price-tier language everywhere else: free=blue, illustrated=silver, gold=gold */
    .avatar-tier-btn[data-tier="free"].active{color:var(--silver);border-color:var(--silver);background:var(--silver-soft)}
    .avatar-tier-btn[data-tier="gold"].active{color:var(--neon);border-color:rgba(var(--neon-rgb),.55);background:rgba(var(--neon-rgb),.10)}

    /* ====================================================
       THE TIER ROW — OPTION 5, chips with a padlock on Gold. 2026-08-12.
       Shane picked this out of eight. What it replaces and why:
       THE GREY WAS THE PROBLEM. The selected tab used `--silver`, which on a
       green sheet reads as a DISABLED button rather than a chosen one — that is
       what made the row look dead. Selected is now the same bright brand green
       the wardrobe row uses, with dark ink.
       THE CONTAINER IS GONE. I had argued for keeping a frame here so the row
       could not be mistaken for the 26 category pills below it; his pick drops
       it, and the padlock plus the bright fill now carry that separation
       instead. If the row and the list ever start reading as one thing again,
       the frame is the thing to bring back.
       SCOPED TO #mask-picker-tier-row — `.avatar-tier-btn` is a shared tier
       control and this shape is only wanted here.
    ==================================================== */
    #mask-picker-tier-row{gap:8px;flex-wrap:nowrap;border:0;border-radius:0;background:none}
    #mask-picker-tier-row .avatar-tier-btn{flex:1;border:0;border-radius:11px;
      background:rgba(255,255,255,.05);color:var(--text-muted);font-weight:700;
      display:inline-flex;align-items:center;justify-content:center;gap:7px;padding:10px 8px}
    #mask-picker-tier-row .avatar-tier-btn:hover{background:rgba(255,255,255,.09);color:var(--text)}
    /* The lock is the SIGNAL, so it stays gold while the tab is resting and flips
       to the dark ink when the tab is chosen — a gold glyph on a bright green
       fill would be the one unreadable pair on this row. */
    #mask-picker-tier-row .tier-lock{width:13px;height:13px;flex:0 0 auto;color:var(--neon)}
    /* NO LOCK FOR SOMEONE WHO ALREADY PAYS. body.is-gold is written by
       isGoldUser() in app.html — which was permanently false until the dead
       `window.identity` guard there was fixed in this same round. */
    body.is-gold #mask-picker-tier-row .tier-lock{display:none}
    /* Named after everything else, and hover/focus named explicitly, because
       `:hover`, `:focus` and `.active` all weigh the same and the last one
       written wins — the exact tie that left the wardrobe fill looking weak. */
    #mask-picker-tier-row .avatar-tier-btn.active,
    #mask-picker-tier-row .avatar-tier-btn.active:hover,
    #mask-picker-tier-row .avatar-tier-btn.active:focus{background:#A8E693;color:#17300F;
      font-weight:800;box-shadow:none}
    #mask-picker-tier-row .avatar-tier-btn.active .tier-lock{color:#17300F}
    body.has-custom-theme #mask-picker-tier-row .avatar-tier-btn.active,
    body.has-custom-theme #mask-picker-tier-row .avatar-tier-btn.active:hover{
      background:rgba(255,255,255,.88);color:#1A1A1A}
    .avatar-tier-note{font-size:.68rem;color:var(--text-muted);margin:8px 2px 10px;line-height:1.4}
    /* Free / Illustrated / Gold pill tabs, left to right, in My Profile — same tier
       colors as everywhere else (blue/red/gold). One segmented control drives both
       the price info box and the icon picker right below it, so there's exactly one
       place to pick a tier, see its price, and browse/buy — no separate duplicate. */
    .ptier-tabs{display:flex;gap:0;margin-top:14px;border-radius:999px;background:rgba(255,255,255,.04);border:1px solid var(--border);padding:3px}
    .ptier-tab{position:relative;flex:1;text-align:center;padding:8px 6px;border-radius:999px;background:transparent;border:none;font-size:.75rem;font-weight:700;color:var(--text-muted);cursor:pointer;font-family:inherit;transition:all .15s ease}
    .ptier-tab[data-tier="free"].active{color:var(--silver-ink);background:var(--silver)}
    .ptier-tab[data-tier="gold"].active{color:#1A1509;background:var(--gold)}
    /* Small filled dot on whichever tab the account actually owns, kept
       visible even while a different tab is open for preview — so "what
       I'm looking at" and "what I actually have" never look the same. */
    .ptier-tab.is-owned::after{content:'';position:absolute;top:4px;right:8px;width:6px;height:6px;border-radius:50%;background:currentColor;opacity:.55}
    .ptier-tab.is-owned.active::after{opacity:.9}
    /* Static "steer toward Gold" marker — standard pricing-page convention
       for the bundle tier, shown regardless of active/owned state. */
    .ptier-best-value{display:block;font-size:.56rem;font-weight:800;letter-spacing:.2px;color:var(--gold);margin-top:1px;opacity:.85}
    .ptier-tab[data-tier="gold"].active .ptier-best-value{color:#7a5b00}
    .ptier-info{margin-top:10px;padding:12px;border-radius:12px;background:rgba(255,255,255,.03);border:1px solid var(--border)}
    .ptier-info-top{display:flex;align-items:center;justify-content:space-between;gap:10px}
    .ptier-info-desc{font-size:.78rem;color:var(--text);font-weight:600}
    .ptier-info-price{font-size:.85rem;font-weight:800;color:var(--gold);white-space:nowrap;flex-shrink:0}
    .ptier-info-unit{font-size:.65rem;font-weight:600;color:var(--text-muted)}
    .ptier-info-features{margin:8px 0 0;padding-left:18px;font-size:.7rem;color:var(--text-muted);line-height:1.6}
    .ptier-info-features:empty{display:none;margin:0}
    .ptier-current-plan-note{margin-top:8px;font-size:.68rem;font-weight:700;color:#39d98a;text-align:center}
    /* FIXED 56px TILES, NOT STRETCHING COLUMNS — 2026-08-11. `minmax(46px,1fr)`
       let every column absorb the leftover width, so on a wide sheet the tiles
       inflated to ~100px while the glyph stayed small and each face floated in
       a big empty box. A fixed track keeps the container SMALL and puts the
       spare width in the margins. Shane asked for this twice; the first fix
       went to .emoji-quick-grid, which this modal does not use — THIS is the
       class #profile-emoji-quick actually carries. */
        /* grid-auto-rows IS LOAD-BEARING — fixed 2026-08-11. The tiles carry
       `aspect-ratio:1`, but a grid item cannot establish its own height from a
       ratio when the ROW is auto-sized: the row collapsed to the content box, so
       48px-wide tiles rendered about 30px tall, the 36px glyph overflowed into
       its `overflow:hidden` box, and every column fused into one strip of faces.
       Shane reported it three times as "dikit dikit" and I twice widened the GAP,
       which could never have helped - there was no gap problem, the tiles were
       squashed. Naming the row height explicitly is what actually fixes it.
       IF YOU CHANGE THE COLUMN WIDTH, CHANGE THIS TO MATCH or the tiles stop
       being square. */
    .avatar-icon-grid{display:grid;grid-template-columns:repeat(auto-fill,48px);grid-auto-rows:48px;justify-content:center;gap:14px 12px;margin-top:2px;max-height:min(300px,36vh);overflow-y:auto;padding-right:4px;scrollbar-width:thin}
    /* THE GAP WAS THE PROBLEM, NOT THE WINDOW — corrected 2026-08-11. My first
       answer to "crowded" was a taller scroll box; Shane came back with "dikit
       dikit" (stuck together), which is about the space BETWEEN tiles. At a 5px
       gap with the emoji filling ~75% of each 48px tile, neighbouring faces were
       almost touching and the grid read as one solid block.
       ROW GAP IS LARGER THAN COLUMN GAP (14/12, 16/13 on mobile) on purpose: a
       grid of round glyphs looks tighter vertically than horizontally because
       nothing separates one row's chins from the next row's foreheads.
       DO NOT fix this by shrinking the tiles: 48px is already at the floor for a
       comfortable thumb tap, and this picker is used on phones more than
       desktop. The taller window stays — it is what keeps the wider gaps from
       costing visible rows. */
    @media (max-width:520px){
      .avatar-icon-grid{max-height:52vh;gap:16px 13px;grid-auto-rows:48px}
    }
    /* Shared by all three tiers (Gold/Illustrated/Free use the same
       .avatar-tile class) — was var(--surface), a near-black #1A1922, so
       every single emoji/avatar cell in the grid read as a plain black box
       regardless of tier. Now the same nested-gradient container as the
       rest of the app. */
    /* TILE ELEVATION, 2026-08-11. Two shadow layers, the same pair the theme
       swatches use: an outer drop shadow lifts the tile off the sheet, an inset
       highlight along the top gives it a curve. Kept low (.06) — an emoji sits
       ON this surface, so a strong highlight competes with the artwork rather
       than framing it. */
    .avatar-tile{aspect-ratio:1;border-radius:11px;background:linear-gradient(180deg,rgba(var(--panel-hi-rgb),0.28) 0%,rgba(var(--panel-lo-rgb),0.22) 100%);border:1px solid rgba(255,255,255,0.08);display:flex;align-items:center;justify-content:center;font-size:36px;cursor:pointer;position:relative;transition:transform .12s ease,border-color .12s ease,box-shadow .12s ease;overflow:hidden;
      box-shadow:0 3px 8px rgba(0,0,0,.30),inset 0 1px 1px rgba(255,255,255,.06)}
    .avatar-tile:hover{transform:translateY(-2px)}
    /* SELECTED CARRIES A TICK, not just a border. On a grid of 16 near-identical
       rounded squares a border alone is easy to miss, and the person is looking
       at the emoji rather than the frame. The tick uses --neon so it follows the
       theme; overflow:hidden on the tile keeps it inside the corner radius. */
    .avatar-tile.selected{border-color:var(--neon);box-shadow:0 4px 14px rgba(var(--neon-rgb),.20),inset 0 1px 1px rgba(255,255,255,.06);
      background:linear-gradient(135deg,rgba(var(--neon-rgb),.16),rgba(var(--neon-rgb),.03))}
    .avatar-tile.selected::after{content:'\2713';position:absolute;top:4px;right:4px;width:15px;height:15px;border-radius:50%;
      background:var(--neon);color:color-mix(in srgb,var(--neon) 22%,#000);font-size:8px;font-weight:700;
      display:flex;align-items:center;justify-content:center;line-height:1}
    .avatar-tile.locked{opacity:.5}
    .avatar-tile.locked:has(.avatar-preview-tag){opacity:.8}
    .avatar-tile.locked:hover{transform:none}
    /* Currently being looked at in the preview popup, not yet bought/equipped
       — same "previewing" treatment onboarding's face picker uses. */
    .avatar-tile.previewing{border-color:var(--gold);box-shadow:0 0 0 2px rgba(var(--neon-rgb),.25)}
    .avatar-tile .avatar-price-tag{position:absolute;bottom:-6px;left:50%;transform:translateX(-50%);font-size:7.5px;font-weight:800;padding:2px 5px;border-radius:6px;white-space:nowrap;background:var(--gold);color:#1A1509}
    .avatar-tile .avatar-preview-tag{background:transparent;border:1px solid var(--gold);color:var(--gold)}
    .avatar-upsell{border-radius:var(--radius-sm);padding:11px 12px;margin:10px 0 4px;display:flex;align-items:center;gap:10px;background:rgba(var(--neon-rgb),.08);border:1px solid var(--border-gold)}
    .avatar-upsell .avatar-upsell-text{flex:1;font-size:.7rem;color:var(--text-muted);line-height:1.4}
    .avatar-upsell .avatar-upsell-text strong{color:var(--gold)}
    .avatar-upsell button{border:none;border-radius:999px;padding:7px 12px;font-weight:800;font-size:.68rem;cursor:pointer;white-space:nowrap;background:var(--gold);color:#1A1509;font-family:inherit}
    /* Separate from .avatar-upsell on purpose — that class is gold-tinted
       because it's pitching a purchase (Illustrated/Gold icons, still gold
       there). The subscription disclaimer isn't a sales pitch, just a plain
       notice, so it uses the same neutral nested-container color as the
       rest of My Profile instead of looking like another upsell. */
    .subs-disclaimer{border-radius:var(--radius-sm);padding:11px 12px;margin:0 0 12px;display:flex;align-items:center;gap:10px;background:linear-gradient(180deg,rgba(var(--panel-hi-rgb),0.28) 0%,rgba(var(--panel-lo-rgb),0.22) 100%);border:1px solid rgba(255,255,255,0.10)}
    .subs-disclaimer .subs-disclaimer-text{flex:1;font-size:.7rem;color:var(--text-muted);line-height:1.4}
    .subs-disclaimer .subs-disclaimer-text strong{color:var(--gold)}
    /* Matches "Choose your mask"'s onb-stage-avatar ring exactly: same
       amber gold (#e8c25a, not the app's brighter --gold yellow) and the
       same onbAvatarRingGlow 2.6s pulsing animation (defined near the
       onboarding styles below) instead of a static box-shadow. No little
       sparkle badge on the avatar itself (removed per feedback — onboarding's
       avatar ring doesn't have one either, just the ring; the separate mask
       badge next to the nickname text is the only badge that should exist). */
    .avatar-gold-ring{position:relative;border-color:#e8c25a!important;border-width:3px!important;animation:onbAvatarRingGlow 2.6s ease-in-out infinite}
    /* MASKARA staff ring. Green (#3FCE7A) is already this app's agent colour -
       it's the "online now" dot on staffBadge() - so the association is half
       built already.
       Deliberately NOT a tier colour: nobody can buy green. It reads as
       verified and here to help rather than ranked above you, which matters
       when a Vent Buddy needs to feel approachable rather than senior.
       Pulses only while they're actually clocked in, so the ring carries real
       information - who is around right now - instead of just a label. */
    /* (Agent ring rules live further down, next to the gold ones.) */
    @keyframes agentRingPulse{
      0%,100%{box-shadow:0 0 10px 1px rgba(63,206,122,.35)}
      50%{box-shadow:0 0 18px 3px rgba(63,206,122,.70)}
    }
    /* Lives on its own page in the mask picker now (not a cramped strip
       inside the profile modal), so every category shows at once — no
       inner max-height/scrollbar fighting the outer modal for space. */
    .avatar-cat-row{display:grid;grid-template-columns:repeat(auto-fill,minmax(150px,1fr));gap:8px;padding:2px 0 10px}
    /* Was a flat var(--surface) pill with white-space:nowrap — against the
       picker's dark background it read as almost invisible (no consistent
       "container" look, unlike everywhere else in onboarding/My Profile),
       and nowrap silently clipped longer labels like "Loneliness & Longing"
       or "Anxiety & Overwhelmed" at the pill's edge instead of wrapping.
       Now uses the same nested-container treatment as #profile-modal
       .settings-tab-content (lighter version of the modal's own purple
       gradient) so it reads as belonging to the same screen, and wraps text
       onto a second line instead of cutting it off. */
    /* min-width:0 IS THE FIX FOR THE CLIPPED PILL — 2026-08-11. A grid item's
       min-width defaults to AUTO, meaning it refuses to shrink below its own
       content. So "Celebration & Milestones" pushed its track wider than the
       1fr column and the text ran off the edge of the scroll container, while
       every shorter label sat fine. white-space:normal was already set; the item
       simply was not allowed to get narrow enough to use it.
       SAME TRAP AS ANY `1fr` GRID: if a cell overflows, suspect min-width before
       suspecting the text. */
    /* .8rem, NOT .68 — 2026-08-12. Shane: "make a little bigger of the font in
       emoticons free categories its too small to read". .68rem is ~11.6px at this
       app's 17px root, which is below the size anything meant to be READ should
       be — and these are 26 two-word names, not labels you learn once.
       PADDING GOES UP WITH IT (8px -> 9px 11px): growing the type inside the same
       box just tightens the text against the border, which reads as cramped rather
       than bigger. The row wraps, so a slightly taller button costs no width. */
    .avatar-cat-btn{flex:0 0 auto;min-width:0;background:linear-gradient(180deg,rgba(var(--panel-hi-rgb),0.28) 0%,rgba(var(--panel-lo-rgb),0.22) 100%);border:1px solid rgba(255,255,255,0.10);border-radius:12px;color:var(--text-muted);font-size:.8rem;font-weight:600;padding:9px 11px;cursor:pointer;white-space:normal;word-wrap:break-word;text-align:center;line-height:1.3;font-family:inherit;transition:border-color .15s ease,color .15s ease,background .15s ease}
    .avatar-cat-btn:hover{border-color:var(--border-gold);color:var(--gold)}
    .avatar-cat-btn.active{color:var(--gold);border-color:var(--border-gold);background:rgba(var(--neon-rgb),.10)}

    /* Card becomes seamless with the full-screen background — no border, no competing bg, no padding gap */
    .nick-card.nick-card-onb{background:transparent;border-color:transparent;padding:0}
    .nick-card.nick-card-onb .onb-skin{margin:0}
    .nick-card.nick-card-onb #nick-step-2{padding:32px 26px}

    /* ============ ONBOARDING v2 SKIN — scoped to #nick-step-1 only, does not touch the rest of the app ============ */
    .onb-skin{
      --onb-bg:var(--midnight); --onb-bg-2:var(--ground-hi);
      --onb-panel:rgba(var(--panel-hi-rgb),.85); --onb-panel-2:var(--surface-2);
      --onb-line:rgba(255,255,255,.10); --onb-ink:#F7FBF4; --onb-muted:#a99fc6;
      --onb-gold:#e8c25a; --onb-gold-soft:rgba(232,194,90,.28);
      --onb-blue:#C7CCD4; --onb-blue-soft:rgba(199,204,212,.26); /* NAME KEPT, VALUE IS SILVER since 2026-08-10 - same churn-avoidance as --gold and --neon no longer meaning what they say. Only user: .onb-tier-tag.free */
      --onb-silver:var(--silver); --onb-silver-soft:var(--silver-soft);
      /* Masquerade Night onboarding palette (violet stage, gold light) */
      --gold:#f6c94c; --gold-soft:#ffe07a; --ink:#241a0d; --cream:#f6f2ff;
      --glass:rgba(var(--panel-hi-rgb),0.55); --glass-line:rgba(255,255,255,0.14); --field:rgba(30,58,24,0.45);
      background:transparent;
      margin:-32px -26px 0; padding:20px 36px 0;
      position:relative;
      color:var(--onb-ink); text-align:left;
    }
    /* MATCHED TO index.html's .lang-toggle AND to the gate chip, 2026-08-09. It was
       the odd one out: a 20px pill at 20/24, .66rem sans, with a stale gold-tinted
       border from before the repaint - so tapping through the gate changed the
       chip's shape, size, font and corner in one step. 7px corners, transparent
       fill, 12/12. */
    .onb-lang-row{position:fixed;top:12px;right:12px;z-index:1001;display:flex;align-items:center;background:transparent;border:1px solid rgba(236,243,232,.4);border-radius:7px;padding:2px 7px;opacity:0;animation:onbLangFadeUp .8s ease .3s forwards;transition:border-color .2s,background .2s}
    .onb-lang-row:hover{border-color:rgba(236,243,232,.65);background:rgba(236,243,232,.07)}
    .onb-lang-row .onb-lang-icon{font-size:.75rem;opacity:.85}
    /* GEORGIA NAMED OUTRIGHT, not var(--font-serif) - app.css's serif token is
       Playfair, index.html's is Georgia, and using the token here would put a
       different face beside the same chip on the entry page. Same note as the
       gate chip; do not "tidy" either back into the token. */
    .onb-lang-btn{background:none;border:none;display:flex;align-items:center;gap:4px;color:var(--text-muted);font-size:.46rem;font-weight:600;font-family:Georgia,'Times New Roman',serif;font-style:italic;letter-spacing:.12em;padding:0;line-height:1.2;cursor:pointer}
    .onb-lang-btn-label{max-width:88px;overflow:hidden;white-space:nowrap;text-overflow:ellipsis}
    .onb-lang-chevron{flex-shrink:0;transition:transform .2s ease}
    .onb-lang-row.open .onb-lang-chevron{transform:rotate(180deg)}
    .onb-lang-list{position:absolute;top:calc(100% + 8px);right:0;margin:0;padding:6px;list-style:none;min-width:170px;max-height:min(320px,calc(100vh - 80px));overflow-y:auto;background:var(--midnight,#0a0c1c);border:1px solid rgba(236,243,232,0.18);border-radius:12px;box-shadow:0 12px 30px rgba(0,0,0,.5);display:none;z-index:1002}
    .onb-lang-row.open .onb-lang-list{display:block}
    .onb-lang-option{padding:8px 12px;border-radius:8px;font-size:.78rem;color:var(--text,#F0EDE8);cursor:pointer;white-space:nowrap;font-family:Georgia,'Times New Roman',serif;font-style:italic}
    .onb-lang-option:hover,.onb-lang-option.active{background:rgba(var(--neon-rgb),.12);color:var(--gold)}
    html[dir="rtl"] .onb-lang-row{right:auto;left:12px}
    html[dir="rtl"] .onb-lang-list{right:auto;left:0}
    .onb-stage{display:flex;flex-direction:column;align-items:center;gap:10px;margin-bottom:20px}
    .onb-stage-avatar-wrap{position:relative;width:44px;height:44px;flex-shrink:0}
    /* Pencil affordance on "Choose your mask" - 2026-08-10, Shane.
       The ring is the only way into the face picker now, and a bare circle does
       not tell anyone it is tappable. Sits at the BOTTOM-RIGHT diagonal, clear
       of the ring, never touching it.
       EVERYTHING HERE IS A PERCENTAGE ON PURPOSE. The wrap is position:relative
       and always exactly the ring's size, and that size runs 44 / 60 / 72 / 92 /
       96 / 152 / 170px across breakpoints (220px on #onb-preview-avatar-wrap).
       Percentages hold the same clock position and the same proportional gap at
       every one of those. A px offset would need retuning eight times and would
       silently drift the day any ring size changes. Do not convert these to px.
       Geometry: width 18% + right:-10%/bottom:-2% puts the chip's centre ~1.33x
       the ring radius from the ring's centre, so the gap is ~0.15 of the radius.
       It was -6%/-6% (a clean 45 degrees); Shane asked for it "a little right
       side up", so it was ROTATED UP THE ARC at the same distance - the gap to
       the ring is unchanged, only the clock position moved.
       DO NOT push right past -10%. The overhang past the wrap is ~0.20 of the
       radius (~15px at a 152px ring) and .onb-face-card's gap to the nickname is
       22px, so there are only about 7px of clearance left. .onb-card is also
       overflow:hidden with 14px padding, so a bigger chip gets clipped at the
       card edge - enlarging it means growing the card padding in the SAME edit.
       COLOUR: silver, NOT gold. The first build used var(--gold) and Shane
       rejected it on sight. Gold in this app MEANS the Gold tier - badge, name
       colour, post stripe - so spending it on an edit button that every FREE
       user sees during onboarding cheapens the one colour Gold buys, on the very
       screen Gold was just removed from. Silver also matches the ring this chip
       hangs off (.onb-stage-avatar::before is var(--silver-ramp)), so it reads as
       part of the avatar rather than a foreign control stuck onto it.
       --silver and --silver-ink are a PAIR and both flip in light mode
       (#C7CCD4/#23262B dark, #6E767F/#F4F6F9 light). The glyph is fill:currentColor
       off the button's color, so the pencil stays legible in both themes on its
       own. Never hardcode one of the two.
       min-width/height is a tap-target floor, not styling - below about a 120px
       ring the percentage alone lands under 22px, which is too small to hit on
       a phone. At those sizes the floor wins and the chip sits a little closer
       to the ring; that trade is deliberate.
       NOTE: transform is left free for the :hover scale below. Position is done
       with right/bottom, NOT translate, so hover cannot cancel the placement. */
    .onb-avatar-tap-hint{position:absolute;right:-10%;bottom:-2%;width:18%;height:18%;min-width:22px;min-height:22px;border-radius:50%;border:none;padding:0;background:transparent;color:var(--silver);display:flex;align-items:center;justify-content:center;cursor:pointer;z-index:3;box-shadow:none;transition:transform .18s}
    .onb-avatar-tap-hint svg{width:80%;height:80%;display:block;fill:none;stroke:currentColor;stroke-width:2.2;stroke-linecap:round;stroke-linejoin:round}
    /* NO CHIP, 2026-08-10 - the pencil is a bare silver stroke on the card.
       THREE THINGS COMPENSATE FOR THE CHIP BEING GONE, do not undo them
       piecemeal: the glyph grew from 55% to 80% of the button (it was sized to
       sit INSIDE a filled circle, and at 55% of nothing it is a speck); the
       stroke went 2 -> 2.2 because a hairline with no plate behind it
       disappears against the card; and the button keeps its 22px min size as an
       INVISIBLE hit area, so the tap target did not shrink with the visible
       mark. background:transparent + box-shadow:none is what removes the chip -
       the element is still a button, just unpainted. */
    /* The ring itself opens the picker too, so it needs to say so on a cursor. */
    #onb-stage-avatar{cursor:pointer}
    .onb-avatar-tap-hint:hover{transform:scale(1.1)}
    @media (prefers-reduced-motion:reduce){.onb-avatar-tap-hint{transition:none}.onb-avatar-tap-hint:hover{transform:none}}
    .onb-name-tag-row{display:flex;align-items:center;gap:12px}
    /* The badge sits next to a serif nickname that changes size across
       breakpoints, so it is sized in em against a font-size set here rather
       than a fixed pixel value - it grows and shrinks WITH the name instead of
       staying small next to a large one. Keep these font-sizes in step with
       .onb-stage-name-tag below. */
    /* LOCKED - final sizing, signed off. Height-driven on purpose (see the
       LOCKED banner on #nick-onb-step-a). Any change here must also change the
       '1.15em' passed to setNameBadge() in both call sites, or the two drift. */
    #onb-stage-name-badge,#onb-preview-name-badge{font-size:1.2rem;line-height:0}
    #onb-stage-name-badge img,#onb-preview-name-badge img{height:1.15em;width:auto}
    /* Every badge slot sits on the SAME baseline as the text beside it.
       Each of these used to be positioned by whatever inline vertical-align
       maskaraBadgeHtml happened to emit, so the badge rode high on one screen
       and low on another. inline-flex with align-items:center is what actually
       centres it against a line of text, whatever that text's size. */
    #logo-tag-gold-badge{flex-shrink:0}
    #logo-tag-gold-badge,#my-anon-name-gold-badge,#profile-name-gold-badge,
    #onb-tier-gold-badge-slot,#mtc-gold-badge-slot,#mtc-pill-gold-badge-slot,
    #onb-stage-name-badge,#onb-preview-name-badge,.author-badge{
      display:inline-flex;align-items:flex-end;line-height:0;margin-left:10px;
      /* BASELINE, not `middle` or a nudge. The badge should sit ON the line the
         letters sit on - its bottom edge flush with the nickname's baseline,
         the way a letter does.
         `middle` centres it against the x-height instead, which floats it
         above the baseline and makes it look like it is hovering next to the
         name rather than written alongside it.
         For an inline-flex box wrapping an image, the box's own baseline IS
         its bottom edge, so `baseline` here does exactly that. align-items:
         flex-end keeps the image pinned to that edge inside the box. */
      vertical-align:baseline}
    /* These two sit inside flex rows that already declare a `gap` (the header
       chip's name row uses gap:4px, the Gold tier label uses gap:5px), so the
       margin above stacked on top of it and left them noticeably further from
       the nickname than the composer's badge, which is in a plain inline
       context with no gap at all. Spacing comes from the parent for these. */
    /* 6px, NOT 0. These four sit in flex rows that already declare a small gap,
       so they need less than the 10px above - but zero left the HEADER badge
       visibly tighter to the nickname than the composer's, which is a real
       inconsistency rather than a preference. 4px of parent gap + 6px here
       lands on the same 10px the plain inline badges get. */
    #logo-tag-gold-badge,#mtc-gold-badge-slot,#mtc-pill-gold-badge-slot,
    #onb-tier-gold-badge-slot{margin-left:6px}
    /* NO MANUAL LIFT. The row is align-items:baseline and .logo-tag now has
       visible overflow, so it exposes a real text baseline; the badge is
       inline-flex with line-height:0, which makes its own baseline its bottom
       edge. Bottom of badge therefore lands exactly on the bottom of the
       letters with no magic number to keep re-tuning. Do not reintroduce
       align-items:center or a margin-bottom nudge here. */
    .onb-name-tag-row .onb-avatar-tap-hint{position:static;box-shadow:none;flex-shrink:0}
    @keyframes onbTapHintPulse{0%,100%{transform:scale(1)}50%{transform:scale(1.15)}}
    @keyframes onbLangFadeUp{from{opacity:0;transform:translateY(22px)}to{opacity:1;transform:translateY(0)}}
    .onb-glow-ring{display:none}
    /* Was onbGoldBlink (1.3s, up to full-opacity flash) — read as a notification blink rather
       than a premium glow, and was noticeably faster than every other animated element on the
       page (wordmark 3.6s, chase ring 3s, price-card ring 2.6s). Reusing onbAvatarRingGlow here
       keeps the gold "premium" language calm and consistent across the whole onboarding flow. */
    /* RING IS GREEN, NOT GOLD, 2026-08-09 (Shane's call, on the merged step-A panel).
       Gold KEEPS THE PULSE - that is still the one thing the subscription buys and
       the locked rule that free/PLUS never animate is untouched. Only the hue moved.
       The glow colour moved with it: a gold halo around a green ring looked like the
       gold had simply been left behind.
       FLAGGED TO HIM, unresolved: green is the RESERVED Vent Buddy staff colour
       (mint ring + the 105-175 degree name band). A green ring on every member's
       avatar collides with that signal. If staff identification matters more, this
       is the rule to reverse. */
    /* PLUS, added 2026-08-09. Written ABOVE the .gold rule on purpose: if an
       element ever carried both classes, the later rule wins and gold must win.
       STATIC - box-shadow, not an animation. PLUS never moves; the pulse is the
       one thing the Gold subscription buys. */
    .onb-stage-avatar-wrap.plus .onb-stage-avatar{border-color:var(--silver);border-width:3px;box-shadow:var(--silver-glow)}
    .onb-stage-avatar-wrap.gold .onb-stage-avatar{border-color:#B8D7AC;border-width:3px;animation:onbAvatarRingGlowGreen 2.6s ease-in-out infinite}
    @keyframes onbAvatarRingGlowGreen{0%,100%{box-shadow:0 0 9px 1px rgba(184,215,172,.45)}50%{box-shadow:0 0 18px 3px rgba(184,215,172,.75)}}
    /* This base rule used to always run onbAvatarGlowPulse (2.6s), meaning EVERY avatar —
       free, illustrated, gold — was constantly pulsing/glowing regardless of tier. That's
       what kept looking like "still blinking" even after the gold-specific fix above.
       Now static by default; the animated glow above is reserved for .gold only, so it
       actually functions as a premium-tier signal instead of a universal effect. */
    /* NO GOLD GLOW HERE ANY MORE, 2026-08-09. The base carried a gold hairline plus
       an outer AND inset gold box-shadow, so every avatar at every tier sat in a
       yellow halo - the thing Shane asked to remove. Ring is now a light green
       hairline and the interior is the PAGE GREEN, so the circle reads as a hole in
       the panel rather than a gold disc sitting on it. Do not put a gold border or
       box-shadow back on this rule. */
    /* RING IS SILVER AT EVERY TIER, 2026-08-09 (Shane asked three times). It was a
       pale green hairline. CONSEQUENCE, stated to him: free and PLUS now differ only
       by WEIGHT - free 1px flat, PLUS 3px with a halo - not by colour. Gold is still
       the green pulse and is the only one that moves. */
    /* THE RAMP IS PAINTED BY ::before, NOT by a background, 2026-08-09. The usual
       two-layer `linear-gradient(fill) padding-box, ramp border-box` trick NEEDS AN
       OPAQUE INTERIOR to cover the middle - and this circle must stay transparent so
       the container shows through. So the ring is drawn the same way the chase light
       is: a gradient box masked to its own 2px padding ring with composite:exclude.
       The transparent 2px border stays on the element itself so the .plus and .gold
       rules can still recolour a real border; they hide this layer instead. */
    /* `mask` MUST BE SET ALONGSIDE `-webkit-mask` — 2026-08-11. Only the webkit
       property was declared, so browsers that support the STANDARD property (and
       the standard mask-composite below it) got a composite rule with nothing to
       composite: the ring's ramp then showed as a stray bar down one side of the
       circle rather than a ring. Both prefixes, always, or neither works. */
    .onb-stage-avatar::before{content:'';position:absolute;inset:0;border-radius:50%;padding:3px;background:var(--silver-ramp);
      /* 2px -> 3px, 2026-08-11. At the sizes this ring is actually drawn (72px
         upward) a 2px band read as a thin white outline rather than metal. The
         big preview overrides this again to 4px; the two scale together. */
      -webkit-mask:linear-gradient(#000 0 0) content-box,linear-gradient(#000 0 0);mask:linear-gradient(#000 0 0) content-box,linear-gradient(#000 0 0);
      mask:linear-gradient(#000 0 0) content-box,linear-gradient(#000 0 0);
      -webkit-mask-composite:xor;mask-composite:exclude;pointer-events:none;z-index:2}
    .onb-stage-avatar-wrap.plus .onb-stage-avatar::before,.onb-stage-avatar-wrap.gold .onb-stage-avatar::before{display:none}
    /* NO FILL AT ALL, 2026-08-09. It was #759B6D, hand-matched to the panel - and
       hand-matched values drift the moment either side is repainted, which is why
       this kept reading darker than the container. `transparent` cannot drift: the
       circle IS the container showing through, whatever colour the container is.
       Illustrated faces are <img> and cover it anyway. Do not put a colour back. */
    /* line-height:1 IS THE CENTRING FIX — 2026-08-11. An emoji inherits the body's
       1.6 line-height, so at 156px the LINE BOX is ~250px tall inside a 220px
       disc: flex centres the box, not the glyph, and the face sits visibly low.
       Forcing the line box to the glyph height makes align-items:center land on
       the face itself. Applies at every size, which is why it is set here rather
       than per breakpoint. */
    .onb-stage-avatar{position:relative;width:44px;height:44px;border-radius:50%;background:transparent;border:2px solid transparent;display:flex;align-items:center;justify-content:center;line-height:1;font-size:26px;z-index:1;overflow:hidden}
    /* NICKNAME IS WHITE AT EVERY TIER on this card (Shane, 2026-08-09). Tier is
       carried by the GLOW and by the Maskara badge beside the name, never by the
       letter colour - the badge already says "Gold", so colouring the letters
       gold as well said it twice.
       NO TEXT-SHADOW OF ANY KIND, 2026-08-09 (Shane: "maybe dont put shadow").
       The name is plain white at every tier. This supersedes TWO earlier rules:
       the 2026-08-04 base `0 2px 5px rgba(0,0,0,.9)`, which on the green card
       rendered as a dark green slab behind the letters, and the per-tier glows
       that briefly replaced it. TIER IS STILL FULLY LEGIBLE without them - the
       avatar ring beside the name carries it (blue free, red PLUS, pulsing amber
       Gold) and the Maskara badge marks Gold outright. A glow on the letters was
       saying the same thing a third time.
       The base rule ALSO used to carry a white+GOLD text-shadow, which handed the
       paid marker to every free user in a dimmer shade - the exact opposite of
       the tier rule. Do not put ANY text-shadow back on this element. */
    .onb-stage-name-tag{color:#fff;font-family:var(--font-serif);font-style:italic;font-weight:700;font-size:1.2rem;margin:2px 0 0;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
    .onb-stage-name-row{display:flex;align-items:center;justify-content:center;gap:8px}
    .onb-stage-name{font-weight:700;font-size:.95rem;color:var(--onb-ink);display:none}
    .onb-stage-name.gold{color:var(--onb-gold)}
    .onb-tier-tag{font-size:.6rem;font-weight:700;padding:2px 5px;border-radius:999px;letter-spacing:.03em;text-transform:uppercase;margin-left:auto}
    .onb-tier-tag.free{color:var(--onb-blue);background:var(--onb-blue-soft)}
    .onb-tier-tag.illustrated{color:var(--onb-silver);background:var(--onb-silver-soft)}
    .onb-tier-tag.gold{color:var(--onb-gold);background:var(--onb-gold-soft)}
    .onb-stage-sub{font-size:.85rem;color:var(--onb-muted);font-style:normal;margin:2px 0 0}
    .onb-hero{text-align:center;padding:4px 4px 18px;margin-bottom:14px}
    .onb-eyebrow{font-size:.8rem;letter-spacing:.14em;text-transform:uppercase;color:var(--onb-muted);font-weight:700;margin-bottom:18px}
    .onb-h1{font-family:var(--font-serif);font-style:italic;font-weight:600;font-size:2.2rem;line-height:1.2;margin:0 0 16px;color:var(--onb-ink)}
    .onb-hero-p{color:var(--onb-muted);font-size:.92rem;margin:0;white-space:nowrap}
    /* Synced with .gate-card (age gate): flat dark bg + thin gold border + var(--radius),
       instead of the old purple glass/blur look, so the mask/username/nickname steps
       match the gate screen's container instead of diverging from it. */
    /* SAME FILL AS index.html's confession card: white at .16 -> .07 over
       whatever the ground is. The old version applied --panel-hi/lo-rgb (which
       ALREADY are that white-over-#4E7A45 result) at a further 0.62/0.5 alpha,
       diluting the card back toward the ground. Keep this white-based so it
       stays correct if the ground colour ever changes again. */
    .onb-card{background:linear-gradient(160deg,rgba(255,255,255,.10) 0%,rgba(255,255,255,.035) 100%);backdrop-filter:blur(16px);-webkit-backdrop-filter:blur(16px);border:1px solid var(--glass-line);border-radius:20px;padding:14px 16px;margin-bottom:10px;position:relative;overflow:hidden;box-shadow:0 20px 50px rgba(0,0,0,0.5),inset 0 1px 0 rgba(255,255,255,0.06);transition:border-color .3s,box-shadow .3s}

    /* STEP A IS ONE CONTAINER, 2026-08-09. The mask row, "Who are you here?" and
       "Pick a username" are three .onb-card boxes, but they are three parts of ONE
       form - three separate borders made them read as three unrelated tasks. They
       are now fused into a single panel: only the OUTER corners round, and the
       shared edges become the dividers between sections.
       THE SELECTORS MUST GO THROUGH .onb-timeline-item. THE THREE CARDS ARE NOT
       SIBLINGS - each one is wrapped in its own
       .onb-timeline-item > .onb-timeline-content, because step A is built as a
       timeline with an optional icon rail down the left. A first attempt used
       `.onb-card + .onb-card` and `:has(+ .onb-card)` and matched NOTHING, so the
       three boxes stayed exactly as they were. If this ever stops working, check
       the wrapper depth in app.html FIRST.
       TWO separate gaps had to go, not one: .onb-card's own margin-bottom AND
       .onb-timeline-content's padding-bottom (14px, 6px on narrow phones). Zeroing
       only the margin leaves a visible seam.
       The gradient is flattened to one flat value because a 160deg ramp restarting
       in each section banded visibly once the gaps closed.
       KNOWN, flagged to Shane: every card carries its own .onb-card-chase, so the
       moving gold light now traces each SECTION rather than the panel. */
    /* THE PANEL IS NOW .onb-timeline ITSELF, 2026-08-09 - not three cards styled to
       look joined. Earlier passes gave each card the same fill and rounded only the
       outer corners; the sections still read as different colours because the LAST
       card's `box-shadow:0 20px 50px rgba(0,0,0,.5)` painted over the bottom of the
       card above it (a later sibling paints on top), darkening one section's lower
       half. One background on one element cannot disagree with itself, so that
       whole class of bug is gone. The cards keep their padding and become
       transparent, borderless, square rows; the divider is a border-bottom on the
       timeline ITEM. If a fourth section is ever added it inherits all of this
       automatically. */
    /* SOLID LITERAL REPLACED — 2026-08-11. #759B6D is a pre-L24 green, so when the
       ground dropped this panel stayed where it was and became the lightest thing
       on the screen. Shane: "the container in who are you here is like a very
       light one". Now the same translucent card material as .post-card and
       .onb-card, so it moves with the ground and with any picked theme. */
    #nick-onb-step-a .onb-timeline{position:relative;background:linear-gradient(180deg,rgba(255,255,255,.10),rgba(255,255,255,.035));border:1px solid rgba(255,255,255,.12);border-radius:20px;box-shadow:0 20px 50px rgba(0,0,0,0.5)}
    #nick-onb-step-a .onb-timeline-content{padding-bottom:0}
    /* NO DIVIDER LINES INSIDE THE PANEL — Shane, 2026-08-10: "i dont want lines
       in the big containr". The border-bottom that used to sit on every item but
       the last is gone; SPACING separates the sections now, which is why each
       row's .onb-card keeps its 14px vertical padding. Do not reinstate the rule
       to "tidy up" a section boundary. */
    #nick-onb-step-a .onb-timeline-item{border-bottom:none}
    #nick-onb-step-a .onb-timeline-item .onb-card{margin-bottom:0;background:none;border:none;border-radius:0;box-shadow:none;backdrop-filter:none;-webkit-backdrop-filter:none}
    /* SPACING PASS — Shane, 2026-08-10: "fix the indnention space theres a lot of
       rroms". Once the divider lines came out, the gap between two sections was
       the sum of FOUR values, not one: the field's error line (min-height 14 +
       8 above + 10 below = 32px, reserved even when the line is empty), plus
       14px of card padding on each side of the seam. That is ~60px of nothing
       between the name field and "Pick a username", which is what he is seeing.
       Both are tightened, and ONLY inside step A so the same rules keep their
       original spacing on the recovery screen and the face picker.
       The error line keeps its min-height on purpose: collapsing it entirely
       makes the whole panel jump the instant "Available" appears. */
    #nick-onb-step-a .onb-timeline-item .onb-card{padding-top:6px;padding-bottom:6px}
    #nick-onb-step-a .nick-field-err{margin:4px 0 0 2px;min-height:12px}
    /* The face row keeps a little more air above it — it is the top of the panel
       and the avatar ring is a big round shape that reads as cramped against a
       square corner. Everything BELOW it is tight. */
    #nick-onb-step-a .onb-timeline-item:first-child .onb-card{padding-top:12px}
    #nick-onb-step-a .onb-timeline-item:last-child .onb-card{padding-bottom:12px}
    /* The two fields sit a step DOWN from the panel so they still read as inputs.
       On the old translucent panel they were var(--glass); against a solid fill an
       alpha would drift again. */
    /* Same story: #688E60 was picked to sit on the old panel above. The field has
       to stay DARKER than its card or it reads as a highlight rather than an
       input, so this one goes down, not up. */
    /* OUTLINE, NO FILL — Shane picked C, 2026-08-11. The 20% black fill made the
       field a hole punched in an already-dark card. A field only has to look
       RECESSED relative to its card, and on a translucent card an outline does
       that without adding a third surface.
       THE BORDER IS DELIBERATELY BRIGHTER (.24) than the card's own hairline
       (.12). With no fill, the edge is the ONLY thing saying "type here" — a
       faint outline on a phone in daylight is an invisible input.
       FOCUS MATTERS MORE NOW, for the same reason: with nothing filled, the only
       feedback that a field is active is its border, so focus gets the accent. */
    #nick-onb-step-a .onb-name-input,#nick-onb-step-a .onb-input{background:none;border-color:rgba(255,255,255,.24)}
    #nick-onb-step-a .onb-name-input:focus,#nick-onb-step-a .onb-input:focus{border-color:var(--neon);box-shadow:0 0 0 1px rgba(var(--neon-rgb),.30)}
    /* ONE CHASE LIGHT AROUND THE WHOLE PANEL, 2026-08-09 (Shane: "make it running in
       the whole big container"). The three per-card .onb-card-chase divs are hidden
       inside step A and the same conic-gradient border is redrawn once on the
       timeline. Identical technique - a padding-box/content-box mask with
       composite:exclude, which paints the gradient in the 2px ring only. It runs
       continuously here rather than waiting for .chase-on, because a panel-wide
       light cannot mark which SECTION you are in, which was the per-card version's
       only job.
       TAIL SOFTENED 2026-08-09 from a single hard stop to a graded ramp (mock A,
       approved): the head stays solid --neon and the tail fades out behind it, so
       it reads as one light travelling rather than a bright wedge appearing. The
       stops use --neon-rgb (an r,g,b triplet) rather than --neon, because a
       conic-gradient cannot fade a hex var() to transparent on its own. The
       triplet is defined in BOTH themes, so light mode still darkens the light. */
    #nick-onb-step-a .onb-card-chase{display:none}
    #nick-onb-step-a .onb-timeline::after{content:'';position:absolute;inset:0;border-radius:inherit;padding:2px;background:conic-gradient(from var(--onb-chase-angle),rgba(var(--neon-rgb),0) 0%,rgba(var(--neon-rgb),0) 62%,rgba(var(--neon-rgb),.18) 76%,rgba(var(--neon-rgb),.85) 88%,var(--neon) 92%,rgba(var(--neon-rgb),.20) 96%,rgba(var(--neon-rgb),0) 100%);-webkit-mask:linear-gradient(#000 0 0) content-box,linear-gradient(#000 0 0);mask:linear-gradient(#000 0 0) content-box,linear-gradient(#000 0 0);-webkit-mask-composite:xor;mask-composite:exclude;pointer-events:none;animation:onbChaseSpin 2s linear infinite}
/* SLOWED, NOT STOPPED, 2026-08-09. `animation:none` here does not remove the
       light - it leaves the conic gradient frozen mid-rotation, so the panel keeps a
       bright gold wedge parked on one edge and looks broken rather than calm. Same
       decision already made for the entry-page carousel. If Windows has "show
       animations" off (or macOS Reduce Motion), THIS is the rule that was making
       the light look dead. */
    @media (prefers-reduced-motion:reduce){#nick-onb-step-a .onb-timeline::after{animation-duration:5s}}
    @property --onb-chase-angle{syntax:'<angle>';inherits:false;initial-value:0deg}
    .onb-card-chase{position:absolute;inset:0;border-radius:inherit;padding:2px;background:conic-gradient(from var(--onb-chase-angle),transparent 0%,transparent 74%,var(--neon) 88%,transparent 100%);-webkit-mask:linear-gradient(#000 0 0) content-box,linear-gradient(#000 0 0);mask:linear-gradient(#000 0 0) content-box,linear-gradient(#000 0 0);-webkit-mask-composite:xor;mask-composite:exclude;opacity:0;pointer-events:none;transition:opacity .25s}
    /* Face card (container 1) keeps the chase running immediately, as intended. */
    #onb-card-face .onb-card-chase{opacity:1;animation:onbChaseSpin 3s linear infinite}
    .onb-card-chase.chase-on{opacity:1;animation:onbChaseSpin 3s linear infinite}
    /* Nickname + username cards (containers 2 & 3): no animation by default. JS assigns
       the animation fresh (with a synced negative delay) on dice roll / first keystroke —
       see onbActivateChase(). Toggling animation-play-state instead was unreliable across
       browsers for landing on the right phase, so we create the animation instance directly. */
    @keyframes onbChaseSpin{from{--onb-chase-angle:0deg}to{--onb-chase-angle:360deg}}
    .onb-timeline-icon.onb-completed{position:relative;background:transparent}
    /* Sized in em so the one mobile override (font-size:9px) shrinks the glyph too. */
    .onb-timeline-glyph{width:1.35em;height:1.35em;flex-shrink:0}
    /* BARE gold tick, no filled disc and no ring behind it. The disc + its
       2px outline existed to lift the badge off a circled icon; with the
       circle gone there is nothing to lift it off, and the disc was the
       heaviest thing on the rail. */
    .onb-timeline-icon-check{position:absolute;bottom:-2px;right:-6px;width:12px;height:12px;border-radius:0;background:none;display:none;align-items:center;justify-content:center;color:var(--neon);box-shadow:none}
    .onb-timeline-icon.onb-completed .onb-timeline-icon-check{display:flex}
    .onb-section-num{font-size:.68rem;font-weight:700;letter-spacing:.1em;text-transform:uppercase;color:var(--onb-gold);margin:0 0 8px}
    .onb-timeline{display:flex;flex-direction:column}
    .onb-timeline-item{display:flex;gap:14px;align-items:stretch}
    /* RAIL REMOVED 2026-08-09 - the three step glyphs, the connector line and
       the completed ticks all live in this column, so one rule takes the whole
       ornament out and the cards run full width. Every rule below still exists
       and is simply inert; DELETE THIS ONE LINE to bring the rail back exactly
       as it was. The cards already report their own state (chase light on the
       active card, filled fields), so no information is lost with it gone. */
    .onb-timeline-icon-col{display:none}
    .onb-timeline-icon-col.rail-on{display:flex;flex-direction:column;align-items:center;width:38px;flex-shrink:0}
    /* STEP CIRCLES ARE NEON. The glyphs used to be system emoji (theatre mask,
       tag, bust) which carry their own baked-in blue/purple/gold and cannot
       inherit a colour - that is why every repaint left them behind. They are
       now inline SVG line icons in app.html taking currentColor from here. */
    /* GLYPH ONLY - no circle, no fill, no glow. The rail is orientation, not
       an action, so it stays NEUTRAL; gold on this screen means brand + press
       this + step done. Only the tick is gold. */
    .onb-timeline-icon{width:38px;height:38px;border-radius:50%;border:none;background:none;color:#EDF3E8;display:flex;align-items:center;justify-content:center;font-size:16px;flex-shrink:0;position:relative;transition:color .3s}
    .onb-timeline-icon.onb-active{box-shadow:none;color:#FFFFFF}
    /* The connector belongs to the same component as the circles above, so it
       follows them to neon. Left gold it read as a mistake, not a decision. */
    .onb-timeline-line{flex:1;width:1px;min-height:22px;background:#DCE7D6;margin:6px 0;opacity:.28}
    .onb-timeline-content{flex:1;min-width:0;padding-bottom:14px}
    .onb-face-card{display:flex;align-items:center;gap:22px}
    /* Avatar interior is TRANSPARENT, not a solid colour, so it shows the card
       through it and matches at every avatar size. .onb-card is a GRADIENT, so
       any single hex would match at one height only and drift above/below. */
    /* Avatar interior = THE PANEL COLOUR, not the page colour. This override exists
       because it beats the base rule on this card; it was `transparent` (gold showed
       through) then the page green (a dark disc on a lighter panel). It has to match
       whatever the avatar SITS ON, which since 2026-08-09 is the timeline panel. */
    .onb-face-card .onb-stage-avatar{background:transparent}
    .onb-face-copy{flex:1;min-width:0;text-align:left}
    .onb-face-card .onb-stage-name-row{justify-content:flex-start}
    .onb-face-card .onb-tier-tag{margin-left:0}
    .onb-face-card .onb-stage-sub{margin-top:2px}
    /* Same colour as .onb-card-h2 ("Who are you here?") - both var(--onb-ink).
       This was --onb-muted, which is why PICK A USERNAME read lavender. */
    .onb-card-label{font-size:.74rem;font-weight:700;color:var(--onb-ink);text-transform:uppercase;letter-spacing:.05em;margin-bottom:7px}
    .onb-card-h2{font-family:var(--font-serif);font-style:italic;font-weight:600;font-size:1.35rem;margin:0 0 5px;color:var(--onb-ink);display:flex;align-items:center;gap:12px}
    .onb-card-how{color:var(--onb-muted);font-size:.85rem;margin:0 0 8px;line-height:1.45}
    .onb-input{width:100%;background:var(--glass);border:1px solid rgba(255,255,255,0.16);border-radius:10px;padding:12px 14px;color:var(--cream);font-size:.95rem;font-weight:600;outline:none;font-family:var(--font-ui)}
    .onb-name-input-row{display:flex;gap:10px}
    .onb-name-input{flex:1;min-width:0;background:var(--glass);border:1px solid rgba(255,255,255,0.16);border-radius:10px;padding:15px 14px;color:var(--cream);font-size:.95rem;font-weight:600;outline:none;font-family:var(--font-ui)}
    .onb-name-input.rolling{opacity:1}
    .onb-name-input.revealed{animation:nameReveal .5s ease forwards}
    .onb-name-input::placeholder,.onb-input::placeholder{color:rgba(255,246,230,0.4)}
    .onb-name-input:focus,.onb-input:focus{border-color:var(--gold);box-shadow:0 0 0 3px rgba(244,197,66,0.18)}
    /* Two-arrow refresh icon (replaces the 3D tumbling die) */
    .onb-refresh-spin{display:flex;align-items:center;justify-content:center}
    .onb-refresh-svg{width:28px;height:28px;display:block;transform-origin:50% 50%}
    .onb-dice-btn.spinning .onb-refresh-svg{animation:onbRefreshSpin .28s linear infinite}
    @keyframes onbRefreshSpin{from{transform:rotate(0deg)}to{transform:rotate(360deg)}}
    .onb-dice-btn{width:60px;flex-shrink:0;border-radius:10px;border:1px solid rgba(255,255,255,0.16);background:var(--glass);color:#DCE7D6;font-size:29px;cursor:pointer;display:flex;align-items:center;justify-content:center}
    /* WHITE, 2026-08-09. Was --onb-muted (#a99fc6, the stale pre-green purple).
       This sits on the FIRST screen a returning user sees, and it was the dimmest
       thing on it - someone coming back to log in had to find the faintest line on
       the page. The comment below still holds: only "Log in" is gold and underlined,
       so the tappable word is still the one that stands out. */
    /* MATCHES index.html's #login-link — Shane, 2026-08-10: "IN INDEX THE ALREADY
       HAVE A MASK IS ITALLIC. IT SHOULD BE SAME IN APP". On the entry page the
       italic is INHERITED, not declared: index's `body` sets
       font-style:italic globally, so every string on that page is italic unless
       something turns it off. app.html has no such body rule, which is the whole
       reason the two screens diverged on a line that reads identically.
       Declared explicitly here rather than on a body — one line, one screen, no
       chance of italicising the whole app by accident. */
    /* SIZES DIVIDED BY --onb-fit-scale — 2026-08-10. #onb-fit-script zooms the
       whole .nick-card up to 1.15 to fill a big monitor; index.html has no such
       script. So identical rem values still rendered up to 15% bigger here,
       which is why this line kept looking wrong next to the entry page even
       after every property matched. Dividing by the published scale cancels the
       zoom for THIS LINE ONLY — the rest of the card still fills the screen.
       Fallback of 1 matters: the variable is only set once the script runs, and
       on mobile it is set to 1 explicitly since mobile never scales.
       Values must stay in step with index.html's .login-link (.8rem) and
       .login-link b (.84rem). Change one, change the other. */
    /* FONT — THE REAL CAUSE OF "IT'S NOT THE SAME", found 2026-08-10 by finally
       reading fonts/fonts.css. That file ships Inter in **font-style:normal
       only**, weights 300/400/500/600. There is NO italic Inter face and NO 700
       face. So in app.html this line was getting SYNTHESISED oblique (the
       browser mechanically slanting the upright face) and SYNTHESISED bold (the
       browser smearing the 600). index.html loads NO webfont at all — a
       deliberate decision recorded in its own head comment — so 'Inter' is
       simply unavailable there and it falls through to system-ui, which has a
       REAL italic and a REAL bold. Same CSS, genuinely different letterforms.
       Matching sizes and colours could never have fixed this.
       Declaring system-ui here makes the app render exactly what the entry page
       renders, AND stops this line from being faked-slanted faked-bold — which
       was bad typography regardless of matching.
       IF fonts.css EVER GAINS AN ITALIC/700 INTER, or index.html starts loading
       fonts.css, revisit this line — the two pages would diverge again. */
    .onb-skip-link{display:block;text-align:center;font-family:system-ui,-apple-system,'Segoe UI',sans-serif;font-size:calc(.8rem / var(--onb-fit-scale,1));font-style:italic;color:var(--text-muted);margin:10px 0 26px;text-decoration:none;cursor:default}
    /* Only the actionable "Log in" word gets the gold accent + underline so it
       visually reads as tappable. */
    /* MATCHES index.html's "Log in" EXACTLY — Shane, 2026-08-10: "EVEN THE LOGIN
       WORD ITS NOT THE SAME. FIX. ALSO DELETE THE LINE IN LOGIN IN APP".
       Was `var(--onb-gold)` (#e8c25a, the older duller amber) plus an underline.
       Now `var(--neon)` (var(--neon)), the loud gold the whole app moved to, and no
       rule — the same two changes he already made on the entry page.
       --neon, NOT a hardcoded var(--neon): the token is already defined in app.css
       (line 16) AND overridden for light mode (line 58, #8F6F1C). A literal hex
       here would stay bright gold on a light background and disappear.
       DELIBERATELY SCOPED TO THIS ONE SELECTOR. --onb-gold also paints the slide
       border, the "Logged in as" name and the completed-slide state, so
       retokenising it would silently repaint the whole signup screen. */
    .onb-skip-link-cta{color:var(--neon);text-decoration:none;cursor:pointer;font-weight:700;font-size:calc(.84rem / var(--onb-fit-scale,1))}
    .onb-skip-link-cta:hover{color:var(--neon);filter:brightness(1.12)}
    .onb-hint-box{font-size:.85rem;color:var(--onb-muted);line-height:1.5;margin:-6px 0 14px}
    .onb-pricing-head{margin:10px 0 4px;text-align:center}
    .onb-pricing-head h2{font-family:var(--font-serif);font-style:italic;font-weight:600;font-size:1.3rem;margin:0 0 3px;color:var(--onb-ink)}
    .onb-pricing-head p{color:var(--onb-muted);font-size:.58rem;margin:0}
    .onb-pricing-row{display:flex;flex-direction:column;gap:10px;margin:10px 0}
    /* "Your mask" isn't inside the onboarding's .onb-skin wrapper, so the
       .onb-price-card color variables (blue/red/gold) are otherwise undefined
       there and the cards render flat with no accent. Supply just the color
       variables here — not .onb-skin's full-bleed layout/margin overrides,
       which would break this modal's own spacing. Scoped to the whole modal
       (not just the cards) so the nickname input and font buttons below can
       also be re-themed to the same panel color as the cards. */
    #profile-modal{
      /* THEME-DERIVED, 2026-08-11. These were the literals
         rgba(106,143,99,.85) / #759B6D / rgba(255,255,255,.10) / #F7FBF4 —
         i.e. the ORIGINAL GREEN, frozen. That is why the Font tab's option
         buttons stayed green on every theme while the sheet around them
         recoloured. Declared HERE on #profile-modal, which is BELOW body, so
         var(--panel-hi-rgb) and friends resolve against whatever applyTheme
         wrote — unlike the :root --void alias, which could not. */
      --onb-panel:rgba(var(--panel-hi-rgb),.85); --onb-panel-2:var(--surface-2);
      --onb-line:var(--border); --onb-ink:var(--text); --onb-muted:var(--text-muted);
      --onb-gold:#e8c25a; --onb-gold-soft:rgba(232,194,90,.28);
      --onb-blue:#C7CCD4; --onb-blue-soft:rgba(199,204,212,.26); /* NAME KEPT, VALUE IS SILVER since 2026-08-10 - same churn-avoidance as --gold and --neon no longer meaning what they say. Only user: .onb-tier-tag.free */
      --onb-silver:var(--silver); --onb-silver-soft:var(--silver-soft);
    }
    /* Nickname input and font buttons were using the app's generic dark
       --surface color, which read as flat black next to the price cards'
       purple panel — re-themed here so every container in this modal is the
       same color. */
    #profile-modal .emoji-input{background:var(--onb-panel);border-color:var(--onb-line);color:var(--onb-ink)}
    /* THE TWO #profile-modal .name-font-btn OVERRIDES WERE DELETED 2026-08-11.
       They pinned the rows to the onboarding panel fill and a plain border,
       which is exactly the flat green box-in-a-box Shane called lousy. The
       specimen rows near the top of this file now carry their own near-
       transparent fill and accent-tinted active state, and a scoped override
       here would silently beat them — the same show-site problem as
       renderNameColorGrid. Do not reinstate them. .emoji-input keeps its
       override because a text field genuinely does need a filled surface. */
    /* SELECTION RING FOLLOWS THE ACCENT, not --onb-gold. Which font you picked
       is a UI state, not a tier signal — --onb-gold stays #e8c25a because the
       tier cards in this same modal genuinely mean Gold by it. */
    /* (scoped .name-font-btn.active override removed — see the note above) */
    /* Live "here's what your name looks like on this tier" preview row —
       mirrors the pill onboarding's Gold card already shows (nickname +
       colored tier tag), added here to Free too so both cards demonstrate
       the actual payoff instead of just listing it as a bullet. */
    #mask-tier-cards .mtc-badge-pill{margin:8px 0 2px}
    #mask-tier-cards .mtc-pill-name{font-weight:700}
    #mask-tier-cards .mtc-pill-tag{border-radius:999px;padding:1px 7px;font-weight:700;font-size:.55rem;flex-shrink:0}
    /* Gold buy button — same primary gradient as onboarding's .onb-step-btn.primary,
       so this is visibly the tier upgrade CTA rather than a plain muted button. */
    #mask-tier-cards .mtc-gold-cta{background:linear-gradient(180deg,#f0d980 0%,var(--gold) 100%)!important;color:#241516!important;font-weight:800;text-align:center;text-transform:uppercase;letter-spacing:.03em;padding:12px 16px!important;border-radius:12px!important;box-shadow:0 2px 10px rgba(var(--neon-rgb),.25);border:none!important}
    #mask-tier-cards .mtc-gold-cta:hover{filter:brightness(1.04)}
    #mask-tier-cards .mtc-gold-cta:disabled{opacity:.5;cursor:not-allowed;filter:grayscale(.4)}
    /* Explicit "open the icon picker" button inside each expanded tier card —
       tapping the card itself still works too (expand, then tap again opens
       the picker), but that second tap was never obvious on its own, so this
       gives every tier (Free/Illustrated/Gold) one unambiguous button instead
       of relying on people guessing "tap it again." */
    .mtc-browse-btn{width:100%;margin-top:10px;padding:10px 14px;border-radius:10px;border:1px solid var(--border-gold);background:rgba(var(--neon-rgb),.08);color:var(--gold);font-weight:700;font-size:.78rem;font-family:inherit;cursor:pointer;transition:background .15s}
    .mtc-browse-btn:hover{background:rgba(var(--neon-rgb),.16)}
    /* Browse button must match EACH tier's own color (blue/red/gold), same as every
       other tier control in the app (.avatar-tier-btn, .ptier-tab, .onb-chip). Gold
       styling is reserved for the actual paid Gold tier — using it on the Free/
       Illustrated buttons too made "Browse Illustrated faces" look like a purchase
       button, when browsing itself is free (payment only happens if you apply a
       locked icon). */
    .mtc-browse-btn[data-tier="free"]{border-color:var(--onb-silver);background:rgba(199,204,212,.10);color:var(--onb-silver)}
    .mtc-browse-btn[data-tier="free"]:hover{background:rgba(199,204,212,.20)}
    /* ONE FILL FOR EVERY CARD, 2026-08-09. Shane: "the background of gold are darker
       compare to free... follow the free container full background color."
       CAUSE: the Free card's silver rule REPLACED this rule's background instead of
       sitting on top of it, so Free became a light wash while Gold kept the darker
       --onb-panel. The fix is one shared fill here rather than a second colour on
       Gold - two cards hand-matched to each other drift apart the moment either is
       repainted, which is the same trap the stage avatar's interior fell into.
       Tier is carried by the BORDER, the avatar ring and the label. Not by the fill. */
    .onb-price-card{background:linear-gradient(180deg,rgba(255,255,255,.20) 0%,rgba(255,255,255,.11) 55%,rgba(255,255,255,.08) 100%),var(--onb-panel);border:1px solid var(--onb-line);border-radius:14px;padding:0;cursor:pointer;transition:border-color .2s ease,box-shadow .2s ease;overflow:hidden;text-align:left}
    .onb-price-card:active{transform:scale(.995)}
    /* FOCUS RING, 2026-08-09. Shane: "the background colors for containers are still
       not the same". MEASURED off his screenshot with PIL - both card interiors are
       exactly (140,164,132). The fills were never different. What differed was a hard
       BLACK ring with a white halo around whichever card had been clicked: these are
       <div role="button" tabindex="0">, so a click gives them keyboard focus, and with
       no focus style of our own the browser draws its stock outline. That ring made one
       card read as heavier and boxed-in.
       Not simply removed - these cards are keyboard-reachable and a keyboard user needs
       to see where they are. Replaced with the tier's own colour, drawn as a box-shadow
       so it paints OUTSIDE the layout and cannot shift the row. MEASURE BEFORE CHANGING
       A COLOUR HERE: twice now the visible problem has turned out not to be the colour
       that was blamed for it. */
    .onb-price-card:focus{outline:none}
    .onb-price-card:focus-visible{outline:none}
    .onb-price-card.silver:focus-visible{box-shadow:0 0 0 2px var(--onb-silver)}
    .onb-price-card.gold:focus-visible{box-shadow:0 0 0 2px var(--onb-gold)}
    /* FREE IS SILVER FROM 2026-08-09 (was blue #4a7cf6). Shane's call, on the
       pricing cards ONLY - the free AVATAR RING, the free name colour, the feed
       card's left edge and the face-picker tabs are all still blue and were
       deliberately not touched. Class renamed .blue -> .silver in both markup
       sites; no JS ever selected it.
       FLAGGED AND OVERRULED: --silver is also PLUS (the 08-09 metal ladder,
       nothing -> silver -> gold), so the same metal now means Free on a card and
       PLUS on a ring. He was shown this side by side and chose silver anyway.
       Silver reads far better than blue on the green ground, which is the win. */
    /* FILLED SILVER, 2026-08-09 - but TINTED, not solid. A solid #C7CCD4 card would
       put white text at roughly 1.6:1, i.e. unreadable, and white body text is what
       Shane asked for one message earlier. These three stops are --silver-ramp's own
       shape held at low alpha over the green panel, so it reads as brushed metal with
       a light source from above while the ground stays dark enough for white to sit
       on it. IF A GENUINELY SOLID SILVER CARD IS WANTED, the text must flip to
       --silver-ink (#23262B) in the same edit - the two cannot both change one at a
       time without a round where the card is unreadable. */
    .onb-price-card.silver{border-color:rgba(199,204,212,.42)}
    .onb-price-card.silver.expanded{border-color:var(--onb-silver)}
    /* CLASS NAME LEFT AS .red ON PURPOSE - it is written by app.html and the JS
       in several places, so renaming it to .silver is churn with no visible gain.
       It means PLUS, whatever colour PLUS happens to be. */
    /* .onb-price-card.red was the PLUS card. Deleted from the markup 2026-08-09;
       these rules are kept only so a stray .red never renders unstyled. */
    .onb-price-card.red{border-color:rgba(199,204,212,.35)}
    .onb-price-card.red.expanded{border-color:var(--onb-silver)}
    .onb-price-card.gold{border-color:rgba(var(--neon-rgb),.65);animation:onbGoldGlow 2.6s ease-in-out infinite}
    .onb-card-summary{display:flex;align-items:center;gap:12px;padding:12px 14px;transition:padding .2s ease}
    .onb-price-card.expanded .onb-card-summary{padding:16px 18px 10px}
    .onb-card-avatar-row{position:relative;display:flex;align-items:center;justify-content:center;flex-shrink:0}
    /* INTERIOR IS TRANSPARENT, 2026-08-09 (Shane, on the Gold ring). It was
       --onb-panel-2 #759B6D, darker than the card it sits on, so the circle read as
       a disc placed on the card rather than a hole in it.
       TRANSPARENT RATHER THAN A LIGHT GREEN VALUE, and this is the same call already
       recorded on .onb-stage-avatar: a hand-matched fill drifts the moment either
       side is repainted, which is exactly how this one ended up wrong. Transparent
       IS the card, whatever colour the card becomes. Do not put a colour back.
       Illustrated and Gold faces are <img> and cover it anyway. */
    .onb-card-avatar{position:relative;width:40px;height:40px;border-radius:50%;margin:0;display:flex;align-items:center;justify-content:center;font-size:22px;background:transparent;border:2px solid var(--onb-line);flex-shrink:0;transition:width .2s ease,height .2s ease,font-size .2s ease}
    .onb-price-card.expanded .onb-card-avatar{width:52px;height:52px;font-size:28px}
    .onb-card-avatar-badge{display:none !important}
    .onb-card-avatar-badge::before{content:'+'}
    .onb-price-card.red .onb-card-avatar-badge{background:var(--onb-silver);color:var(--silver-ink)}
    .onb-price-card.gold .onb-card-avatar-badge{background:var(--onb-gold);color:#241b16}
    .onb-card-mask-icon{position:absolute;left:100%;top:50%;transform:translateY(-50%);margin-left:6px;height:14px;width:auto}
    /* SAME RING AS "CHOOSE YOUR MASK", 2026-08-09 (Shane: "like in choose your mask").
       Copied from .onb-stage-avatar::before, including WHY it is built this way: the
       usual `linear-gradient(fill) padding-box, ramp border-box` trick needs an OPAQUE
       interior to cover the middle, and a hand-matched fill colour drifts the moment
       either side is repainted - which is exactly what made the ring read darker than
       its card. A masked ::before ring leaves the interior transparent, so the circle
       is the card showing through whatever colour the card becomes.
       My previous pass used the padding-box trick here. That was the drifting version. */
    .onb-price-card.silver .onb-card-avatar{position:relative;border-color:transparent;background:transparent;overflow:hidden}
    .onb-price-card.silver .onb-card-avatar::after{content:'';position:absolute;inset:0;border-radius:50%;padding:2px;background:var(--silver-ramp);-webkit-mask:linear-gradient(#000 0 0) content-box,linear-gradient(#000 0 0);mask:linear-gradient(#000 0 0) content-box,linear-gradient(#000 0 0);-webkit-mask-composite:xor;mask-composite:exclude;pointer-events:none;z-index:2}
    /* 40-52px, so the metal ramp is visible here: the ramp paints the BORDER
       box and a flat fill covers the padding box, leaving the gradient in the
       2px ring only. Needs a solid interior colour - an alpha would let the
       ramp show through the middle. */
    .onb-price-card.red .onb-card-avatar{border-color:transparent;background:linear-gradient(var(--onb-panel-2),var(--onb-panel-2)) padding-box,var(--silver-ramp) border-box;box-shadow:0 0 10px 2px rgba(199,204,212,.40)}
    .onb-price-card.gold .onb-card-avatar{border-color:var(--onb-gold);animation:onbAvatarRingGlow 2.6s ease-in-out infinite;padding-bottom:3px}
    @keyframes onbGoldGlow{
      0%,100%{box-shadow:0 0 10px rgba(var(--neon-rgb),.3), 0 0 0 1px rgba(var(--neon-rgb),.16)}
      50%{box-shadow:0 0 22px rgba(var(--neon-rgb),.6), 0 0 0 1px rgba(var(--neon-rgb),.32)}
    }
    /* The INSET half matters as much as the outer glow. Without it this
       animation replaces .onb-stage-avatar's box-shadow outright, killing the
       base rule's inset — so the glow lit the CARD around the ring while the
       interior stayed unlit, and the circle read as a dark disc. */
    @keyframes onbAvatarRingGlow{
      0%,100%{box-shadow:0 0 8px 2px rgba(var(--neon-rgb),.45),inset 0 0 10px 2px rgba(var(--neon-rgb),.28)}
      50%{box-shadow:0 0 18px 4px rgba(var(--neon-rgb),.85),inset 0 0 16px 3px rgba(var(--neon-rgb),.45)}
    }
    .onb-card-info{flex:1;min-width:0}
    .onb-tier-label{font-size:.7rem;font-weight:800;text-transform:uppercase;letter-spacing:.04em;display:flex;align-items:center;gap:5px}
    /* WHITE, 2026-08-09 - same call as .onb-stage-name-tag on "Choose your mask",
       where every nickname is plain #fff with no colour and no shadow at any tier.
       The silver is not lost: it is still the card's border, its fill and its avatar
       ring, so the tier is stated by the metal around the word rather than by the
       word itself. Colour is NOT copied to Gold, which keeps its gold label. */
    .onb-price-card.silver .onb-tier-label{color:#fff}
    .onb-price-card.red .onb-tier-label{color:var(--onb-silver)}
    .onb-price-card.gold .onb-tier-label{color:var(--onb-gold)}
    /* WHITE. Shane asked for this line in white when he asked for the FREE label;
       I changed only the label and left this on --onb-muted (#a99fc6, the stale
       pre-green purple). Same for .onb-unit below. */
    .onb-card-desc{font-size:.58rem;color:var(--onb-ink);margin-top:2px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
    .onb-price-card.expanded .onb-card-desc{white-space:normal;opacity:.85}
    .onb-price-card.gold .onb-card-desc{white-space:normal;opacity:.85}
    /* Gold's one-line "priority…" blurb only shows when Gold itself is the
       selected/expanded card; hide it while the user is previewing Illustrated
       or Free (the full checklist still appears when Gold is expanded). */
    .onb-price-card.gold:not(.expanded) .onb-card-desc{display:none}
    .onb-price-card.gold.expanded .onb-card-desc{display:none}
    .onb-price-card.gold .onb-card-summary{padding:14px 18px 8px}
    .onb-price-card.gold .onb-card-avatar{width:46px;height:46px;font-size:25px}
    .onb-card-price{flex-shrink:0;text-align:right;margin-left:auto;padding-left:8px}
    .onb-amount{font-family:var(--font-serif);font-style:italic;font-weight:700;font-size:1.05rem;margin:0;color:var(--onb-ink);display:flex;align-items:center;gap:4px;justify-content:flex-end;white-space:nowrap;letter-spacing:.12em}
    .onb-price-card.gold .onb-amount{color:var(--onb-gold)}
    .onb-unit{font-size:.56rem;color:var(--onb-ink);margin:0}
    .onb-card-body{display:grid;grid-template-rows:0fr;overflow:hidden;transition:grid-template-rows .35s cubic-bezier(0.4,0,0.2,1)}
    .onb-price-card.expanded .onb-card-body{grid-template-rows:1fr}
    /* Fade the inner content out/in on top of the height animation so a
       collapsed card never shows a readable sliver of its bullet list —
       opacity hits 0 quickly on close, and only fades back in once the card
       has started opening, for a clean closed state either way. */
    .onb-card-body-inner{padding:0 18px 12px;overflow:hidden;min-height:0;opacity:0;transition:opacity .12s ease}
    .onb-price-card.expanded .onb-card-body-inner{opacity:1;transition:opacity .2s ease .12s}
    .onb-tier-badge-pill{display:inline-flex;align-items:center;gap:5px;background:rgba(var(--neon-rgb),.12);border:1px solid rgba(var(--neon-rgb),.3);border-radius:999px;padding:4px 10px;font-size:.58rem;font-weight:700;color:var(--onb-gold);margin:2px 18px 9px}
    .onb-tier-divider{height:1px;background:var(--onb-line);margin:0 0 8px}
    .onb-price-card ul{list-style:none;margin:0;padding:0;text-align:left}
    /* WHITE, NOT GREY - Shane 2026-08-09. --onb-muted is #a99fc6, a PURPLE-grey
       left over from the pre-green palette, and these lines are set at .58rem.
       Small type renders optically thinner, so it needs MORE contrast than body
       copy, not less - a muted purple at 9px on a green card was the hardest
       thing on the screen to read. Hierarchy still works: the gold tick and the
       gold tier label lead, the words just stop being dim. */
    .onb-price-card li{font-size:.58rem;color:var(--onb-ink);margin-bottom:4px;line-height:1.35}
    .onb-price-card li::before{content:'— ';opacity:.5}
    .onb-price-card.gold li{padding-left:2px}
    .onb-price-card.gold li::before{content:'✓  ';opacity:1;color:var(--onb-gold);font-weight:700}
    .onb-price-card.gold .onb-card-body-inner ul{display:grid;grid-template-columns:1fr 1fr;gap:2px 14px}
    /* Empty by default now (see onboardSetAvatarTier). Colour fixed anyway so a future
       status message does not come back in --onb-muted #a99fc6, the stale pre-green
       purple. --text-muted is a soft green-white: readable on the panel, and still
       subordinate to the button above it, which is what a status line should be. */
    .onb-tier-status{text-align:center;font-size:.68rem;color:var(--text-muted);margin-top:10px}
    .onb-tier-status b{color:var(--onb-ink);font-weight:700}
    /* WHITE, 2026-08-09. --onb-muted is #a99fc6, a leftover PURPLE from the pre-green
       palette, and this label is set at .6rem - roughly 9.6px. Same reasoning as the
       feature lines: the smallest type on the screen needs the most contrast, not
       the least. */
    .onb-chip-row-label{font-size:.6rem;font-weight:800;letter-spacing:.08em;color:var(--onb-ink);margin:10px 0 4px}
    .onb-chip-row{display:flex;gap:6px;margin:0 0 8px}
    /* CHIP INK IS PER TIER AT REST, 2026-08-09. The base was --onb-muted (#a99fc6,
       the stale pre-green purple), so the unselected chip read as a disabled control
       rather than the other option. Gold now carries gold, Free carries white -
       Shane's call, matching the tier labels on the cards above. */
    /* LIGHT GREEN, 2026-08-09 (Shane, on the Gold pill). Was flat --onb-panel-2, which
       sat visibly darker than the cards above it. Same white-lift technique as the
       cards so the two rows track each other through any future repaint. */
    .onb-chip{flex:1;min-width:0;text-align:center;padding:6px 4px;border-radius:10px;background:linear-gradient(180deg,rgba(255,255,255,.20),rgba(255,255,255,.09)),var(--onb-panel-2);border:1px solid var(--onb-line);font-size:.6rem;font-weight:700;color:var(--onb-ink);cursor:pointer;transition:all .15s ease;overflow-wrap:break-word}
    .onb-chip[data-tier="gold"]{color:var(--onb-gold)}
    .onb-chip[data-tier="free"]{color:#fff}
    /* SELECTED = TINTED FILL + TIER BORDER, KEEPING THE TIER INK. Option B of a
       three-way mock; Shane picked it for Free and it is applied to Gold too so one
       control does not have two different selected states.

       WHY NOT A SOLID FILL, which is what both of these used to be: silver and gold
       are both LIGHT surfaces, so white-on-silver lands at roughly 1.5:1 and the word
       has to be hunted for. Filling the chip meant the label could not keep the tier
       colour Shane asked for - it had to flip to dark ink. A 20% tint over the green
       panel keeps the surface dark enough for the tier colour to stay, and it matches
       the Free CARD directly above, which is a tinted silver fill for the same reason.
       A solid chip does shout "selected" harder. That is the trade taken. */
    /* SUPERSEDED SAME DAY - Shane: "even a user will not click the container or
       click it should be the light green only". The tinted fills above are gone;
       BOTH chips keep the light green ground at all times, selected or not.
       Selection is now the tier-coloured BORDER, doubled by a 1px box-shadow ring
       rather than a thicker border - border-width changes the box and would shift
       the row by a pixel every time you tapped a chip. The tier ink on the label
       stays put either way. */
    .onb-chip:focus{outline:none}
    .onb-chip:focus-visible{outline:none;box-shadow:0 0 0 2px var(--onb-ink)}
    .onb-chip.active[data-tier="free"]{border-color:var(--onb-silver);box-shadow:0 0 0 1px var(--onb-silver)}
    .onb-chip.active[data-tier="gold"]{border-color:var(--onb-gold);box-shadow:0 0 0 1px var(--onb-gold)}
    .onb-grid-toggle{display:flex;align-items:center;justify-content:center;gap:5px;width:100%;padding:7px;border-radius:10px;background:var(--onb-panel-2);border:1px dashed var(--onb-line);color:var(--onb-ink);font-size:.6rem;font-weight:700;cursor:pointer;margin-bottom:7px}
    .onb-grid-toggle:hover{border-color:var(--onb-gold);color:var(--onb-gold)}
    .onb-grid-toggle .arrow{transition:transform .2s ease}
    .onb-grid-toggle.open .arrow{transform:rotate(180deg)}
    .onb-preview-note{font-size:.75rem;color:var(--onb-gold);font-weight:700;margin-top:4px;animation:onbPreviewPulse 1.6s ease-in-out infinite}
    @keyframes onbPreviewPulse{0%,100%{opacity:.75}50%{opacity:1}}
    #nick-step-1-faces{
      position:fixed;inset:0;z-index:55;display:none;
      /* SCRIM, 2026-08-09. Shane: "i dont want users can see the silohuueete of
         background when they are in the emoticons / emojis".
         CAUSE: background was `transparent` with only backdrop-filter:blur(6px).
         A BLUR DOES NOT HIDE ANYTHING - it smears it, and a blurred heading, a
         blurred gold NEXT button and a blurred link are all still plainly readable
         as shapes. That is the silhouette. Only an opaque layer removes it.
         NOT A DARK SCRIM. My first version was rgba(27,50,21,.92) and Shane rejected
         it immediately - "i dont want a dark background". He is right: dimming is the
         convention on apps with a dark ground, but MASKARA's ground is mid green, so a
         dark wash reads as a black box dropped on the page rather than the page itself.
         This value is the PAGE GREEN at 96%, a shade under --onb-bg so the card still
         lifts off it. It hides just as completely - hiding is about ALPHA, not about
         being dark - while the screen stays the colour it already was.
         The scrim also means a browser WITHOUT backdrop-filter support still hides
         the page - blur alone would have left those users seeing everything. */
      background:rgba(74,116,66,.96);
      backdrop-filter:blur(10px);
      align-items:stretch;justify-content:center;padding:24px;overflow-y:auto;
    }
    #nick-step-1-faces.onb-modal-open{display:flex}
    .onb-faces-modal-card{
      margin:auto;
      /* Synced to the pricing modal card — same Masquerade Night violet + warm-gold border. */
      position:relative;background:linear-gradient(165deg,rgba(var(--panel-hi-rgb),.96) 0%,rgba(var(--panel-lo-rgb),.94) 55%,rgba(var(--panel-hi-rgb),.99) 100%);border:1px solid rgba(232,194,90,.16);
      border-radius:18px;padding:24px;max-width:640px;width:100%;max-height:88vh;
      box-shadow:0 24px 60px rgba(0,0,0,.5);
      display:flex;flex-direction:column;gap:14px;
    }
    .onb-faces-scroll{overflow-y:auto;flex:1 1 auto;min-height:0;padding-right:6px;scrollbar-width:thin;scrollbar-color:rgba(var(--neon-rgb),.45) rgba(255,255,255,.04)}
    .onb-faces-scroll::-webkit-scrollbar{width:6px}
    .onb-faces-scroll::-webkit-scrollbar-track{background:rgba(255,255,255,.04);border-radius:999px}
    .onb-faces-scroll::-webkit-scrollbar-thumb{background:rgba(var(--neon-rgb),.45);border-radius:999px}
    .onb-faces-scroll::-webkit-scrollbar-thumb:hover{background:rgba(var(--neon-rgb),.7)}
    .onb-tile.previewing{border-color:var(--onb-gold);box-shadow:0 0 0 2px var(--onb-gold-soft,rgba(var(--neon-rgb),.25))}
    .onb-faces-back-row{display:flex;align-items:center;gap:12px;margin-bottom:16px}
    .onb-faces-back{background:none;border:1px solid var(--onb-line);border-radius:10px;color:var(--onb-ink);font-size:.75rem;font-weight:700;padding:7px 12px;cursor:pointer}
    .onb-faces-back:hover{border-color:var(--onb-gold);color:var(--onb-gold)}
    .onb-filter-row{display:flex;gap:6px;margin-bottom:8px;flex-wrap:wrap}
    .onb-filter-pill{font-size:11px;font-weight:600;color:var(--onb-ink-dim,#999);background:rgba(255,255,255,.04);border:1px solid var(--onb-line);padding:5px 11px;border-radius:999px;cursor:pointer;white-space:nowrap}
    .onb-filter-pill.active{background:var(--onb-gold);color:#241b16;border-color:var(--onb-gold)}
    .onb-section-label{font-family:var(--font-serif);font-style:italic;font-size:.7rem;color:var(--onb-ink);margin:14px 0 10px}
    .onb-cat-row{display:grid;grid-template-columns:repeat(auto-fill,minmax(150px,1fr));gap:8px;padding:2px 0 10px}
    .onb-cat-btn{width:100%}
    /* WHITE, 2026-08-09 (Shane, on the Free picker). Was --onb-muted #a99fc6, the
       stale pre-green purple, so every category you were NOT on read as disabled -
       and on this screen every tab except one is unselected, so nearly the whole row
       looked switched off. Active stays gold, which is what marks the current one. */
    .onb-cat-btn{flex:0 0 auto;background:var(--onb-panel-2);border:1px solid var(--onb-line);border-radius:999px;color:var(--onb-ink);font-size:.6rem;font-weight:600;padding:6px 12px;cursor:pointer;white-space:nowrap;transition:border-color .15s ease,color .15s ease,background .15s ease}
    .onb-cat-btn:hover{border-color:var(--onb-gold);color:var(--onb-gold)}
    .onb-cat-btn.active{color:var(--onb-gold);border-color:var(--onb-gold);background:var(--onb-gold-soft)}

    /* ── PHONE LAYOUT FOR THE FACE PICKER, 2026-08-09 ──────────────────────────
       Raising the emoji to 30px forced the tile minimum to 62px, and on a 390px
       phone that dropped the grid from five across to FOUR, with each tile
       stretching to ~74px - a 30px face marooned in a 74px box, and a third fewer
       faces per screen than before the change. Two fixes, both about reclaiming
       width rather than shrinking the emoji back down.

       (1) The card's 24px padding is desktop spacing. 16px gives back 16px of row.
       (2) The CATEGORY BLOCK was the bigger problem. .onb-cat-row is a grid at
           minmax(150px), so a phone gets two columns - and with a dozen categories
           that is six stacked rows of buttons sitting ABOVE the faces, pushing the
           actual choices off the bottom of the screen. On mobile it becomes ONE
           horizontally scrollable strip, which is what every phone app does with
           category tabs, and the emoji grid starts near the top instead.

       Scrollbar is hidden on the strip because a visible bar under a row of pills
       reads as a broken layout on touch; the row's own overflow is the affordance,
       and the partially-cut last chip is what tells you it scrolls. */
    @media (max-width:560px){
      .onb-faces-modal-card{padding:16px;border-radius:14px;max-height:92vh}
      .onb-cat-row{display:flex;flex-wrap:nowrap;overflow-x:auto;gap:6px;padding:2px 0 10px;scrollbar-width:none;-webkit-overflow-scrolling:touch}
      .onb-cat-row::-webkit-scrollbar{display:none}
      .onb-cat-btn{width:auto;flex:0 0 auto}
      /* 5 across on a 390px phone: 5x58 + 4x8 = 322 inside ~328 of usable row.
         Tiles land at ~64px, so a 34px face sits at the same ~53% of its tile as on
         desktop - that is what keeps the two screens looking like one design. */
      .onb-grid{grid-template-columns:repeat(auto-fill,minmax(58px,1fr));grid-auto-rows:58px;gap:16px 8px}
    }

    /* SMALL PHONES (iPhone SE / mini, 375px and under). Without this the row loses a
       column and jumps to four fat tiles, which looks like a different screen rather
       than the same one scaled. Tile min and COLUMN gap both come down so FIVE still
       fit; the ROW gap is held at 15-16px instead, because 2026-08-11 Shane reported
       "5 emoji from the left to the right and its dikit dikit to the bottom" - five
       across was correct, the rows were the problem. ROW GAP AND COLUMN GAP ARE SET
       SEPARATELY HERE ON PURPOSE: shrinking the row gap to match the column gap is
       what made the faces stack chin-to-forehead.
       5x52 + 4x7 = 288 inside ~313. The glyph steps down with them, holding the same
       ratio - the tile and the emoji are always changed together. */
    @media (max-width:400px){
      .onb-faces-modal-card{padding:14px}
      .onb-grid{grid-template-columns:repeat(auto-fill,minmax(52px,1fr));grid-auto-rows:52px;gap:15px 7px}
      .onb-tile{font-size:30px}
    }
    /* TILES NO LONGER STRETCH ON A WIDE SCREEN — 2026-08-11. `minmax(62px,1fr)`
       let every column absorb the leftover width, so on a desktop the tiles grew
       to ~100px while the glyph stayed 34px and each face floated in an empty
       box. A FIXED 72px track keeps the tile the size it was designed at and
       puts the spare width in the margins instead, where it belongs.
       justify-content:center stops the row hugging the left edge once the
       tracks stop growing. The two phone breakpoints above still use minmax,
       which is correct there: on a narrow screen the tiles SHOULD flex to fit
       five across. */
    .onb-grid{display:grid;grid-template-columns:repeat(auto-fill,72px);grid-auto-rows:72px;justify-content:center;gap:16px 10px;margin-bottom:14px}
    /* EMOJI SIZE 18px -> 30px -> 34px -> 42px, the last step 2026-08-11 ("big the
       all emojis"). THE TILE AND THE GLYPH ARE A PAIR - never change one alone.
       At a fixed 72px tile a 42px face is ~58% of the tile, a little fuller than
       the old ~53% and about as far as it goes before the grid reads as a solid
       wall of faces with no room to breathe.
       Phone sizes are in the two breakpoints further down and are set from the
       same ratio, so a face is proportionally the same on every screen. */
    .onb-tile{aspect-ratio:1;border-radius:10px;background:var(--onb-panel-2);border:1px solid var(--onb-line);display:flex;align-items:center;justify-content:center;font-size:42px;cursor:pointer;position:relative;transition:transform .12s ease,border-color .12s ease;overflow:hidden}
    .onb-tile:hover{transform:translateY(-2px)}
    .onb-tile.selected,.onb-tile.pending{border-color:var(--onb-gold);box-shadow:0 0 0 2px var(--onb-gold-soft)}
    .onb-faces-modal-card .onb-step-btn.primary{width:100%;flex:0 0 auto}
    .onb-faces-modal-card .onb-step-btn.primary:disabled{opacity:.45;filter:grayscale(.5);cursor:not-allowed;box-shadow:none}
    .onb-tile.locked{opacity:.5}
    .onb-tile.locked:has(.onb-preview-tag){opacity:.8;cursor:pointer}
    .onb-preview-tag{background:transparent;border:1px solid var(--onb-gold);color:var(--onb-gold);font-size:9px}
    .onb-tile.locked:hover{transform:none}
    .onb-price-tag{position:absolute;bottom:-6px;left:50%;transform:translateX(-50%);font-size:7px;font-weight:800;padding:2px 5px;border-radius:6px;white-space:nowrap;background:var(--onb-gold);color:#2a2000}
    .onb-upsell{border-radius:12px;padding:11px 12px;margin:0 0 10px;display:flex;align-items:center;gap:10px;background:var(--onb-gold-soft);border:1px solid rgba(var(--neon-rgb),.3)}
    .onb-upsell .onb-upsell-text{flex:1;font-size:.68rem;color:var(--onb-muted);line-height:1.4}
    .onb-upsell .onb-upsell-text strong{color:var(--onb-gold)}
    /* THE SOLID GOLD FILL IS GONE — 2026-08-10. Shane: "gold fill is overkill".
       It painted a full-strength gold gradient across the whole track, sitting
       directly under the mask card and dominating the screen before the user
       had done anything.
       NOW: the track is an outline at rest, the fill is the SAME gold at 28%
       (var(--onb-gold-soft), which already existed beside --onb-gold, so the
       wash can never drift out of sync with the thumb), and the label is white.
       Gold is EARNED as they pull instead of shouting up front - hairline
       border at rest, soft wash while moving, solid border + gold label on
       complete. One hue, three weights.
       THE FILL IS NOT DECORATION: it is the drag progress indicator, which is
       why it is restyled rather than deleted. Removing it would leave the
       gesture with no feedback at all. */
    /* TRACK FILL — Shane, 2026-08-10: "the hide the real me is light green filled
       background not a drak one". It was `transparent`, so its interior was
       literally whatever sat behind it, which is why he read it as the same
       colour as the panel. #8CB183 is a real step LIGHTER than the panel's
       #759B6D, so the row reads as a control sitting ON the panel. DO NOT set it
       to #759B6D to "match" — matching the panel is the disappearing-track bug
       he reported, in a new form.
       margin-bottom is 0 now: the row lives inside .onb-card, which owns the
       spacing. The old 12px was for a track standing on its own below the panel. */
    .onb-skin .nick-slide-track.onb-slide{border-color:rgba(232,194,90,.55);background:rgba(255,255,255,.10);margin-bottom:0}
    .onb-skin .nick-slide-track.onb-slide .nick-slide-fill{background:var(--onb-gold-soft);box-shadow:none}
    .onb-skin .nick-slide-track.onb-slide .nick-slide-label{color:var(--onb-ink)}
    .onb-skin .nick-slide-track.onb-slide.complete{border-color:var(--onb-gold)}
    .onb-skin .nick-slide-track.onb-slide.complete .nick-slide-label{color:var(--onb-gold)}

    /* ===== MOBILE (default): single column, stacked — unchanged from before ===== */
    .onb-layout{display:block;padding-top:40px}
    .onb-left{margin-bottom:4px;min-width:0}
    .onb-right{min-width:0}

    /* =========================================================================
       DESKTOP ONBOARDING — Facebook/Google-style capped, centered signup card.
       Rules:
        1. Card width capped at 1080px at all desktop breakpoints; the centered
           flexbox overlay turns extra width into side margin — not bigger content.
        2. Type scales in discrete steps at 880/1200/1440, fixed rem values only,
           no continuous vw-based scaling.
        3. Base font-size steps on .onb-skin (16→17→18px); rem values scale the
           whole card (headline, avatar, padding) in proportion.
        4. Inputs/reading width capped so line-length stays comfortable on wide screens.
       ========================================================================= */

    /* ===== LAPTOP (>=880px): two-column split, base unit 16px, card capped at 1080px ===== */
    @media (min-width:99999px){
      .onb-skin{font-size:16px}
      .nick-card{max-width:1080px}
      .onb-layout{display:grid;grid-template-columns:minmax(0,.78fr) minmax(0,1.22fr);gap:48px;align-items:stretch;padding-top:48px}
      .onb-left{display:flex;flex-direction:column}
      .onb-right{display:flex;flex-direction:column}
      .onb-left .onb-id-card{flex:1;display:flex;flex-direction:column}
      .onb-left .onb-skip-link{margin-top:auto;padding-top:10px}
      .onb-right .onb-id-card{flex:1;display:flex;flex-direction:column}
      .onb-right .onb-name-input-row{margin-top:auto}
      .onb-right{padding-bottom:16px}
      .onb-h1{font-size:2.4rem;margin:0 0 .75rem}
      .onb-eyebrow{font-size:.8rem;margin-bottom:.6rem}
      .onb-hero-p{font-size:.95rem}
      .onb-card-h2,.preview-card h2{font-size:1.2rem}
      .onb-stage-avatar-wrap{width:72px;height:72px}
      .onb-stage-avatar{width:72px;height:72px;font-size:48px}
      .onb-stage-name{font-size:1.1rem}
      .onb-tier-tag{font-size:.72rem;padding:3px 8px}
      .onb-stage-sub,.nick-hint{font-size:.9rem}
      .onb-card-how{font-size:.9rem}
      .onb-input,.onb-name-input{font-size:1rem;padding:12px 14px}
      .onb-name-input-row,.onb-input{max-width:460px}
      .onb-dice-btn{width:50px;font-size:21px}
      .onb-pricing-head h2{font-size:1.25rem}
      .onb-pricing-head p{font-size:.85rem}
      .onb-tier-label{font-size:.85rem}
      .onb-amount{font-size:1.9rem;margin:12px 0 4px}
      .onb-unit{font-size:.85rem;margin-bottom:16px}
      .onb-price-card li{font-size:.7rem;margin-bottom:7px}
      .preview-label{font-size:14px}
      .feat-card-item{width:190px;padding:16px}
      .feat-name{font-size:16px}
      .feat-desc{font-size:13px}
      .onb-trust-line{font-size:.9rem}
    }
    /* ===== DESKTOP (>=1152px): one step up, base unit 17px. Card width STAYS 1080px. Lowered from 1200px so narrower desktop windows (e.g. 1166px) still get the desktop treatment instead of falling back to the 880px laptop tier. ===== */
    @media (min-width:99999px){
      .onb-skin{font-size:17px}
      .onb-layout{gap:56px}
      .onb-h1{font-size:3.2rem;margin:0 0 1rem}
      .onb-eyebrow{font-size:.85rem}
      .onb-hero-p{font-size:1.05rem}
      .onb-card-h2,.preview-card h2{font-size:1.4rem}
      .onb-stage-avatar-wrap{width:92px;height:92px}
      .onb-stage-avatar{width:92px;height:92px;font-size:61px}
      .onb-stage-name{font-size:1.3rem}
      .onb-tier-tag{font-size:.8rem;padding:4px 10px}
      .onb-stage-sub,.nick-hint{font-size:1rem}
      .onb-card-how{font-size:1rem}
      .onb-input,.onb-name-input{font-size:1.1rem;padding:15px 18px}
      .onb-dice-btn{width:58px;font-size:25px}
      .onb-pricing-head h2{font-size:1.45rem}
      .onb-pricing-head p{font-size:.92rem}
      .onb-tier-label{font-size:.95rem}
      .onb-amount{font-size:2.2rem;margin:14px 0 5px}
      .onb-unit{font-size:.92rem;margin-bottom:18px}
      .onb-price-card li{font-size:.74rem;margin-bottom:8px}
      .preview-label{font-size:15px}
      .feat-card-item{width:210px;padding:20px}
      .feat-name{font-size:18px}
      .feat-desc{font-size:14px}
      .onb-trust-line{font-size:1rem}
    }
    /* ===== LARGE DESKTOP (>=1440px): active at user's 1596px screen — avatar, headline, pricing cards all substantially bigger. ===== */
    @media (min-width:99999px){
      .onb-skin{font-size:18px}
      .nick-card{max-width:1320px}
      .onb-layout{gap:80px;padding-top:64px}
      .onb-h1{font-size:5.2rem}
      .onb-card-h2,.preview-card h2{font-size:1.7rem}
      .onb-stage-avatar-wrap{width:152px;height:152px}
      .onb-stage-avatar{width:152px;height:152px;font-size:100px}
      .onb-stage-name{font-size:1.7rem}
      .onb-tier-tag{font-size:.9rem;padding:4px 12px}
      .onb-stage-sub,.nick-hint{font-size:1.1rem}
      .onb-pricing-head h2{font-size:1.7rem}
      .onb-pricing-head p{font-size:1rem}
      .onb-tier-label{font-size:1.05rem}
      .onb-amount{font-size:3.2rem;margin:16px 0 6px}
      .onb-unit{font-size:1rem;margin-bottom:22px}
      .onb-price-card li{font-size:.8rem;margin-bottom:9px}
      .feat-card-item{width:260px;padding:22px}
      .feat-name{font-size:20px}
      .feat-desc{font-size:15px}
    }
    /* ===== ULTRA-WIDE (>=2000px): card widens to 1450px, base unit 19px, stays larger than 1440px tier. ===== */
    @media (min-width:99999px){
      .onb-skin{font-size:19px}
      .nick-card{max-width:1450px}
      .onb-layout{gap:96px}
      .onb-h1{font-size:5.8rem}
      .onb-stage-avatar-wrap{width:170px;height:170px}
      .onb-stage-avatar{width:170px;height:170px;font-size:112px}
      .onb-amount{font-size:3.6rem}
    }
    .onb-top-center{text-align:center;padding-top:8px}
    .onb-trust-line{color:var(--onb-muted);font-size:.68rem;text-align:center;margin-top:10px;opacity:.85}
    /* Was a separate dark blue-teal gradient (#141a28→#0f1420) with an
       olive-gold border — its own color, matching neither the pricing cards
       nor the small feature cards inside it. Switched to the same
       var(--onb-panel)/var(--onb-line) used everywhere else in this screen
       so the outer "big container" matches the small containers it holds. */
    .preview-card{background:var(--onb-panel);border:1px solid var(--onb-line);border-radius:14px;padding:18px;margin-top:20px;position:relative;overflow:hidden}
    .preview-card::before{content:"";position:absolute;top:-40px;right:-40px;width:140px;height:140px;background:radial-gradient(circle,rgba(201,161,59,.18),transparent 70%)}
    .preview-label{color:var(--onb-ink,#F7FBF4);font-size:11px;letter-spacing:1.5px;margin-bottom:12px;display:flex;align-items:center;gap:6px}
    .preview-label .dot{width:6px;height:6px;border-radius:50%;background:#3ddc84;box-shadow:0 0 6px #3ddc84}
    .feat-marquee-mask{overflow:hidden;position:relative;mask-image:linear-gradient(90deg,transparent,black 6%,black 94%,transparent);-webkit-mask-image:linear-gradient(90deg,transparent,black 6%,black 94%,transparent)}
    .feat-marquee-track{display:flex;gap:14px;width:max-content;animation:featMarqueeScroll 26s linear infinite}
    .feat-marquee-mask:hover .feat-marquee-track{animation-play-state:paused}
    @keyframes featMarqueeScroll{0%{transform:translateX(0)}100%{transform:translateX(-50%)}}
    /* Was var(--card-bg)/var(--border) (the app's generic dark card look) —
       switched to the same var(--onb-panel)/var(--onb-line) the "Choose your
       mask" pricing cards use, so the small feature-marquee cards here match
       the big pricing cards' purple panel instead of standing out as their
       own separate color. Only lives inside .onb-skin (see the markup at
       nick-onb-step-d), so --onb-panel/--onb-line are always in scope. */
    .feat-card-item{background:var(--onb-panel);border:1px solid var(--onb-line);border-radius:10px;padding:12px;width:150px;flex-shrink:0}
    .feat-icon{font-size:18px;margin-bottom:6px}
    .feat-name{font-weight:700;font-size:13px;margin-bottom:2px}
    .feat-desc{color:var(--onb-ink,#F7FBF4);font-size:11px;line-height:1.4}
    .onb-slide-centered{max-width:640px;margin:24px auto 0}

    /* ===== ONBOARDING STEP FLOW: dot progress + Back/Next nav, reuses existing onb-* theme tokens only ===== */
    .nick-onb-substep{animation:card-in .3s ease;transition:opacity .25s ease}
    .onb-pricing-modal-backdrop{
      /* Masquerade Night palette — matched to the "Choose your mask" onboarding
         screen (violet base, warm gold #e8c25a for accents/badges). */
      --onb-bg:var(--midnight); --onb-bg-2:var(--ground-hi);
      --onb-panel:rgba(var(--panel-hi-rgb),.85); --onb-panel-2:var(--surface-2);
      --onb-line:rgba(255,255,255,.10); --onb-ink:#F7FBF4; --onb-muted:#a99fc6;
      --onb-gold:#e8c25a; --onb-gold-soft:rgba(232,194,90,.28);
      --onb-blue:#C7CCD4; --onb-blue-soft:rgba(199,204,212,.26); /* NAME KEPT, VALUE IS SILVER since 2026-08-10 - same churn-avoidance as --gold and --neon no longer meaning what they say. Only user: .onb-tier-tag.free */
      --onb-silver:var(--silver); --onb-silver-soft:var(--silver-soft);
      /* Match the NEXT (.onb-step-btn.primary) gold to the "Choose your mask"
         screen's softer gold instead of the global bright var(--neon). */
      --gold:#f6c94c; --gold-soft:#ffe07a;
      position:fixed;inset:0;z-index:50;
      /* SCRIM, 2026-08-09. Shane: "i dont want users can see the silohuueete of
         background when they are in the emoticons / emojis".
         CAUSE: background was `transparent` with only backdrop-filter:blur(6px).
         A BLUR DOES NOT HIDE ANYTHING - it smears it, and a blurred heading, a
         blurred gold NEXT button and a blurred link are all still plainly readable
         as shapes. That is the silhouette. Only an opaque layer removes it.
         NOT A DARK SCRIM. My first version was rgba(27,50,21,.92) and Shane rejected
         it immediately - "i dont want a dark background". He is right: dimming is the
         convention on apps with a dark ground, but MASKARA's ground is mid green, so a
         dark wash reads as a black box dropped on the page rather than the page itself.
         This value is the PAGE GREEN at 96%, a shade under --onb-bg so the card still
         lifts off it. It hides just as completely - hiding is about ALPHA, not about
         being dark - while the screen stays the colour it already was.
         The scrim also means a browser WITHOUT backdrop-filter support still hides
         the page - blur alone would have left those users seeing everything. */
      background:rgba(74,116,66,.96);
      backdrop-filter:blur(10px);
      align-items:center;justify-content:center;padding:20px;overflow:hidden;
    }
    .onb-pricing-modal-backdrop.onb-modal-open{display:flex !important}
    .onb-pricing-modal-card{
      position:relative;background:linear-gradient(165deg,rgba(var(--panel-hi-rgb),.96) 0%,rgba(var(--panel-lo-rgb),.94) 55%,rgba(var(--panel-hi-rgb),.99) 100%);border:1px solid rgba(232,194,90,.16);
      border-radius:18px;padding:28px 24px 20px;max-width:640px;width:100%;margin:0 auto;
      box-shadow:0 24px 60px rgba(0,0,0,.5);transform-origin:center center;
    }
    .onb-pricing-modal-close{
      position:absolute;top:12px;right:12px;width:30px;height:30px;border-radius:50%;
      background:var(--onb-panel-2,rgba(255,255,255,.06));border:1px solid var(--onb-line,rgba(255,255,255,.1));
      color:var(--onb-muted,#8b93a8);font-size:14px;cursor:pointer;display:flex;align-items:center;justify-content:center;
      transition:all .15s ease;
    }
    .onb-pricing-modal-close:hover{color:var(--onb-ink,#f4f5f8);border-color:var(--onb-gold,var(--neon-dim))}
    .onb-pricing-modal-card .onb-layout{padding-top:0}
    .onb-preview-modal-card{max-width:400px;text-align:center;padding:44px 24px 30px}
    #onb-avatar-preview-modal{z-index:1050;position:fixed}
    .onb-preview-modal-label{font-size:.7rem;letter-spacing:.08em;text-transform:uppercase;color:var(--onb-gold,var(--neon-dim));font-weight:700;margin-bottom:18px}
    #onb-preview-avatar-wrap{width:220px;height:220px;margin:0 auto}
    /* GLYPH 120 -> 156px, 2026-08-11. At 120 inside a 220 disc the face filled
       barely half the circle and the ring read as an empty hoop with a small
       emoji floating in it — Shane: "thats too small". 156 is ~71% of the disc,
       the same ratio the picker tiles and the My mask avatar now use, so the
       face is the same size relative to its circle everywhere in the app.
       RING 2px -> 4px AND EXPLICITLY SILVER. This preview is the FREE tier, and
       the ramp on ::before is only 2px — at 220px across, a 2px hairline reads as
       a thin white circle rather than a metal ring. The wider band is what makes
       it look like silver instead of an outline. */
    #onb-preview-avatar-wrap .onb-stage-avatar{width:220px;height:220px;font-size:156px}
    #onb-preview-avatar-wrap .onb-stage-avatar::before{padding:4px}
    @keyframes onbPreviewGoldBlink{
      0%,100%{box-shadow:0 0 14px 3px rgba(var(--neon-rgb),.55)}
      50%{box-shadow:0 0 26px 7px rgba(var(--neon-rgb),.9)}
    }
    #onb-preview-avatar-wrap.gold .onb-stage-avatar{animation:onbPreviewGoldBlink 2.8s ease-in-out infinite}
    /* THE GLOW GOES, 2026-08-02. .onb-stage-name-tag carries a white/gold
       text-shadow that, on this card's dark pill, reads as a second lighter
       box around the nickname - so the Maskara badge beside it looked like it
       had been left outside a container it was never in. The pill has always
       wrapped both; the glow just made it look otherwise.
       gap 12 -> 7 as well, since the badge belongs to the name rather than
       sitting near it. */
    .onb-preview-modal-card .onb-name-tag-row{justify-content:center;margin-top:18px;background:rgba(0,0,0,.35);border-radius:999px;padding:8px 16px;display:inline-flex;max-width:100%;gap:7px}
    .onb-preview-modal-card .onb-stage-name-tag{text-shadow:none}
    .onb-faces-action-row{display:flex;gap:10px}
    .onb-faces-action-row .onb-step-btn.primary{width:auto;flex:1}
    .onb-faces-action-row .onb-step-btn:not(.primary){flex:0 0 auto;white-space:nowrap}
    
    .onb-step-dots{display:flex;align-items:center;justify-content:center;gap:8px;margin:12px 0 22px}
    .onb-step-dot{width:7px;height:7px;border-radius:50%;background:var(--onb-line);transition:all .2s ease}
    .onb-step-dot.active{background:var(--onb-gold);width:20px;border-radius:4px}
    .onb-step-nav{display:flex;align-items:center;justify-content:space-between;gap:12px;margin-top:22px}
    /* NO left offset on step A. A `#nick-onb-step-a .onb-step-nav{margin-left:52px}`
       rule used to live here (28px in the <=430px block below). Step A has no
       Back button, so NEXT is the only child and flex:1 stretches it across the
       row - the margin then shrank the row from the LEFT only, leaving NEXT
       flush right with the .onb-card stack but inset 52px on the left. That is
       the misalignment Shane reported 2026-08-09. Both rules removed so NEXT
       spans exactly the same column as the cards above it. Do not re-add. */
    .onb-step-btn{flex:0 0 auto;padding:10px 22px;border-radius:999px;font-size:.85rem;font-weight:700;font-family:inherit;cursor:pointer;transition:all .15s ease;border:1px solid var(--onb-line);background:var(--onb-panel-2);color:var(--onb-ink)}
    .onb-step-btn:hover{border-color:var(--onb-gold);color:var(--onb-ink)}
    /* Back lights up (gold) when tapped/clicked — press feedback that also works
       on touch (no hover). The .lit class is applied briefly on click so the
       glow is visible even as the step navigates away. */
    .onb-step-btn:active,.onb-step-btn.lit{background:var(--onb-gold);border-color:var(--onb-gold);color:#241516}
    .onb-step-btn:focus-visible{outline:none;border-color:var(--onb-gold);color:var(--onb-ink)}
    /* Unified with #account-login-modal .login-cta below — same brand gold gradient
       (#f0d980 -> --gold/var(--neon)) so the primary action color is consistent across
       the whole onboarding + login flow instead of two different golds. */
    /* EXACT COPY of index.html's "Wear My Mask" CTA (.cta-label) so the button
       a visitor pressed on the entry page is the same button here: same 140deg
       ramp, same #1E3A18 deep-green label, same 999px pill, same shadow pair.
       The ramp is SATURATED end to end on purpose - the old #f0d980 top stop
       was near-cream, which is what made this button read as a large pale slab
       instead of gold. Height also drops 16px -> 13px. If index's CTA changes,
       change this with it.

       THE SHEEN SWEEP IS NOW COPIED TOO (2026-08-09) — the keyframes are
       redeclared here as onbSheen because index.html's `sheen` lives only in
       that file. Same geometry as index: z-index:-1 with isolation:isolate so
       the glare passes UNDER the label instead of washing it out. */
    .onb-step-btn.primary{flex:1;border:none;color:#1E3A18;background:linear-gradient(140deg,#FFE08A 0%,var(--neon) 42%,var(--neon-dim) 74%,#F5CF5F 100%);text-transform:uppercase;font-weight:600;letter-spacing:.04em;font-size:.95rem;padding:13px 20px;border-radius:999px;box-shadow:0 8px 26px -12px rgba(var(--neon-rgb),.55),inset 0 1px 0 rgba(255,255,255,.5);position:relative;overflow:hidden;isolation:isolate}
    .onb-step-btn.primary::before{content:'';position:absolute;top:-60%;bottom:-60%;width:38%;left:-50%;z-index:-1;background:linear-gradient(100deg,transparent,rgba(255,255,255,.62),transparent);transform:skewX(-18deg);animation:onbSheen 4.6s ease-in-out infinite 1.8s;pointer-events:none}
    @keyframes onbSheen{0%{left:-50%}55%{left:120%}100%{left:120%}}

    /* WHAT LIGHTS THIS BUTTON UP IS VALIDITY, NOT THE CURSOR — Shane, 2026-08-09:
       "the next button is pale then when a user pointed a mouse in the next
       button it light up, i dont want that."

       Two problems were being reported as one:
       (1) HOVER was doing the announcing. The old :hover added
           filter:brightness(1.07) on top of the gradient, so the button only
           reached full strength under a cursor — and on a phone there is no
           cursor at all, so mobile NEVER saw the lit state. Brightness is gone;
           hover now only lifts 2px and deepens the shadow, which is press
           affordance, not a colour change.
       (2) DISABLED still looked like a gold button, just washed out
           (opacity .45 + grayscale .55). A dimmed version of the live state
           reads as "gold, but the screen is dim", which is why it looked pale
           rather than off. It is now a flat neutral panel with NO gold in it,
           so enabling is a real change of state you can see across the room.

       The outline is an INSET box-shadow, not a border: the enabled button has
       border:none, and adding a real 1px border on the disabled state would
       make the button 2px taller and jump when it unlocks.

       Every interactive state is neutralised while disabled — hover, active,
       focus-visible and the sheen — so nothing on the dead button responds to
       anything. Gating logic is unchanged, in updateStepANextLock(): mask
       chosen + nickname safe & free + username valid & free. */
    .onb-step-btn.primary:hover{background:linear-gradient(140deg,#FFE08A 0%,var(--neon) 42%,var(--neon-dim) 74%,#F5CF5F 100%);transform:translateY(-2px);box-shadow:0 12px 32px -12px rgba(var(--neon-rgb),.66),inset 0 1px 0 rgba(255,255,255,.58)}
    .onb-step-btn.primary:disabled,
    .onb-step-btn.primary:disabled:hover,
    .onb-step-btn.primary:disabled:active,
    .onb-step-btn.primary:disabled:focus-visible{background:var(--onb-panel-2);color:rgba(247,251,244,.42);opacity:1;filter:none;transform:none;box-shadow:inset 0 0 0 1px rgba(255,255,255,.12);cursor:not-allowed}
    .onb-step-btn.primary:disabled::before{display:none}
    /* Same call index.html makes on its CTA: a repeating glare is exactly what
       motion-sensitive people react to, on an app opened when already low. */
    @media (prefers-reduced-motion:reduce){.onb-step-btn.primary::before{animation:none;opacity:0}}
    .onb-id-divider{display:flex;align-items:center;gap:10px;margin:2px 0 12px}
    .onb-id-divider::before,.onb-id-divider::after{content:'';flex:1;height:1px;background:linear-gradient(90deg,transparent,var(--onb-line),transparent)}
    .onb-id-divider-badge{flex:0 0 auto;width:26px;height:26px;border-radius:50%;background:var(--onb-panel-2);border:1px solid var(--onb-gold);display:flex;align-items:center;justify-content:center;color:var(--onb-gold);position:relative}
    .onb-id-divider-badge svg{width:13px;height:13px}
    .onb-card-tag{display:inline-flex;align-items:center;gap:4px;font-size:.58rem;font-weight:600;letter-spacing:.06em;text-transform:uppercase;padding:0;border-radius:0;margin-left:9px;vertical-align:middle}
    .onb-card-tag.priv{color:var(--onb-muted);background:rgba(255,255,255,.05);border:1px solid var(--onb-line);font-size:.34rem;font-weight:700;letter-spacing:.02em;padding:1px 5px;border-radius:999px;gap:2px}
    /* LOCKED - final sizing, signed off.
       This pill was shrunk twice over: a .5rem font AND transform:scale(.62)
       on top of it, landing the text at roughly 5px - unreadable at 100% zoom.
       The scale is gone and the size is set once, honestly, in the font-size.
       Adjust THIS number to resize the pill; do not reintroduce a transform. */
    .onb-card-tag.pub{font-family:var(--font-ui);font-style:normal;font-size:.62rem;line-height:1;font-weight:700;letter-spacing:.05em;color:#DCE7D6;background:var(--glass);border:1px solid rgba(255,255,255,0.16);padding:5px 10px;border-radius:9px;margin-left:0;flex-shrink:0;gap:5px}
    .onb-card-tag.pub svg{opacity:.95;width:12px;height:12px;flex-shrink:0}
    .onb-id-card{position:relative;overflow:hidden;width:100%;max-width:100%}
    /* Removed: leftover static gold bar on the left edge (::before) that pre-dated the
       animated chase-glow ring. It rendered as a permanent yellow strip on the left side
       of every card (face / nickname / username) regardless of the chase animation state
       below — that was the "left side yellow" bug that kept showing. */

    /* ===== SUPER-ULTRA-WIDE (>=2400px): one tier bigger than the 2000px block above.
       Previously this whole block had NO media query and applied at every screen size
       and every browser zoom level — that was the bug where zooming out (e.g. to 25%)
       made the onboarding page render bigger instead of smaller: the effective CSS
       viewport width grows as you zoom out, but these !important rules were firing
       regardless of that width, permanently locking every element to its largest size.
       Now properly gated behind min-width so it only applies on genuinely huge screens.
       Each value is set explicitly larger than the biggest existing tier (the 2000px
       ultra-wide breakpoint) rather than as a relative em multiplier, because
       `.nick-card *{font-size:1.3em}` compounds once per nested DOM level (1.3^depth)
       and blew up deeply-nested pricing/list text to 2-3x its intended size, overlapping
       the layout. Explicit absolute values avoid that. ===== */
    @media (min-width:99999px){
    .nick-card { max-width: 1500px; }
    .onb-skin { font-size: 20px; }
    .onb-h1 { font-size: 6.2rem; }
    .onb-eyebrow { font-size: .85rem; }
    .onb-hero-p { font-size: 1.15rem; }
    .onb-card-h2, .preview-card h2 { font-size: 1.8rem; }
    .onb-card-how { font-size: 1.05rem; }
    .onb-stage-avatar-wrap, .onb-stage-avatar {
      width: 180px;
      height: 180px;
    }
    .onb-stage-avatar { font-size: 85px; }
    .onb-stage-name { font-size: 1.8rem; }
    .onb-tier-tag { font-size: 1rem; padding: 5px 14px; }
    .onb-stage-sub, .nick-hint { font-size: 1.2rem; }
    .onb-input, .onb-name-input { font-size: 1.2rem; padding: 17px 20px; }
    .onb-dice-btn { width: 64px; font-size: 27px; }
    .onb-pricing-head h2 { font-size: 1.8rem; }
    .onb-pricing-head p { font-size: 1.05rem; }
    .onb-tier-label { font-size: 1.1rem; }
    .onb-amount { font-size: 3.8rem; margin: 18px 0 7px; }
    .onb-unit { font-size: 1.05rem; margin-bottom: 24px; }
    .onb-price-card li { font-size: 1.05rem; margin-bottom: 13px; }
    .feat-card-item { width: 280px; padding: 24px; }
    .feat-name { font-size: 22px; }
    .feat-desc { font-size: 16px; }
    .onb-trust-line { font-size: 1.05rem; }
    .preview-label { font-size: 16px; }
    }


    /* ===== LOCKED CANVAS DESIGN (replaces old dvh-based "one-viewport fit" system):
       every value below is now a FIXED px/rem number — one true design, drawn once
       at comfortable/legible sizes. It no longer reacts to viewport height/width on
       its own. #onb-fit-script below measures this card's real (unscaled) size and
       applies one uniform CSS transform:scale() to the whole card so it always fits
       the available space, like scaling a photo — never squishing letters/paddings
       differently at different sizes. No scrollbar, ever; no empty dead space either,
       since the overlay's own themed background already fills 100% of the real
       screen behind the card. ===== */
    .nick-overlay{padding:16px 24px;overflow-y:auto;scrollbar-width:none;-ms-overflow-style:none}
    .nick-overlay::-webkit-scrollbar{display:none}
    .nick-card.nick-card-onb{overflow:visible;transform-origin:top center}
    /* THE DARK RECTANGLE, 2026-08-09. Shane: "i dont want it its too dark. align it
       and same with the full background."
       It was not a colour problem at all. fitOnbCard does
       `overlay.querySelector('.nick-card').style.transform = 'scale(n)'`, and BOTH
       onboarding overlays (#nick-step-1-faces and #nick-onb-step-c) sit INSIDE that
       card. A transformed element becomes the containing block for every
       position:fixed descendant, so `inset:0` stopped meaning the viewport and
       started meaning the card - the scrim only ever covered a card-width column,
       and that column read as a dark box painted on the page.
       Clearing the transform WHILE AN OVERLAY IS OPEN is the whole fix: the card is
       completely hidden behind a 96% scrim at that moment, so whether it is scaled
       or not is invisible, and the scrim goes full-screen the instant it stops being
       trapped. !important is required because the fit script writes an INLINE
       transform and re-writes it on every resize.
       THE OTHER FIX, not taken: move both overlays out to be children of <body>,
       which is what #onb-avatar-preview-modal already does. That is the more correct
       structure and is worth doing if these overlays gain more independent
       behaviour - but it moves ~120 lines of markup to solve a problem four lines
       solve here.
       IF A FIXED ELEMENT INSIDE ONBOARDING EVER MISBEHAVES AGAIN, look for a
       transform on an ancestor FIRST. */
    /* TRANSITION KILLED WITH IT, 2026-08-09 - "i dont want to have the blink of the
       dark container... then it will go to the full container light background."
       .nick-card carries `transition:transform .55s`, so clearing the scale ANIMATED
       over half a second. For that entire half second the card is still transformed,
       which means the overlay is still trapped inside it - so the scrim opened as the
       narrow dark column and then visibly grew out to full screen. That growth WAS
       the blink. The transform has to drop in ONE frame, not ease out, or the trap
       simply lasts longer instead of not happening. */
    .nick-card:has(.onb-modal-open){transform:none !important;transition:none !important}
    .nick-step.onb-skin{display:flex;flex-direction:column;justify-content:center;overflow:visible}
    .onb-step-dots{margin:14px 0 24px}
    .onb-stage{gap:8px;margin-bottom:16px}
    .onb-stage-avatar-wrap,.onb-stage-avatar{width:96px;height:96px}
    .onb-stage-avatar{font-size:63px}
    .onb-stage-name{font-size:1.3rem}
    .onb-tier-tag{font-size:.78rem;padding:3px 9px}
    .onb-stage-sub{font-size:.9rem}
    .onb-hero{padding:4px 4px 16px;margin-bottom:12px}
    .onb-eyebrow{font-size:.8rem;margin-bottom:16px}
    .onb-h1{font-size:2rem;margin:0 0 14px}
    .onb-hero-p{font-size:.85rem}
    .onb-card{padding:10px 14px;margin-bottom:6px}
    .onb-section-num{margin:0 0 5px}
    .onb-id-divider{margin:2px 0 8px}
    .onb-step-nav{margin-top:10px}
    .onb-pricing-head{margin:8px 0 4px}
    .onb-pricing-row{margin:8px 0}
    .preview-card{padding:14px;margin-top:16px}
    .feat-card-item{padding:10px}

    /* This mobile media query is placed here deliberately, AFTER the LOCKED CANVAS
       DESIGN block above — CSS gives the later rule priority when specificity ties,
       so placing this earlier (as it originally was) meant the unconditional LOCKED
       CANVAS values silently won on mobile too, undoing every mobile fix below. */
    @media (max-width:768px){
      .gate-overlay{overflow-y:auto}
      .nick-overlay{overflow-y:auto}
      .onb-pricing-modal-backdrop{overflow:hidden;padding:12px}
      .onb-pricing-modal-card{padding:20px 16px 16px}
      .onb-hero-p{white-space:nowrap;font-size:.72rem}
      .onb-hero{padding-left:14px;text-align:center;margin-bottom:6px;padding-bottom:8px}
      .nick-overlay{align-items:center}
      .onb-stage-name-tag{flex:0 1 auto;min-width:0;font-size:1rem;max-width:100%}
      #onb-stage-name-badge,#onb-preview-name-badge{font-size:1rem}
      .onb-name-tag-row{gap:10px}
      .onb-dice-btn{width:48px;font-size:22px}
      .nick-overlay{padding:4px 3px}
      .nick-card{padding:8px 4px}
      .onb-step-btn.primary{padding:10px 16px;font-size:.8rem}
      .onb-card{padding:6px 6px;margin-bottom:4px}
      .onb-timeline-item{gap:6px}
      .onb-timeline-icon-col{width:18px}
      .onb-timeline-icon{width:18px;height:18px;font-size:9px}
      .onb-timeline-icon-check{width:9px;height:9px;bottom:-1px;right:-4px;box-shadow:none}
      .onb-timeline-icon-check svg{width:5px;height:5px}
      .onb-timeline-content{padding-bottom:6px}
      .onb-timeline-line{min-height:10px;margin:4px 0;width:1px}
      .onb-step-dots{margin:8px 0 12px}
      .onb-h1{font-size:1.3rem;margin:0 0 4px;white-space:nowrap}
      .onb-stage-avatar-wrap{width:60px;height:60px;margin:4px 0 0 10px}
      .onb-stage-avatar{width:60px;height:60px;font-size:40px}
      .onb-name-input{padding:10px 12px;font-size:.9rem}
      .onb-input{padding:9px 12px;font-size:.9rem}
      .onb-step-nav{margin-top:8px}
      /* 4px -> 12px, 2026-08-06. Shane: the first letter of the nickname sat
         almost touching the avatar. The gap LOOKED bigger than it measured
         because .onb-stage-avatar carries a gold box-shadow glow that spreads
         ~9px past the circle, so 4px of gap plus 8px of .onb-face-copy margin
         left only ~3px of clear space between the glow and the letter.
         12px + the same 8px margin gives about 11px of real clearance, which
         is what the header row already reads as. Deliberately not more - he
         asked for room, "not too much". Mobile only; the desktop rule above
         is already at 22px. */
      .onb-face-card{gap:12px}
      .onb-name-input-row{gap:6px}
      .onb-face-copy{margin-left:8px}
      #nick-whoareyou-label{margin-left:6px}
      #nick-pickuser-label{margin-left:6px}
      #onb-card-how-text{margin-left:6px;text-align:justify}
    }

    /* ====================================================
       DASHBOARD REFRESH — borderless, soft-depth (2026 pass)
       Overrides earlier rules. Kept in one block for easy revert.
    ==================================================== */
    /* Tabs bar: drop the divider line, add breathing room */
    .tabs-bar{border-bottom:none !important;box-shadow:none}
    .tabs-inner{gap:4px}
    .tab-btn{border-radius:12px;transition:color .2s,background .2s}
    /* Was rgba(var(--neon-rgb),.06) - a gold so faint it read as no fill at all.
       Matched to the My posts pills. */
    /* PURPLE, not gold - back to how it was before 2026-08-01. Gold was tried
       and Shane picked this: the tab row sits directly above the gold Maskara
       card, and two golds stacked made the top of the screen compete with
       itself. Purple marks the tab just as clearly and leaves gold meaning
       one thing on that screen. The gold TEXT still carries the state. */
    .tab-btn.active{background:linear-gradient(180deg,rgba(var(--panel-hi-rgb),0.28) 0%,rgba(var(--panel-lo-rgb),0.22) 100%)}
    .tab-btn.active::after{left:50%;transform:translateX(-50%);width:20px;right:auto;height:2.5px;border-radius:2px;box-shadow:0 0 8px rgba(var(--neon-rgb),.5)}
    body.light-mode .tab-btn.active{background:rgba(83,71,49,.08)}

    /* Stat pills: borderless, quieter fill */
    .stat-pill{background:rgba(255,255,255,.04);border:none !important}
    body.light-mode .stat-pill{background:rgba(0,0,0,.05)}

    /* Compose card: no outline — lift it with a soft raised surface + shadow */
    .compose-wrapper{padding:16px 0 10px}
    .compose-card{
      border:none !important;
      background:linear-gradient(180deg,rgba(255,255,255,.035),rgba(255,255,255,.012));
      border-radius:18px;
      padding:18px 16px;
      box-shadow:0 1px 0 rgba(255,255,255,.04) inset,0 12px 30px -18px rgba(0,0,0,.7);
    }
    body.light-mode .compose-card{
      background:var(--card-bg);
      box-shadow:0 1px 2px rgba(0,0,0,.04),0 8px 24px -16px rgba(0,0,0,.25);
    }

    /* Toolbar: kill the top hairline, use spacing to separate */
    .compose-toolbar{border-top:none !important;padding-top:2px;margin:10px 0 4px;gap:6px}
    .toolbar-btn{background:rgba(255,255,255,.04);border:none !important;border-radius:10px;padding:6px 12px}
    .toolbar-btn.active{background:rgba(178,34,34,.22)}
    body.light-mode .toolbar-btn{background:rgba(0,0,0,.05)}

    /* Post button: gold accent to match the primary CTA language */
    /* ACCENT, NOT --gold — 2026-08-11. This read `linear-gradient(180deg,#f0d980,
       var(--gold))`, and --gold is the NEUTRAL decoration ink, so the Post button
       stayed pale whatever accent was picked. It is the single most accent-looking
       control on the Wall. Ink is derived from the accent rather than a fixed
       #241516 so it stays legible on a light accent as well as a dark one. */
    .post-btn{border:none !important;background:linear-gradient(180deg,color-mix(in srgb,var(--neon) 82%,#fff),var(--neon));color:color-mix(in srgb,var(--neon) 26%,#000);box-shadow:0 8px 20px -10px rgba(var(--neon-rgb),.5)}
    .post-btn:disabled{box-shadow:none}

    /* Section label (Stories): remove the trailing divider line */
    .feed-label{padding:16px 0 8px}
    .feed-label::after{display:none !important}

    /* ====================================================
       DASHBOARD LOOK PASS — mockup-inspired shapes/effects.
       Uses existing theme vars only (safe for light/gabi/modals).
    ==================================================== */

    /* Feed posts: rounder + mood-colored LEFT accent (like the mockup),
       keeping the existing top-bar off in favor of the side accent */
    .post-card{border-radius:18px;border-left-width:3px;border-left-style:solid;border-left-color:var(--border)}
    .post-card::before{display:none}
    .post-card[data-mood="sawi"]{border-left-color:#E91E63}
    .post-card[data-mood="galit"]{border-left-color:#FF5722}
    .post-card[data-mood="sawa"]{border-left-color:#607D8B}
    .post-card[data-mood="pagasa"]{border-left-color:var(--gold)}
    .post-card[data-mood="malalim"]{border-left-color:#9C27B0}
    .post-card[data-mood="nakakatawa"]{border-left-color:#4CAF50}
    .post-card:hover{box-shadow:0 12px 30px -22px rgba(0,0,0,.8)}

    /* Post cards v2: a real container per post — soft translucent glass fill +
       full thin border + rounded corners, so it's obvious where one person's
       post ends and the next begins, PLUS a thicker (4px) mood-colored left
       accent as the at-a-glance "whose post is this" cue. Still translucent,
       not the old solid #141319 block, so the shared gradient shows through. */
    .post-card{
      background:linear-gradient(180deg,rgba(255,255,255,.04),rgba(255,255,255,.015));
      border-radius:14px;
      border:1px solid rgba(255,255,255,.08);
      /* 4px -> 6px, 2026-08-11 ("i want it more bolder"). This is the LAST
         .post-card rule in the file, so it is the one that wins - the 3px
         declaration ~15 lines above is dead weight kept for its mood-colour
         list. Change the WIDTH here, the COLOURS there. */
      border-left-width:6px;border-left-style:solid;
      margin-bottom:14px;
      padding:2px;
    }
    .post-card:last-child{margin-bottom:0}
    .post-card:hover{box-shadow:0 12px 30px -22px rgba(0,0,0,.8);border-color:rgba(255,255,255,.14)}
    body.light-mode .post-card{background:var(--card-bg)}

    /* Composer v2: same soft glass container as the posts, plus a thin gold
       left accent so it visually belongs to the same family as the feed
       below it instead of looking like an orphaned flat block. */
    .compose-card{
      background:linear-gradient(180deg,rgba(255,255,255,.04),rgba(255,255,255,.015)) !important;
      border-radius:14px !important;
      border:1px solid rgba(255,255,255,.08) !important;
      /* Was a hardcoded gold edge on EVERY composer, free or paying - which
         both wasted the one colour that is meant to signal Gold, and made the
         box you write in look nothing like the posts you write. The colour is
         set inline by applyComposerAccent(); this is only the fallback for
         before that runs. */
      border-left:6px solid var(--silver) !important;
      box-shadow:none !important;
    }
    #compose-wrapper .compose-card{box-shadow:none !important}
    body.light-mode .compose-card{background:var(--card-bg) !important}

    /* Composer: glow in the currently-selected mood color.
       --compose-mood defaults to gold, set by JS on mood pick. */
    #compose-wrapper .compose-card{
      --compose-mood:var(--gold);
      --compose-mood-glow:rgba(var(--neon-rgb),.22);
      border-radius:22px;
      box-shadow:0 1px 0 rgba(255,255,255,.04) inset,
                 0 18px 40px -22px var(--compose-mood-glow),
                 inset 0 0 44px -22px var(--compose-mood-glow);
      transition:box-shadow .4s,background .4s;
    }
    /* Used to ring the compose avatar in the selected-mood color, but the main
       composer's mood picker (#mood-picker-wrap) was removed from the markup
       — this rule kept firing with --compose-mood's default gold value
       forever, so "Posting as" always showed a gold ring no matter your
       tier. Removed so the avatar's border-color (set by applyAvatarTierRing
       in updateHeaderAvatarRing) is the only ring color shown here — same
       static blue/red/gold as the header and My Profile. */

    /* Active tab highlight — was a solid gold gradient blob with inverted
       dark text, the only nav element that didn't match the rest of the
       app's container language. Now the same nested-gradient container as
       everywhere else, with the usual gold text (no more color inversion). */
    /* THE RULE THAT WAS WINNING, 2026-08-01. This sits ~1800 lines below the
       tab styles and repainted the active tab PURPLE, so setting gold up there
       did nothing at all. Same purple-vs-gold fill the My posts pills settled;
       this is the last rule standing, so the value has to be right HERE.
       Anything that restyles .tab-btn.active must come after this line or it
       will be silently overridden the same way. */
    /* THE LAST RULE STANDING for .tab-btn.active - anything that restyles it
       must come after this line or be silently overridden, which is exactly
       what happened when the gold was first set 1800 lines above. */
    /* NO PADDING CHANGE ON SELECT — 2026-08-11. Shane: "when u clicking trending
       and following its like its dancing the indention spacing."
       THE PADDING WAS: base .tab-btn is `12px 6px`, this rule made the selected
       one `6px 10px`. So the tapped tab got 8px WIDER and 12px SHORTER than it
       had been — its neighbours slid sideways to make room and the whole bar
       changed height. Four tabs sharing flex:1 means ANY size change to one is
       a size change to all four.
       RULE: a selected state may change COLOUR, and it may draw a shape, but it
       must never change the BOX. Anything that alters padding, border-width,
       font-size or letter-spacing on :active/.active moves everything beside it.
       The pill still reads — it is a fill plus a radius on the space the tab
       already occupied. */
    .tab-btn.active{background:linear-gradient(180deg,rgba(var(--panel-hi-rgb),0.28) 0%,rgba(var(--panel-lo-rgb),0.22) 100%);border-radius:999px;box-shadow:none;color:var(--neon)}
    .tab-btn.active::after{display:none}

    /* Breathing profile avatar (mockup) */
    .profile-chip-avatar{animation:avatarBreathe 4.5s ease-in-out infinite}
    /* Gold overrides the ambient "breathing" animation above with the same
       Choose-Your-Mask pulse the composer/profile avatars use. Needed because
       .profile-chip-avatar and .avatar-gold-ring have equal specificity, so
       without this, whichever rule is later in the file wins the whole
       `animation` property outright (not merged) — avatarBreathe was winning
       and its baked-in "0 0 0 3px var(--midnight)" solid ring is exactly the
       stray black line around the header avatar. Two classes here beats
       either single-class rule regardless of source order, so gold always
       gets the clean pulse ring instead of the breathing one. */
    .profile-chip-avatar.avatar-gold-ring{animation:onbAvatarRingGlow 2.6s ease-in-out infinite}
    /* Staff ring needs the exact same two-class treatment, for the exact same
       reason as gold above: .profile-chip-avatar's ambient avatarBreathe wins
       the `animation` property on specificity, and its baked-in
       "0 0 0 3px var(--midnight)" paints a solid black ring around the green.
       Offline agents get no animation at all so the green shows clean; online
       agents get the pulse, which carries its own box-shadow and never
       includes the midnight ring. */
    /* !important because specificity alone has already lost this fight once -
       .profile-chip-avatar's avatarBreathe carries BOTH the midnight ring and
       a gold halo, and any later rule anywhere in this file can take the
       `animation` property back outright. The agent ring must never inherit
       either: black over green looks broken, and a GOLD halo on a staff
       avatar actively misreads as a paid tier. */
    .avatar-agent-ring{animation:none!important;box-shadow:0 0 12px 1px rgba(63,206,122,.45)!important;border-color:#3FCE7A!important}
    .avatar-agent-ring.online{animation:agentRingPulse 2.4s ease-in-out infinite!important}
    @keyframes avatarBreathe{
      0%,100%{box-shadow:0 0 0 3px var(--midnight),0 0 16px rgba(var(--neon-rgb),.35)}
      50%{box-shadow:0 0 0 3px var(--midnight),0 0 26px rgba(var(--neon-rgb),.6)}
    }

    /* Bottom nav: mockup emoji icons + glowing headphone Call */
    .nav-emoji,.call-emoji{font-family:"Apple Color Emoji","Segoe UI Emoji","Noto Color Emoji",sans-serif}
    .nav-emoji{font-size:1.35rem;line-height:1}

    /* ====================================================
       WARM-DARK "INVITING SPOTLIGHT" BACKGROUND
       Exact reference values. One fixed z-index:0 container.
    ==================================================== */
    :root{
      /* DEAD, AND DELIBERATELY LEFT DECLARED — these three have ZERO consumers
         as of 2026-08-11 (grep var(--void / var(--ember) to confirm). They are
         aliases of themed tokens declared on :root, which means they freeze at
         :root's values and can never follow a picked theme. DO NOT USE THEM.
         If you need the ground, read --midnight / --ground-hi / --ground-lo. */
      --void:var(--midnight); --void-2:var(--ground-hi); --ember:var(--ground-lo);
      --spot-gold:#DCE7D6; --gold-hi:#F2F7EE; --gold-lo:#B6C7AE;
      --spot-ink:#F7FBF4; --spot-muted:#D2E0CB;
    }
    /* Old radial glow layer superseded by .bg */
    body::before{display:none !important}
    /* ONE FLAT GROUND — 2026-08-11. Shane, on the notifications and the
       hamburger: "different color form down. make it one."

       MEASURED OFF HIS SCREENSHOT, not guessed: top-centre read (114,144,88)
       and the same page lower and to the right read (101,136,84). Thirteen
       levels, warm-tinted, concentrated exactly where the bell and the
       hamburger sit — because `.app-header` is background:transparent by an
       override further down, so what looks like the menu's colour IS the page
       behind it.

       THREE LAYERS WERE ALL SAYING SOMETHING DIFFERENT: a gold radial washing
       the top, a dark radial at the bottom, and a vertical gradient between
       them. A panel, a card or a chrome bar is one flat value, so over that it
       can only agree at ONE scroll position and one screen width. Every round
       of "the panel doesn't match" this file has been through — and there have
       been several — traces back here.

       So the ground is now a single value, and #4E7A45 because that is already
       the brand green in theme-color and manifest.json. Cards stay
       --surface-card #5F8E55, one measured step above it, everywhere, forever.
       IF ANY OF THESE GRADIENTS COME BACK, the mismatch comes back with them —
       the depth they add is not worth the bug they cause. */
    /* var(--midnight), NOT a literal — 2026-08-11. This flat #4E7A45 was the
       ONE thing actually painting the app's ground (body::before is display:none,
       so the body background at the top of this file never shows), which meant
       every token in :root could change and the screen would not. The theme
       picker in My mask writes --midnight on <body>, and this is the line that
       lets that reach the page. */
    .bg{position:fixed;inset:0;z-index:0;overflow:hidden;pointer-events:none;
      background:var(--midnight);
    }
    .bg .emb{display:none}  /* removed the drifting round glow blobs ("circle circle") to match index */
    @keyframes drift{0%,100%{transform:translate(0,0) scale(1)}50%{transform:translate(-3%,2.5%) scale(1.07)}}
    /* VIGNETTE OFF, same reason. It darkened the edges by up to .22, so on a
       wide desktop the left and right of the page sat visibly below the middle
       — the horizontal half of the same complaint. Kept as an empty rule rather
       than deleted so the .vig element in the markup still has a home and this
       note stays attached to it. */
    .bg .vig{position:absolute;inset:0;background:none}
    .bg .grain{position:absolute;inset:0;opacity:.05;mix-blend-mode:overlay;
      background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='140' height='140'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='.9' numOctaves='3'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)'/%3E%3C/svg%3E")}
    .mote{position:absolute;border-radius:50%;background:radial-gradient(circle,rgba(var(--neon-rgb),.75),transparent 70%);animation:rise linear infinite;opacity:0}
    @keyframes rise{0%{transform:translateY(30px);opacity:0}12%{opacity:.8}88%{opacity:.5}100%{transform:translateY(-140px);opacity:0}}
    @media (prefers-reduced-motion: reduce){
      .bg .emb, .mote{animation:none !important}
    }

    /* Chrome (header/tabs/nav) — neutral violet-black translucent, matched to the
       "Choose your mask" onboarding screen's vignette tone (nick-overlay) instead of
       a reddish wash, so the gold/violet .bg spotlight reads the same on every screen.
       Alpha dropped from .55 → .22: at .55 the tint was muddying/darkening the gold
       glow behind the header instead of just softening it for legibility.

       TINT REMOVED ENTIRELY 2026-08-06, Shane: "i want like a whole same
       background". Even at .22 the film plus saturate(1.1) made the strip
       behind the header visibly richer than the pixel below it, and .app-header
       is a hard rectangle with no mask, so the change landed on a single pixel
       row - a visible seam under the avatar.
       THE BLUR STAYS. Blurring a smooth gradient gives back the same gradient,
       so blur alone is invisible when nothing is scrolled behind the chrome,
       but still softens post text passing underneath. It was the SATURATE and
       the violet film that were visible, never the blur.
       TRADE-OFF ACCEPTED: with no tint, blur is the only thing separating the
       logo/nickname from a post scrolling under them. If that reads as busy,
       bring back a tint at ~.10 WITHOUT saturate rather than reinstating .22. */
    body:not(.light-mode):not(.gabi-mode) .app-header,
    body:not(.light-mode):not(.gabi-mode) .bottom-nav{
      background:transparent !important;
      backdrop-filter:blur(18px);
    }
    /* Tabs bar: background/blur/mask on ::before (not the bar itself) so the
       feathered fade at the bottom edge only affects the backdrop tint, never
       the tab pills sitting in front of it (.tabs-inner is z-index:1 above). */
    body:not(.light-mode):not(.gabi-mode) .tabs-bar::before{
      background:transparent !important;
      backdrop-filter:blur(18px);
      -webkit-mask-image:linear-gradient(to bottom,#000 0%,#000 65%,transparent 100%);
      mask-image:linear-gradient(to bottom,#000 0%,#000 65%,transparent 100%);
    }
    /* Body base fill → warm void so no black shows behind the fixed .bg.
       READS --midnight DIRECTLY, NOT --void. 2026-08-11: --void is declared on
       :root as `var(--midnight)`, and a custom property is SUBSTITUTED AT THE
       ELEMENT THAT DECLARES IT — so --void resolved against :root's #4E7A45 and
       inherited that green down, frozen, no matter what theme was picked.
       applyTheme writes --midnight on document.BODY, which is below :root, so it
       could never reach --void. The visible symptom was a green strip down the
       scrollbar gutter: .bg is position:fixed and covers the viewport but NOT
       the gutter, so the canvas fill is the only thing painting those few px.
       RULE: never alias a themed token from :root. Read the token itself. */
    body:not(.light-mode):not(.gabi-mode){background:var(--midnight) !important}

    /* ====================================================
       GABI MODE — "warm lamp glow in a dark room" full background.
       Previously gabi-mode was explicitly EXCLUDED from the chrome-tint
       rules above, so the header/tabs/bottom-nav fell back to --chrome-bg
       at gabi-mode's near-opaque rgba(5,5,7,.96) — a flat solid black bar
       across the top and bottom of the screen. That's the "black spot" bug.
       And .bg itself was never overridden for gabi-mode, so night mode was
       just showing the same daytime gold/violet gradient, not its own look.
       This block gives gabi-mode its own full-height gold glow (one lamp-like
       source, not the daytime's separate top/bottom hotspots) and matches
       the chrome to it so header/tabs/nav blend in instead of reading as
       black slabs. */
    body.gabi-mode:not(.light-mode) .bg{
      background:
        radial-gradient(65% 50% at 50% 16%, rgba(255,220,150,.34) 0%, rgba(246,201,76,.18) 32%, rgba(180,130,60,.07) 52%, transparent 72%),
        radial-gradient(90% 65% at 50% 78%, rgba(246,201,76,.09) 0%, transparent 62%),
        radial-gradient(140% 100% at 50% 45%, transparent 42%, rgba(0,0,0,.4) 100%),
        linear-gradient(180deg, #16250F 0%, #122009 30%, #0E1A08 60%, #0A1206 100%) !important;
    }
    body.gabi-mode:not(.light-mode){background:#0A1206 !important}
    body.gabi-mode:not(.light-mode) .app-header,
    body.gabi-mode:not(.light-mode) .bottom-nav{
      background:rgba(20,14,8,.4) !important;
      backdrop-filter:blur(18px) saturate(1.1);
    }
    body.gabi-mode:not(.light-mode) .tabs-bar::before{
      background:rgba(20,14,8,.4) !important;
      backdrop-filter:blur(18px) saturate(1.1);
      -webkit-mask-image:linear-gradient(to bottom,#000 0%,#000 65%,transparent 100%);
      mask-image:linear-gradient(to bottom,#000 0%,#000 65%,transparent 100%);
    }
    /* The "Reveal" button and hugot tags use a hardcoded bright crimson
       (rgba(139,0,0,.85)) that reads as a jarring red spot against the new
       warm-gold room — it barely showed on the old near-black void, but
       stands out now. Shift it toward the same warm gold/amber family in
       gabi-mode instead of muting it away entirely (it still needs to read
       as an actionable button). */
    body.gabi-mode:not(.light-mode) .mask-reveal-btn{
      background:rgba(120,70,20,.85);
      border-color:rgba(246,201,76,.4);
      box-shadow:0 0 16px rgba(246,201,76,.25);
    }
    .call-emoji{font-size:1.5rem;line-height:1;display:block;animation:callGlow 2.4s ease-in-out infinite}
    @keyframes callGlow{
      0%,100%{filter:drop-shadow(0 0 3px rgba(57,255,136,.5))}
      50%{filter:drop-shadow(0 0 10px rgba(57,255,136,.95))}
    }
    /* Was overriding .nav-post-btn, which this button no longer uses - the
       ring and sizing are gone with the wrapper. Only the badge offset is
       still worth keeping. */
    #bn-call-btn .nav-icon::after{transform:translate(6px,-50%) scale(.72) !important}

    /* Save-code modal: widen so the sentence sits on one line (no justify gaps) */
    #show-login-code-modal .modal-sheet{max-width:520px}

    /* Login error: red + attention bounce (only when .is-error is set) */
    #login-status.is-error{color:#FF5A5A !important;font-weight:600;animation:loginErrShake .5s cubic-bezier(.36,.07,.19,.97)}
    @keyframes loginErrShake{
      0%,100%{transform:translateX(0)}
      15%{transform:translateX(-7px)}
      30%{transform:translateX(6px)}
      45%{transform:translateX(-5px)}
      60%{transform:translateX(4px)}
      75%{transform:translateX(-2px)}
    }
  
@keyframes spin{to{transform:rotate(360deg)}}
/* ---- Call modal container: same purple/plum glass look as the other
   containers app-wide (room cards, compose box, gate card, login/recover
   modals) instead of the flat solid .modal-sheet default. ---- */
/* FLAT DARK GREEN, NOT THE PANEL GRADIENT — 2026-08-13. Shane asked for this
   sheet to be one dark green ground. It was a two-stop gradient over a blur, so
   the top and bottom of the card were different greens and the header strip sat
   on a third; against the page it read as a stack of panels rather than one
   card. #2A4524 is flat, and the blur goes with it - there is nothing behind a
   centred modal worth blurring, and it costs a repaint on every scroll.
   THIS RULE IS WHY THE INLINE STYLE ON THE SHEET WAS REMOVED. Colour belongs
   here with the other modal rules, not scattered across app.html. */
#call-vb-modal .modal-sheet{
  /* #21361C, 2026-08-13. Shane picked it off the SELECTED pack card and asked
     for the whole sheet to be that colour - so this is not a new green, it is
     the measured result of #2A4524 under the card's rgba(0,0,0,.22) overlay.
     Taking the value from what he pointed at beats inventing a darker green
     and asking whether it matches. */
  background:#21361C;
  border:1px solid rgba(236,243,232,0.13);
  box-shadow:0 20px 50px rgba(0,0,0,0.5);
}
#call-vb-modal .modal-header{background:transparent;border-bottom:1px solid rgba(236,243,232,0.11)}
/* ---- Buy call minutes (Stripe) ---- */
.cvb-buy{margin-top:22px;padding-top:18px;border-top:1px solid var(--border);text-align:left}
.cvb-buy-head{display:flex;align-items:center;justify-content:flex-end;margin-bottom:10px;gap:8px}
.cvb-currency-select{background:var(--surface);border:1px solid var(--border);color:var(--text);
  font-size:.72rem;font-family:var(--font-ui);padding:5px 8px;border-radius:8px;cursor:pointer}
/* A real reassurance banner, not quiet fine print — this is the thing that
   proves to someone "nobody picked up last time" that their money is safe,
   so it needs to be impossible to miss, not something you'd scroll past. */
.cvb-balance{font-size:.85rem;color:#8effb0;font-weight:600;margin-bottom:14px;
  background:rgba(57,255,136,.08);border:1px solid rgba(57,255,136,.3);
  border-radius:10px;padding:10px 12px;text-align:center}
.cvb-balance b{color:#8effb0}
/* THE CARDS ARE STYLED HERE, NOT FROM JS — 2026-08-13. Shane: "theres like a
   bug blink in the call the containers is like stretch".
   THAT WAS EXACTLY WHAT IT LOOKED LIKE. The row painted at three equal full
   width columns from this stylesheet, then cvbSetDuration ran a moment later
   and rewrote every card's inline cssText to the narrow centred version - so
   the cards visibly stretched and snapped. A network-timed restyle can never
   beat first paint, so the resting look has to be correct here and JS must only
   toggle WHICH card is chosen. Same lesson as the Private Room card and the
   staff sidebar: do not paint something you are about to take back.
   GREEN BORDER RESTING, GOLD WHEN PICKED, per Shane. No gold in the FILL at any
   point - gold over green averages to olive, which two earlier attempts proved.
   The chosen card goes DARKER instead and the border carries the signal. */
/* 80px cards, spread across the 260px column - 2026-08-13. Was 92px centred.
   space-between rather than center so the FIRST card's left edge and the LAST
   card's right edge land exactly on the column edges, which is what makes the
   CHOOSE DURATION label and the currency chip line up with them. */
/* 1fr columns, not a fixed px - 2026-08-13. At the 260px phone column this
   computes to exactly 80px each, the value he approved; on the 380px desktop
   column the cards GROW with it instead of leaving three small boxes stranded
   in the middle. The outer edges land on the column edges either way, which is
   what aligns them with CHOOSE DURATION and the currency chip. */
.cvb-packs{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:10px;margin-bottom:10px}
.cvb-pack{border:1.5px solid rgba(168,227,154,0.32);
  background:rgba(255,255,255,0.05);
  border-radius:12px;padding:13px 4px 11px;text-align:center;cursor:pointer;position:relative;
  transition:background .15s,border-color .15s}
.cvb-pack:hover{border-color:rgba(247,200,56,0.55)}
/* GOLD UNDERLINE - 2026-08-13, option C from a four-way mock. The pale gold
   FILL that briefly lived here is gone: "i dont want this filled of container".
   The picked card now keeps the SAME fill as its neighbours and is marked by a
   gold border plus a 4px gold bar along its bottom edge - so the row reads as
   three identical cards with one underlined, not one card of a different
   material. overflow:hidden is what keeps the bar inside the 12px radius. */
.cvb-pack.is-on{border-color:#F7C838;overflow:hidden}
.cvb-pack.is-on::after{content:'';position:absolute;left:0;right:0;bottom:0;height:4px;background:#F7C838}
.cvb-pack.busy{opacity:.5;pointer-events:none}
.cvb-pack .mins{font-weight:700;font-size:.84rem;color:var(--text-soft)}
.cvb-pack.is-on .mins{color:var(--text)}
/* PRICE COLOURS PUT BACK THE WAY THE MOCK HAS THEM — 2026-08-13. Shane: "i
   dont want this design. i said follow the screenshot that i send u".
   The built version had them INVERTED against his own reference: gold on the
   picked card, plain on the other two. That reads as "the highlighted one is
   the expensive one". In his mock the two unpicked prices are gold and the
   PICKED one is plain white, so the gold border and the white price together
   say chosen. Do not swap these again without checking the reference image. */
.cvb-pack .usd{font-size:.86rem;color:#F7C838;font-weight:700;margin-top:5px}
.cvb-pack.is-on .usd{color:#FFFFFF}
.cvb-pack .peso{font-size:.62rem;color:var(--text-muted);margin-top:4px}
.cvb-pack.is-on .peso{color:var(--text-soft)}
.cvb-pack .conv{font-size:.64rem;color:var(--text-muted);margin-top:2px;min-height:12px}
.cvb-buy-note{font-size:.65rem;color:var(--text-muted);line-height:1.65;margin-top:8px;
  text-align:justify;padding:0 2px}

    /* ====================================================
       DESKTOP RAIL — 2026-08-10
       Desktop gets a left sidebar for DESTINATIONS; the bottom bar is mobile
       only. The hamburger is untouched and still owns ACCOUNT things (gold
       bars, profile, notifications, lists, settings). Nothing appears in both:
       the rail is places you go, the hamburger is things about you.

       220px + body padding-left is the whole layout change. The content
       columns keep their own max-width and stay centred in what is left, so
       post cards, the composer and the tabs bar are all untouched - widening
       the column would have meant re-tuning every one of them.

       BREAKPOINT LOWERED 1024 -> 860 on 2026-08-10. Shane: "what if a user is
       in 100% or more scale all browser in desktop, the side bar is not
       showing. so users will not be able to use that."
       HE IS RIGHT AND THE REASON IS ZOOM. A media query measures CSS pixels,
       not the screen. Browser zoom DIVIDES the CSS width: a 1366px laptop is
       1366 CSS px at 100%, 1093 at 125% and 911 at 150%. Windows also ships
       125% display scaling as the DEFAULT on many laptops, on top of browser
       zoom. So a 1024 floor was quietly dropping a large share of real desktop
       users onto the phone bottom bar, on a screen with obvious room for a
       rail. Accessibility guidance expects a layout to survive 200% zoom;
       collapsing a whole navigation pattern at 150% is not that.
       THE MATHS AT THE NEW FLOOR: 860 - 200px rail = 660 available, and the
       content column at that width is capped at 620, leaving ~20px each side.
       That is the tightest it gets and it still fits. The rail steps back up
       to 220px at 1024 where there is room for it.
       DO NOT raise this floor again without redoing that sum, and do not widen
       the rail past ~250px at the 1024 step.
    ==================================================== */
    .desk-rail{display:none}
    @media (min-width:860px){
      body{padding-left:200px}
      .bottom-nav{display:none}
      .desk-rail{display:flex;flex-direction:column;position:fixed;left:0;top:0;bottom:0;width:200px;z-index:120;
        box-sizing:border-box;padding:18px 8px;overflow-y:auto;overscroll-behavior:contain;
        background:var(--chrome-bg);border-right:1px solid var(--border)}
    }
    @media (min-width:1024px){
      body{padding-left:220px}
      .desk-rail{width:220px;padding:18px 10px}
      /* The rail gains 20px here, so the tagline can breathe. Kept in the SAME
         block as the width it depends on — split them and the next person to
         change the rail width will not know this is tied to it.
         .desk-rail .rgc-tag, NOT .rgc-tag: this block sits ~180 lines ABOVE the
         base rule, and at equal specificity the LATER rule wins — a plain
         .rgc-tag here would be silently overridden by the .8rem below. Two
         classes beat one whatever the order. */
      .desk-rail .rgc-tag{font-size:.88rem}
    }
    /* THE RAIL WORDMARK IS THE INDEX.HTML LOGO — 2026-08-10. Shane: "the maskara
       in the side bar fix it. it should be the orirignal logo from the index
       page".
       It was `color:var(--gold)`, and --gold in this app NO LONGER MEANS GOLD —
       it is the neutral decoration ink (#DCE7D6). So the rail wordmark was
       off-white, while the entry page's is the saturated GOLD RAMP. Same word,
       two different brands on two screens a user sees minutes apart.
       The ramp is index.html's, stop for stop, and it deliberately opens at
       #FFDF7A rather than a cream: on a coloured ground a metal ramp that starts
       near-white reads as WHITE at the top of every letter, which is the exact
       "theres white. i need gold loud" complaint from 2026-08-08. Saturation
       carries the loud, not lightness.
       -webkit-background-clip:text is what paints the gradient INSIDE the
       letters; `color:transparent` is required for it to show, and the
       background-color fallback keeps the word visible if clipping is ever
       unsupported.
       Letter-spacing .06em matches .logo-name, .gate-logo and the recovery
       watermark — all four wordmarks now agree. */
    /* Georgia here too — this is the same wordmark as .logo-name, just in the
       desktop rail. See the note on that rule. */
    .desk-rail-brand{font-family:var(--font-mark);font-style:italic;font-weight:700;font-size:1.35rem;
      letter-spacing:.06em;padding:0 10px;margin-bottom:34px;
      background-image:linear-gradient(180deg,color-mix(in srgb,var(--neon) 72%,#fff) 0%,var(--neon) 24%,var(--neon) 54%,var(--neon-dim) 80%,color-mix(in srgb,var(--neon-dim) 82%,#000) 100%);
      -webkit-background-clip:text;background-clip:text;color:transparent;
      -webkit-text-fill-color:transparent}
    .desk-rail-list{display:flex;flex-direction:column;gap:2px}
    /* THE 3px TRANSPARENT LEFT BORDER IS LOAD-BEARING, not decoration. It is on
       the RESTING state so the active row's bar has somewhere to appear without
       shifting the label a single pixel — the same trick the wardrobe tabs use.
       Never remove it "because it is invisible". */
    /* padding 10 -> 12px, gap 11 -> 12, 2026-08-13. Five rows at 10px read cramped
       once the labels came in at full size; this is still well inside the
       height budget recorded on .desk-rail-foot below. The short-window
       override still takes it to 7px under 760px. */
    .desk-rail-item{display:flex;align-items:center;gap:12px;width:100%;padding:12px 10px;border:none;border-left:3px solid transparent;background:transparent;
      border-radius:0 9px 9px 0;color:var(--text-muted);font-family:var(--font-ui);font-size:.82rem;text-align:left;cursor:pointer;
      transition:background .18s,color .18s,border-color .18s}
    .desk-rail-item:hover{background:rgba(255,255,255,.06);color:var(--text)}
    .desk-rail-item .nav-icon{display:flex;align-items:center;justify-content:center;font-size:1.15rem;flex:none}
    .desk-rail-item .nav-icon svg{width:20px;height:20px}
    /* Active mirrors the bottom bar's own active colour so one tab cannot look
       selected in the rail and unselected in the bar. */
    /* LEFT BAR + A WASH THAT FADES OUT, 2026-08-11. The old version filled the
       whole row with a gold tint, which made the active item the only gold
       SURFACE in the rail — competing with the Go Gold card directly beneath it,
       the one thing there that is actually trying to sell something.
       A bar says "you are here" more clearly than a fill, and it echoes the mood
       edge on every post card, so the app uses one visual language instead of
       two. The label goes to full --text; only the ICON keeps the accent, which
       is enough gold for a nav row. */
    .desk-rail-item.active{border-left-color:var(--neon);color:var(--text);
      background:linear-gradient(90deg,rgba(var(--neon-rgb),.14),rgba(255,255,255,.03) 70%)}
    .desk-rail-item.active .nav-icon{color:var(--neon)}
    /* Call keeps its green at rest AND when active, exactly as it does in the
       bottom bar, where the green icon rule already beats .nav-item.active. */
    #rail-bn-call-btn,#rail-bn-call-btn.active{color:#39ff88}
    /* Call's bar and wash go GREEN too — 2026-08-11. With the new left-bar active
       state, leaving this rule alone would have given Call a GOLD bar above
       GREEN text, the only row in the rail wearing two colours. The reserved
       Vent Buddy green wins here, exactly as it does for the icon. */
    #rail-bn-call-btn.active{border-left-color:#39ff88;
      background:linear-gradient(90deg,rgba(57,255,136,.14),rgba(255,255,255,.03) 70%)}
    #rail-bn-call-btn.active .nav-icon{color:#39ff88}
    /* The LIVE pill is a normal right-aligned flex child here, not an absolutely
       positioned pseudo-element. That whole class of bug (see the bottom bar's
       pill, which escaped its bar because it hung off an inline .nav-icon)
       cannot happen in a row this wide. */
    /* THE RAIL'S LIVE PILL — 2026-08-10. Shane: "the live button should be
       blinking in the side bar. also it needs to be beside the call and make the
       live button and text smaller than the call nav in the side bar".

       `margin-left:auto` IS WHAT MOVED IT AWAY. That pushed the pill to the far
       right of a 200px row, so it read as a separate column of the sidebar
       rather than something attached to Call. Removed — the pill now sits in the
       normal flex order, immediately after the word, with its own small gap.

       IT BLINKS WITH THE SAME `liveBlink` the bottom bar uses (app.css 206), not
       a second animation. One keyframe for both means the two navs can never
       fall out of step, and whenever this is finally wired to real availability
       there is one place to stop it pulsing.

       SMALLER THAN THE LABEL, deliberately and by a clear margin: the row's text
       is .82rem, this is .5rem. A status marker that competes with the
       destination it marks stops reading as a marker. Same reason it keeps
       `line-height:1` and tight padding — it should look pinned to the word, not
       stacked beside it. */
    .desk-rail-live{margin-left:7px;width:8px;height:8px;border-radius:50%;flex:none;
      background:#39ff88;padding:0;font-size:0;
      animation:liveBlink 1.2s ease-in-out infinite}
    /* SLOWED, NOT STOPPED — 2026-08-10, after Shane reported "the button is not
       blinking" twice while the CSS was correct both times. `animation:none`
       here was the cause: Windows "show animations" off (the default on many
       laptops) and macOS Reduce Motion both trigger this query, and the dot then
       sits perfectly still. On his machine it has probably never blinked.
       4s instead of none, matching what the onboarding chase light already does
       at app.css ~2033. The hazard reduced-motion protects against is fast
       repetitive motion; a slow opacity fade on an 8px dot is not that, and
       killing it entirely removes the only signal Call carries. If someone truly
       wants zero motion the dot is still legible — it is a green dot either way. */
    @media (prefers-reduced-motion:reduce){ .desk-rail-live{animation-duration:4s} }
    /* THE CARD SITS UNDER THE NAV, THE CHIP STAYS AT THE BOTTOM — 2026-08-11.
       margin-top:auto used to push the WHOLE foot down as one block, so the card
       floated in the middle of a large empty gap. The foot is now a column: a
       fixed gap after the nav puts the card near it, and the identity chip below
       carries the auto margin instead, so it stays pinned to the bottom. */
    /* min-height:auto, NOT 0 — FIXED 2026-08-12. THIS IS THE WHOLE "IT ONLY
       WORKS AT 80% ZOOM" BUG, and it was never about width.
       The foot is a flex column child with `flex:1`. `min-height:0` let it
       shrink BELOW its own content, and .rail-gold-card carries
       `overflow:hidden` — so on a window shorter than the rail needs, the card
       was squeezed and its own contents sliced off, with no scrollbar to say
       so. Nothing looked broken; the Go Gold button just was not there.
       WHY 80% "WORKED": browser zoom changes the CSS viewport. At 80% a
       720px-tall window reports 900 CSS px, which is enough room; at 100% it
       reports 720 and the rail runs out. The rail needs roughly 610px of
       height at the default type scale — that is the actual number.
       min-height:auto means the foot can never be smaller than its content, so
       the overflow goes to .desk-rail, which already carries overflow-y:auto
       and scrolls. NOTHING CHANGES ON A TALL WINDOW — flex-grow still fills the
       space and .desk-rail-identity's margin-top:auto still pins the chip to
       the bottom.
       DO NOT put min-height:0 back. It is the copy-paste default for scroll
       containers and it is wrong here: the scroll belongs to the RAIL, not to
       the foot. */
    /* 56 -> 76 and padding 14 -> 20, 2026-08-13. Shane at 75% browser zoom: the
       Go Gold card was sitting too close under Rooms, so the nav list and an
       upsell read as one continuous block instead of two separate things.
       SAFE TO GROW because of the min-height:auto fix above: extra height now
       pushes the overflow onto .desk-rail, which scrolls, instead of slicing
       the card. And the short-window override below still pulls both values
       back down under 760px, so no laptop loses the card below the fold. */
    .desk-rail-foot{margin-top:76px;padding-top:20px;display:flex;flex-direction:column;flex:1;min-height:auto}

    /* SHORT DESKTOP WINDOWS — 2026-08-12. The fix above guarantees nothing is
       ever CLIPPED again, but a scrolling sidebar on a laptop is still a
       downgrade: Go Gold and the profile chip would sit below the fold on a
       screen that has room for them if the padding is honest.
       So the vertical fat comes out below 760px of height, and only there:
       rail padding 18 -> 12, the wordmark's 34px gap -> 18, the foot's 56px
       gap -> 18, nav rows 10px -> 7px. That is about 90px recovered, which
       brings the rail's requirement from ~610px down to ~520px and puts a
       standard 1366x768 laptop comfortably back inside one screen.
       HEIGHT-GATED, NOT WIDTH-GATED, and paired with min-width:860px so it can
       only ever touch the rail — the phone layout has no rail and must not
       inherit these. On a tall monitor the roomy spacing is untouched.
       Type size is deliberately NOT reduced here. Shrinking text to win
       vertical space is the accessibility failure already recorded on the gate
       card: it fights the user's own zoom. Padding is free, type is not. */
    @media (min-width:860px) and (max-height:760px){
      .desk-rail{padding-top:12px;padding-bottom:12px}
      .desk-rail-brand{margin-bottom:18px}
      .desk-rail-foot{margin-top:18px;padding-top:10px}
      .desk-rail-item{padding-top:7px;padding-bottom:7px}
    }
    /* ===== MASKARA GOLD CARD (rail foot) — 2026-08-11 =====
       Replaces the outlined "Go Gold" row. A bare row assumed people already
       knew what Gold was; the card names it, gives one reason, states the price
       and offers a way to read more.
       THE GOLD HERE IS A REAL METAL RAMP, NOT --neon. Shane: "why is your gold
       yellow". --neon is #F7C838, a saturated yellow he chose in August when the
       old gold was washing out to white. True gold is less saturated and warmer,
       so this component uses literals: pale #F4E3B0 at the top through #9E7C1E
       at the foot. LITERALS ARE DELIBERATE - a ramp cannot be expressed by one
       token, and this card must look like gold even on a themed accent.
       IF --neon IS EVER REPOINTED APP-WIDE toward a truer gold, revisit these
       and consider deriving them again. */
    /* TRANSLUCENT, THE SAME MATERIAL AS THE POST CARDS — 2026-08-11. The first
       build used a near-opaque #2C4A26 and Shane was right that it was overkill:
       against the mid-green rail it was the darkest thing on the whole screen and
       read as an advert pasted into the app. White-over-green at .10/.035 is what
       .post-card and .compose-card already use, so this now belongs to the app;
       the gold button is what makes it stand out, not a different material. */
    /* LIT FROM ABOVE, IN GOLD — 2026-08-11. The wash fades out by 42% so the
       gold reads as LIGHT FALLING ON the card, not as a gold card. A full gold
       fill would make this an advert; a gold edge makes it lit.
       The top hairline is 1.5px and warmer than the body border, and the outward
       shadow is gold rather than black — a black shadow under a lit edge reads
       as a seam. */
    .rail-gold-card{background:linear-gradient(180deg,rgba(245,215,130,.15),rgba(255,255,255,.03) 42%);
      border:1px solid rgba(244,227,176,.20);
      /* LIGHT GOLD, NOT CREAM — 2026-08-11. This was rgba(248,239,200,.75),
         which is so desaturated it reads WHITE against green. A light gold has
         to keep its yellow: #F5D782 is pale but unmistakably gold. The glow
         under it was lifted to match, or the edge looks painted on. */
      border-top:1.5px solid rgba(245,215,130,.92);
      box-shadow:0 -1px 14px rgba(245,215,130,.16);
      border-radius:14px;padding:18px 16px;
      /* HARD BACKSTOP — 2026-08-12. Nothing inside this card may paint outside
         its rounded border again. See .rgc-tag below for what happened. */
      overflow:hidden}
    /* .rgc-label REMOVED 2026-08-11. Shane: "maskara gold is redundant here
       because there already maskara logo on top" — and he is right, the rail
       wordmark sits a few inches above it in the same face. The card now opens
       on the tagline, and the gold badge after it is what says which tier this
       is. If a label is ever wanted back, it should NOT repeat the wordmark. */
    /* THE TAGLINE IS GILDED, and the badge sits after it like a signature.
       FALLBACK MATTERS HERE: background-clip:text leaves the text with no solid
       fill, so a `color` is set FIRST and only overridden inside the @supports
       block. Without that, a browser that cannot paint the ramp renders an
       invisible sentence — a risk worth guarding on a sales line, even though
       every wordmark in this app uses the same technique. */
    /* CENTRED, AND SITTING LOWER IN THE CARD — 2026-08-11. justify-content
       centres the tagline WITH its badge as one group, so the badge stays a
       signature on the line rather than drifting to the right edge.
       flex-wrap is deliberately OFF now: wrapped, a long line would drop the
       badge onto its own centred row and it would read as an icon, not a
       signature. The rail is a fixed width, so this line cannot surprise us. */
    /* GEORGIA ITALIC, NOT --font-serif — Shane picked option 3 and asked for it
       italic. The distinction matters: --font-serif is PLAYFAIR, whose italic he
       rejected as "not good", and Georgia's italic is a completely different,
       more traditional letterform. It is also the face already used by the
       "My mask" heading and the entry page, so the app's display voice is now
       one font in every place it speaks.
       NO WEBFONT: Georgia is a system face, so this cannot flash on load.
       ANDROID HAS NO GEORGIA and falls back to Noto Serif — close in weight and
       colour, not identical. Gelasio is the metric-compatible fix if it matters. */
    /* SIZED FOR THE 200px RAIL, NOT THE 220px ONE — 2026-08-12. Shane sent a
       screenshot at 100% zoom with the sentence painting OUTSIDE the card's
       rounded border on both sides, and he was right that it looks broken.
       WHY: this was .95rem, and the note below says to check it fits "at this
       width" — but there are TWO widths. The rail is 220px only at >=1024px; at
       860-1023px it is 200px with 8px padding, which leaves the card just 156px
       of content. The line plus its badge did not fit, and because both this
       flex row and the text carry white-space:nowrap, it could not shrink — so
       it overflowed instead of wrapping, which is why it left the card entirely.
       .8rem is measured against the NARROW rail; the >=1024px block lifts it to
       .88rem where there is room. RULE: any fixed-width text in the rail has to
       be checked at 860px, not only on a wide monitor. */
    .rgc-tag{font-family:Georgia,'Times New Roman',serif;font-style:italic;font-weight:700;font-size:.8rem;line-height:1.3;margin-top:6px;color:#F0CE6A;
      display:flex;align-items:center;justify-content:center;gap:6px;text-align:center;white-space:nowrap;max-width:100%}
    /* ONE LINE, ENFORCED. white-space:nowrap on the TEXT is what holds it —
       without it the flex item shrinks and the sentence breaks after "your",
       which is what happened at 1.05rem. Size came down to .95rem so the line
       plus its badge clears the 220px rail with room to spare.
       IF THE COPY EVER CHANGES, check it still fits on one line at this width.
       A longer tagline needs a smaller size or a shorter sentence, not wrapping. */
    .rgc-tag .rgc-tag-text{white-space:nowrap}
    /* GOLD, AND THIS ONE IS SETTLED — Shane 2026-08-11: "wear it your way needs
       to be gold since that's already our advertising". He is right and it
       outranks my restraint argument: this line IS the pitch, and the pitch
       should look like the thing it is selling.
       THE COUNT IS BACK TO THREE GOLDS (edge, tagline, button) and that is fine
       — what made it overkill at FOUR was the perks link, which is now muted.
       SOLID COLOUR, NOT A GRADIENT: background-clip:text leaves the text with no
       real fill, so anything that stops the ramp painting renders an invisible
       sentence. Not a risk worth taking on the one line that sells.
       IF IT EVER NEEDS TRIMMING AGAIN, take the gold off the TOP EDGE. The
       tagline and the filled button both earn theirs. */
    /* THE GILDING IS GONE, deliberately. A gold sentence directly above a gold
       button gave the card two gold things and no hierarchy — the eye had
       nowhere to land. White text, gold button: the button is the thing to
       press. The @supports fallback that guarded the gradient is no longer
       needed, since the text now has a real colour. */
    /* Sized in em so the badge scales with the line it signs, never against it. */
    .rgc-badge{height:1.05em;width:auto;flex-shrink:0}
    /* #rail-gold-btn keeps this class and its id — the click handler in app.html
       is unchanged. It is now the filled CTA rather than the whole row. */
    .desk-rail-gold{display:block;width:100%;margin-top:13px;padding:9px;border:0;border-radius:10px;
      font-family:var(--font-ui);font-size:.78rem;font-weight:700;text-align:center;cursor:pointer;color:#211903;
      /* NO CREAM STOP — corrected 2026-08-11. Shane: "why its like lampa pale
         lousy". The ramp opened at #FBEFC6, which is near-white, so the top half
         of the button rendered WHITE and the whole thing looked washed out.
         This is a rule already recorded from 2026-08-08 and I broke it: on a
         coloured ground a metal ramp must NOT start at cream — SATURATION
         carries the loud, not lightness. Every stop here holds its yellow. */
      background:linear-gradient(180deg,#F5D25C 0%,#E0B63A 42%,#C69A22 100%);
      box-shadow:inset 0 1px 0 rgba(255,255,255,.35);transition:filter .18s}
    .desk-rail-gold:hover{filter:brightness(1.06)}
    /* Price is QUIET on purpose. The tagline sells; the number only has to be
       honest and findable. A large gold price turns the card into a price tag. */
    /* Slightly smaller than the perks link below it, so the two stop reading as
       two lines of equal importance — one is a fact, one is an action. */
    .rgc-price{text-align:center;margin-top:8px;font-size:.66rem;color:var(--text-muted)}
    /* ONE VIEW, NO INNER SCROLL. The shared .modal-sheet caps at 85vh and scrolls
       its own body, which on this page meant the Go Gold button and the price sat
       below the fold — the two things the page exists to lead to. This sheet is
       allowed to run to 94vh and never scrolls internally; the tile type above is
       sized so six tiles plus the CTA fit inside that.
       IF A SEVENTH TILE IS EVER ADDED, something has to give — shrink the type or
       accept the scroll. Do not just add a row and hope. */
    /* THE PAGE SIZES ITSELF TO THE WINDOW — 2026-08-11. Shane: it must be one
       page at 100% zoom in every browser, and must not shrink into a scroll when
       the window gets smaller.
       svh, NOT vh: on mobile browsers vh is the height WITH the address bar
       hidden, so a 94vh sheet is taller than the screen actually is until you
       scroll. svh is the small viewport height and is honest from the first
       paint.
       Everything inside is clamped against vh, so the tiles, type and gaps all
       shrink together as the window shortens instead of the page growing a
       scrollbar. The clamps have FLOORS: below roughly a 560px-tall window the
       type would stop being readable, and at that point scrolling is the honest
       failure rather than illegible text — the media query at the foot of this
       block hands the scroll back there. */
/* WIDTH SCALES WITH THE WINDOW — 2026-08-11. It was pinned at 360px inline, so
       on a big monitor, or at 90% zoom, the whole page stayed a small card in the
       middle of an empty screen and every clamp inside it was capped by a sheet
       that never grew. Tying the width to viewport HEIGHT (not width) keeps the
       proportions sane: the page is height-constrained by design, so a sheet that
       grows with available height stays balanced instead of turning into a wide
       letterbox on an ultrawide monitor.
       The 94vw term is the phone guard — never wider than the screen. */
    .gold-perks-sheet{width:min(94vw,clamp(400px,max(76vh,41vw),1020px));max-width:none;
      max-height:96svh;max-height:96vh;overflow-y:visible;display:flex;flex-direction:column}
/* THE WORDMARK CENTRES, THE CLOSE BUTTON STAYS PINNED — 2026-08-11. The header
       is space-between by default, which would leave the wordmark hard left. It is
       centred here by absolutely positioning the close button out of the flow and
       letting the title stack take the full width; centring inside a space-between
       row would only centre it in the space LEFT OVER by the button, which is
       off-centre by half the button's width and looks like a mistake. */
    .gold-perks-sheet .modal-header{padding-bottom:6px;flex:none;position:relative;justify-content:center}
    .gold-perks-sheet .modal-title-stack{text-align:center;flex:1 1 auto;min-width:0}
    .gold-perks-sheet .modal-close{position:absolute;right:15px;top:50%;transform:translateY(-50%)}
    /* CENTRED ONLY WHILE IT FITS — 2026-08-12. Shane at 100% zoom: "its messy and
       did show the 3.99 line below gold. and look the maskara logo its really
       messy."
       ONE CAUSE, BOTH SYMPTOMS. This element is `flex:1;min-height:0`, so on a
       short window it is SMALLER than its own content, and `justify-content:center`
       then pushes that overflow out of BOTH ENDS at once: the tiles ride UP over
       the wordmark (this body sits later in the DOM, so it paints on top of the
       header) and the price under .gp-cta falls off the BOTTOM. Neither is
       reachable — you cannot scroll to content that starts above a box, which is
       why the sheet's own overflow-y:auto did not save it.
       `safe center` is the real fix: it centres while there is room and falls
       back to flex-start the moment content would overflow the start edge. It
       ships in every current Chrome, Edge, Safari and Firefox. A browser that
       does not know it simply drops the line and keeps the plain `center` above
       — which is why BOTH declarations are written, in this order.
       THE max-height:880px BLOCK BELOW forces flex-start outright, so the fix
       does not depend on `safe` being understood at all. That block already
       existed for this exact symptom and never worked, because it only added a
       scrollbar and shrank the type — it never touched the centring, which is
       what was actually eating the content.
       DO NOT collapse these two lines into one. */
    .gold-perks-sheet .gp-body{flex:1;min-height:0;display:flex;flex-direction:column;justify-content:center;justify-content:safe center}

    /* ===== GOLD PERKS PAGE — 2026-08-11 =====
       Six SQUARE tiles, two per row. aspect-ratio:1 with an explicit
       grid-auto-rows is deliberate: a ratio alone collapses when the grid row is
       auto-sized (the same bug that squashed the emoji tiles earlier today).
       Two columns rather than three — at 420px, three squares are too small to
       hold a sentence, and every tile here carries one. */
    /* TILES SIZE TO THEIR CONTENT NOW, 2026-08-11. aspect-ratio:1 made every tile
       a ~180px square, and the shortest copy ("A badge beside your name") left
       half the box empty — the page read as six big boxes rather than six perks.
       align-items:stretch keeps the two tiles in a ROW equal height, so the grid
       still looks even; the row simply shrinks to whichever copy is longest. */
    /* EVERY TILE THE SAME HEIGHT — 2026-08-11. grid-auto-rows:1fr makes both rows
       equal and every tile fill its row, so the six line up on a clean grid
       instead of each one ending wherever its sentence happens to stop.
       THE WHOLE PAGE FITS WITHOUT SCROLLING, which is the point: the Go Gold
       button and the price have to be visible at the same time as the perks, or
       people read the list and never see the price. Type and padding below are
       sized to make six tiles plus the CTA clear a short laptop window. */
    .gp-grid{display:grid;grid-template-columns:1fr 1fr;grid-auto-rows:1fr;gap:clamp(9px,max(1.7vh,.9vw),18px);align-items:stretch}
    .gp-tile{display:flex;flex-direction:column;padding:clamp(13px,max(2.3vh,1.25vw),26px);border-radius:14px;min-width:0;
      background:linear-gradient(180deg,rgba(255,255,255,.09),rgba(255,255,255,.03));
      border:1px solid rgba(255,255,255,.10);border-top-color:rgba(245,215,130,.40)}
    /* The icon gets its own small container so it reads as a mark rather than
       floating text. Faint gold, never filled — a solid gold chip six times over
       would out-shout the one gold button at the foot. */
/* ICON BESIDE THE TITLE — Shane picked layout A, 2026-08-11. The head row puts
       the chip and the title on one line and the description underneath at full
       tile width, which saves a whole row of vertical space per tile. That space
       goes straight into the no-scroll budget on short windows.
       align-items:center keeps the chip centred against a title that wraps to two
       lines ("AI-generated avatars" is the one that does). */
    .gp-head{display:flex;align-items:center;gap:clamp(10px,max(1.5vh,.8vw),16px)}
    .gp-ico{width:clamp(34px,max(5.2vh,2.8vw),62px);height:clamp(34px,max(5.2vh,2.8vw),62px);flex:none;border-radius:9px;background:rgba(245,215,130,.14);
      border:1px solid rgba(245,215,130,.26);display:flex;align-items:center;justify-content:center;color:#F0CE6A}
    .gp-ico svg{width:56%;height:56%}
    /* THE RING SITS IN THE SAME CHIP AS EVERY OTHER ICON — 2026-08-11. It used to
       be a bare 32px circle, which made this the one tile whose mark was a
       different shape from the other five. It is now a small ring INSIDE .gp-ico,
       so the row of six reads as one set.
       EMPTY ON PURPOSE — a face inside it would be a face nobody chose, and the
       perk is the RING, not what it surrounds. */
    /* WIDTH AND HEIGHT ARE BOTH EXPLICIT — fixed 2026-08-11. This was
       `width:58%;aspect-ratio:1`, and .gp-ico is a FLEX container: a flex item
       cannot get its height from a ratio when its height is auto, so the ring
       collapsed to zero height and there was nothing on screen to blink. Shane
       asked why it was not pulsing three times — the animation was running
       perfectly on an invisible element.
       SAME TRAP AS THE EMOJI TILES. If a box vanishes or squashes, check whether
       aspect-ratio is being asked to do a job its parent will not let it do. */
    .gp-ring{width:58%;height:58%;flex:none;box-sizing:border-box;border-radius:50%;border:2px solid #F0CE6A;
      animation:gpRingBlink 1.6s ease-in-out infinite}
    /* IT BLINKS, IT DOES NOT JUST BREATHE. Two earlier versions only expanded a
       soft halo and Shane could not see them at all — "im just seeing just a
       ring". A halo is invisible at this size; what reads is the RING ITSELF
       changing. This animates border-colour, opacity AND the glow together, so
       the ring visibly brightens and dims rather than shedding a faint aura.
       LESSON: at 20px, animate the shape, not the space around it. */
    @keyframes gpRingBlink{
      0%,100%{border-color:rgba(240,206,106,.30);opacity:.55;box-shadow:0 0 0 0 rgba(240,206,106,0)}
      50%{border-color:#FFE08A;opacity:1;box-shadow:0 0 9px 2px rgba(240,206,106,.75)}
    }
/* Reduced motion gets a STATIC BRIGHT ring, not a stopped mid-animation one —
       `animation:none` alone would leave whatever the last computed frame was. */
    @media (prefers-reduced-motion:reduce){.gp-ring{animation:none;border-color:#FFE08A;opacity:1}}
    /* THE TITLE HAS TO OUTRANK THE DESCRIPTION — 2026-08-11. It was .68-.8rem
       against a .58-.68rem description: barely a pixel apart, so neither read as
       a heading and Shane could not scan them. Now roughly 1.35x the description
       at every window size, at weight 700 and full --text. The description also
       steps DOWN to --text-soft, so the contrast comes from two directions
       rather than size alone.
       letter-spacing is slightly negative because a short bold line at this size
       looks loose otherwise. */
/* ONE LINE, ALWAYS. nowrap plus titles short enough to fit the ~120px left
       beside the chip on a 360px sheet. IF A TITLE IS EVER LENGTHENED, check it
       still fits — nowrap will not wrap it, it will clip it.
       THE CEILING IS HIGH ON PURPOSE (1.5rem). Shane: it must still be big when
       the browser is under 100%. Zooming OUT makes the viewport taller in CSS
       pixels, so a vh-based size grows — but only until it hits its max. A low
       ceiling meant the text stopped growing and then simply rendered smaller and
       smaller on screen. Raising it lets the type keep scaling with the zoom-out
       instead of stalling. */
    .gp-title{font-size:clamp(1.05rem,max(2.9vh,1.55vw),2.1rem);font-weight:700;color:var(--text);line-height:1.2;letter-spacing:-.01em;min-width:0}
    .gp-desc{font-size:clamp(.82rem,max(2.05vh,1.12vw),1.5rem);color:var(--text-soft);line-height:1.45;margin-top:clamp(6px,1vh,9px)}
    /* Same saturated ramp as the rail card's button — one gold, two places. */
    .gp-cta{display:block;width:100%;margin-top:clamp(12px,max(2.2vh,1.2vw),22px);padding:clamp(11px,max(2vh,1.1vw),18px);border:0;border-radius:11px;cursor:pointer;
      font-family:var(--font-ui);font-size:clamp(1.05rem,max(2.6vh,1.4vw),1.9rem);font-weight:700;color:#241802;
      background:linear-gradient(180deg,#F5D25C 0%,#E0B63A 42%,#C69A22 100%);
      box-shadow:inset 0 1px 0 rgba(255,255,255,.35);transition:filter .18s}
    .gp-cta:hover{filter:brightness(1.06)}
    .gp-price{text-align:center;margin-top:clamp(9px,max(1.4vh,.75vw),16px);font-size:clamp(.82rem,max(1.85vh,1vw),1.3rem);color:var(--text-muted)}

    /* Perks link, restored 2026-08-11. Underlined on hover only, and set in the
       UI font at .7rem so it reads as a link rather than a second button — the
       card must have exactly one thing that looks pressable. */
    /* MUTED, ARROW KEPT, NO UNDERLINE — Shane's call. The arrow is what says
       "this goes somewhere"; colour and underline were both doing that job a
       second and third time. Hover brightens the text only. */
    .rgc-perks{display:block;width:100%;margin-top:10px;padding:0;border:0;background:none;cursor:pointer;
      text-align:center;font-family:var(--font-ui);font-size:.7rem;font-weight:600;color:var(--text-soft)}
    .rgc-perks:hover{color:var(--text);text-decoration:none}
    @media (prefers-reduced-motion:reduce){.desk-rail-item,.desk-rail-gold{transition:none}}

    /* GOLD HIDDEN FROM ONBOARDING — 2026-08-10.
       Shane: "i said hide the gold there. just free only". Onboarding offers
       the free emoji faces and nothing else; Gold is sold from the dashboard
       instead, where someone has actually seen the app first.

       HIDDEN, NOT DELETED, and this matters: setNickLang writes translated
       strings into the onb-tier-* ids inside these blocks with no guard for the
       element being missing, so deleting the markup throws there and takes
       every other translated string on the screen with it. That exact mistake
       cost a session on 2026-08-09.

       The pricing modal (#nick-onb-step-c) is also skipped by the NEXT handler
       now, so these rules are a second line of defence rather than the only
       thing hiding Gold - if the modal is ever reachable again, it still must
       not offer a tier the flow cannot sell. */
    #nick-onb-step-c .onb-price-card[data-tier="gold"],
    #onb-chip-gold,
    #nick-step-1 .onb-chip[data-tier="gold"]{display:none !important}
    /* With Gold gone the chip row holds a single "Emoticons" chip, which is a
       control offering no choice. */
    #nick-onb-step-c .onb-chip-row,
    #nick-onb-step-c .onb-chip-row-label{display:none !important}


    /* STEP-D REPAINT — 2026-08-10. Shane: "also inside maskara, make it WHITE
       color, back white color, all white colors".
       THREE SEPARATE LEFTOVERS, all fixed above, worth naming because they look
       like one bug and are not:
       .preview-label and .feat-desc were #8b93a7, a gray-blue from the
       pre-green palette (only 2 uses in the whole file, so contained).
       .onb-step-btn - the Back button - was never its own colour: it inherits
       --onb-muted, which is #a99fc6, the STALE PURPLE that caused most of the
       2026-08-09 session. Third time that token has surfaced. Changing the base
       rule repaints Back on every onboarding step, which is what he asked for.
       .feat-name was hardcoded inline five times in the markup; those inline
       styles are removed and this rule replaces them, so the next repaint
       cannot miss them the way this one nearly did. */
    .feat-name{color:var(--onb-ink,#F7FBF4)}
    /* "Hide the real me" says it is tappable on a cursor. TAP WAS ALREADY
       BUILT and always has been - the pointerup handler measures how far the
       pointer moved and, under TAP_THRESHOLD, calls nickAutoPlay() instead of
       nickFinish(). I briefly added a second click handler on 2026-08-10 and
       REMOVED IT the same day: it would have fired on top of that pointerup
       path and started account creation twice. Do not add one. If a tap ever
       appears not to work, the fault is inside nickAutoPlay/nickFinish, not in
       the absence of a click listener. */
    .nick-slide-track.onb-slide{cursor:pointer}

    /* THE RAIL MUST NOT SHOW DURING ONBOARDING — 2026-08-10.
       Shane: "its already showing while u r also inside maskara page".
       WHY IT LEAKED, and it is not a z-index bug: .nick-overlay is fixed at
       z-index 999 and DOES sit above the rail, but its background is a radial
       gradient that is TRANSPARENT for the middle 55% and only darkens at the
       edges. So the overlay was catching the clicks while the rail showed
       straight through it. Raising the rail's z-index would have made it worse;
       darkening the overlay would change every onboarding screen.
       Two selectors on purpose: the sibling form works wherever the overlay and
       the rail are siblings of body, and :has covers the case where they are
       not. Either one alone leaves a gap depending on how the markup nests. */
    .nick-overlay.show ~ .desk-rail{display:none !important}
    body:has(.nick-overlay.show) .desk-rail{display:none !important}
    /* THE RAIL WAS NOT THE ONLY THING LEAKING THROUGH. Shane's screenshot also
       showed the APP HEADER's language button sitting behind the onboarding
       card, because the same transparent overlay lets every piece of app chrome
       show through - the rail was just the most obvious one.
       This mirrors the element list body.gate-locked already uses (app.css
       ~1515) and for the same reason. visibility, not display, so nothing
       reflows when onboarding ends and the reveal animation runs on elements
       that kept their boxes. */
    .nick-overlay.show ~ .app-header,
    .nick-overlay.show ~ .tabs-bar,
    .nick-overlay.show ~ .main-content,
    .nick-overlay.show ~ .gabi-bar,
    .nick-overlay.show ~ .bottom-nav{visibility:hidden}
    body:has(.nick-overlay.show) .app-header,
    body:has(.nick-overlay.show) .tabs-bar,
    body:has(.nick-overlay.show) .main-content,
    body:has(.nick-overlay.show) .gabi-bar,
    body:has(.nick-overlay.show) .bottom-nav{visibility:hidden}


    /* STEP DOTS REMOVED — 2026-08-10. #onb-step-dots is painted from
       ONB_STEP_IDS, and with the finish page deleted and the pricing modal
       skipped there is one live screen left. A progress indicator for a
       one-step flow tells the user nothing and implies screens that no longer
       exist. Hidden rather than deleted: onbShowStep still walks the dots with
       querySelectorAll, which no-ops harmlessly on a hidden set. */
    #onb-step-dots{display:none !important}


    /* GROUND REVERTED — 2026-08-10. I darkened it earlier the same day
       (#48733F/#3E6337/#34522D) to make the cards lift off it; Shane rejected
       that: "the background that you put in the dashboard is darker. i dont want
       that, make sure its the same full background in index and in choose ur
       mask".
       THESE THREE STOPS ARE index.html's GROUND, unchanged. The app, the entry
       page and the onboarding card now paint the same green, which is the point
       — a visitor crosses all three within a minute and any difference reads as
       a different site, not a different screen.
       THE FLATNESS IS BACK AND IT IS KNOWN: --card-bg is #5A8B4F, identical to
       the FIRST STOP of this gradient, so a card at the top of the page has
       nothing to lift off. The remaining lever is the CARD, not the ground —
       raising --card-bg (and --surface / --surface-2 with it) separates them
       without touching a ground that now has to match two other files.
       DO NOT DARKEN THIS AGAIN. */

    /* ====================================================
       FLOATING PANELS ARE OPAQUE — 2026-08-10
       Shane, screenshot of the "Post to" dropdown with the feed readable
       straight through it: "thatsa annoying. fix ni everytgubg thats hows the
       backgrounbd".
       THE RULES ALREADY SAID 0.97/0.96, which is a 3% wash and could never look
       like that on its own. Two things stack up to produce it: an alpha over a
       ground that is now DARKER than the panel (the .bg was dropped to
       #48733F/#3E6337/#34522D earlier today) reads far more transparent than
       the same alpha did over the old lighter ground, and if
       `rgba(var(--panel-hi-rgb),0.97)` ever fails to resolve the WHOLE
       background declaration is invalid and the element falls back to fully
       transparent — there is no partial failure with var() in a colour
       function.
       So this block does two things at once: a SOLID background-color that
       needs no variable at all, and an opaque gradient layered on top. If the
       token resolves you get the intended two-stop panel; if it does not, the
       solid colour is still there. Either way nothing shows through.
       These are all OVERLAY surfaces — a menu, a picker, a sheet. Content
       behind them is not context, it is noise, and on this app it is other
       people's confessions sitting under a control you are about to use.
    ==================================================== */
    .post-dest-list,
    .emoji-picker-panel,
    .settings-panel,
    .post-more-menu,
    .modal-sheet{
      background-color:rgb(var(--panel-hi-rgb));
      background-image:none;
    }
    body.light-mode .post-dest-list,
    body.light-mode .emoji-picker-panel,
    body.light-mode .settings-panel,
    body.light-mode .post-more-menu,
    body.light-mode .modal-sheet{background-color:#C6E0B7}
    body.gabi-mode:not(.light-mode) .post-dest-list,
    body.gabi-mode:not(.light-mode) .emoji-picker-panel,
    body.gabi-mode:not(.light-mode) .settings-panel,
    body.gabi-mode:not(.light-mode) .post-more-menu,
    body.gabi-mode:not(.light-mode) .modal-sheet{background-color:#20301C}


    /* THE HEADER WORDMARK IS HIDDEN ONLY WHERE THE RAIL EXISTS — 2026-08-10.
       Shane: "in the 2nd maskara in the dashbaoar delete that because its
       redundant". It IS redundant on desktop — two MASKARAs, one above the
       other, a few hundred pixels apart.
       IT IS NOT REDUNDANT ON MOBILE. There is no rail below 860px, so deleting
       .logo-name outright would leave the phone app with NO wordmark at all —
       and phones are the whole audience. Hiding it inside the same breakpoint
       that creates the rail means exactly one wordmark exists at every width,
       automatically, with no second rule to keep in step. */
    @media (min-width:860px){
      .logo-name{display:none}
    }

    /* IDENTITY IN THE RAIL FOOTER — 2026-08-10. The .profile-chip node is moved
       here by script at >=860px (see the block at the foot of app.html) and moved
       back to the header below that width. These rules only reshape it for the
       narrower container; the avatar is ALREADY 52px in .profile-chip-avatar, so
       the "make the ring big" ask needed no size change at all — it was the
       header's cramped row that made it read small.
       NO "LOGGED IN AS" CAPTION, deliberately: an avatar and a nickname in your
       own sidebar cannot be anyone else, and the caption costs exactly the
       vertical space the mask wants. */
    /* margin-top:auto PINS THE CHIP TO THE BOTTOM of the foot, so the gold card
       sits up near the nav and the space falls between them instead of below the
       chip. THIS IS THE RULE THAT WINS — an earlier .desk-rail-identity higher up
       this file was setting a fixed 12px and quietly beating it, which left the
       whole foot floating with dead space underneath. One rule, here. */
    .desk-rail-identity{margin-top:auto;padding-top:12px;border-top:1px solid rgba(255,255,255,.10)}
    .desk-rail .profile-chip{margin-top:0;padding:6px 8px;border-radius:10px;gap:12px}
    .desk-rail .profile-chip:hover{background:rgba(255,255,255,.06)}
    /* THE NICKNAME WAS BEING CUT OFF — FIXED 2026-08-11. Shane at 100% zoom:
       "Shattered St" with the rest sliced away mid-word.
       THE OLD COMMENT HERE WAS WRONG. It claimed .logo-tag carries "ellipsis +
       nowrap". It carries `white-space:nowrap` and `overflow:visible` and NO
       text-overflow at all — so there was never an ellipsis. The rail sets
       overflow-y:auto, which forces overflow-x to compute to auto as well, and
       that is what chopped the word. Not a truncation anyone designed; a clip.
       THE MATH: rail is 200px (220px above 1024), less its own padding, less
       the chip's, less the 52px mask and the 12px gap — the name gets about
       104px. At .9rem that is roughly twelve characters, and the nickname
       generator hands out two-word names far longer than that. So this was
       never about zoom; every long nickname was already broken at 100%.
       THE FIX IS TO WRAP, NOT TO SHRINK. The 52px mask stays — it is there
       because he asked for the ring to read big, and cutting it to buy width
       would undo that to solve a text problem. Two lines, clamped, with the
       base rule's 10px left padding dropped to buy back a little more room.
       overflow-wrap:anywhere so a single unbroken word breaks instead of
       clipping. Both -webkit-line-clamp and the standard line-clamp are set:
       the prefixed one is what actually ships in every current browser,
       including Firefox. */
    .desk-rail .logo-tag{
      font-size:.9rem;
      padding-left:0;
      min-width:0;
      flex:1 1 auto;
      white-space:normal;
      overflow-wrap:anywhere;
      line-height:1.28;
      display:-webkit-box;
      -webkit-box-orient:vertical;
      -webkit-line-clamp:2;
      line-clamp:2;
      overflow:hidden;
    }
    /* align-items:center on the chip so a name that wraps to two lines stays
       centred against the mask instead of hanging below it. */
    .desk-rail .profile-chip{align-items:center}

    /* ====================================================
       REPLY THREAD + DESTINATION DROPDOWN — 2026-08-10
       Built from the two mocks Shane approved.
    ==================================================== */

    /* A LINE BETWEEN REPLIES. Three messages from the same person, with the same
       avatar and the same timestamp, ran together as one block - there was no
       edge telling you where one ended. `.reply-card:last-child{border-bottom:none}`
       already existed further up, so the last row stays clean on its own. */
    .reply-card{border-bottom:1px solid rgba(255,255,255,.09)}

    /* The timestamp now sits inline after the nickname (the markup moved in
       app.html). Smaller and muted so the name still leads the line. */
    .reply-author .reply-time{margin-left:7px;font-size:.62rem;opacity:.85}

    /* 22px was smaller than any other avatar in the app and the mask inside it
       was unreadable - which matters here more than anywhere, because in a
       thread the face is the ONLY thing telling two anonymous people apart.
       30px with a real ring, matching the feed card's own author avatar. */
    .reply-avatar{width:30px;height:30px;font-size:17px;border:1.5px solid var(--silver)}

    /* SEND IS THE ONE ACTION IN THE THREAD and it was wearing the same outlined
       pill as the emoji button beside it, so the two read as equals. Gold with
       the deep-green ink used on every other primary button in the app - never
       white on gold, which lands around 1.6:1. */
    .reply-send-btn{background:linear-gradient(180deg,var(--neon),var(--neon-dim));border:none;color:#1E3A18;font-weight:600}
    .reply-send-btn:hover{filter:none;box-shadow:0 2px 10px rgba(var(--neon-rgb),.28)}

    /* THE DROPDOWN WAS TALLER THAN ITS CONTENT NEEDED. Six destinations at the
       old 320px cap all fitted, so nothing scrolled and the list hung over the
       post card below it - which is why the last row looked like it was sitting
       on the React button.
       226px cuts the sixth row in half. A partially visible row is the oldest
       "there is more below" signal there is, and it needs no fade, no shadow and
       no extra element - the list's own overflow does the work. */
    .post-dest-list{max-height:min(226px,45vh)}


    /* THE "BLACK LINES" IN THE CORNER WERE THE BROWSER'S RESIZE GRIP — 2026-08-10.
       Shane: "u can see the container thers like a black lines in the right
       bottom side of the container fix that bug".
       Not a bug in the CSS: `resize:vertical` on a <textarea> makes the browser
       draw its own diagonal grabber in the bottom-right corner, and it is drawn
       by the OS, so it ignores the palette entirely — which is why it reads as
       stray dark marks on a green box. `resize:none` removes it.
       Nothing is lost: the field is 280 characters and already scrolls. Manual
       resizing on a reply box is a control almost nobody uses and the handle
       costs a permanent visual defect to offer it. */
    .reply-input{resize:none}


    /* REPLIES ARE UPRIGHT NOW — 2026-08-10. Shane asked why replies were italic
       and whether it came from the writer's font setting. IT DID NOT. The font
       PICKER only applies to POSTS: the chosen key is stored on the post row and
       read when the post body is drawn. Replies carry no such field and nothing
       reads one — every reply from every user rendered the same way because
       .reply-text hardcoded it.
       AND THE ITALIC WAS NOT THE `font-style` LINE. Playfair Display ships
       ITALIC-ONLY in fonts/fonts.css (all six @font-face blocks), so a browser
       asked for upright Playfair returns the italic file anyway. That is the
       same trap as 2026-07-31, when the italic was removed from .post-content
       and nothing changed until the family moved to Inter.
       So both rules move to var(--font-ui), matching .post-content. `font-style:
       normal` is belt-and-braces: it does nothing while the family is Inter, and
       it stops the slant returning if anyone ever points these back at a serif.
       Italic survives where it is a BRAND MARK rather than reading matter — the
       wordmark, the share card, the Gold name default. A thread is reading
       matter. */
    .reply-text,.reply-input{font-family:var(--font-ui);font-style:normal}


    /* PANELS AND CARDS ARE ONE COLOUR NOW — 2026-08-10. Shane: "fix the
       containers backgrounds it should be light green right same with other
       containers?" and "need to fix the background of the wall dropdown, and
       others drop down its annoying".
       They were opaque already, but the wrong VALUE: the dropdowns painted a
       #5F8757 -> #588352 gradient that darkens downward, while a card is
       rgb(--panel-hi) = #6A8F63 flat. So every menu read a shade heavier than
       the card it opened over.
       FLAT rgb(--panel-hi), gradient removed. That token IS the card colour
       (white .16 over the ground, solved once), so a panel and a card can no
       longer disagree — and if the palette moves, both move together.

       THE AVATAR INTERIOR IS AN ALPHA, NOT A HEX. --avatar-bg was #3E5B38, a
       deepened green fixed at one value, so the disc was darker than the
       sidebar, the composer and the feed card it sat inside. A white lift
       matches whatever is behind it, in every container, forever. Hand-matching
       four hexes is what drifts.
       Note it is now LIGHTER than the surface rather than darker: the mask is
       the subject, and a dark well around it read as a hole in the card. */
    :root{--avatar-bg:rgba(255,255,255,.06)}

    /* THE REPLY THREAD WAS DARKER THAN THE POST IT ANSWERS. rgba(0,0,0,.2) put
       the replies HEAVIER on the page than the confession above them, which is
       backwards — the post is the subject, the thread is the response. A white
       lift instead, so it reads as a recess in the card rather than a hole
       punched through it. */
    .replies-section{background:rgba(255,255,255,.05)}

    /* THE DROPDOWNS, ONE MORE TIME — 2026-08-10. Shane: "this is really annoying
       the backround text".
       Everything above is correct and a console check confirmed the computed
       background really is the solid colour. So if it still reads through, it is
       not the VALUE losing — it is something else winning: an inherited opacity,
       a filter on an ancestor, or a stale copy of this file.
       This block removes every remaining way that can happen, on the panel
       itself, with !important so nothing later or more specific can take it
       back. `opacity:1` matters because a parent's opacity cannot be undone from
       a child, but the panel's OWN opacity can — and that is the one thing left
       that a colour check would not have revealed. `backdrop-filter:none` and
       `mix-blend-mode:normal` are the other two ways a fully opaque fill can
       still show what is behind it.
       IF IT STILL BLEEDS AFTER THIS, it is an ancestor's opacity or filter, and
       the only fix is finding that ancestor — no rule on the panel can win
       against it. */
    .post-dest-list,
    .emoji-picker-panel,
    .settings-panel,
    .post-more-menu{
      background-color:rgb(var(--panel-hi-rgb)) !important;
      background-image:none !important;
      opacity:1 !important;
      backdrop-filter:none !important;
      -webkit-backdrop-filter:none !important;
      mix-blend-mode:normal !important;
      isolation:isolate;
    }

    /* ====================================================
       ONE GREEN FOR EVERY SURFACE — 2026-08-10
       Shane: "the notifications and hamburger menu has different background
       green compare to all the greens to the down, thats annoying".
       He is right, and there were TWO different mistakes making it happen.

       (1) THE PANELS WERE TOO LIGHT. They were painting rgb(--panel-hi-rgb)
       = #6A8F63 flat, while a feed card renders as the PAGE plus a ~4% white
       lift — around #5F8B55. A panel was landing a full step above every card
       under it. They now use var(--card-bg), the same token the cards do.

       (2) THE NOTIFICATION ROWS WERE ALPHA, NOT SOLID. `.notif-row` used
       rgba(--panel-hi,.28) over whatever was behind, so its colour changed with
       scroll position on a gradient ground — the exact bug already recorded on
       the confession cards in index.html. Solid now, same token as everything
       else.

       THE INSET SHADOW IS NOT DECORATION. `box-shadow: inset 0 0 0 9999px` fills
       the entire box with an opaque colour ABOVE the background layer and BELOW
       the text. Three rounds were spent on dropdowns that read through despite a
       solid background-color and a verified-clean ancestor chain; whatever is
       defeating the background cannot defeat this, because it is not painted as
       a background at all. If the see-through ever returns, this is the line
       that will still be holding.
    ==================================================== */
    .post-dest-list,
    .emoji-picker-panel,
    .settings-panel,
    .post-more-menu{
      background-color:var(--card-bg) !important;
      background-image:none !important;
      box-shadow:inset 0 0 0 9999px var(--card-bg), 0 12px 30px rgba(0,0,0,.5) !important;
    }
    .notif-row{
      background:var(--card-bg);
      box-shadow:inset 0 0 0 9999px var(--card-bg);
    }
    .notif-row.unread{
      background:var(--card-bg);
      box-shadow:inset 0 0 0 9999px var(--card-bg), inset 0 0 0 9999px rgba(232,194,90,.12);
    }

    /* ====================================================
       EVERY SURFACE IS ONE SOLID GREEN — 2026-08-10
       Shane, for the third time: "theres still change color in notification and
       hamburger menu background to the trending etc. its like not the same
       color background and theres like a line".

       THE REAL CAUSE, and every previous attempt of mine missed it: the CARDS
       are not a colour at all. `.post-card` and `.compose-card` paint
       `rgba(255,255,255,.04)` — a translucent LIFT over the page. The page is a
       three-stop GRADIENT. So a card's actual rendered colour is different at
       the top of the feed than halfway down, and NO solid value can ever match
       it everywhere. I kept adjusting the panels to match a target that moves.

       That is also the "line": a solid panel laid over a card whose value drifts
       has to disagree somewhere, and the eye reads that disagreement as an edge.

       FIX: stop mixing the two. `--surface-card` is a real colour — the page's
       first stop with the same 4% white already baked in — and cards, panels,
       menus and notification rows all use it. Nothing is computed against the
       ground any more, so nothing can drift with scroll position.

       DO NOT put a translucent fill back on any of these. If a surface needs to
       sit above another, give it --surface-card-2, which is one measured step
       lighter, NOT an alpha.
    ==================================================== */
    :root{ --surface-card:#40643A; --surface-card-2:#497043; }
    body.light-mode{ --surface-card:#C6E0B7; --surface-card-2:#D0E7C3; }
    body.gabi-mode:not(.light-mode){ --surface-card:#1D2C19; --surface-card-2:#243522; }

    .post-card,
    .compose-card,
    #compose-wrapper .compose-card{
      background:var(--surface-card) !important;
      background-image:none !important;
    }
    .post-dest-list,
    .emoji-picker-panel,
    .settings-panel,
    .post-more-menu,
    .modal-sheet{
      background-color:var(--surface-card) !important;
      background-image:none !important;
      box-shadow:inset 0 0 0 9999px var(--surface-card), 0 12px 30px rgba(0,0,0,.5) !important;
    }
    .notif-row{
      background:var(--surface-card) !important;
      box-shadow:inset 0 0 0 9999px var(--surface-card) !important;
    }
    .notif-row.unread{
      box-shadow:inset 0 0 0 9999px var(--surface-card), inset 0 0 0 9999px rgba(232,194,90,.12) !important;
    }
    /* Nested surfaces step UP with a measured colour, never an alpha: the reply
       thread and each reply card sit inside a post card, so they need to be
       distinguishable from it without being computed against it. */
    .replies-section{background:var(--surface-card-2) !important}

    /* THE DESTINATION LIST WAS CUT TO TWO ROOMS — 2026-08-10. His screenshot
       shows Wall and Love Confessions and nothing else, so four of the six
       destinations are unreachable without knowing to scroll inside a box that
       gives no sign it scrolls.
       TWO CAUSES, both fixed here. `.compose-card` is `overflow:hidden` (it has
       to be — the card has rounded corners and a left stripe to clip), and the
       `.dropdown-open` rule that lifts that was being out-specified; it is
       !important now so nothing can clip the list again. And the 226px cap set
       earlier today assumed six rows would overflow it, which was the right
       instinct with the wrong number once row padding is counted.
       320px shows five rooms whole and cuts the sixth in half — still the "there
       is more below" signal, but with five reachable instead of two. */
    .compose-card.dropdown-open{overflow:visible !important}
    .post-dest-list{max-height:min(320px,55vh) !important}

    /* THE TEXT-SHRINKING OVERRIDE THAT USED TO LIVE HERE IS GONE — 2026-08-10.
       It set font-size/weight/padding/letter-spacing with !important to squeeze
       the word 'live' down. The rail pill is a DOT now with no text node at all,
       so every one of those declarations was aimed at nothing — and the
       !important padding would have fought the dot's own `padding:0`.
       That whole chase is the lesson: three rounds went into making a word
       smaller, and the floor was legibility, not CSS. The bottom bar still shows
       the word and still needs its own sizing — see `#bn-call-btn::after`. */

    /* IT SCROLLS ON A SHORT WINDOW — threshold raised 560px -> 880px, 2026-08-11.
       At the current (larger) type scale the six tiles plus the CTA need roughly
       880px of height. Below that, `overflow-y:visible` meant the sheet grew past
       the window: the STICKY HEADER then sat on top of the tiles and the price
       was unreachable, which is exactly what Shane hit at 100% zoom.
       Letting it scroll fixes both — a sticky header only behaves when its
       container actually scrolls.
       THE SIZES ALSO STEP DOWN HERE so the scroll is short rather than endless:
       the width-driven term is dropped, because on a short window it is height,
       not width, that is scarce. This is the honest trade — one page when there
       is room, a small scroll when there is not. */
    @media (max-height:880px){
      .gold-perks-sheet{overflow-y:auto}
      /* THE LINE THIS BLOCK WAS ALWAYS MISSING — 2026-08-12. Everything else
         here shrinks the contents; nothing stopped .gp-body CENTRING them, so
         the overflow still went up over the wordmark and down past the price.
         Below 880px of window height the page is scrolling anyway, so top
         alignment costs nothing and guarantees the order on screen is
         header -> tiles -> button -> price, in every browser, `safe` or not.
         The bottom padding keeps the price clear of the sheet's rounded edge
         when it is scrolled to. */
      .gold-perks-sheet .gp-body{justify-content:flex-start;padding-bottom:6px}
      .gp-title{font-size:clamp(1.05rem,2.7vh,1.6rem)}
      .gp-desc{font-size:clamp(.8rem,1.95vh,1.15rem)}
      .gp-ico{width:clamp(30px,4.4vh,44px);height:clamp(30px,4.4vh,44px)}
      .gp-tile{padding:clamp(12px,2vh,18px)}
      .gp-grid{gap:clamp(8px,1.4vh,12px)}
      .gold-perks-sheet .modal-title{font-size:clamp(1.25rem,3vh,1.8rem)}
      .gp-cta{margin-top:clamp(11px,1.8vh,16px);padding:clamp(10px,1.7vh,14px);font-size:clamp(1rem,2.2vh,1.35rem)}
      .gp-price{font-size:clamp(.78rem,1.6vh,1rem)}
    }

    /* PULLED BACK FROM DOUBLE — 2026-08-11. Doubling was taken literally and it
       broke two things: titles hit `text-overflow:ellipsis` and truncated
       ("Maskara ba…"), and the sheet outgrew the window so the sticky header
       overlapped the tiles. This is roughly HALFWAY between the original and
       double — noticeably bigger, still whole.
       nowrap AND ellipsis ARE BOTH GONE from .gp-title. A truncated perk name on
       a sales page is worse than one that wraps to two lines, and locking the
       title to one line is what made growth dangerous in the first place.

       ORIGINALLY DOUBLED at Shane's instruction ("yes twice as big"). Every
       number on this page — type, chips, padding, gaps, the CTA and the sheet
       itself — was multiplied by roughly two, together. Scaling the type alone
       would have crammed big words into small boxes.
       THE SHEET GREW FROM 860px TO 1240px because doubled type needs somewhere
       to live; the 96vw guard still stops it exceeding a phone screen.
       THE NO-SCROLL PROMISE IS NOW TIGHT. At this size the six tiles plus the CTA
       need roughly a 900px-tall window to fit in one view; below that the
       max-height:560px rule hands back the scroll. If it starts scrolling on a
       normal laptop, this scale is the reason.

       WHY max(vh, vw) AND NOT vh ALONE — 2026-08-11, and this was the actual bug.
       Everything on this page was sized against viewport HEIGHT so it could stay
       one view. But Shane's window is SHORT AND WIDE: at a 700px-tall window,
       2.15vh is ~15px no matter how big the monitor is, so the type physically
       could not grow and raising the ceilings four times changed nothing.
       max() takes whichever of the two is larger, so a wide window drives the
       size from width and a tall narrow one still drives it from height. The
       clamp floor and ceiling keep both ends sane.
       IF THIS PAGE EVER LOOKS TOO SMALL AGAIN, check the WINDOW SHAPE first —
       not the numbers. */
    /* THE HEADER SCALES TOO — 2026-08-11. The tiles were growing while "What Gold
       gives you" stayed a fixed size, so at 75% zoom the page looked like small
       print under a normal heading. Everything on this sheet is now tied to the
       same vh scale. */
/* THE HEADER IS THE WORDMARK — 2026-08-11. "What Gold gives you" + a subtitle
       became "MASKARA GOLD" set exactly as the rail wordmark: --font-mark italic,
       .06em tracking, and the SAME metal ramp derived from --neon, so this and the
       brand in the top-left corner are one design and both follow the accent. Do
       not pin a literal gold here — it would drift from the rail the first time
       the accent changes.
       -webkit-text-fill-color IS REQUIRED alongside color, exactly as on
       .desk-rail-brand: in WebKit, `color` alone loses to the fill colour and the
       text renders solid over the ramp. The @supports guard keeps a solid gold
       fallback for anything that cannot clip a background to text — a wordmark
       that renders invisible is worse than one without a gradient. */
    .gold-perks-sheet .modal-title{font-size:clamp(1.3rem,max(3.4vh,1.85vw),2.5rem);
      font-family:var(--font-mark);font-style:italic;font-weight:700;letter-spacing:.06em;color:var(--neon)}
    @supports ((-webkit-background-clip:text) or (background-clip:text)){
      .gold-perks-sheet .modal-title{
        background-image:linear-gradient(180deg,color-mix(in srgb,var(--neon) 72%,#fff) 0%,var(--neon) 24%,var(--neon) 54%,var(--neon-dim) 80%,color-mix(in srgb,var(--neon-dim) 82%,#000) 100%);
        -webkit-background-clip:text;background-clip:text;color:transparent;-webkit-text-fill-color:transparent}
    }

    /* PHONES KEEP THE ORIGINAL SCALE — 2026-08-11. The doubling above is for
       DESKTOP, where there is room for it. On a 420px phone the sheet is ~400px
       wide, so each tile is ~180px: a 37px nowrap title ("Maskara badge") needs
       about 260px and would simply be CLIPPED, and the six tiles would not fit
       one screen either.
       This block restores the pre-doubling numbers below 700px wide. It is not a
       compromise — it is the size that was already correct there. If the desktop
       scale changes again, this block does NOT need to follow it. */
    @media (max-width:700px){
      .gold-perks-sheet{width:min(96vw,520px)}
      .gold-perks-sheet .modal-title{font-size:clamp(1.05rem,max(2.5vh,4.4vw),1.7rem)}
      .gold-perks-sheet .modal-subtitle{font-size:clamp(.7rem,max(1.45vh,2.6vw),1rem)}
      .gp-grid{gap:9px}
      .gp-tile{padding:12px}
      .gp-head{gap:9px}
      .gp-ico{width:clamp(26px,7.2vw,34px);height:clamp(26px,7.2vw,34px)}
      .gp-title{font-size:clamp(.8rem,3.5vw,1rem)}
      .gp-desc{font-size:clamp(.62rem,2.8vw,.78rem)}
      .gp-cta{margin-top:13px;padding:11px;font-size:clamp(.85rem,3.6vw,1.05rem)}
      .gp-price{margin-top:8px;font-size:clamp(.68rem,2.6vw,.85rem)}
    }
