
/* The renderer's ACTUAL font, served from the app (see /client/meme-font.ttf). The preview used to
   fall back to whatever the OS had — Windows has no Liberation Sans — so the caption on screen had
   different glyphs AND different widths than the export. Same file in both now. */
@font-face{font-family:'PCMemeFont';src:url('/client/meme-font.ttf') format('truetype');
  font-weight:700;font-style:normal;font-display:swap}
/* ===== PosterChan Nostr — cyberpunk theme (matches the main PosterChanAI webui: cyan-primary,
   magenta accent, neutral-dark bg, Orbitron/Rajdhani) ===== */
/* Self-hosted fonts (no Google Fonts dependency) — variable woff2, one file per family. */
@font-face{font-family:'Inter';font-style:normal;font-weight:100 900;font-display:swap;src:url('/static/fonts/inter.woff2') format('woff2')}
@font-face{font-family:'Orbitron';font-style:normal;font-weight:400 900;font-display:swap;src:url('/static/fonts/orbitron.woff2') format('woff2')}
:root{
  --bg:#0a0a0f; --bg2:#12121a; --panel:rgba(18,18,28,.72); --panel2:rgba(26,26,42,.85);
  /* --rail: the surface behind the left sidebar and the right column.
     TRANSPARENT on purpose — the rails are not panels sitting on the page, they ARE the page. Painting
     them (the left rail was a hardcoded near-opaque dark, the right one the translucent --panel) split
     the shell into three tones: on Professional the rails came out pure white against a #f5f7fa feed,
     a 10/255 seam down each side, and Dark/Monero were worse at 15 and 12. Letting the body background
     run edge to edge makes left, centre and right the SAME pixels by construction — no value to keep in
     sync, and the ambient corner washes flow across the whole width instead of stopping at a column
     boundary. The separation is now done by the hairline rules on .sidebar/.rightbar, which is what a
     divider is for. Overridable per theme if one ever wants a painted rail (winxp's Luna pane does). */
  --rail:transparent;
  /* --canvas: the page's own background, as a reusable value. A sticky bar CANNOT be transparent (notes
     scroll under it), so to stay invisible it has to repaint exactly what the body paints — including the
     two ambient corner washes. Painting a flat var(--bg) instead left the bar a slightly-unwashed
     rectangle sitting in a washed area (~7/255 on Cherry Blossom): the seam that read as "a rectangle at
     the top". Used with background-attachment:fixed so the gradients line up with the viewport-positioned
     originals rather than with the bar's own little box.
     --sticky-sh: the shadow under those bars. rgba(0,0,0,.8) was written for the dark flagship and turned
     into a grey smear across every LIGHT theme — each one softens it below. */
  --canvas:
    radial-gradient(ellipse 900px 600px at 50% -8%,var(--amb1) 0%,transparent 55%),
    radial-gradient(ellipse 800px 600px at 100% 100%,var(--amb2) 0%,transparent 55%),
    var(--bg);
  --sticky-sh:0 10px 18px -14px rgba(0,0,0,.8);
  /* The accents are deliberately NOT the pure #00ffff / #ff00ff primaries any more. Fully saturated
     channel-max colour is the one thing that reads as "default HTML neon" rather than as a designed
     palette — it also clips on wide-gamut displays and vibrates against dark backgrounds. These sit a
     few degrees off primary with the saturation pulled back: same unmistakable cyan/magenta identity,
     but it looks chosen. --accent-rgb/--accent2-rgb MUST track them (dozens of rgba(var(--accent-rgb),…)). */
  --neon:#3ce8ff; --neon2:#ff5cf0; --cyan:#3ce8ff; --green:#00ff88; --amber:#ffcf2b; --accent-rgb:60,232,255; --accent2-rgb:255,92,240;
  --danger:#ff6b8b;   /* delete/danger action color; overridden per-theme so it stays readable */
  /* A hairline is STRUCTURE, not decoration. At .22 cyan every card, input and divider was an accent-
     coloured outline, so nothing could stand out by being accented — the whole page was. Barely-tinted
     and much fainter: the boxes still read as boxes, and an accent border now means something. */
  --text:#edeefa; --muted:#9fa1c6; --line:rgba(125,210,255,.16);
  --glow:0 0 18px -2px rgba(60,232,255,.45); --glow-cy:0 0 18px -2px rgba(60,232,255,.4);
  --r:14px;
  /* Derived from each theme's own --r, so a theme that squares its corners (win98) stays square and
     every theme gets the same 3-step rhythm for free without declaring anything. */
  --r-sm:calc(var(--r) - 5px); --r-lg:calc(var(--r) + 6px);
  /* Elevation, separate from glow. Depth was previously ONLY coloured bloom, which is why everything
     looked lit rather than layered. Neutral and low-alpha, so it reads correctly on light themes too —
     no theme overrides these, so all seven inherit the same ladder. */
  --sh-1:0 1px 2px rgba(0,0,0,.28);
  --sh-2:0 6px 16px -8px rgba(0,0,0,.45);
  --sh-3:0 22px 48px -24px rgba(0,0,0,.6);
  --ring:0 0 0 2px rgba(var(--accent-rgb),.55);   /* keyboard focus, one definition for the whole app */
  /* Spacing + type scale. The stylesheet WAS 4,700 lines of ad-hoc px — 7px here, 8px there, 12.5px,
     14.5px — and that inconsistency IS most of what "unpolished" means; nothing lines up with anything
     because no two values were ever chosen together. That has now been swept:

       spacing  every positive padding/margin/gap sits on an EVEN 2px grid (39 distinct values -> 25;
                the survivors above 32px are layout dimensions written as padding, not spacing steps).
                Odd values rounded UP, so nothing tightened. Deliberately NOT converted to var(--sp-N)
                literals: `padding:8px 10px` is readable and `padding:var(--sp-2) var(--sp-?)` is not,
                10px has no name in a 4px ladder, and 1,500 mechanical edits buy zero visual change.
                The GRID is the invariant worth keeping; the names below just label its common steps.
       type     8 steps, integers only (31 distinct values -> 8). Every half-step is gone — a 13px label
                beside a 13.5px one is the single most legible symptom of "nobody chose these".
       rhythm   3 line-heights (1.2 / 1.45 / 1.6). `line-height:1` and `:0` are EXEMPT — 1 centres a
                glyph in an icon button, 0 kills the descender gap under an inline-block <img>. Both
                are layout idioms a typographic scale has no opinion about.

     Two classes are exempt by design and must stay that way: NEGATIVE margins (they cancel a border or
     a deliberate overlap — snapping one by 1px breaks the alignment it exists to create) and lengths
     inside calc()/clamp()/env(). No theme overrides any of this, so all seven inherit one rhythm.
     Re-check the type ladder with: scripts/check_css_scale.py (fails if a new off-ladder value lands). */
  --sp-1:4px; --sp-2:8px; --sp-3:12px; --sp-4:16px; --sp-5:24px; --sp-6:32px;
  --fs-0:11px; --fs-1:13px; --fs-2:15px; --fs-3:17px; --fs-4:20px; --fs-5:24px;
  --lh-tight:1.2; --lh:1.45; --lh-loose:1.6;
  /* Max width of the reading column. 760px puts a post at ~76 characters per line at --fs-3, i.e. the
     top of the comfortable 45-75 range, with the avatar gutter taken out. Declared here (and NOT
     overridden by any theme) so the measure is one number to tune, not a figure buried in a grid. */
  --feed-max:920px;
  /* The capped width of the whole shell = the three tracks + both column gaps + .app's own padding
     (300 + 20 + 920 + 20 + 340 + 64). Kept in lockstep with --feed-max: change one and the middle
     column stops landing on the other. .rb-toggle's fixed offsets are derived from this number.
     NOTE these are CSS px, and the client applies body{zoom} by viewport width, so the cap engages
     EARLIER than the raw number suggests — at 1280 device px the layout viewport is ~1910 CSS px.
     760px measured a 49-character line once zoom was accounted for, which is under the comfortable
     band; 920 puts it at ~59-74 across 1280..2560. Tune against measured chars/line, never on paper. */
  --app-max:1664px;
  /* ONE mono stack. There were seven ('Courier New', "JetBrains Mono", four spellings of ui-monospace),
     so an npub, a hash and a code block each rendered in a different typeface depending on the surface.
     --mono was already referenced 8 times as var(--mono,monospace) but never DEFINED, so every one of
     those silently fell back to bare `monospace` — the worst available default. */
  --mono:ui-monospace,"SF Mono",SFMono-Regular,"JetBrains Mono",Menlo,Consolas,monospace;
  --amb1:rgba(0,255,255,.06); --amb2:rgba(255,0,255,.045);  /* body ambient glow (themeable) */
}

/* ============================ THEMES ============================
   Cyberpunk (above, the flagship default) is :root. Each theme overrides the same custom
   properties; the client sets <html data-theme="…"> from the user's saved choice (mirrored to
   Nostr). Slugs MUST match schemas.CLIENT_THEMES + the client Settings <select>. */

/* Cherry Blossom — soft sakura light theme */
:root[data-theme="cherryblossom"]{
  --bg:#fff5f8; --bg2:#ffe9f0; --panel:rgba(255,255,255,.74); --panel2:rgba(255,231,240,.9);
  --neon:#ff6fa5; --neon2:#c77dff; --cyan:#ff8fb0; --green:#3fae7e; --amber:#e08a2e; --danger:#d61f4e; --accent-rgb:255,143,176; --accent2-rgb:199,125,255;
  --text:#4a2b38; --muted:#7a5260; --line:rgba(255,111,165,.30);
  --glow:0 0 10px rgba(255,111,165,.35); --glow-cy:0 0 10px rgba(255,143,176,.3);
  --r:16px; --amb1:rgba(255,143,176,.16); --amb2:rgba(199,125,255,.10);

  --sticky-sh:0 4px 10px -10px rgba(0,0,0,.16);
}

/* Professional Modern — clean neutral business light theme (no neon glow) */
:root[data-theme="professional"]{
  --bg:#f5f7fa; --bg2:#eaeef3; --panel:rgba(255,255,255,.92); --panel2:#ffffff;
  --neon:#2563eb; --neon2:#4f46e5; --cyan:#0891b2; --green:#16a34a; --amber:#d97706; --danger:#dc2645; --accent-rgb:8,145,178; --accent2-rgb:79,70,229;
  --text:#1f2937; --muted:#6b7280; --line:rgba(15,23,42,.12);
  --glow:0 1px 3px rgba(15,23,42,.12); --glow-cy:0 1px 3px rgba(15,23,42,.12);
  /* soft blue/indigo corner washes so the login + app don't read as flat white */
  --r:10px; --amb1:rgba(37,99,235,.07); --amb2:rgba(99,102,241,.06);

  --sticky-sh:0 4px 10px -10px rgba(15,23,42,.16);
}

/* Windows 98 — silver chrome, navy accents, square corners. NB: --bg is the silver "window" colour
   (not the teal desktop) so the feed's black text stays readable — the feed sits directly on --bg. */
:root[data-theme="win98"]{
  --bg:#c0c0c0; --bg2:#b8b8b0; --panel:#c0c0c0; --panel2:#d4d0c8;
  --neon:#000080; --neon2:#1084d0; --cyan:#000080; --green:#008000; --amber:#808000; --danger:#c00000; --accent-rgb:0,0,128; --accent2-rgb:16,132,208;
  --text:#000000; --muted:#404040; --line:#808080;
  --glow:none; --glow-cy:none; --r:0; --amb1:transparent; --amb2:transparent;

  --sticky-sh:none;
}

/* Windows XP — Luna blue accents on a light "window" surface (the feed sits on --bg, so --bg must be
   light enough for the dark navy text; the saturated XP blue lives in the accents + ambient glow). */
:root[data-theme="winxp"]{
  --bg:#eaf1fb; --bg2:#d6e4f7; --panel:rgba(255,255,255,.93); --panel2:#ece9d8;
  --neon:#0a5bc4; --neon2:#2f8f2f; --cyan:#1c7fd6; --green:#2f8f2f; --amber:#f0a000; --danger:#c81e3a; --accent-rgb:28,127,214; --accent2-rgb:47,143,47;
  --text:#0a1a3a; --muted:#5a6a8a; --line:rgba(10,91,196,.30);
  --glow:0 0 8px rgba(10,91,196,.22); --glow-cy:0 0 8px rgba(10,91,196,.22);
  --r:8px; --amb1:rgba(255,255,255,.10); --amb2:rgba(10,91,196,.06);

  --sticky-sh:0 4px 10px -10px rgba(10,91,196,.18);
}
/* Anime Girl — kawaii pastel: lavender + bubblegum pink + mint, extra-round corners, soft glow */
:root[data-theme="animegirl"]{
  --bg:#fbf0ff; --bg2:#f3e3ff; --panel:rgba(255,255,255,.78); --panel2:rgba(247,227,255,.9);
  --neon:#ff7ec8; --neon2:#9d7bff; --cyan:#3fb6c8; --green:#3fbf9c; --amber:#f0a43a; --danger:#d61f4e; --accent-rgb:63,182,200; --accent2-rgb:157,123,255;
  --text:#5a3a6e; --muted:#a888bd; --line:rgba(157,123,255,.30);
  --glow:0 0 10px rgba(255,126,200,.40); --glow-cy:0 0 10px rgba(157,123,255,.30);
  --r:18px; --amb1:rgba(255,126,200,.16); --amb2:rgba(157,123,255,.12);

  --sticky-sh:0 4px 10px -10px rgba(157,123,255,.20);
}

/* Soviet Gothic — oppressive dark with communist red + tarnished gold, parchment text, sharp edges */
:root[data-theme="sovietgothic"]{
  --bg:#17110f; --bg2:#211713; --panel:rgba(34,24,20,.82); --panel2:rgba(46,32,27,.92);
  --neon:#b81e22; --neon2:#7a0d10; --cyan:#c79a3a; --green:#6b7f3a; --amber:#c79a3a; --accent-rgb:199,154,58; --accent2-rgb:122,13,16;
  --text:#e7d8be; --muted:#a08b6f; --line:rgba(184,30,34,.34);
  --glow:0 0 10px rgba(184,30,34,.40); --glow-cy:0 0 9px rgba(199,154,58,.30);
  --r:3px; --amb1:rgba(184,30,34,.10); --amb2:rgba(199,154,58,.05);
}

/* Dark — generic neutral dark mode: clean grays + a calm blue accent, no neon */
:root[data-theme="dark"]{
  --bg:#17181c; --bg2:#202229; --panel:rgba(38,40,46,.82); --panel2:#2a2c33;
  --neon:#4493f8; --neon2:#7c5cff; --cyan:#3fb6c8; --green:#3fb950; --amber:#d29922; --accent-rgb:63,182,200; --accent2-rgb:124,92,255;
  --text:#e6e7ea; --muted:#9aa0aa; --line:rgba(255,255,255,.13);
  --glow:0 1px 3px rgba(0,0,0,.5); --glow-cy:0 1px 3px rgba(0,0,0,.5);
  --r:12px; --amb1:transparent; --amb2:transparent;
}

/* Monero — sexy dark: deep near-black with the signature Monero orange (#FF6600) as a warm neon.
   Orange primary (routes through --neon/--cyan so buttons, links, glows all glow orange), warm-white
   text, subtle orange corner washes. A dark theme, so the hardcoded dark surfaces stay readable. */
:root[data-theme="monero"]{
  --bg:#0c0b0e; --bg2:#151217; --panel:rgba(28,22,20,.78); --panel2:rgba(41,31,27,.92);
  --neon:#ff6600; --neon2:#ff8c42; --cyan:#ff6600; --green:#4fb783; --amber:#ffab40; --danger:#ff5a7a; --accent-rgb:255,102,0; --accent2-rgb:255,140,66;
  --text:#f4ede7; --muted:#a2968c; --line:rgba(255,102,0,.30);
  --glow:0 0 12px rgba(255,102,0,.50); --glow-cy:0 0 12px rgba(255,102,0,.42);
  --r:14px; --amb1:rgba(255,102,0,.09); --amb2:rgba(255,140,66,.05);
}

/* The cyberpunk-only decor (city skyline "buildings", CRT scanlines, neon grid) belongs to the
   flagship theme only — it looks wrong over the light/retro palettes. Cyberpunk is the bare :root
   (no data-theme attr), so any themed page hides all three. */
:root[data-theme] .city-bg,
:root[data-theme] .scanlines,
:root[data-theme] .grid-bg{ display:none !important; }

/* ===== Readability layer for the non-cyberpunk themes (ADDITIVE — cyberpunk is the bare :root and
   is never matched here, so it stays pixel-perfect). The base CSS hardcodes cyberpunk colors
   (near-white/neon-cyan text like .note .txt #eceefb, deep-violet fills like rgba(8,5,18,…), and
   neon text-shadows) that bypass the theme vars and go unreadable on the light/retro palettes. Route
   the recurring ones back to the theme's --text/--panel and flatten the glow. ===== */
:root[data-theme] *{ text-shadow:none !important; }   /* neon glow → flat (a halo kills text on light bg) */

/* text that was hardcoded light/near-white → theme text colour */
:root[data-theme] .note .txt,
:root[data-theme] .note .name,
:root[data-theme] .usercount,
:root[data-theme] .nav-item.active,
:root[data-theme] .me .mn{ color:var(--text); }

/* neon drop-shadow filters on glyphs/badges → none */
:root[data-theme] .usercount .wot-ico,
:root[data-theme] .usercount .live-ico,
:root[data-theme] .usercount .relay-ico,
:root[data-theme] .note .pin-badge,
:root[data-theme] .nav-item:hover .ic,
:root[data-theme] .nav-item.active .ic{ filter:none; }

/* deep-violet hardcoded fills → theme panels (otherwise dark patches hide themed-dark text) */
:root[data-theme] .input,
:root[data-theme] textarea{ background:var(--panel2); color:var(--text); }
/* …except the timeline composer's textarea. It is not a form field sitting on a page, it is the first
   row of the feed, so it must stay unpainted like the row around it. `.tl-cmp-ta{background:transparent}`
   further down is only (0,1,0) and lost to the (0,2,0) selector above — which is why every themed palette
   drew a white/panel2 pill across the top of the timeline while bare-:root cyberpunk looked correct. */
:root[data-theme] .tl-cmp-ta{ background:transparent; }
:root[data-theme] .note .av{ background:var(--panel2); }
:root[data-theme] .note .txt img:not(.emoji-inline),
:root[data-theme] .note .txt video,
:root[data-theme] .note .media img,
:root[data-theme] .note .media video{ background:var(--panel2); }

/* nav / rightbar active+hover states were neon gradients on near-black → theme panel + line */
:root[data-theme] .nav-item.active{ background:var(--panel2); box-shadow:inset 0 0 0 1px var(--line); }
:root[data-theme] .nav-item:hover{ background:var(--panel); color:var(--text); }
:root[data-theme] .note:hover{ background:var(--panel); }

/* more hardcoded light-on-X text → theme text: form labels, the AI/permission gate
   heading (the "permissions modal" is a modal() with an .ai-gate h2 that was hardcoded #fff). */
:root[data-theme] .fld,
:root[data-theme] .ai-gate h2,
:root[data-theme] .me .mk,
:root[data-theme] .conn{ color:var(--text); }
:root[data-theme] .conn{ color:var(--muted); }
/* active settings tab: dark #04020c label on the accent fill is unreadable when --neon is dark
   (win98 navy) → force white, which reads on every theme's accent. */
:root[data-theme] .us-tab.active{ color:#fff; }
/* .btn-cyan hardcodes a dark text (#04222a) over a cyan that goes DARK on some themes (win98 navy →
   New-post label unreadable). Repaint accent buttons as white-on-accent so the label always reads. */
:root[data-theme] .btn-cyan{ background:linear-gradient(135deg,var(--neon),var(--neon2)); color:#fff; }
/* deep-violet hardcoded fills inside cards/modals → theme panel */
:root[data-theme] .keybox,
:root[data-theme] .ended-badge,
:root[data-theme] .planned-badge{ background:var(--panel2); }
/* embedded/quoted nostr note referenced inside a post: its bg was hardcoded rgba(8,5,18,.4) (deep
   violet) → a muddy "brown" patch with low-contrast text on the light themes. Route to a theme panel. */
:root[data-theme] .quoted{ background:var(--panel2); }
/* The Settings cards (incl. User Settings) ALSO hardcoded rgba(8,5,18,.4) as their background, so the
   dark-theme text sat on a dark card → unreadable on every light theme. Route to a theme panel; route
   the toggle-switch track + the .fld input fills too so the whole settings surface themes cleanly. */
:root[data-theme] .set-card{ background:var(--panel); }
:root[data-theme] .switch .slider{ background:var(--panel2); }
/* URL / Twitter(X) link-preview cards + the generic note-preview also hardcoded the deep-violet bg
   (the "ugly color" on an embedded tweet) → theme panel. */
:root[data-theme] .link-card,
:root[data-theme] .note-preview{ background:var(--panel2); }
/* AI splash command names: the inline links were var(--cyan), too light on the light themes' light
   surface → route to the darker --neon accent; and the chip hover's dark #04020c label → white. */
:root[data-theme] .ai-welcome .ai-cmd{ color:var(--neon); border-bottom-color:var(--neon); }
:root[data-theme] .ai-cmd:hover{ color:#fff; }
/* AI chat: the assistant reply bubble + code blocks hardcoded the deep-violet fill → dark-on-light
   unreadable on light themes; route to a theme panel. The user-bubble keeps the accent gradient. */
:root[data-theme] .ai-msg.assistant .ai-bubble,
:root[data-theme] .ai-bubble pre{ background:var(--panel2); }
/* AI splash command examples (.ai-eg) hardcoded a deep-violet bg + var(--cyan) command names →
   dark-on-light / low-contrast on the light themes. Theme the button + route the name to --neon. */
:root[data-theme] .ai-eg{ background:var(--panel2); }
:root[data-theme] .ai-eg:hover{ background:var(--panel); }
:root[data-theme] .ai-eg b{ color:var(--neon); }
:root[data-theme] .ai-gate .btn-cyan,
:root[data-theme] .ai-welcome .btn-cyan{ background:linear-gradient(135deg,var(--neon),var(--neon2)); color:#fff; }

/* Sidebar identity cluster (name under the avatar → connection status → user/online counts, right
   above Logout): force the theme's text colour so it's high-contrast on every theme. (Winxp's later
   !important rules override this back to white-on-its-blue-rail.) */
/* No per-theme rail colour is needed any more: --rail is transparent for every theme, so each rail
   shows that theme's own body background and its text sits on exactly the surface the feed does.
   (This replaces an older `.sidebar{background:var(--panel)!important}` patch that existed only to
   stop the hardcoded dark rail from swallowing dark text on the light themes.) */
:root[data-theme] .me .mn,
:root[data-theme] .usercount,
:root[data-theme] .conn{ color:var(--text) !important; }

/* Right column: its cards are built from theme vars natively (flat, no hardcoded neon), so the old
   per-theme colour patches for them are gone — only the heading still needs routing. */
:root[data-theme] .rb-section h3{ color:var(--text); }
:root[data-theme] .rb-github{ color:var(--text); }

/* search box + any input placeholder readable on every theme */
:root[data-theme] .input::placeholder,
:root[data-theme] textarea::placeholder{ color:var(--muted); }
/* ========================== END THEMES ========================== */

/* ===== Per-OS flourishes — placed AFTER the readability layer so they win by source order. Make the
   retro OS themes actually read like the OS (not just a recoloured cyberpunk). ===== */
/* Windows XP "Luna": Tahoma, glossy blue 3D buttons, a blue title-bar topbar, white/beige controls. */
:root[data-theme="winxp"] body,
:root[data-theme="winxp"] .input,
:root[data-theme="winxp"] textarea,
:root[data-theme="winxp"] button,
:root[data-theme="winxp"] .btn{ font-family:Tahoma,'Segoe UI',Verdana,sans-serif !important; }
:root[data-theme="winxp"] .btn-neon,
:root[data-theme="winxp"] .btn-cyan,
:root[data-theme="winxp"] .pill,
:root[data-theme="winxp"] #btn-logout{
  background:linear-gradient(180deg,#7db4f0 0%,#3a82da 46%,#1f63c4 54%,#3f86dc 100%) !important;
  color:#fff !important; border:1px solid #15539f !important; border-radius:7px !important;
  box-shadow:inset 0 1px 0 rgba(255,255,255,.55) !important; text-shadow:0 1px 1px rgba(0,0,0,.4) !important;
}
:root[data-theme="winxp"] .btn-neon:hover,
:root[data-theme="winxp"] .btn-cyan:hover,
:root[data-theme="winxp"] #btn-logout:hover{ filter:brightness(1.07); }
:root[data-theme="winxp"] .btn-ghost,
:root[data-theme="winxp"] .mini{ background:linear-gradient(180deg,#fff,#ece9d8) !important; color:#143c7a !important; border:1px solid #7f9db9 !important; border-radius:4px !important; }
:root[data-theme="winxp"] .topbar{ background:linear-gradient(180deg,#3f8df2,#1357c4) !important; border-bottom:1px solid #0a3d96 !important; color:#fff !important; }
/* the search box + reload button live IN the topbar — keep their dark text on their light surfaces
   (a blanket .topbar *{color:#fff} turned the search text and the ↻ icon white-on-white). */
:root[data-theme="winxp"] .topbar .input,
:root[data-theme="winxp"] .topbar input,
:root[data-theme="winxp"] .topbar textarea{ color:#143c7a !important; background:#fff !important; }
:root[data-theme="winxp"] .topbar input::placeholder{ color:#5a6a8a !important; }
:root[data-theme="winxp"] .topbar .mini{ background:linear-gradient(180deg,#fff,#ece9d8) !important; color:#143c7a !important; border:1px solid #7f9db9 !important; border-radius:4px !important; }
:root[data-theme="winxp"] .nav-item.active,
:root[data-theme="winxp"] .us-tab.active{ background:linear-gradient(180deg,#6aa6ec,#2a72d4) !important; color:#fff !important; box-shadow:none !important; border-radius:5px !important; }
:root[data-theme="winxp"] .input,
:root[data-theme="winxp"] textarea{ background:#fff !important; border:1px solid #7f9db9 !important; border-radius:3px !important; }
/* XP Luna left panel: blue side rail with white text (the avatar name + user/online counts were
   dark-on-light and hard to read; white-on-blue is both readable and authentically XP). */
:root[data-theme="winxp"] .sidebar{ background:linear-gradient(180deg,#5390e0,#2c66c8) !important; border-inline-end:1px solid #1a4fa0 !important; }
:root[data-theme="winxp"] .sidebar .nav-item,
:root[data-theme="winxp"] .me .mn,
:root[data-theme="winxp"] .me .mk,
:root[data-theme="winxp"] .conn,
:root[data-theme="winxp"] .usercount,
:root[data-theme="winxp"] .usercount .wot-ico,
:root[data-theme="winxp"] .usercount .live-ico,
:root[data-theme="winxp"] .conn .uc-dot{ color:#fff !important; }
:root[data-theme="winxp"] .sidebar .nav-item:hover{ background:rgba(255,255,255,.18) !important; }
/* XP's --neon is a DARK blue (#0a5bc4) and its active row is a blue gradient, so the accent-coloured
   active icon the other themes get would be blue-on-blue here — keep it white with the label. */
:root[data-theme="winxp"] .sidebar .nav-item.active svg.ic,
:root[data-theme="winxp"] .mobilenav .nav-item.active svg.ic{ color:inherit !important; }

/* Professional + Windows 98: the cluster between the avatar and the Logout button (name, connection
   status, user/online counts). The var(--text) route was reported still unreadable on these two light
   themes, so pin a strong near-black with max specificity — incl. child spans/icons — to be sure. */
:root[data-theme="professional"] .me .mn, :root[data-theme="professional"] .me .mk,
:root[data-theme="professional"] .conn, :root[data-theme="professional"] .conn span,
:root[data-theme="professional"] .usercount, :root[data-theme="professional"] .usercount *,
:root[data-theme="win98"] .me .mn, :root[data-theme="win98"] .me .mk,
:root[data-theme="win98"] .conn, :root[data-theme="win98"] .conn span,
:root[data-theme="win98"] .usercount, :root[data-theme="win98"] .usercount *{ color:#16161c !important; }
*{box-sizing:border-box}

/* ---- Scrollbars -------------------------------------------------------------
   Chromium on Windows paints the classic wide light-grey bar, which is why the desktop app showed
   one down the right rail against the dark UI while Linux/macOS looked fine. Style it once, here,
   from the theme vars so every scrolling panel (right rail, modals, settings, menus) matches.
   `color-scheme` is the other half: it tells the browser which palette to draw native chrome with —
   the Firefox scrollbar, form controls and select popups included. Panels that deliberately hide
   their bar (.feed, .dm-msgs, .mc-track, …) keep doing so: those rules are class-scoped and so
   outrank these universal/element ones regardless of order. */
:root{color-scheme:dark}
:root[data-theme="cherryblossom"],:root[data-theme="professional"],:root[data-theme="win98"],
:root[data-theme="winxp"],:root[data-theme="animegirl"]{color-scheme:light}
/* No painted scrollbars anywhere — the same call .feed and the DM panes already made, now global, so a
   new scrolling panel can't reintroduce one. Scrolling itself is untouched (wheel, drag, keys, touch).
   NB: `scrollbar-width` is not just the Firefox half — Chromium 121+ implements it too, and once it or
   `scrollbar-color` is set on an element Chromium IGNORES that element's ::-webkit-scrollbar rules. So
   `thin` here would have OVERRIDDEN the pseudo-element styling below and left a bar on screen. */
*{scrollbar-width:none}
::-webkit-scrollbar{width:0;height:0}
::-webkit-scrollbar-track,::-webkit-scrollbar-corner,::-webkit-scrollbar-thumb{background:transparent}

html,body{margin:0;height:100%}
body{
  /* Inter for all UI/body/input text — clean + highly readable. Rajdhani (condensed display) read
     as cramped/"plain" for body & form text; Orbitron stays for the brand/headings below. */
  font-family:'Inter','Segoe UI',system-ui,-apple-system,'Apple Color Emoji','Segoe UI Emoji','Noto Color Emoji',sans-serif;
  font-weight:400;
  background:var(--canvas);
  color:var(--text);overflow:hidden;-webkit-tap-highlight-color:transparent;
}
/* cyberpunk display font for brand + headings, like the main webui */
.logo,.brand,.av-title,.markdown h2,h1.glitch{font-family:'Orbitron',sans-serif;letter-spacing:1px}
.hidden{display:none !important}
/* Settings → Sidebar: a row the USER switched off. Its own class, never `.hidden` — that one is
   owned by applyInstanceGating, which resets it on every re-run and would put the row back. */
.nav-off{display:none !important}
a{color:var(--cyan);text-decoration:none}
code{font-family:var(--mono);word-break:break-all}

/* fx layers */
/* Scanlines: a faint full-screen overlay. We do NOT use mix-blend-mode — a full-screen blend mode
   re-composites against the ENTIRE page every frame (can't be cached/promoted) and was the dominant
   GPU cost. A plain low-opacity overlay (slightly darker lines) looks ~the same and is nearly free. */
.scanlines{position:fixed;inset:0;pointer-events:none;z-index:9990;
  background:repeating-linear-gradient(to bottom,rgba(0,0,0,.16) 0 1px,transparent 1px 3px);opacity:.55;
  transform:translateZ(0);contain:strict}
/* .10, not .18: an animated grid sitting behind a column of body text is a legibility tax the content
   pays on every screen, and it was the loudest thing in the empty regions of the page. Still present —
   it is part of the flagship's character — just no longer competing with what you came to read. */
.grid-bg{position:fixed;inset:0;pointer-events:none;z-index:-1;opacity:.10;
  background-image:linear-gradient(var(--line) 1px,transparent 1px),linear-gradient(90deg,var(--line) 1px,transparent 1px);
  background-size:46px 46px;mask-image:radial-gradient(circle at 50% 30%,#000 0%,transparent 75%);
  transform:translateZ(0);contain:strict}   /* rasterize the masked grid once → no per-frame mask cost */

/* ===== Live cyberpunk city skyline (pure CSS, GPU-cheap) =====
   Sits deepest (z-index:-2, behind the grid + all content). READABILITY-SAFE BY DESIGN: the buildings
   are near-black silhouettes DARKER than --bg (#0a0a0f), so text that sits over a transparent .main
   only ever gains contrast over them; the only added light is the neon top-edges + a horizon haze,
   both faint and bottom-weighted so the upper feed area stays clean. Two depth rows drift slowly in
   parallax (transform-only) — disabled under prefers-reduced-motion. */
.city-bg{position:fixed;inset:0;pointer-events:none;z-index:-2;overflow:hidden}
.city-bg::before{content:"";position:absolute;inset:0;   /* rising neon horizon glow */
  background:
    radial-gradient(150% 85% at 50% 118%,rgba(var(--accent-rgb),.20),transparent 60%),
    radial-gradient(95% 62% at 14% 122%,rgba(var(--accent2-rgb),.16),transparent 58%),
    radial-gradient(95% 62% at 88% 120%,rgba(0,200,255,.13),transparent 58%);
  animation:city-haze 12s ease-in-out infinite}
/* Each band = a window-grid + neon-capped DARK silhouette (body fill stays near-black, ~= --bg, so
   text over the transparent .main never loses contrast) PLUS glowing neon signs, masked into towers
   and drifting in parallax. Three bands rise to ~70vh for a deep downtown look. Buildings loop
   seamlessly (drift == mask period); the faint window/sign phase is imperceptible at loop reset. */
.city-far,.city-mid,.city-near{position:absolute;inset-inline-start:-260px;inset-inline-end:-260px;bottom:0;display:block;will-change:transform}
.city-far{height:min(70vh,560px);opacity:.9;   /* deepest: tall thin towers, dim */
  background-image:
    radial-gradient(ellipse 6px 12px at 70px 22%,rgba(var(--accent-rgb),.45),transparent 72%),
    repeating-linear-gradient(0deg,transparent 0 8px,rgba(var(--accent-rgb),.04) 8px 9px),
    repeating-linear-gradient(90deg,transparent 0 8px,rgba(var(--accent-rgb),.04) 8px 9px),
    linear-gradient(to bottom,rgba(var(--accent-rgb),.28) 0 1.5px,rgba(var(--accent-rgb),.07) 1.5px,transparent 6px),
    linear-gradient(to bottom,#090b1c,#06080f);
  background-size:300px 100%,auto,auto,100% 100%,100% 100%;
  background-repeat:repeat-x,repeat,repeat,no-repeat,no-repeat;
  -webkit-mask:repeating-linear-gradient(90deg,#000 0 24px,transparent 24px 96px,#000 96px 120px,transparent 120px 192px);
  mask:repeating-linear-gradient(90deg,#000 0 24px,transparent 24px 96px,#000 96px 120px,transparent 120px 192px);
  animation:city-far 185s linear infinite}
.city-mid{height:min(52vh,430px);   /* middle band: medium towers, cyan+magenta signs */
  background-image:
    radial-gradient(ellipse 8px 5px at 50px 42%,rgba(var(--accent-rgb),.55),transparent 72%),
    radial-gradient(ellipse 6px 13px at 185px 24%,rgba(var(--accent2-rgb),.5),transparent 72%),
    repeating-linear-gradient(0deg,transparent 0 8px,rgba(var(--accent-rgb),.055) 8px 9px),
    repeating-linear-gradient(90deg,transparent 0 8px,rgba(var(--accent-rgb),.055) 8px 9px),
    linear-gradient(to bottom,rgba(var(--accent-rgb),.34) 0 1.5px,rgba(var(--accent-rgb),.1) 1.5px,transparent 7px),
    linear-gradient(to bottom,#080b16,#05060f);
  background-size:240px 100%,300px 100%,auto,auto,100% 100%,100% 100%;
  background-repeat:repeat-x,repeat-x,repeat,repeat,no-repeat,no-repeat;
  -webkit-mask:repeating-linear-gradient(90deg,#000 0 48px,transparent 48px 72px,#000 72px 104px,transparent 104px 128px);
  mask:repeating-linear-gradient(90deg,#000 0 48px,transparent 48px 72px,#000 72px 104px,transparent 104px 128px);
  animation:city-mid 135s linear infinite}
.city-near{height:min(36vh,300px);   /* front: wide dense dark buildings, magenta+amber signs */
  background-image:
    radial-gradient(ellipse 7px 14px at 40px 30%,rgba(var(--accent2-rgb),.6),transparent 72%),
    radial-gradient(ellipse 13px 5px at 150px 58%,rgba(255,191,0,.6),transparent 72%),
    repeating-linear-gradient(0deg,transparent 0 8px,rgba(var(--accent-rgb),.07) 8px 9px),
    repeating-linear-gradient(90deg,transparent 0 8px,rgba(var(--accent-rgb),.07) 8px 9px),
    linear-gradient(to bottom,rgba(var(--accent2-rgb),.4) 0 1.5px,rgba(var(--accent2-rgb),.12) 1.5px,transparent 7px),
    linear-gradient(to bottom,#06070f,#030409);
  background-size:200px 100%,260px 100%,auto,auto,100% 100%,100% 100%;
  background-repeat:repeat-x,repeat-x,repeat,repeat,no-repeat,no-repeat;
  -webkit-mask:repeating-linear-gradient(90deg,#000 0 64px,transparent 64px 88px,#000 88px 152px,transparent 152px 176px);
  mask:repeating-linear-gradient(90deg,#000 0 64px,transparent 64px 88px,#000 88px 152px,transparent 152px 176px);
  animation:city-near 96s linear infinite}
/* drift by EXACTLY one mask period → buildings loop seamlessly; over-extend (260px) > period so no edge shows */
@keyframes city-far{from{transform:translate3d(0,0,0)}to{transform:translate3d(-192px,0,0)}}
@keyframes city-mid{from{transform:translate3d(0,0,0)}to{transform:translate3d(-128px,0,0)}}
@keyframes city-near{from{transform:translate3d(0,0,0)}to{transform:translate3d(-176px,0,0)}}
@keyframes city-haze{0%,100%{opacity:.55}50%{opacity:.85}}
@media(max-width:820px){   /* trim the towers on phones so they don't dominate a short screen */
  .city-far{height:min(58vh,380px)} .city-mid{height:min(44vh,270px)} .city-near{height:min(30vh,190px)}
}
@media(prefers-reduced-motion:reduce){
  .city-far,.city-mid,.city-near{animation:none}
  .city-bg::before{animation:none;opacity:.6}
}
/* Static city = a cached raster texture (effectively a static image), NOT a live parallax. The old
   continuous transform forced backdrop-filter to re-blur a MOVING background each frame (~50% GPU).
   Now: animation:none (no motion) + translateZ(0)/contain (rasterize the ~18 stacked gradients to a
   GPU texture ONCE). The glass panels then blur a flat cached bitmap instead of re-evaluating all
   those gradients on every repaint, so even the always-on glow animations stay cheap. Near-idle GPU,
   identical neon look. */
.city-far,.city-mid,.city-near{
  animation: none !important;
  transform: translateZ(0);          /* promote to a cached raster layer (blur samples the texture) */
  contain: strict;                    /* isolate so foreground glow repaints don't re-rasterize it */
}
.city-bg::before{ animation: none !important; transform: translateZ(0); }

.glass{background:var(--panel);backdrop-filter:blur(7px);-webkit-backdrop-filter:blur(7px);border:1px solid var(--line)}
/* Perf: the ALWAYS-ON panels (sidebar/topbar/rightbar/mobilenav) re-blur on every content update
   (timeline prepend, image decode, hover) — the source of the small idle GPU spikes. Drop their
   backdrop-filter (flat semi-transparent + neon border, no frost). Blur stays on the TRANSIENT
   surfaces (modals/popups/lightbox/game cards) where it actually reads as a design feature and
   isn't on screen long enough to cause idle spikes. */
.sidebar.glass,.topbar.glass,.rightbar.glass,.mobilenav.glass{
  backdrop-filter: none; -webkit-backdrop-filter: none;
}
/* Perf/battery: stop ALL CSS animations when the tab/PWA is backgrounded (app.js toggles
   body.anim-off — see _animOff) so the GPU idles when you're not looking — big win for laptop heat
   AND mobile battery, with zero change to the visible/foreground look.

   OFF, NOT FROZEN, and that one word is the whole Android "empty timeline" bug.

   This rule used to be `animation-play-state: paused`. Freezing an animation holds the element at
   whatever keyframe it had reached — and a card INSERTED while the class is on has reached its
   FIRST one. `.note`'s entry animation is `noteRise ... both`, which starts at `opacity:0`, so every
   post drawn while backgrounded was stranded fully transparent for as long as the class stayed on.
   It kept its full height, so nothing measured it as missing, and `.reply-ctx` (no animation of its
   own) rendered perfectly above it: a screen of "↩ REPLYING TO alice" with nothing underneath, and
   plain posts simply absent. That is the report, and it survived the ghost-pair sweep because the
   sweep looks for a card of zero height and this one is 51px tall and invisible.

   `animation: none` cannot strand anything: with no animation an element renders at its resting
   style, which for content is visible. The failure direction is the safe one. What it costs is that
   an infinite decoration restarts from 0% instead of resuming mid-cycle when you come back — all of
   them here are spinners, pulses, glows and shimmers where that is invisible, and the one animation
   where it would have shown (the city parallax) is already `animation:none` above for perf. */
body.anim-off *, body.anim-off *::before, body.anim-off *::after{ animation: none !important; }
/* Mobile: drop the GPU-heavy continuous parallax + backdrop-blur (battery drain on phones, and the
   city parallax is a desktop flourish barely visible on a small screen). */
@media(max-width:820px){
  .fx-search-hit{grid-template-columns:88px minmax(0,1fr) 72px;padding:10px 6px;gap:7px}
  .fx-search-path,.fx-search-date{display:none}
  .fx-search-size{text-align:end}
  .city-far,.city-mid,.city-near,.city-bg::before{ animation: none !important; }
  .glass{ backdrop-filter: none; -webkit-backdrop-filter: none; }
  /* .modal-bg was NOT covered by the .glass rule above, so on a phone every open sheet — the composer
     most of all — kept a full-screen blur(4px) over the LIVE timeline. The feed behind it goes on
     receiving and inserting posts, and each insert forces the backdrop to re-blur the whole viewport.
     Reported as "it gets laggy when I have an open window like the one used to write this message".
     The scrim's own 72%-opaque fill already hides what is behind it; the blur was buying nothing. */
  .modal-bg,.news-modal-bg{ backdrop-filter: none; -webkit-backdrop-filter: none; }
}
.neon-border{box-shadow:0 0 0 1px rgba(var(--accent-rgb),.35),0 0 30px rgba(var(--accent-rgb),.2) inset}

/* ===== buttons / inputs ===== */
.btn{cursor:pointer;border:1px solid var(--line);background:var(--panel2);color:var(--text);
  padding:12px 16px;border-radius:11px;font-weight:600;font-size:15px;transition:.15s;letter-spacing:.3px}
.btn:hover{transform:translateY(-1px)}
.btn.full{width:100%;margin:6px 0}
a.btn{display:inline-block;text-align:center;text-decoration:none}
a.btn.full{display:block}
.btn.small{padding:8px 14px;font-size:13px}
/* The action row under a rendered post card. Six buttons do not fit across a phone, and a plain
   flex row squeezes them until the labels overrun — the same failure as the Passwords sidebar
   footer. So it WRAPS, and every button gets a floor wide enough for its own text; on a narrow
   screen that lands two per row instead of six unreadable slivers. */
.ss-acts{justify-content:center;gap:8px;margin-top:14px;flex-wrap:wrap}
.ss-acts>*{flex:1 1 auto;min-width:132px;justify-content:center;white-space:nowrap}
@media (max-width:420px){ .ss-acts>*{min-width:calc(50% - 4px)} }
/* Glow is a STATE, not a resting style. Every filled button glowing at rest is why a row of six reads
   as six equally-urgent things and the eye finds no primary — the single loudest "unpolished" signal in
   the UI. At rest they get real elevation (--sh-1); the bloom is what hover/focus adds. Shared rules, so
   all seven themes inherit the behaviour and only the colour differs. */
.btn-neon{background:linear-gradient(135deg,var(--neon),var(--neon2));border:none;color:#fff;box-shadow:var(--sh-1)}
.btn-cyan{background:linear-gradient(135deg,#0bb6cf,var(--cyan));border:none;color:#04222a;box-shadow:var(--sh-1)}
.btn-neon:hover{box-shadow:var(--glow),var(--sh-2)}
.btn-cyan:hover{box-shadow:var(--glow-cy),var(--sh-2)}
/* filled "danger/negative" action (e.g. Hold'em Fold) — red reads as stop across all themes, so it's
   intentionally a fixed red gradient like the admin's btn-danger, not theme vars. */
.btn-red{background:linear-gradient(135deg,#ff3366,#ff5a7d);border:none;color:#fff}
/* Secondary buttons: FILLED with the theme's accent colour (a tint of --neon over the panel) with a
   matching accent border + label, so they read as real coloured buttons next to the solid
   btn-neon/btn-cyan primaries instead of looking empty/plain. Theme-safe (all via --neon/--panel2);
   the plain-panel line is a fallback for browsers without color-mix. */
.btn-ghost{background:var(--panel2);color:var(--neon)}
.btn-ghost{background:color-mix(in srgb, var(--neon) 15%, var(--panel2));border-color:var(--neon);color:var(--neon)}
.btn-ghost:hover{background:color-mix(in srgb, var(--neon) 30%, var(--panel2));border-color:var(--neon);color:var(--neon)}
/* Sprite icon INSIDE a button label. Sized in em so it tracks the button's own font-size (a .btn.small
   gets a smaller icon for free) and sits on the text baseline rather than floating above it. The button
   becomes an inline-flex row so the icon and label centre against each other at any label length. */
.btn:has(.b-ic){display:inline-flex;align-items:center;justify-content:center;gap:8px}
.btn.full:has(.b-ic){display:flex}
svg.ic.b-ic{width:1.05em;height:1.05em;stroke-width:1.9;opacity:.95}
/* Icon-only dismiss buttons (.mini-x, .dm-att-x, .track-del, .ai-chip-x, …). Every one of those classes
   already sized its ✕ with its own font-size, so sizing this in em means each keeps exactly the size it
   had and not one of their rules needs touching. vertical-align centres it on the text baseline for the
   ones that are inline rather than flex. */
svg.ic.x-ic{width:1em;height:1em;stroke-width:2.1;vertical-align:-.12em}
/* The icon in front of a settings label (`.fld`), where an emoji used to sit.
   Sized in em so it tracks the label's own 13px uppercase type rather than the 20px base, and
   `vertical-align` puts it on the text baseline instead of hanging below it.
   `flex:none` matters more than it looks: several of these labels carry an inline
   `flex-direction:row;justify-content:space-between`, so the moment anything makes `.fld` a flex
   container the icon becomes a flex ITEM and space-between throws it to the opposite end of the row
   from its own text. Costing nothing where the label is a block, it is the difference between a
   label and a scattered one — the same lesson as the icon+label buttons above. */
svg.ic.fld-ico{flex:none;width:1.15em;height:1.15em;stroke-width:1.9;vertical-align:-.2em;margin-right:6px;opacity:.9}
/* Status lines ("uploads go to your server", "not linked"). These replaced ✅ and ⚠️ — the only two
   emojis on this screen carrying real MEANING in their colour, green and amber — so the colour has
   to be restated or the line loses the at-a-glance answer it existed to give. A sprite icon strokes
   `currentColor` and would otherwise come out the same grey as the sentence.
   `--green` / `--amber` are the existing per-theme tokens, redefined by all seven themes, so this
   stays readable on winxp and the light themes without a single per-theme rule. */
svg.ic.st-ico{flex:none;width:1.05em;height:1.05em;stroke-width:2;vertical-align:-.15em;margin-right:5px}
svg.ic.st-ico.ok{color:var(--green)}
svg.ic.st-ico.warn{color:var(--amber)}
/* Any button whose ENTIRE label is a sprite icon, whatever its class. 54 of them are not .btn — the
   New Post FAB, .mini, the chat controls, the media-player transport, every ✕ — and the centering
   above only reaches .btn. A text glyph was centred by line-height/text-align, which does nothing for
   an <svg>, so each of those had its icon sitting low and left. `:only-child` keeps this off buttons
   that also carry a count or a label (.act has a <span class="n">), which keep their own layout.
   Specificity comes from the :has() argument, so it beats a bare class rule; .tl-fab is excluded
   because it needs block-level flex for its margin-inline-start:auto. */
button:not(.tl-fab):has(> svg.ic.b-ic:only-child),
button:not(.tl-fab):has(> svg.ic.x-ic:only-child){display:inline-flex;align-items:center;justify-content:center;flex:none}
/* One keyboard-focus treatment for everything interactive, declared after the button variants so it
   wins over their resting shadow. :where() keeps it at zero specificity, so any control with its own
   focus styling still overrides it. Most controls had none at all, which made tabbing invisible. */
:where(a,button,.btn,input,select,textarea,summary,[tabindex]):focus-visible{outline:none;box-shadow:var(--ring)}
.input,textarea{width:100%;background:rgba(8,5,18,.7);border:1px solid var(--line);border-radius:11px;
  color:var(--text);padding:12px 14px;font-size:15px;outline:none;margin:6px 0;font-family:inherit}
.input:focus,textarea:focus{border-color:var(--cyan);box-shadow:var(--glow-cy)}
/* A native <select>'s POPUP is painted by the browser from the element's own background/color, and
   an <option> inherits NEITHER from the rule above — so a select styled `background:none` (which is
   how the quiet toolbars here style theirs) drew light theme text on the UA's white list. On the
   dark themes that is white on white: the Notes editor's folder menu opened completely unreadable.
   Set on the option itself, so it survives whatever a surrounding rule does to the closed control,
   and in theme variables so it follows the light themes too. Deliberately ONLY the <option>:
   painting `select.input` as well would repaint the closed control on every quiet toolbar in the
   app, which is a different change entirely. */
option,optgroup{background-color:var(--bg2);color:var(--text)}
.mini{cursor:pointer;background:var(--panel2);border:1px solid var(--line);color:var(--cyan);
  border-radius:8px;padding:4px 10px;font-size:12px;transition:.15s}
.mini:hover{color:var(--neon);border-color:var(--neon)}
#tl-media{font-size:17px;line-height:1;padding:4px 8px}
#tl-media.active{color:var(--cyan);border-color:var(--cyan);background:rgba(var(--accent-rgb),.12)}
.muted{color:var(--muted)} .small{font-size:12px} .full{width:100%}

/* ===== auth ===== */
/* flex + margin:auto centers the card when it fits but lets it scroll (top-aligned) when the
   content is taller than the screen — place-items:center clipped the tall Amber/nostrconnect pane */
.auth-gate{position:fixed;inset:0;display:flex;flex-direction:column;overflow-y:auto;padding:calc(18px + env(safe-area-inset-top)) calc(18px + env(safe-area-inset-right)) calc(18px + env(safe-area-inset-bottom)) calc(18px + env(safe-area-inset-left));z-index:50}
/* Native Android app (Capacitor, body.native from the bundle shim): the WebView is edge-to-edge under the
   system status bar, and tablets report env(safe-area-inset-top)=0, so the login logo and the topbar brand
   collided with the status-bar clock/date ('date over logo'). Reserve at least a status-bar height up top. */
/* Native app: a WebView has no NIP-07 browser extension (window.nostr is never injected), so the
   "Sign in with extension" button can't work — hide it. Amber / remote signer (NIP-46) is the mobile
   equivalent and stays; nsec + create-identity also work. */
body.native #btn-nip07{display:none}
/* System-bar insets. The app opts OUT of Android-15 edge-to-edge (styles.xml windowOptOutEdgeToEdge...),
   so Android already insets the WebView below the status/nav bars and env(safe-area-*) is 0 here — these
   rules add nothing then. They remain as a safety net (plain env(), no fixed fallback → no extra gap) in
   case a device still renders edge-to-edge, in which case env() clears the bars. */
body.native .auth-gate{padding-top:calc(18px + env(safe-area-inset-top))}
body.native .topbar{padding-top:calc(14px + env(safe-area-inset-top))}
body.native .sidebar{padding-top:calc(18px + env(safe-area-inset-top));padding-bottom:calc(18px + env(safe-area-inset-bottom))}
.auth-card{width:min(420px,94vw);border-radius:20px;padding:28px;margin:auto}
/* Banner ABOVE the login card — Nostr ostrich + headline, to pull new users in. Stacked above the
   modal (not inside it). Theme-aware: surface = --panel2, text = --text (readable on every theme,
   light or dark); the Nostr-purple ostrich + accent are a fixed brand colour that reads on all. */
.auth-stack{width:min(420px,94vw);margin:auto;display:flex;flex-direction:column;gap:14px}
.nostr-banner{display:flex;flex-direction:column;align-items:center;gap:12px;margin:0;
  padding:20px 18px;border-radius:16px;background:var(--panel2);
  border:1px solid rgba(157,78,221,.5);box-shadow:0 0 22px rgba(157,78,221,.20)}
.nostr-banner .ostrich{width:108px;height:108px;flex:none;object-fit:cover;border-radius:50%;
  border:2px solid rgba(157,78,221,.7);box-shadow:0 0 20px rgba(157,78,221,.55);
  animation:ostrich-float 3.6s ease-in-out infinite}
@keyframes ostrich-float{0%,100%{transform:translateY(0)}50%{transform:translateY(-5px)}}
.nb-title{font-family:'Orbitron',sans-serif;font-weight:800;font-size:20px;line-height:1.2;
  letter-spacing:1px;text-transform:uppercase;color:var(--text);text-align:center}
.nb-title span{background:linear-gradient(90deg,#c77dff,#7b2cbf);-webkit-background-clip:text;
  background-clip:text;color:transparent}
@media(max-width:480px){.auth-stack{gap:12px}.nostr-banner{padding:16px;gap:10px}
  .nostr-banner .ostrich{width:84px;height:84px}.nb-title{font-size:17px}}
/* Short / landscape phones: compact-horizontal so the banner doesn't push the login form off-screen
   (with so little vertical room, a big stacked hero would force scrolling past the buttons). */
@media(max-height:640px){.nostr-banner{flex-direction:row;gap:12px;padding:12px 16px}
  .nostr-banner .ostrich{width:54px;height:54px}.nb-title{font-size:17px;text-align:start}}
@media(prefers-reduced-motion:reduce){.nostr-banner .ostrich{animation:none}}
.logo-img{display:block;width:84px;height:84px;margin:0 auto 6px;border-radius:20px;
  border:1px solid var(--neon);box-shadow:var(--glow);object-fit:cover}
.brand{display:flex;align-items:center;justify-content:center;gap:8px}
.brand-logo{width:58px;height:58px;border-radius:14px;border:1px solid var(--neon);box-shadow:var(--glow);object-fit:cover}
.logo{font-size:30px;font-weight:800;letter-spacing:2px;margin:0;text-align:center;
  background:linear-gradient(90deg,var(--cyan),var(--neon));-webkit-background-clip:text;background-clip:text;color:transparent}
.tagline{text-align:center;color:var(--muted);margin:4px 0 22px;font-size:13px;letter-spacing:2px;text-transform:uppercase}
.auth-pane{display:flex;flex-direction:column;gap:2px}
/* Login card sections. The pane used to be one column of identical full-width buttons with dividers
   between them, so every route in — extension, remote signer, pasted key, brand-new identity — read as
   equally likely. Grouping gives the card three obvious blocks and lets the key paste fold away. */
.auth-sec{display:flex;flex-direction:column;gap:8px;margin-bottom:14px}
.auth-sec-t{color:var(--muted);font-size:12px;letter-spacing:.6px;text-transform:uppercase}
.auth-sec .btn{margin:0}
/* Sits UNDER the account buttons, not above them: it is read as a caption of what they do. */
.auth-accounts-note{margin:2px 0 0;line-height:1.45;font-size:12px}
.auth-more{border:1px solid var(--line);border-radius:12px;padding:0 12px;margin-bottom:14px}
.auth-more>summary{cursor:pointer;padding:12px 0;font-size:15px;color:var(--text);list-style:none}
.auth-more>summary::-webkit-details-marker{display:none}
.auth-more>summary::after{content:'▾';float:inline-end;color:var(--muted);transition:.15s}
.auth-more[open]>summary::after{transform:rotate(180deg)}
.auth-more>summary:hover{color:var(--cyan)}
.auth-more .input{margin:0 0 8px}
.auth-more .btn{margin:0 0 12px}
.auth-foot{display:flex;flex-direction:column;gap:6px;align-items:stretch}
.auth-foot .btn{margin:0}
/* "Server: poster.place / change" on the sign-in card (native builds only). Deliberately quieter than a
   .btn: it is a statement of where you are with a way out, not a fourth way to sign in competing with
   the three above it. */
.auth-conn{margin-top:14px;padding-top:12px;border-top:1px solid var(--line)}
.auth-conn-btn{display:flex;align-items:center;gap:8px;width:100%;padding:9px 11px;border-radius:11px;
  border:1px solid var(--line);background:transparent;color:var(--muted);font-size:12.5px;font-family:inherit;
  cursor:pointer;text-align:start}
.auth-conn-btn:hover{border-color:var(--cyan);color:var(--text)}
.auth-conn-btn>span:first-of-type{flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.auth-conn-btn .ic{flex:none;width:15px;height:15px}
.auth-conn-go{flex:none;color:var(--cyan);text-transform:uppercase;letter-spacing:.6px;font-size:11px}
/* "— where your posts live" / "— optional" beside a section heading: same line, not shouted. The
   heading itself is uppercase + tracked; this stays sentence case so it reads as the aside it is. */
.auth-sec-n{text-transform:none;letter-spacing:0;opacity:.75}
/* The relay box is a list of URLs, so wrapping mid-url is worse than a scrollbar. */
#conn-relays{font-family:ui-monospace,SFMono-Regular,Menlo,monospace;font-size:12.5px;line-height:1.5;
  white-space:pre;overflow-wrap:normal;overflow-x:auto}
.divider{display:flex;align-items:center;gap:10px;color:var(--muted);font-size:12px;margin:12px 0}
.divider::before,.divider::after{content:"";flex:1;height:1px;background:var(--line)}
.auth-error{color:#ff6b8b;font-size:13px;min-height:18px;text-align:center;margin-top:6px}
.keybox{background:rgba(8,5,18,.6);border:1px solid var(--line);border-radius:12px;padding:12px;margin:8px 0}
/* 300px, not 230px. The signer URI carries a 19-entry `perms` list, which puts it at QR version 18
   — 89x89 modules — and 230px with 10px of padding drew those at 2.26 device px each, under the ~3px
   a phone camera needs off a screen. The QR is the whole instruction on this screen, so it gets the
   room. max-width keeps it inside a narrow phone. */
/* 380px, not 300. The signer URI carries a 15-entry `perms` list, so the real payload is ~556 bytes
   → QR version 21, i.e. 101x101 modules: at 300px that was ~2.5 device pixels per module before the
   camera's own scaling, which a webcam pointed at a screen decodes only in good light. The symbol
   already carries the spec's 4-module quiet zone (qr.js), so the win here is purely physical size. */
.nc-qr{display:block;width:380px;max-width:86vw;height:auto;margin:10px auto;background:#fff;padding:12px;border-radius:12px;box-shadow:0 0 24px rgba(var(--accent-rgb),.25)}
.qr-video{display:block;width:100%;max-width:360px;aspect-ratio:1/1;object-fit:cover;margin:10px auto;background:#000;border-radius:12px;border:1px solid var(--line)}
.keybox label{font-size:11px;color:var(--muted);text-transform:uppercase;letter-spacing:1px;display:block;margin:8px 0 4px}
.keyrow{display:flex;gap:8px;align-items:center}
.keyrow code{flex:1;font-size:11px;color:var(--cyan)}

/* glitch */
.glitch{position:relative}
.glitch::before,.glitch::after{content:attr(data-text);position:absolute;inset-inline-start:0;top:0;width:100%;overflow:hidden;clip-path:inset(0)}
.glitch::before{color:var(--cyan);transform:translate(-2px,0);animation:gl 2.6s infinite linear alternate}
.glitch::after{color:var(--neon);transform:translate(2px,0);animation:gl 2s infinite linear alternate-reverse}
@keyframes gl{0%{clip-path:inset(0 0 86% 0)}25%{clip-path:inset(60% 0 20% 0)}50%{clip-path:inset(20% 0 60% 0)}75%{clip-path:inset(86% 0 0 0)}100%{clip-path:inset(40% 0 40% 0)}}

/* ===== app shell ===== */
/* Keep height:100vh (NOT 100dvh): 100vh sizes to the TALL viewport, which lets mobile browsers
   (Firefox especially) auto-hide their address bar for a fullscreen feel. 100dvh sizes to the small
   dynamic viewport that RESERVES room for the chrome, so the toolbar stays pinned — a regression. */
/* column-gap (not padding on .main): the topbar is sticky and full-bleed, so padding there would inset the
   header too. A grid gap lets the background show between the panels instead of butting them together.
   Collapses to nothing on mobile, where the grid is a single column. */
/* Inset the whole app from the screen edges (ditto-style) so the panels float in space instead of running
   edge-to-edge, plus a gap between the columns. Safe because *{box-sizing:border-box} — the padding comes
   out of the 1fr middle column, not out of the viewport. Dropped to 0 on mobile, where every px counts. */
.app{display:grid;grid-template-columns:300px 1fr;height:100vh;column-gap:20px;padding:0 32px;position:relative}   /* positioned: .rb-toggle is offset from THIS box, not the viewport */
/* No box: .glass outlines all four sides, which boxed the rail in. Just ONE vertical rule on the inner
   edge — the divider between the rail and the feed. Its twin is .rightbar's border-left, so the shell
   reads as three columns of one surface separated by hairlines, not as three floating panels. */
.sidebar{display:flex;flex-direction:column;gap:8px;padding:18px 14px;min-height:0;overflow:hidden;
  border:0;border-inline-end:1px solid var(--line);
  background:var(--rail)}
/* Only the NAV list scrolls; the brand (top) and the me-card→Logout cluster (bottom) are pinned and never
   shrink — so on a short/tall viewport or the app the Logout button is ALWAYS visible, never clipped or
   squished by the long nav list. (Previously the whole sidebar scrolled, pushing Logout off the bottom.) */
.sidebar>.brand,.sidebar>.me,.sidebar>.conn,.sidebar>.usercount,.sidebar>#btn-install,.sidebar>#btn-logout{flex-shrink:0}
.brand{font-size:24px;font-weight:800;text-align:center;color:var(--neon);margin-bottom:8px}
.nav{display:flex;flex-direction:column;gap:4px;flex:1 1 auto;min-height:0;overflow-y:auto}
.nav-group{display:flex;flex-direction:column;gap:4px}
.nav-grouphd .chev{margin-inline-start:auto;font-size:12px;opacity:.65}
.nav-sub{display:flex;flex-direction:column;gap:4px;overflow:hidden}
.nav-sub.collapsed{display:none}
.nav-item.sub{padding-inline-start:30px;font-size:17px;font-weight:500}
.nav-item{display:flex;align-items:center;gap:14px;background:transparent;border:none;color:var(--muted);
  padding:12px 16px;border-radius:11px;cursor:pointer;font-size:17px;font-weight:550;text-align:start;transition:.15s;position:relative}
.nav-item .ic{font-size:24px;width:30px;text-align:center;line-height:1;display:inline-flex;align-items:center;justify-content:center;
  filter:drop-shadow(0 0 5px rgba(var(--accent-rgb),.45))}
.nav-item.sub .ic{font-size:17px}
.nav-item:hover .ic,.nav-item.active .ic{filter:drop-shadow(0 0 7px rgba(var(--accent-rgb),.75))}
/* ===== Flat line icons (see the sprite in client.html) ==============================================
   `stroke:currentColor` + `fill:none` is the whole trick: an icon is drawn in whatever colour its row
   already resolves to, so the muted→text→accent transitions below carry the glyph with them and every
   theme (incl. the light ones) gets a legible icon for free. Shapes that need to be solid (the LIVE
   dot, the gamepad buttons) carry fill="currentColor" as a presentation attribute on the child, which
   out-ranks this inherited fill. No drop-shadow: flat is the point — only the ACTIVE row keeps a faint
   halo, and the theme layer already strips even that off the non-cyberpunk palettes. */
svg.ic{flex:none;width:20px;height:20px;fill:none;stroke:currentColor;stroke-width:1.7;stroke-linecap:round;stroke-linejoin:round;overflow:visible}
.nav-item svg.ic{width:21px;height:21px;filter:none;opacity:.92}
.nav-item.sub svg.ic{width:18px;height:18px}
.nav-item:hover svg.ic{opacity:1;filter:none}
.nav-item.active svg.ic{opacity:1;color:var(--neon);filter:drop-shadow(0 0 6px rgba(var(--accent-rgb),.45))}
.nav-item:hover{color:var(--text);background:rgba(var(--accent-rgb),.12);text-shadow:0 0 6px rgba(var(--accent-rgb),.35)}
.nav-item.active{color:#fff;background:linear-gradient(135deg,rgba(var(--accent-rgb),.22),rgba(var(--accent2-rgb),.14));box-shadow:inset 0 0 0 1px var(--line);text-shadow:0 0 8px rgba(var(--accent-rgb),.6)}
.nav-item.active::before{content:"";position:absolute;inset-inline-start:0;top:18%;height:64%;width:3px;border-radius:3px;background:var(--neon);box-shadow:var(--glow)}
/* Timeline ＋ — last child of #feed and position:sticky, so it rides the bottom-right of the FEED.
   #feed is the actual scroll container, so its bottom edge is the real visible bottom; .main/.app are
   sized from the body{zoom:.85} / calc(100dvh/.85) pair and end up taller than the viewport in Firefox,
   which is what cut off every fixed/absolute attempt. margin-inline-start:auto pushes it right; the negative
   top margin keeps it from adding a blank row under the last note. */
/* display:FLEX, not inline-flex — `margin-inline-start:auto` is what pins this to the right edge, and that
   only works on a block-level box. The old line-height:58px + text-align:center centred a TEXT glyph;
   an <svg> sits on the baseline instead, which is why the + sat low and left in its circle. */
.tl-fab{position:sticky;bottom:22px;z-index:20;display:flex;align-items:center;justify-content:center;
  margin:-70px 22px 8px auto;
  width:58px;height:58px;padding:0;border-radius:50%;cursor:pointer;border:0;
  background:linear-gradient(135deg,var(--neon),var(--neon2));color:#06121a;
  font-family:inherit;font-size:30px;font-weight:400;line-height:1;
  box-shadow:0 8px 22px -6px rgba(0,0,0,.7),var(--glow-cy);transition:transform .15s}
.tl-fab svg.ic{width:26px;height:26px;stroke-width:2.2}
.tl-fab:hover{transform:scale(1.06)}
@media(max-width:820px){ .tl-fab{display:none} }   /* the bottom nav already has ✏️ */
/* Notification context — the post the notification is about, rendered with the app's normal embedded-post
   card (quotedDiv). Bounded so one long post can't dominate the list: the body clamps to 6 lines and its
   media is capped, but the header, text and images are all really there. */
/* What the person actually SAID — its own line under "<name> replied", so the name/verb and the words
   don't run together into one unreadable string. Wraps to 3 lines, then clamps. */
/* "<name> replied" is its own line; the quoted words and the context card sit clearly below it. */
.notif-hd{margin:0}
.notif-said{display:-webkit-box;-webkit-line-clamp:4;-webkit-box-orient:vertical;overflow:hidden;margin:6px 0 0;padding:0;border:0;background:none;font-family:inherit;font-size:15px;font-weight:400;line-height:1.45;letter-spacing:0;color:var(--muted);white-space:pre-wrap;overflow-wrap:anywhere}
@media(max-width:820px){ .notif-said{font-size:15px;-webkit-line-clamp:3} }
.notif-ctx{margin:14px 0 2px}
.notif-ctx .quoted{margin:0}
.notif-ctx .quoted .txt{display:-webkit-box;-webkit-line-clamp:6;-webkit-box-orient:vertical;overflow:hidden}
/* A notification is a ROW, so its context preview is capped hard. The `:only-child` selectors are here
   because without them this cap silently LOST: `.notif-ctx .media-row video` and
   `.media-row video:only-child` are both specificity (0,2,1), and the :only-child rule (which lifts a
   lone attachment to 720px so a single photo can fill the feed column) comes later in the file — so it
   won the tie, and a lone video in a notification was allowed to be 720px tall. Measured at 400x640 in a
   390px-wide notification row before this. That was true of the old max-height cascade too; --mh just
   inherited it. Adding the :only-child variants takes this rule to (0,3,1) so the intended cap applies. */
.notif-ctx .media-row img,.notif-ctx .media-row video,
.notif-ctx .media-row img:only-child,.notif-ctx .media-row video:only-child,
.notif-ctx .mc-item img,.notif-ctx .mc-item video{--mh:220px;max-height:220px}
@media(max-width:820px){
  .notif-ctx .quoted .txt{-webkit-line-clamp:5}
  .notif-ctx .media-row img,.notif-ctx .media-row video,
  .notif-ctx .media-row img:only-child,.notif-ctx .media-row video:only-child,
  .notif-ctx .mc-item img,.notif-ctx .mc-item video{--mh:170px;max-height:170px}
}
/* The context card is a "…" one-liner until the referenced post arrives from the relay, and patchLoaded
   then swaps in the real card — author row, text and media. Reserving the header + two text lines turns
   that from a 20px-to-300px lurch into a small settle; the media on top of it is capped just above. */
.notif-ctx[data-nctx]{min-height:74px}
.me{display:flex;flex-direction:column;align-items:center;justify-content:center;text-align:center;gap:8px;margin:auto auto 0;padding:8px 6px;cursor:pointer}
.me:hover img{box-shadow:0 0 16px rgba(var(--accent-rgb),.4)}
.me img{width:72px;height:72px;border-radius:50%;object-fit:cover;border:1px solid var(--neon);transition:.15s}
.me .mn{font-weight:700;font-size:20px;line-height:1.2;max-width:100%;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.me .mk{font-size:12px;color:var(--muted)}
/* Filled (btn-neon) logout — white text on the neon gradient (the old red #ff6b8b was unreadable on the fill) */
/* compact, content-width logout (was full-width: too wide in the sidebar) — align-self stops the
   flex-column from stretching it; centered with a little breathing room */
#btn-logout{align-self:center;margin:6px 0;font-size:15px;padding:10px 22px;color:#fff;text-shadow:0 1px 2px rgba(0,0,0,.35);transition:.15s}
#btn-logout:hover{filter:brightness(1.08);box-shadow:0 0 22px rgba(var(--accent2-rgb),.45)}
.conn{display:flex;align-items:center;justify-content:center;gap:10px;font-size:15px;font-weight:700;letter-spacing:1px;text-transform:uppercase;color:var(--muted);padding:6px 8px 10px}
/* Stats WRAP horizontally (flex) rather than each on its own line — with 5 stats (users/online/relay/live/
   call) the old stacked layout grew tall enough that the fixed-height, overflow-hidden sidebar clipped the
   bottom ones on a short (tablet-landscape) viewport. Wrapping keeps them to ~2 compact lines and never clips.
   `flex-shrink:0` on the whole block keeps it from being squeezed to nothing by a tall scrolling nav above. */
.usercount{display:flex;flex-wrap:wrap;justify-content:center;align-items:center;gap:2px 10px;flex-shrink:0;
  font-size:13px;font-weight:700;line-height:1.45;letter-spacing:.3px;color:#bdfcff;text-shadow:0 0 8px rgba(var(--accent-rgb),.6);padding:2px 8px 10px;margin-top:-2px}
.usercount .uc-dot{color:var(--muted);text-shadow:none;margin:0 2px}
.usercount .wot-ico{width:17px;height:17px;vertical-align:-4px;margin-inline-end:2px;filter:drop-shadow(0 0 6px rgba(var(--accent-rgb),.75))}
.usercount .live-ico{width:14px;height:14px;color:var(--neon2);vertical-align:-2px;margin-inline-end:2px;filter:drop-shadow(0 0 6px rgba(var(--accent2-rgb),.9));animation:conn-pulse 1.6s ease-in-out infinite}
.usercount .uc-stat{white-space:nowrap}
.usercount .relay-ico{width:15px;height:15px;color:var(--green);vertical-align:-2px;margin-inline-end:4px;filter:drop-shadow(0 0 6px rgba(0,255,136,.6))}
/* Drafts + Calls are sidebar nav rows now (was: topbar buttons). Mobile still reaches both through the
   ☰ More sheet, which already lists them — the sidebar is hidden ≤820px. */
/* Instance quick-pick chips (User Settings → Media, native app). Wrap on small screens. */
.instance-pick{display:flex;flex-wrap:wrap;gap:6px;margin-top:4px}
.instance-chip{background:var(--panel2,#1a1a2e);border:1px solid var(--line,#333);color:var(--text,#eee);border-radius:16px;
  padding:6px 12px;font-size:12px;cursor:pointer;max-width:100%;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.instance-chip.on{border-color:var(--neon,#0ff);color:var(--neon,#0ff);background:rgba(var(--accent-rgb),.08)}
.instance-chip:hover{filter:brightness(1.15)}
/* Live-activity ticker glyphs. Streams pulse red like a LIVE badge; calls are a steady cyan handset.
   Both stats only render when their count > 0, so the ticker never shows an idle "0". */
.usercount .stream-ico{width:15px;height:15px;color:#ff3b5c;vertical-align:-2px;margin-inline-end:4px;filter:drop-shadow(0 0 6px rgba(255,59,92,.7))}
.usercount .uc-live{color:#ffd7de}
.usercount .call-ico{width:14px;height:14px;color:var(--neon,#0ff);vertical-align:-2px;margin-inline-end:4px;filter:drop-shadow(0 0 6px rgba(var(--accent-rgb),.5))}
/* The mobile More-sheet stats: two centred rows (.ms-row) — headcounts, then live activity. Each stat
   is itself a centred flex row, so its glyph aligns against its number by layout rather than by a
   per-icon `vertical-align` fudge.
   THE SIZES ARE NOT OPTIONAL: `svg.ic{width:20px;height:20px}` is (0,1,1), so it beats the bare
   `.wot-ico,.relay-ico,…{width:13px}` defaults — which is why WoT and "online" were the only two
   glyphs here without a `.more-stats` rule and rendered at 20px next to three 12-13px ones, throwing
   the row's baselines out. Anything added to this line needs an entry here. */
.more-stats{display:flex;flex-direction:column;align-items:center;gap:6px;margin:2px 0 12px}
.more-stats .ms-row{display:flex;align-items:center;flex-wrap:wrap;justify-content:center;gap:6px 16px}
.more-stats span{display:inline-flex;align-items:center;gap:6px;white-space:nowrap}
.more-stats .wot-ico{width:14px;height:14px}
.more-stats .live-ico{width:12px;height:12px;color:var(--neon2)}
.more-stats .relay-ico{width:13px;height:13px;color:var(--green)}
.more-stats .stream-ico{width:13px;height:13px;color:#ff3b5c}
.more-stats .call-ico{width:12px;height:12px;color:var(--neon,#0ff)}
/* media-server destination choice (Settings → Media): stacked radio cards. */
.media-modes{display:flex;flex-direction:column;gap:8px;margin:12px 0 4px}
.media-mode{display:flex;align-items:flex-start;gap:10px;padding:12px 14px;border:1px solid var(--line));border-radius:10px;cursor:pointer;transition:border-color .15s,background .15s}
.media-mode:hover{border-color:var(--neon)}
.media-mode:has(input:checked){border-color:var(--neon);background:color-mix(in srgb, var(--neon) 12%, transparent)}
.media-mode input{margin-top:4px;flex:none;accent-color:var(--neon)}
/* media-server quick-pick preset chips (Settings → Media) — wrap on narrow screens. */
.media-presets{display:flex;flex-wrap:wrap;align-items:center;gap:6px;margin:8px 0 2px}
.conn .dot{width:14px;height:14px;border-radius:50%;background:var(--amber);box-shadow:0 0 10px var(--amber);animation:conn-pulse 1.6s ease-in-out infinite}
.conn.ok{color:var(--green)}
.conn.ok .dot{background:var(--green);box-shadow:0 0 12px var(--green),0 0 22px var(--green)}
.conn.off{color:#ff4d6d}
.conn.off .dot{background:#ff4d6d;box-shadow:0 0 12px #ff4d6d,0 0 22px #ff4d6d}
@keyframes conn-pulse{0%,100%{transform:scale(1);opacity:1}50%{transform:scale(1.35);opacity:.55}}

.main{display:flex;flex-direction:column;height:100vh;overflow:hidden;position:relative}
/* "↑ N new posts" pill — floats below the topbar so live posts don't shift the feed while reading */
.new-posts-pill{position:absolute;top:64px;inset-inline-start:50%;transform:translateX(-50%);z-index:30;cursor:pointer;
  background:linear-gradient(135deg,var(--neon),var(--neon2));color:#fff;border:none;font-family:inherit;
  padding:10px 18px;border-radius:999px;font-weight:700;font-size:15px;letter-spacing:.3px;
  box-shadow:0 4px 18px rgba(0,0,0,.45),var(--glow);animation:pillin .18s ease-out}
.new-posts-pill:hover{transform:translateX(-50%) translateY(-1px)}
.osw-body:has(> .new-posts-pill){position:relative}
@keyframes pillin{from{opacity:0}to{opacity:1}}   /* opacity-only so it stays centered (translateX) */
@media(max-width:820px){ .new-posts-pill{top:calc(50px + env(safe-area-inset-top))} }
/* desktop-only right column: Hot / Trending + GitHub link (hidden below 1100px) */
.rightbar,.rb-toggle{display:none}   /* hidden by default; shown at >=1180px where the right column exists */
@media(min-width:1180px){
  /* THE READING COLUMN IS CAPPED, and the whole three-column cluster centres on wide screens.
     `minmax(0,1fr)` handed the feed every spare pixel of the viewport, so the wider the monitor the
     worse it read: MEASURED at --fs-3, a post ran 79 characters per line at 1280, 92 at 1440, 140 at
     1920 and 160 at 2560. Comfortable prose is 45-75 characters and the classic optimum is 66, so on
     any large screen the feed sat at roughly double the readable maximum — most of why it scanned as
     an undifferentiated wall rather than as separate posts.
     Capping .app rather than the feed alone keeps the three columns ADJACENT and spends the surplus
     as one gutter either side of the cluster. Capping just the feed also fixes the measure, but it
     opens ~400px of dead space between the feed and each rail, which reads as three drifting islands.
     --app-max is the sum of the tracks so the middle column lands exactly on --feed-max:
     300 + 20 gap + 920 + 20 gap + 340 + 64 padding = 1664. */
  .app{grid-template-columns:300px minmax(0,1fr) 340px;max-width:var(--app-max);margin-inline:auto}
  /* Mirrors .sidebar exactly: same --rail surface, same single hairline on the edge that faces the feed. */
  .rightbar{display:flex;flex-direction:column;gap:16px;padding:18px 16px;overflow-y:auto;min-height:0;
    border:0;border-inline-start:1px solid var(--line);background:var(--rail)}
  /* Collapse toggle — a neon arrow tab on the panel's left edge (vertically centered). Persisted in
     localStorage. Only present where the right column exists (>=1180px). */
  .rb-toggle{display:flex;align-items:center;justify-content:center;position:absolute;z-index:25;
    /* Anchored to the RAIL's left edge, which is no longer the viewport's edge: .app is capped at
       --app-max and centred, so past that width the cluster pulls away from the right of the screen.
       Below --app-max the app is full width and this resolves to the old rail(340) + padding(32).
       Above it the gutter is (100vw - 1664)/2, and 100vw - gutter - (1664 - 32 - 340) reduces to
       is the rail (340px) plus .app's own outer padding (32px), measured from .app's box — so it
       lands on the panel edge whatever the app's width or offset, with no viewport arithmetic. */
    top:50%;inset-inline-end:calc(340px + 32px);transform:translateY(-50%);width:20px;height:74px;cursor:pointer;padding:0;
    font-size:13px;line-height:1;color:var(--cyan);font-family:inherit;
    /* Opaque var(--bg), not var(--rail): the rail is transparent now and the feed scrolls past behind
       this tab, so it needs its own fill — the canvas colour, so it still reads as part of the rail.
       (It used to hardcode a near-black base: a dark tab stuck to a white panel on every light theme.) */
    background:linear-gradient(135deg,rgba(var(--accent-rgb),.16),rgba(var(--accent2-rgb),.12)),var(--bg);
    border:1px solid var(--line);border-inline-end:none;border-radius:9px 0 0 9px;
    box-shadow:-3px 0 16px rgba(var(--accent-rgb),.18),inset 1px 0 0 rgba(var(--accent-rgb),.3);
    transition:inset-inline-end .2s ease, background .15s, color .15s, box-shadow .15s}
  /* var(--text), not #fff: the hover base is now the themed rail, and white-on-white hid the arrow. */
  .rb-toggle:hover{color:var(--text);background:linear-gradient(135deg,rgba(var(--accent-rgb),.32),rgba(var(--accent2-rgb),.22)),var(--bg);
    box-shadow:-3px 0 22px rgba(var(--accent-rgb),.5),inset 1px 0 0 rgba(var(--accent-rgb),.6)}
  body.rb-collapsed .app{grid-template-columns:300px minmax(0,1fr)}   /* drop the 3rd column → main widens */
  body.rb-collapsed .rightbar{display:none}
  body.rb-collapsed .rb-toggle{inset-inline-end:32px}   /* tab slides to the app's edge (= its outer padding) */
  /* Per-VIEW hide. app.js allowlists the timeline-shaped views (RB_SHOW_VIEWS) and sets .rb-off on
     everything else — AI chat, News, Markets, Stats, Torrents, Shopping, Streams, chat rooms, Files,
     Settings, the games… all of which would rather have the width than a rail of unrelated posts.
     Same column drop as the manual collapse but a SEPARATE class — rb-collapsed is the user's
     persisted preference and must not be written by navigation. The ▸ tab goes too: leaving it
     behind would be a visible control that does nothing. */
  body.rb-off .app{grid-template-columns:300px minmax(0,1fr)}
  body.rb-off .rightbar,body.rb-off .rb-toggle{display:none}
}
.rb-section h3{margin:0 0 8px;font-size:20px;letter-spacing:1px;color:var(--text)}
/* Unread count beside the Notifications heading (it rode on a segmented-control tab before Hot/Follows moved
   out to the Trending tab) — smaller than the sidebar bell's badge, and baseline-aligned to the text. */
.rb-section h3 #rb-notif-badge{min-width:17px;height:17px;padding:0 6px;font-size:11px;margin-inline-start:8px;
  box-shadow:none;vertical-align:middle}
/* The Notifications heading is a real control (it opens the full view, which is what marks read), so it
   has to LOOK clickable — an <h3> that navigates without a hover state is a trap. */
.rb-h3-btn{cursor:pointer;transition:color .15s}
/* Matched as `.rb-section h3.rb-h3-btn` to TIE `:root[data-theme] .rb-section h3{color:var(--text)}` on
   specificity and win on order — a bare `.rb-h3-btn:hover` loses to that theme rule and does nothing. */
.rb-section h3.rb-h3-btn:hover{color:var(--neon)}
.rb-h3-btn:focus-visible{outline:2px solid var(--neon);outline-offset:3px;border-radius:6px}
/* Rail rows: the SAME .notif markup the Notifications view renders, only COMPACTED — padding and type
   size, nothing else. No card treatment: the base .notif is already a flat row with a hairline
   border-bottom, so five bordered+filled tiles were five boxes inside a box. Letting the base rule show
   through means one hairline between rows and no fill, matching the rails' own borderless look. */
.rightbar .notif{padding:10px 2px;font-size:13px;cursor:pointer;transition:background .15s}
.rightbar .notif:hover{background:rgba(var(--accent-rgb),.08)}
.rightbar .notif:last-child{border-bottom:0}   /* no trailing rule above "Get the app" */
.rightbar .notif *{max-width:100%}          /* quoted context / previews can't widen the column */
.rightbar .notif .notif-av{width:24px;height:24px}
.rightbar .notif > .ic{font-size:15px;width:18px}
.rightbar .notif .muted.small{font-size:11px}
.rbq-bar{display:flex;gap:6px;margin-top:8px}
.rbq{padding:4px 12px;border:1px solid var(--line);border-radius:999px;background:transparent;color:var(--muted);
  font-family:inherit;font-size:13px;line-height:1.2;cursor:pointer;transition:.15s}
.rbq:hover{border-color:var(--neon);color:var(--neon);background:rgba(var(--accent-rgb),.12)}
/* Notification rows are NOT a fixed height: a reply/mention carries _notifCtx — a full quotedDiv with
   author header, whole text and the real media gallery — while a bare reaction is one line. Five of
   the former overflowed the column that five of the latter fit. Clamp the variable parts (the quoted
   text, the actor's own words) to two lines and cap the quoted media, so the row budget holds
   whatever KIND of notifications happen to be on top. Only inside the rail — the Notifications view
   itself still shows them in full. */
.rightbar .notif .notif-ctx .txt,
.rightbar .notif .notif-said{display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden}
/* The quoted post it happened to: an indent rule, not a bordered tile. With the row's own card gone this
   was the only box left in the column, and a box nested in a flat row reads as the row's frame. */
.rightbar .notif .notif-ctx .quoted{padding:1px 0 1px 10px;border:0;border-inline-start:2px solid var(--line);
  border-radius:0;background:transparent;box-shadow:none}
.rightbar .notif .notif-ctx .hd .handle,
.rightbar .notif .notif-ctx .hd .time{display:none}   /* the name + a 2-line snippet is the whole job here */
/* width:auto OPTS OUT of the --arn/--nw reservation (see the layout-stable media block): this is a
   deliberately cover-CROPPED 74px thumbnail, not a scaled copy of the post's media, so a ratio-derived
   width is the wrong shape for it. Without the opt-out it inherits --mh:220px from .notif-ctx above,
   reserves a 220px-tall box and then clamps to 74 — a shift, in the one place there wasn't one. */
.rightbar .notif .notif-ctx img,
.rightbar .notif .notif-ctx video{width:auto;max-height:74px;object-fit:cover}
.rightbar .notif .notif-ctx .qav{max-height:none;width:18px;height:18px}   /* the avatar is not "media" */
/* Native builds — a compact 2×2 of platform links, pinned above the GitHub row (it's .rb-apps that
   takes the margin-top:auto now, so the pair sits together at the foot of the column). */
/* NOT margin-top:auto. The rail is a flex column, so that pinned this block to the BOTTOM — and with
   an empty Notifications list (the common case) it opened ~600px of void in the middle of the column,
   with a heading and a divider floating in the dead centre. Empty space at the END of a column is
   invisible; empty space in the MIDDLE reads as broken layout. The rail scrolls, so on a busy account
   this simply sits below the notifications instead of being reachable only past them. */
.rb-apps{padding-top:var(--sp-3);border-top:1px solid var(--line)}
.rb-apps h4{margin:0 0 10px;font-size:11px;font-weight:700;letter-spacing:.7px;text-transform:uppercase;color:var(--muted)}
.rb-appgrid{display:grid;grid-template-columns:1fr 1fr;gap:8px}
.rb-app{display:flex;align-items:center;gap:8px;padding:10px 10px;border:1px solid var(--line);border-radius:10px;
  background:var(--panel);color:var(--text);text-decoration:none;font-size:13px;font-weight:600;transition:.15s}
.rb-app:hover{border-color:var(--neon);color:var(--neon)}
.rb-app svg.ic{width:17px;height:17px}
.rb-github{margin-top:10px;display:flex;align-items:center;justify-content:center;gap:10px;color:var(--text);
  border:1px solid var(--line);border-radius:12px;padding:12px;font-weight:600;transition:.15s}
.rb-github:hover{border-color:var(--cyan);color:var(--cyan);box-shadow:var(--glow-cy)}
/* Same shape as the GitHub link directly below it — they read as one pair of actions. width:100% because
   a <button> does not stretch to its flex parent the way the <a> does. */
.rb-report{margin-top:10px;width:100%;display:flex;align-items:center;justify-content:center;gap:10px;
  color:var(--text);background:none;border:1px solid var(--line);border-radius:12px;padding:12px;
  font:inherit;font-weight:600;cursor:pointer;transition:.15s}
.rb-report:hover{border-color:var(--neon);color:var(--neon);box-shadow:var(--glow)}
/* No box: .glass gave it a border on all four sides AND a translucent panel, so every view header read as
   a floating rectangle with the city skyline murkily visible through it. The title is just a heading on the
   page now, matching the borderless side panels. Safe to be transparent because .main is a flex COLUMN —
   #feed is a sibling that scrolls inside itself, so nothing ever passes behind this bar. */
.topbar{display:flex;align-items:center;justify-content:space-between;gap:12px;padding:20px 14px 12px;
  background:transparent;border:0;position:sticky;top:0;z-index:5}
.topbar h2{margin:0;font-size:24px;letter-spacing:.4px;font-weight:800;line-height:1.2}
/* The thread's own "←", matching the header button every other detail view already has. */
.thread-top{display:flex;align-items:center;margin:0 0 10px}
.searchbox{flex:1;min-width:0;max-width:720px;margin:0 16px}   /* min-width:0 → shrinks so the topbar buttons (Calls/Drafts/New post) always fit, never clipped */
/* Exactly ONE search box at any width: the sidebar owns it on desktop, the topbar on mobile (where the
   sidebar is display:none). Two visible inputs would be duplicate UI; two hidden ones would be no search. */
@media(min-width:821px){ .searchbox{display:none} }
.nav-search{flex-shrink:0;margin:0 0 10px}
/* Ditto-style search: a soft pill, not a boxed form field. Borderless at rest with a panel fill, and the
   accent only appears on focus — so it reads as a control rather than competing with the nav items.
   Applies to BOTH inputs (sidebar on desktop, topbar on mobile) so they can't drift apart. */
.nav-search .input,.searchbox .input{width:100%;margin:0;padding:10px 16px;font-size:17px;   /* >=16px or iOS Safari zooms the page on focus */
  border:1px solid transparent;border-radius:999px;background:var(--panel2);color:var(--text);transition:.15s}
.nav-search .input::placeholder,.searchbox .input::placeholder{color:var(--muted)}
.nav-search .input:hover,.searchbox .input:hover{background:var(--panel)}
.nav-search .input:focus,.searchbox .input:focus{border-color:var(--neon);background:var(--panel);
  box-shadow:0 0 0 3px rgba(var(--accent-rgb),.13)}

/* ===== timeline header: inline composer + Home/Nostrverse tabs (ditto.pub-style) =====
   Emitted as DIRECT children of #feed, not wrapped: .tl-tabs is position:sticky, and a sticky element can
   only stick within its own parent — nesting it in a short wrapper would scroll the tabs away instantly. */
/* Guest card — shown where the composer sits for a logged-out visitor: logo, instance name, and the
   two actions plus a source link. Same box geometry as .tl-cmp so the timeline doesn't shift when you
   sign in and the composer takes its place. */
.guest-card{display:flex;flex-direction:column;align-items:center;text-align:center;gap:10px;
  margin:0;padding:18px 20px;background:transparent;border:0;box-shadow:none}
.guest-logo{width:56px;height:56px;border-radius:14px;object-fit:cover}
.guest-name{font-size:20px;font-weight:800;letter-spacing:.3px;color:var(--text)}
.guest-acts{display:flex;align-items:center;gap:10px;flex-wrap:wrap;justify-content:center}
.guest-acts .btn{margin:0}
/* Three DIFFERENT filled colours: Sign up = orange (btn-neon), Log in = cyan (btn-cyan), Source =
   violet (here). All filled, all distinct. (0,2,0) beats btn-* so the violet fill wins. */
.guest-src{text-decoration:none}
.guest-src.btn{background:linear-gradient(135deg,#8b5cf6,#b794ff);border:none;color:#fff;box-shadow:0 0 14px rgba(139,92,246,.35)}
.guest-src.btn:hover{filter:brightness(1.08)}
@media(max-width:820px){ .guest-card{margin:0;padding:16px 14px} .guest-logo{width:48px;height:48px}
  .guest-name{font-size:20px} }
/* The composer is the FIRST ROW OF THE TIMELINE, not a widget parked above it. It used to be an inset
   var(--panel) card with rounded corners, which made it the one floating box left on the page (10/255
   lighter than the canvas on Professional, 12 on Dark) — the seam the rails no longer have. So: no fill,
   no radius, full-bleed, and the SAME padding/gap/avatar as .note, which puts its textarea on the exact
   x as every note's text. The responsive padding/gap/avatar values are shared with .note in the
   breakpoint rules further down rather than duplicated, so the two can't drift apart. */
.tl-cmp{display:flex;gap:12px;align-items:flex-start;margin:0;padding:16px 20px;
  border:0;border-radius:0;background:transparent}
.tl-cmp-av{width:52px;height:52px;border-radius:50%;border:1px solid var(--line);object-fit:cover;flex-shrink:0}
.tl-cmp-body{flex:1;min-width:0}
/* overflow-y:AUTO, not hidden. grow() caps the box at 320px, so with hidden overflow everything past that
   was unreachable — you could keep typing (the caret drags the view along) but could not scroll back to
   read what you had written. The bar itself is hidden in every engine: this sits inside a feed row and a
   gutter appearing mid-sentence would shove the text. overscroll-behavior keeps the gesture in the box
   rather than yanking the timeline once you hit the end. */
.tl-cmp-ta{width:100%;min-height:54px;margin:0;padding:8px 0;border:0;background:transparent;color:var(--text);
  resize:none;overflow-y:auto;overflow-x:hidden;overscroll-behavior:contain;-webkit-overflow-scrolling:touch;
  scrollbar-width:none;-ms-overflow-style:none;font-family:inherit;font-size:17px;line-height:1.45;outline:none}
.tl-cmp-ta::-webkit-scrollbar{width:0;height:0;display:none}
.tl-cmp-ta::placeholder{color:var(--muted)}
/* Kill the global `textarea:focus` treatment here: border-color is a no-op (this one is border:0) but its
   box-shadow glow drew a ring around the composer the moment you clicked in. */
.tl-cmp-ta:focus{border:0;box-shadow:none;outline:none}
/* Always visible — the composer is a real box, not a one-line field that unfolds on focus. */
.tl-cmp-tools{display:flex;align-items:center;flex-wrap:wrap;gap:10px;margin-top:10px}   /* wrap, so a very narrow phone drops Post to its own line instead of overflowing */
/* Phone: the tool row appears when the box is engaged (.cmp-open — focus, text, or a restored
 * draft) and folds away otherwise, returning its height to the posts. Desktop never collapses. */
@media(max-width:700px){
  .tl-cmp:not(.cmp-open) .tl-cmp-tools{display:none}
  /* The COMPACT row, for real: the inline grow() height is cleared by the collapse handler (an
   * inline style beats all of this), the box drops to one line, the avatar shrinks, and the row's
   * padding halves — ~130px of header becomes ~56px of "How was your weekend?". */
  .tl-cmp:not(.cmp-open) .tl-cmp-ta{min-height:32px;height:32px;font-size:15px;padding:6px 0}
  .tl-cmp:not(.cmp-open){padding:6px 16px}
  /* No avatar while idle — it is decoration, the placeholder already says what the box is, and on
   * a phone every vertical pixel here is a post below the fold. Focus brings it back. */
  .tl-cmp:not(.cmp-open) .tl-cmp-av{display:none}
}
/* Borderless icon buttons (📎 / ＋): the .btn outline boxed each one in and made the row look like a
   toolbar. Bigger hit area too — these were .btn.small, which is a text-button size, not an icon one. */
.tl-cmp-btn,.cmp-ico{display:inline-flex;align-items:center;justify-content:center;width:46px;height:46px;padding:0;
  border:0;border-radius:50%;background:transparent;color:var(--muted);cursor:pointer;
  font-family:inherit;font-size:24px;line-height:1;transition:.15s}
.tl-cmp-btn:hover,.cmp-ico:hover{background:rgba(var(--accent-rgb),.12);color:var(--cyan)}
/* Post (submit) stays the prominent glossy btn-neon CTA — only the NEW-POST trigger (the ＋ FAB) goes
   flat to match the navbar; the button that actually publishes should stand out. */
.tl-cmp-post{margin:0;padding:12px 30px;font-size:17px;font-weight:700;border-radius:24px}
/* (Post's right-alignment lives further down, AFTER `.tl-cmp-tools .btn{margin:0}` — see there.) */
/* Inline panels — the timeline composer never opens the modal, so poll + content-warning live here. */
.tl-cmp-btn.on,.cmp-ico.on{background:rgba(var(--accent-rgb),.18);color:var(--cyan)}
.tl-cmp-cw{margin-top:8px}
.tl-cmp-cw .input{margin:0}
.tl-cmp-poll{margin-top:8px;padding:10px 12px;border:1px solid var(--line);border-radius:12px;background:var(--panel2)}
.tl-cmp-poll .input{margin:6px 0 0}
.tl-cmp-poll .row{margin-top:8px;align-items:center}
/* Composer context: the post you're replying to / quoting. Capped + scrollable so a long original can't
   push the textarea off a phone screen. */
.cmp-ctx{margin:0 0 10px}
.cmp-ctx-lbl{color:var(--muted);font-size:12px;text-transform:uppercase;letter-spacing:1.2px;margin-bottom:6px}
.cmp-ctx .quoted{max-height:230px;overflow-y:auto;margin:0}
/* The original's media. .media-row's default 300px would eat the whole preview, so cap it — but it MUST
   be visible: replying to a photo with no image shown tells you nothing about what you're answering. */
.cmp-ctx-media{margin-top:8px}
.cmp-ctx-media img,.cmp-ctx-media video{max-height:120px;max-width:100%;width:auto;border-radius:10px}
@media(max-width:820px){ .cmp-ctx .quoted{max-height:190px} .cmp-ctx-media img,.cmp-ctx-media video{max-height:96px} }
/* AI composer: 📎 and 🎤 were .mini (a 28px chrome button) next to a full-size ▶ send, so they read as
   an afterthought. Match them to the send button's height. */
.ai-compose .mini{width:44px;height:44px;padding:0;font-size:20px;border-radius:50%;display:inline-flex;align-items:center;justify-content:center;flex:0 0 auto}
/* Send is a circle, not a pill: at 52x42 with a triangle in it, it read as a stray oval next to two
   round buttons. Square it up and it becomes the fourth item in an evenly spaced row. */
.ai-compose #ai-send{flex:0 0 auto;width:44px;height:44px;min-width:0;padding:0;font-size:20px;border-radius:50%;
  display:inline-flex;align-items:center;justify-content:center}
@media(max-width:820px){
  .ai-compose{gap:6px}
  .ai-compose .mini,.ai-compose #ai-send{width:40px;height:40px;font-size:19px}
  .ai-compose .input{min-height:40px;padding:9px 13px;border-radius:20px}
}
/* Go-live cover preview — confirms the thumbnail Discover will show before you announce. */
.gl-img-prev{display:block;width:100%;max-height:150px;object-fit:cover;border-radius:12px;margin:6px 0 2px;border:1px solid var(--line)}
.tl-cmp-status{flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.tl-cmp-tools .btn{margin:0}

/* The notes scroll directly under this bar, so it MUST be fully opaque: var(--bg) is a solid colour in
   every theme, unlike the translucent var(--panel) used by .glass — and mobile drops backdrop-filter
   entirely, so a blur-based frost would degrade to see-through exactly where it matters.
   top:-8px + the taller top padding swallow .feed's own 8px padding strip, which otherwise left a sliver
   of moving notes visible ABOVE the bar as you scrolled. */
.tl-tabs{display:flex;position:sticky;top:-8px;z-index:4;
  margin:12px 0 0;padding:8px 0 0;
  background:var(--canvas);background-attachment:fixed;border-bottom:1px solid var(--line);
  box-shadow:var(--sticky-sh)}
/* Plain text tabs with an underline indicator (ditto-style) — NOT pill buttons: two big filled pills
   read as a toolbar, not as "which timeline am I looking at". */
.tltab{flex:1;position:relative;padding:14px 8px;border:0;background:none;
  color:var(--muted);cursor:pointer;font-family:inherit;font-size:15px;font-weight:600;
  letter-spacing:.2px;white-space:nowrap;transition:color .15s}
.tltab:hover{color:var(--text)}
.tltab.on{color:var(--neon)}
.tltab.on::after{content:'';position:absolute;inset-inline-start:50%;transform:translateX(-50%);bottom:-1px;
  width:min(72px,64%);height:3px;border-radius:3px 3px 0 0;
  background:linear-gradient(90deg,var(--neon),var(--neon2));box-shadow:0 0 10px var(--neon)}
.tl-cmp-tools .btn{white-space:nowrap}
/* Post sits at the RIGHT end of the tools row — including when the row WRAPS it onto a line of its own
   on a narrow phone, where it was landing directly under the 📎 icon.
   This has lost the cascade to `.tl-cmp-tools .btn{margin:0}` above TWICE now: a bare
   `.tl-cmp-post{margin-inline-start:auto}` is (0,1,0) and loses on specificity, and `.tl-cmp-tools
   .tl-cmp-post` is (0,2,0) — a TIE, decided by source order, and the reset comes later. The button is
   class="btn btn-neon tl-cmp-post", so match both classes for (0,3,0) AND sit after the reset: it now
   wins on specificity and on order. */
.tl-cmp-tools .btn.tl-cmp-post{margin-inline-start:auto}
/* ▦ media-grid toggle, right-aligned in the tab row (it moved out of the topbar). */
.tltab-media{flex:0 0 auto;margin-inline-start:auto;padding:10px 12px;border:0;border-radius:11px;
  background:transparent;color:var(--muted);cursor:pointer;font-family:inherit;font-size:15px;line-height:1;transition:.15s}
.tltab-media:hover{background:rgba(var(--accent-rgb),.12);color:var(--cyan)}
.tltab-media.on{background:linear-gradient(135deg,var(--neon),var(--neon2));color:#06121a;border-color:transparent}
/* ===== Trending tab: the Hot / From-follows source toggle + the per-card heat line ===== */
/* Chips, NOT a full-width segmented strip: directly under .tl-tabs, a second edge-to-edge bar of two
   buttons reads as a rival tab row. These name which RANKING you're reading, so they sit inline, left,
   on the same x as every note's avatar. */
.tr-bar{display:flex;gap:8px;align-items:center;padding:12px 20px 2px}
.tr-tab{padding:8px 14px;border:1px solid var(--line);border-radius:999px;background:var(--panel);color:var(--muted);
  font-family:inherit;font-size:13px;font-weight:600;line-height:1.45;cursor:pointer;transition:.15s;white-space:nowrap}
.tr-tab:hover{color:var(--text);border-color:var(--neon)}
.tr-tab.on{background:rgba(var(--accent-rgb),.16);color:var(--text);border-color:rgba(var(--accent-rgb),.55)}
/* Why this post is here — the ranking is the only thing Trending adds over a timeline, so it has to be
   on the row itself (same idea as the "🔁 reposted" line above a repost). Aligned with .note's padding
   so it reads as that card's first line, not as a separator between cards. */
.tr-heat{padding:10px 20px 0;color:var(--amber);font-size:12px;font-weight:700;letter-spacing:.2px}
.tr-item>.note,.tr-item>.reply-pair>.note{padding-top:6px}
/* On Home/Nostrverse the topbar had nothing left in it on desktop — title hidden (the tabs name the
   timeline), search lives in the sidebar, compose is the inline box, ▦ moved into the tabs. An empty
   sticky bar above the composer is just a floating rectangle, so drop it. Mobile KEEPS the topbar: its
   search field is the only one at that width. */
@media(min-width:821px){ body.tl-view .topbar{display:none} }
@media(max-width:820px){
  .tl-cmp-ta{font-size:17px}   /* keep >=16px or iOS Safari zooms the page on focus */
  /* Five icons + Post on ONE row at 360px (the common phone width). Enlarging the buttons earlier pushed
     this to ~294px against 279px available, which silently wrapped Post onto a second row. */
  .tl-cmp-tools{gap:6px}
  .tl-cmp-btn{width:36px;height:36px;font-size:20px}
  .tl-cmp-post{padding:10px 16px;font-size:15px}   /* right-alignment is in the base rule above */
  .tl-tabs{margin-top:10px;padding:16px 10px 10px}
  /* 8px sides, not 12: the row carries THREE labels now (Home / Nostrverse / Trending) plus ▦, and they
     all have to fit a 320px phone on one line — flex:1 + nowrap means overflow, not a graceful wrap. */
  .tltab{padding:8px 8px;font-size:15px}
  .tltab-media{padding:8px 10px;font-size:15px}
  /* Trending: match .note's mobile 12px gutter so the chips and each heat line sit on the card's x. */
  .tr-bar{padding:10px 12px 2px;gap:8px}
  .tr-tab{padding:6px 12px;font-size:13px}
  .tr-heat{padding:10px 12px 0;font-size:12px}
}
/* Tiny phones: the status line is the only expendable thing in the tools row — dropping it keeps
   📎 / ⤢ More / Post at a tappable size instead of letting them all squeeze. */
@media(max-width:380px){ .tl-cmp-status{display:none} }
.search-section-title{padding:14px 20px 6px;color:var(--muted);font-size:13px;text-transform:uppercase;letter-spacing:1.5px}
/* Section headers carry a leading icon. They are plain blocks, and `gap` does NOTHING on a non-flex
   box — an icon dropped in one sits flush against the first letter (the same trap the icon checker
   guards for on buttons). One shared rule so every header surface inherits the same rhythm. */
.search-section-title:has(> svg.ic),.fxs-sec:has(> svg.ic),.set-title:has(> svg.ic){
  display:flex;align-items:center;gap:var(--sp-2)}
/* the header icon tracks the header's own colour and optical size, never the 20px default */
.search-section-title > svg.ic,.fxs-sec > svg.ic{width:1.15em;height:1.15em;flex:none;opacity:.9}
.set-title > svg.ic{width:.9em;height:.9em;flex:none}
.psearch{display:flex;gap:12px;align-items:center;padding:12px 20px;border-bottom:1px solid var(--line);cursor:pointer}
.psearch:hover{background:rgba(var(--accent-rgb),.08)}
.psearch img{width:42px;height:42px;border-radius:50%;border:1px solid var(--line);flex:none}
.psearch .pinfo{flex:1;min-width:0;overflow:hidden}
.psearch .pinfo b{display:block;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.psearch .pfollow{flex:none;white-space:nowrap}
.follows-you{display:inline-block;margin-inline-start:8px;padding:1px 8px;border-radius:10px;font-size:11px;font-weight:600;vertical-align:middle;color:var(--green);border:1px solid rgba(0,255,136,.45);background:rgba(0,255,136,.1)}
.rep-row{cursor:pointer}
/* Same underline treatment as the timeline's .tl-tabs. Six tabs don't get flex:1 (that squeezes the
   labels); they size to content and the bar scrolls horizontally when it runs out of room.
   top:-8px + the taller top padding swallow .feed's 8px padding strip — without it a sliver of scrolling
   notifications stayed visible ABOVE the sticky bar, same bug the timeline tabs had. */
.notif-tabs{display:flex;gap:2px;position:sticky;top:-8px;z-index:4;
  padding:16px 10px 0;background:var(--canvas);background-attachment:fixed;border-bottom:1px solid var(--line);
  box-shadow:var(--sticky-sh);
  overflow-x:auto;-webkit-overflow-scrolling:touch;scrollbar-width:none}
.notif-tabs::-webkit-scrollbar{display:none}
.ntab{position:relative;flex:0 0 auto;padding:12px 14px;border:0;background:none;
  color:var(--muted);cursor:pointer;font-family:inherit;font-size:15px;font-weight:600;
  letter-spacing:.2px;white-space:nowrap;transition:color .15s}
.ntab:hover{color:var(--text)}
.ntab.on{color:var(--neon)}
.ntab.on::after{content:'';position:absolute;inset-inline-start:11px;inset-inline-end:11px;bottom:-1px;height:3px;
  border-radius:3px 3px 0 0;background:linear-gradient(90deg,var(--neon),var(--neon2));box-shadow:0 0 10px var(--neon)}
@media(max-width:820px){ .ntab{padding:10px 12px;font-size:15px} .ntab.on::after{inset-inline-start:9px;inset-inline-end:9px} }
.zap-presets{display:flex;gap:8px;flex-wrap:wrap;margin-top:10px}
.zap-amt{flex:1 1 28%;padding:10px 6px;border:1px solid var(--line);border-radius:12px;background:var(--panel);color:var(--amber);font-weight:700;cursor:pointer;font-family:inherit;font-size:15px}
.zap-amt:hover{border-color:var(--amber);box-shadow:0 0 12px rgba(255,207,43,.3)}
.pics-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(155px,1fr));gap:6px;padding:8px 8px 90px}
.pic-card{aspect-ratio:1/1;overflow:hidden;border-radius:10px;border:1px solid var(--line);background:#0d0a1a;cursor:pointer}
.pic-card img{width:100%;height:100%;object-fit:cover;display:block;transition:.15s}
.pic-card:hover img{transform:scale(1.05)}
.pic-card.cw{position:relative}
.pic-card.cw img{filter:blur(22px)}
.pic-card.cw .pic-cw{position:absolute;inset:0;display:flex;align-items:center;justify-content:center;font-size:24px;pointer-events:none}
@media(max-width:820px){ .pics-grid{grid-template-columns:1fr 1fr;gap:6px} }
.report-detail{padding:4px 2px;max-height:70vh;overflow:auto}
.report-detail .rd-row{margin:6px 0;display:flex;gap:8px;align-items:center;flex-wrap:wrap}
.report-detail .lnk{cursor:pointer;color:var(--cyan)}
.rep-type{display:inline-block;padding:1px 10px;border-radius:10px;font-size:12px;font-weight:600;color:#ff9b6b;border:1px solid rgba(255,107,107,.5);background:rgba(255,107,107,.1);text-transform:uppercase;letter-spacing:.3px}
.rd-reason{margin:8px 0;padding:10px 12px;border-inline-start:3px solid var(--neon2);background:rgba(var(--accent2-rgb),.06);border-radius:0 8px 8px 0;line-height:1.45;white-space:pre-wrap;overflow-wrap:anywhere}
@media(max-width:820px){.searchbox{max-width:none}}

/* ---- News (built-in RSS reader) ---- */
.news-badge{margin-inline-start:auto;min-width:18px;height:18px;padding:0 6px;border-radius:9px;background:var(--neon2);color:#0a0512;font-size:11px;font-weight:800;line-height:18px;text-align:center;box-shadow:0 0 8px rgba(var(--accent2-rgb),.5)}
.more-item{position:relative}
.more-item .news-badge{position:absolute;top:8px;inset-inline-end:8px;margin:0}
.news-wrap{max-width:none;margin:0 auto;padding:0 6px 40px}
/* top:-8px + the matching padding swallow .feed's 8px top padding — with top:0 a sliver of the
   scrolling list stayed visible ABOVE the bar. Same fix as .tl-tabs / .notif-tabs. */
#news-head{position:sticky;top:-8px;padding-top:8px;z-index:3;background:var(--bg)}
/* Feed strip scrolls; the action buttons sit OUTSIDE it so they never get pushed off the edge by a
   long feed list (70 feeds made ✓✓ and ＋ unreachable on desktop). */
/* One row, fixed height — the header is sticky, so it must never grow with the feed count (a wrapped
   chip wall left no room to scroll the articles). The active feed is a single control that opens the
   picker sheet; only ✓✓ and ＋ sit beside it. */
.news-bar{display:flex;align-items:center;gap:8px;padding:10px 2px}
.news-pick{flex:1 1 auto;min-width:0;display:flex;align-items:center;gap:8px;background:var(--panel2);
  border:1px solid var(--line);color:var(--text);border-radius:999px;padding:8px 14px;font-size:15px;
  font-weight:600;cursor:pointer;text-align:start;transition:.15s}
.news-pick:hover{border-color:var(--cyan)}
.news-picklbl{flex:1 1 auto;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.news-pickcar{flex:0 0 auto;color:var(--muted);font-size:12px}
/* picker sheet rows */
.news-flist{display:flex;flex-direction:column;gap:6px;max-height:min(52vh,420px);overflow-y:auto;margin-top:10px}
.news-frow{display:flex;align-items:baseline;gap:10px;width:100%;background:var(--panel2);
  border:1px solid var(--line);color:var(--text);border-radius:12px;padding:12px 14px;font-size:15px;
  cursor:pointer;text-align:start;transition:.15s}
.news-frow:hover{border-color:var(--cyan)}
.news-frow.on{border-color:var(--cyan);color:var(--cyan)}
.news-fname{flex:1 1 auto;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-weight:600}
.news-fsub{flex:0 0 auto;max-width:45%;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.news-fq{margin-top:10px}
/* NOT .news-acts — that class already belongs to each article card's Share/Summarize row, and
   restyling it as a pinned flex item wrecked every card. */
.news-baracts{flex:0 0 auto;display:flex;gap:6px}
.news-chip{flex:0 0 auto;background:var(--panel2);border:1px solid var(--line);color:var(--muted);border-radius:999px;padding:6px 14px;font-size:15px;cursor:pointer;white-space:nowrap;transition:.15s}
.news-chip:hover{color:var(--text)}
.news-chip.on{background:linear-gradient(135deg,rgba(var(--accent-rgb),.22),rgba(var(--accent2-rgb),.14));color:#fff;border-color:var(--neon)}
.news-chip.news-add{font-weight:800}
.news-card{display:flex;gap:12px;padding:12px;border:1px solid var(--line);border-radius:12px;margin:0 0 10px;background:var(--panel);transition:opacity .2s,border-color .15s}
.news-card:hover{border-color:var(--neon)}
.news-card.read{opacity:.48}
.news-thumb{width:120px;height:82px;object-fit:cover;border-radius:8px;flex:0 0 auto;background:var(--panel2)}
.news-body{min-width:0;flex:1}
.news-title{display:block;font-weight:700;font-size:17px;line-height:1.45;color:var(--text);text-decoration:none}
.news-title:hover{color:var(--neon)}
.news-meta{font-size:12px;color:var(--muted);margin:4px 0}
.news-snip{font-size:15px;color:var(--muted);line-height:1.45;margin:4px 0;display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden}
.news-acts{display:flex;gap:8px;margin-top:6px;flex-wrap:wrap}
.news-modal-bg{position:fixed;inset:0;background:rgba(0,0,0,.62);z-index:400;display:flex;align-items:center;justify-content:center;padding:16px}
.news-modal{position:relative;background:var(--panel);border:1px solid var(--line);border-radius:14px;max-width:560px;width:100%;max-height:85dvh;overflow:auto;padding:20px;box-shadow:var(--glow)}
.news-modal-x{position:absolute;top:8px;inset-inline-end:12px;background:none;border:none;color:var(--muted);font-size:20px;cursor:pointer}
.news-summary{line-height:1.6;font-size:15px;color:var(--text)}
.news-mlist{max-height:210px;overflow:auto;margin:8px 0}
.news-mrow{display:flex;justify-content:space-between;align-items:center;gap:8px;padding:8px 0;border-bottom:1px solid var(--line)}
.news-msep{margin:16px 0 8px;font-size:13px;color:var(--muted);border-top:1px solid var(--line);padding-top:12px}
@media(max-width:820px){ .news-thumb{width:100px;height:72px} .news-title{font-size:15px} .news-card{padding:10px} }
@media(max-width:480px){ .news-card{flex-direction:column} .news-thumb{width:100%;height:170px} }

/* Web Search (SearXNG front end + reader + AI overview).
   Everything here wraps rather than scrolls sideways: a result carries a raw URL and a page title
   from an arbitrary site, which is the classic source of a phone-width horizontal scrollbar, so
   overflow-wrap is on every long-string element and the filter chips wrap onto a second row instead
   of becoming a strip that hides half its options off-screen. */
.ws-wrap{max-width:none;margin:0 auto;padding:0 6px 40px}
/* top:-8px + the matching top padding swallow .feed's own 8px padding strip. With top:0 a sliver of
   the scrolling RESULTS stayed visible ABOVE the search bar as you scrolled — the same bug .tl-tabs,
   .notif-tabs and #news-head each had. --canvas (not --panel) because it is opaque in every theme,
   and mobile drops backdrop-filter, so a frosted bar would degrade to see-through exactly here. */
.ws-bar{position:sticky;top:-8px;z-index:5;background:var(--canvas);background-attachment:fixed;
  padding:14px 0 10px;margin:0 0 6px;box-shadow:var(--sticky-sh)}
.ws-form{display:flex;align-items:center;gap:8px;position:relative}
.ws-ic{position:absolute;inset-inline-start:12px;width:18px;height:18px;color:var(--muted);pointer-events:none}
/* 16px minimum: anything smaller and iOS Safari zooms the page on focus and never zooms back. */
.ws-input{flex:1 1 auto;min-width:0;font-size:16px;padding:12px 12px 12px 38px;border-radius:999px}
.ws-go{flex:0 0 auto;padding:10px 16px;min-height:40px}
.ws-filters{display:flex;align-items:center;gap:8px;margin-top:8px;flex-wrap:wrap}
.ws-chips{display:contents}   /* the chips wrap in the SAME flow as the time filter, so the filter
                                    row is two tight lines on a phone instead of three */
.ws-chip{background:var(--panel2);border:1px solid var(--line);color:var(--muted);border-radius:999px;
  padding:7px 14px;font-size:14px;cursor:pointer;white-space:nowrap;transition:.15s;min-height:34px}
.ws-chip:hover{color:var(--text)}
.ws-chip.on{background:linear-gradient(135deg,rgba(var(--accent-rgb),.22),rgba(var(--accent2-rgb),.14));color:#fff;border-color:var(--neon)}
/* 16px, like the query box: a <select> under 16px triggers the same iOS focus-zoom, and this one
   sits next to the field people are typing in. */
.ws-time{flex:0 0 auto;width:auto;font-size:16px;padding:6px 10px;min-height:36px}
.ws-results{min-height:40vh}
/* SOFTER CORNERS AND A LITTLE DEPTH.
   The results were square-ish cards with a hairline border — legible, and flat next to the rest of
   this client, which rounds everything (chips and the search field are already pills). Radii go up to
   14/16px so a card reads as a surface rather than a box; the answer card keeps its neon left edge,
   which is what marks it as the AI overview, and gains a matching radius on that side only so the
   accent does not fight the rounding. The hover is a lift, not just a border colour — a whole card is
   clickable, so it should look like an object you can pick up. */
.ws-answer{border:1px solid var(--line);border-inline-start:3px solid var(--neon);border-radius:6px 16px 16px 6px;
  background:linear-gradient(135deg,rgba(var(--accent-rgb),.07),transparent 60%), var(--panel);
  padding:14px 16px;margin:0 0 12px;font-size:15px;line-height:1.5;color:var(--text);
  overflow-wrap:anywhere;box-shadow:0 2px 10px rgba(0,0,0,.22)}
.ws-card{display:flex;gap:12px;padding:14px;border:1px solid var(--line);border-radius:16px;margin:0 0 12px;
  background:var(--panel);cursor:pointer;box-shadow:0 1px 4px rgba(0,0,0,.18);
  transition:border-color .15s,box-shadow .15s,transform .15s}
.ws-card:hover{border-color:var(--neon);box-shadow:0 6px 18px rgba(0,0,0,.3);transform:translateY(-1px)}
.ws-card:active{transform:translateY(0)}
@media(prefers-reduced-motion:reduce){ .ws-card{transition:border-color .15s} .ws-card:hover{transform:none} }
.ws-thumb{width:112px;height:78px;object-fit:cover;border-radius:12px;flex:0 0 auto;background:var(--panel2)}
.ws-body{min-width:0;flex:1 1 auto}
.ws-src{font-size:12px;color:var(--muted);margin-bottom:2px;overflow-wrap:anywhere}
.ws-title{display:block;font-weight:700;font-size:17px;line-height:1.4;color:var(--text);text-decoration:none;overflow-wrap:anywhere}
.ws-title:hover{color:var(--neon)}
.ws-snip{font-size:15px;color:var(--muted);line-height:1.45;margin:4px 0;overflow-wrap:anywhere;
  display:-webkit-box;-webkit-line-clamp:3;-webkit-box-orient:vertical;overflow:hidden}
.ws-acts{display:flex;gap:8px;margin-top:8px;flex-wrap:wrap}
.ws-err{border:1px solid var(--line);border-inline-start:3px solid var(--danger);border-radius:6px 16px 16px 6px;
  background:var(--panel);padding:14px 16px;margin:0 0 12px;color:var(--text);font-size:15px;
  overflow-wrap:anywhere}
.ws-more{display:block;width:100%;margin:12px 0 0;min-height:44px}
.ws-sugg{display:flex;gap:6px;flex-wrap:wrap;align-items:center;margin:14px 0 0}
/* images */
.ws-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(150px,1fr));gap:8px}
.ws-img{position:relative;display:block;border:1px solid var(--line);border-radius:14px;overflow:hidden;
  background:var(--panel2);transition:border-color .15s,box-shadow .15s}
.ws-img:hover{border-color:var(--neon);box-shadow:0 4px 14px rgba(0,0,0,.28)}
.ws-imga{display:block}
.ws-img img{display:block;width:100%;aspect-ratio:4/3;object-fit:cover}
/* ⧉ = the page this picture is on, as opposed to the picture itself. Always visible on touch (there
   is no hover to reveal it) and a real thumb target. */
.ws-imgpg{position:absolute;top:6px;inset-inline-end:6px;width:32px;height:32px;border-radius:8px;cursor:pointer;
  border:1px solid var(--line);background:rgba(0,0,0,.55);color:#fff;font-size:15px;line-height:1}
.ws-imgpg:hover{border-color:var(--neon);color:var(--neon)}
.ws-imglbl{position:absolute;inset-inline-start:0;inset-inline-end:0;bottom:0;padding:4px 6px;font-size:11px;color:#fff;
  background:linear-gradient(transparent,rgba(0,0,0,.72));white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
/* AI overview */
.ws-ov{border:1px solid var(--line);border-radius:12px;background:var(--panel);padding:14px;margin:0 0 12px;
  box-shadow:0 0 0 1px rgba(var(--accent-rgb),.10) inset}
.ws-ov-cta{display:flex;align-items:center;gap:10px;flex-wrap:wrap}
.ws-ovhd{font-weight:800;font-size:15px;color:var(--neon);margin-bottom:8px}
.ws-ovtext{font-size:15px;line-height:1.6;color:var(--text);overflow-wrap:anywhere}
.ws-cite{color:var(--neon);text-decoration:none;font-weight:700;padding:0 1px}
.ws-srcs{margin:12px 0 0;padding-inline-start:20px;font-size:14px;color:var(--muted)}
.ws-srcs li{margin:4px 0;overflow-wrap:anywhere}
.ws-srcs li.hit{background:rgba(var(--accent-rgb),.14);border-radius:6px}
.ws-srcs a{color:var(--text);text-decoration:none}
.ws-srcs a:hover{color:var(--neon)}
.ws-srchost{color:var(--muted);font-size:12px;margin-inline-start:6px}
/* reader */
.ws-reader{max-width:760px;margin:0 auto}
/* Page mode is the whole width and the whole height: it is showing a web page, not an article. */
.ws-reader-page{max-width:none;display:flex;flex-direction:column;min-height:0}
.ws-rbar{position:sticky;top:-8px;z-index:5;display:flex;gap:8px;justify-content:space-between;align-items:center;
  background:var(--canvas);background-attachment:fixed;padding:14px 0 8px;box-shadow:var(--sticky-sh)}
.ws-rbar .ws-rsrc{flex:1 1 auto;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;text-align:center}
.ws-rbar-acts{flex:0 0 auto;display:flex;gap:6px}
.ws-back{min-height:36px}
/* The framed page. 100% of a fixed height rather than flex:1, because #feed is the scroller: the
   frame has to be a definite size or it collapses to 150px (the iframe default) and the page inside
   it scrolls in a letterbox. dvh, never vh — vh on a phone is the address-bar-expanded height, so the
   bottom of the page would sit permanently under the browser chrome. The subtractions are the bar +
   the action row; on mobile also the fixed .mobilenav and the safe area.

   DIVIDE BY --zf. Desktop scales the whole page with body{zoom:.67-.77}, and viewport units are NOT
   rescaled by zoom — that is why every other full-height container here is height:calc(100dvh/.77).
   A raw 100dvh inside a 0.67 body paints at 0.67 of the height it asked for, in an .app that is
   1/0.67 taller than the screen: the page came up as a small box floating in a large empty column,
   worse the smaller the laptop. --zf is unset on mobile, where the fallback of 1 is correct. */
.ws-frame{width:100%;height:calc((100dvh / var(--zf, 1)) - 190px);min-height:340px;
  border:1px solid var(--line);border-radius:12px;background:#fff;display:block}

.ws-rtitle{font-size:22px;line-height:1.3;margin:6px 0 2px;color:var(--text);overflow-wrap:anywhere}
.ws-rsrc{font-size:12px;color:var(--muted);overflow-wrap:anywhere}
.ws-racts{margin:10px 0 6px}
.ws-rtext{font-size:16px;line-height:1.7;color:var(--text)}
.ws-rtext p{margin:0 0 14px;overflow-wrap:anywhere}
.ws-sumtext{line-height:1.6;font-size:15px;color:var(--text);overflow-wrap:anywhere}
.ws-modacts{display:flex;gap:8px;flex-wrap:wrap;margin-top:14px}
@media(max-width:820px){
  .ws-thumb{width:92px;height:66px}
  .ws-title{font-size:16px}
  .ws-card{padding:10px}
  .ws-rtitle{font-size:19px}
  .ws-grid{grid-template-columns:repeat(auto-fill,minmax(46%,1fr))}
  /* Every action is a thumb target on a phone, and there are four of them per card. */
  .ws-acts .btn{min-height:36px;font-size:13px;padding:7px 11px}
  .ws-chip{font-size:13px;padding:6px 12px;min-height:32px}
  .ws-time{font-size:16px;padding:5px 8px;min-height:32px}   /* 16px: iOS zooms anything smaller */
  .ws-bar{padding-top:10px}
  .ws-snip{-webkit-line-clamp:2}
  /* …and the framed page has to clear the fixed bottom nav (62px) plus the safe area, or its last
     inch is under the nav and un-scrollable-to. */
  .ws-frame{height:calc(100dvh - 250px - env(safe-area-inset-bottom));min-height:300px}
}
@media(max-width:480px){
  .ws-card{flex-direction:column}
  .ws-thumb{width:100%;height:150px}
  .ws-go{padding:10px 12px}
}

/* Calendar (the bundled CalDAV server's own screen).
   A month grid has one hard constraint on a phone: 7 columns must fit 360px without the page
   scrolling sideways, so the cells size from a fraction of the row and the day number is what
   shrinks — never the grid. */
.cal-wrap{max-width:none;margin:0 auto;padding:0 6px 40px}
.cal-bar{position:sticky;top:-8px;z-index:5;background:var(--canvas);background-attachment:fixed;
  padding:14px 0 10px;box-shadow:var(--sticky-sh);display:flex;gap:10px;align-items:center;flex-wrap:wrap}
.cal-nav{display:flex;gap:6px;align-items:center;min-width:0}
.cal-nav .btn{min-height:36px;min-width:36px}
.cal-title{font-weight:800;font-size:17px;color:var(--text);margin-inline-start:6px;white-space:nowrap}
.cal-tools{display:flex;gap:8px;align-items:center;margin-inline-start:auto;flex-wrap:wrap}
.cal-tools .btn{min-height:36px}
.cal-pick{width:auto;font-size:16px;padding:6px 10px;min-height:36px}   /* 16px: iOS zooms below it */
.cal-grid{display:grid;grid-template-columns:repeat(7,1fr);gap:4px;margin:10px 0 14px}
.cal-dow{text-align:center;font-size:12px;color:var(--muted);padding:2px 0}
.cal-day{aspect-ratio:1/1;display:flex;flex-direction:column;align-items:center;justify-content:flex-start;
  gap:4px;padding:6px 2px;border:1px solid var(--line);border-radius:10px;background:var(--panel);
  color:var(--text);cursor:pointer;min-width:0;transition:border-color .15s}
.cal-day:hover{border-color:var(--neon)}
.cal-day.other{opacity:.42}
.cal-day.today .cal-num{background:var(--neon);color:#04121a;border-radius:999px;padding:1px 7px}
.cal-day.sel{outline:2px solid var(--neon);outline-offset:1px}
.cal-num{font-size:14px;font-weight:700;line-height:1.3}
.cal-dots{display:flex;gap:3px;flex-wrap:wrap;justify-content:center;align-items:center}
.cal-dot{width:6px;height:6px;border-radius:999px;display:inline-block;flex:0 0 auto}
.cal-more{font-size:10px;color:var(--muted)}
/* Text previews. Hidden by default and switched on once a cell is wide enough to hold words — see
   the media query below; the dots are the narrow fallback and the two are never both shown. */
.cal-chips{display:none}
@media (min-width:560px){
  .cal-day{aspect-ratio:auto;min-height:96px;align-items:stretch;justify-content:flex-start;
    padding:5px 5px 6px;gap:3px}
  .cal-num{text-align:start;font-size:12.5px;padding-inline-start:2px}
  .cal-day.today .cal-num{align-self:flex-start}
  .cal-dots{display:none}
  .cal-chips{display:flex;flex-direction:column;gap:2px;min-width:0;overflow:hidden}
  .cal-chip{display:block;font-style:normal;font-size:11px;line-height:1.45;text-align:start;
    padding:1px 5px;border-radius:4px;min-width:0;overflow:hidden;text-overflow:ellipsis;
    white-space:nowrap;color:var(--text);
    /* The calendar colour as a tint plus a solid edge: a fully saturated fill at this size makes the
       title unreadable on half the palette. */
    background:color-mix(in srgb, var(--c) 26%, transparent);
    border-inline-start:3px solid var(--c)}
  .cal-chip b{font-weight:600;color:var(--muted)}
  .cal-day .cal-more{text-align:start;padding-inline-start:3px}
}
@media (min-width:900px){ .cal-day{min-height:112px} .cal-chip{font-size:11.5px} }
.cal-day-panel{border-top:1px solid var(--line);padding-top:12px}
.cal-day-hd{font-weight:800;font-size:15px;margin-bottom:10px;color:var(--text)}
.cal-ev{display:flex;gap:10px;align-items:center;padding:10px;border:1px solid var(--line);
  border-radius:12px;background:var(--panel);margin-bottom:8px;cursor:pointer}
.cal-ev:hover{border-color:var(--neon)}
.cal-evbar{width:4px;align-self:stretch;border-radius:999px;flex:0 0 auto}
.cal-evbody{min-width:0;flex:1 1 auto}
.cal-evtitle{font-weight:700;font-size:15px;overflow-wrap:anywhere}
.cal-evmeta{font-size:12px;color:var(--muted);overflow-wrap:anywhere}
.cal-ev .btn{flex:0 0 auto;min-height:34px}
.cal-off{border:1px solid var(--line);border-radius:12px;background:var(--panel);padding:18px;margin-top:12px}
.cal-offhd{font-weight:800;font-size:16px;margin-bottom:6px;color:var(--text)}
/* 16px, on a phone, on every text field in the event editor. Below 16px iOS Safari zooms the page
   the moment the caret lands and never zooms back out — on a form whose whole job is typing. Scoped
   to this form rather than restyling every modal in the app, which would move layouts elsewhere. */
@media(max-width:820px){
  .cal-form .input, .cal-form textarea, .cal-form input[type="date"], .cal-form input[type="time"]{font-size:16px}
}
.cal-times{display:flex;gap:10px}
.cal-times .fld{flex:1 1 0;min-width:0}
.cal-allday{display:flex;align-items:center;gap:8px}
.cal-list{max-height:46vh;overflow:auto}
/* Subscribed calendars. The tag is a fact about the row (this one is a mirror, editing it is
   pointless), so it sits with the name rather than in a column of its own. */
/* "3 waiting to sync" / "offline copy" — a fact about the data, next to the month it belongs to. */
.cal-pending{font-size:10.5px;letter-spacing:.04em;color:var(--amber,#ffcf2b);border:1px solid rgba(255,207,43,.35);
  border-radius:999px;padding:2px 9px;margin-inline-start:10px;white-space:nowrap;align-self:center}
.cal-pending.cached{color:var(--muted);border-color:var(--line)}
.cal-sub-tag{font-size:10px;letter-spacing:.06em;text-transform:uppercase;color:var(--neon);
  border:1px solid var(--line);border-radius:999px;padding:1px 7px;margin-inline-start:6px;vertical-align:1px}
.cal-sub-when{flex:none;font-size:11px;color:var(--muted);white-space:nowrap}
.cal-sub-warn{margin:10px 0 0;padding:10px 12px;border-radius:10px;font-size:12.5px;line-height:1.5;
  border:1px solid rgba(255,207,43,.4);background:rgba(255,207,43,.08);color:var(--text)}
.cal-row{display:flex;gap:8px;align-items:center;padding:8px 0;border-bottom:1px solid var(--line)}
.cal-name{flex:1 1 auto;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-weight:600}
.cal-swatches{display:flex;gap:8px;flex-wrap:wrap;margin-top:6px}
.cal-sw{width:30px;height:30px;border-radius:999px;border:2px solid transparent;cursor:pointer}
.cal-sw.on{border-color:var(--text)}
/* An import of a real calendar is hundreds of signed, encrypted writes — tens of seconds with no
   output. Without this panel the modal simply sits there, which reads as "nothing happened". */
.cal-importing{display:flex;flex-direction:column;align-items:center;gap:10px;text-align:center;
  padding:20px 8px}
/* An imported calendar is usually a history: the month you land on can be genuinely empty while the
   calendar holds hundreds of events. Say so, and offer the jump. */
.cal-hint{display:flex;gap:10px;align-items:center;flex-wrap:wrap;justify-content:space-between;
  border:1px solid var(--line);border-radius:12px;background:var(--panel);padding:10px 12px;
  margin:10px 0 0;font-size:13px;color:var(--muted)}
.cal-hint .btn{flex:0 0 auto}
.cal-repeats{margin-top:2px}

/* ---- Contacts ------------------------------------------------------------------------------- */
.ct-wrap{max-width:none;margin:0 auto;padding:0 6px 40px}
.ct-bar{position:sticky;top:-8px;z-index:5;background:var(--canvas);background-attachment:fixed;
  display:flex;gap:8px;align-items:center;flex-wrap:wrap;padding:10px 2px 8px}
.ct-search{flex:1 1 240px;min-width:0;display:flex;align-items:center;gap:8px;
  border:1px solid var(--line);border-radius:12px;background:var(--panel);padding:0 10px}
.ct-search .input{border:0;background:transparent;flex:1 1 auto;min-width:0}
/* 36px, the same floor the calendar's toolbar uses: `.btn.small` alone lands at 31px, which is under
   the size a thumb reliably hits. */
.ct-tools{display:flex;gap:8px;align-items:center;margin-inline-start:auto}
.ct-tools .btn{min-height:36px}
.ct-pick{width:auto;font-size:16px;padding:6px 10px;min-height:36px}   /* 16px: iOS zooms below it */
.ct-count{width:100%}
.ct-listwrap{display:flex;gap:6px;align-items:flex-start}
.ct-list{flex:1 1 auto;min-width:0}
.ct-letter{position:sticky;top:52px;background:var(--canvas);font-weight:800;font-size:12px;
  color:var(--muted);padding:6px 4px 4px;letter-spacing:.08em}
.ct-row{display:flex;gap:10px;align-items:center;width:100%;text-align:start;padding:8px;
  border:1px solid transparent;border-radius:12px;background:transparent;cursor:pointer;color:inherit}
.ct-row:hover{border-color:var(--neon);background:var(--panel)}
.ct-face{width:40px;height:40px;border-radius:999px;flex:0 0 auto;object-fit:cover;
  background:var(--panel2);border:1px solid var(--line)}
.ct-init{display:flex;align-items:center;justify-content:center;font-style:normal;font-weight:800;
  font-size:15px;color:var(--muted)}
.ct-body{min-width:0;flex:1 1 auto;display:flex;flex-direction:column}
.ct-name{font-weight:700;font-size:15px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.ct-sub{font-size:12px;color:var(--muted);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
/* The A–Z rail is the only way to reach the middle of a long book on a phone without flinging. */
.ct-rail{position:sticky;top:56px;display:flex;flex-direction:column;gap:0;flex:0 0 auto;
  padding:2px 0;max-height:calc(100dvh - 140px);overflow:hidden}
.ct-jump{font-size:10px;line-height:1.15;padding:0 4px;background:none;border:0;cursor:pointer;
  color:var(--neon);font-weight:700}
.ct-jump.off{color:var(--muted);opacity:.35;cursor:default}
.ct-empty{border:1px solid var(--line);border-radius:12px;background:var(--panel);padding:22px 16px;
  margin-top:14px;text-align:center}
.ct-emptyhd{font-weight:800;font-size:16px;margin-bottom:6px;color:var(--text)}
.ct-names{display:flex;gap:10px}
.ct-names .fld{flex:1 1 0;min-width:0}
.ct-group{margin:10px 0}
.ct-glabel{font-size:12px;color:var(--muted);font-weight:700;margin-bottom:4px}
.ct-multi{display:flex;gap:6px;align-items:center;margin-bottom:6px}
.ct-mv{flex:2 1 0;min-width:0}
.ct-mt{flex:1 1 0;min-width:0;max-width:110px}
.ct-mdel{flex:0 0 auto;min-height:34px}
.ct-bigface{width:84px;height:84px;border-radius:999px;object-fit:cover;display:block;margin:0 auto 10px;
  border:1px solid var(--line)}
/* 16px on a phone, for the reason the event editor does it: below 16px iOS zooms on focus and never
   zooms back, on a form that is nothing but text fields. */
@media(max-width:820px){
  .ct-form .input, .ct-form textarea{font-size:16px}
  .ct-search .input{font-size:16px}
  .ct-tools{width:100%;margin-inline-start:0}
  .ct-mt{max-width:88px}
}
.cal-pw{display:flex;gap:8px;align-items:center;margin-top:8px;flex-wrap:wrap}
.cal-pw code{font-size:15px;background:var(--panel2);border:1px solid var(--line);border-radius:8px;
  padding:8px 10px;overflow-wrap:anywhere}
@media(max-width:820px){
  .cal-title{font-size:15px;margin-inline-start:2px}
  .cal-tools{width:100%;margin-inline-start:0}
  .cal-pick{flex:1 1 auto;min-width:0}
  .cal-num{font-size:13px}
  .cal-grid{gap:3px}
  .cal-day{padding:4px 1px;border-radius:8px}
}

/* Markets (crypto digest) */
.mkts-wrap{max-width:none;margin:0 auto;padding:8px 6px 40px}
.mkts-top{font-weight:800;font-size:15px;color:var(--text);padding:6px 2px 12px}
.mkts-note{color:var(--muted);padding:14px 4px;font-size:15px}
.mkts-muted{color:var(--muted)}
.mkts-card{border:1px solid var(--line);border-radius:12px;margin:0 0 12px;background:var(--panel);padding:14px}
.mkts-card:hover{border-color:var(--neon)}
.mkts-head{display:flex;align-items:center;gap:10px}
.mkts-sym{font-weight:800;font-size:15px;color:var(--neon);background:var(--panel2);border:1px solid var(--line);border-radius:8px;padding:2px 10px;letter-spacing:.5px}
.mkts-name{font-weight:700;font-size:17px;color:var(--text);min-width:0;flex:1;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.mkts-post{flex:0 0 auto;padding:6px 14px;font-size:13px;border-radius:999px}
/* Live price + 24h move, next to the ticker. flex:0 0 auto so .mkts-name keeps absorbing the slack and the
   Share button stays pinned right. Green/red are fixed (not theme vars): up/down must read the same in
   every theme, and --green/--danger drift per theme. */
.mkts-px{flex:0 0 auto;font-weight:800;font-size:15px;color:var(--text);font-variant-numeric:tabular-nums}
.mkts-chg{flex:0 0 auto;font-weight:700;font-size:12px;font-variant-numeric:tabular-nums}
.mkts-chg.up{color:#2fbf71} .mkts-chg.down{color:#ff5a7a}
.mkts-sum{line-height:1.6;font-size:15px;color:var(--text);margin:10px 0 0;white-space:pre-wrap;overflow-wrap:anywhere}
.mkts-arts{display:flex;flex-direction:column;gap:6px;margin-top:10px;border-top:1px solid var(--line);padding-top:10px}
.mkts-art{font-size:13px;color:var(--muted);text-decoration:none;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.mkts-art:hover{color:var(--neon)}
@media(max-width:480px){ .mkts-card{padding:12px} .mkts-name{font-size:15px} .mkts-head{flex-wrap:wrap} }
/* overflow-anchor:none — the live-prepend (_prependLive) manages scrollTop itself; the browser's own
   scroll-anchoring double-corrects on every prepend, which is what made the timeline "bounce"/look unstable
   at the top. overscroll-behavior:contain kills the rubber-band overscroll + scroll-chaining at the top. */
.feed{flex:1;min-height:0;overflow-y:auto;overflow-anchor:none;overscroll-behavior-y:contain;padding:8px 0 90px;scrollbar-width:none}
/* Pull-to-refresh indicator (mobile) — a fixed pill near the top that follows the drag; .ready when
   past the trigger, .spin while the refresh runs. Centring is via inset-inline-start:50% + translateX(-50%); the JS
   composes translateY/rotate onto the same transform as you pull. */
.ptr-ind{position:fixed;top:calc(48px + env(safe-area-inset-top));inset-inline-start:50%;transform:translateX(-50%);width:36px;height:36px;border-radius:50%;display:flex;align-items:center;justify-content:center;font-size:20px;line-height:1;background:var(--panel2);border:1px solid var(--line);color:var(--cyan);opacity:0;pointer-events:none;z-index:40;box-shadow:0 2px 12px rgba(0,0,0,.35)}
.ptr-ind.ready{color:#04222a;background:linear-gradient(135deg,#0bb6cf,var(--cyan));border-color:transparent}
.ptr-ind.spin{animation:ptr-spin .6s linear infinite}
@keyframes ptr-spin{to{transform:translateX(-50%) translateY(8px) rotate(360deg)}}
.feed.feed-dm,.feed.feed-ai,.feed.feed-translate,.feed.feed-texts{padding:0;overflow:hidden}
/* NIP-30 custom emoji rendered inline in note text, names and notifications (sized to the line). */
.emoji-inline{height:1.4em;width:auto;max-width:none;vertical-align:-0.28em;display:inline-block;margin:0 1px}
/* public read-only (guest) mode: no write affordances (the CTA is the guest card up top) */
body.guest #btn-compose,body.guest #btn-compose-m{display:none}
/* ?embed=1 — strip all app chrome so a screenshot/preview shows just the note, instance-branded.
   .app is a GRID with fixed sidebar columns; hiding the asides isn't enough — collapse the grid to a
   single full-width column AND let main/feed grow to content height (they're normally 100vh + clipped),
   so the full-page capture gets the whole note instead of a tiny offset sliver. */
body.embed .app{display:block !important;height:auto !important}
body.embed .sidebar,body.embed .rightbar,body.embed .topbar,body.embed .mobilenav{display:none !important}
body.embed .main{margin:0 !important;width:100% !important;height:auto !important;overflow:visible !important}
body.embed .feed{max-width:680px;margin:0 auto;padding:14px;height:auto !important;overflow:visible !important}
.act.actq .q-ico{width:22px;height:22px}   /* quote: no idle colour — uniform --muted like every .act / .nav-item */
.act .rp-ico{width:20px;height:20px}
/* No visible scrollbar on the timeline: a 14px magenta bar down the side of the feed read as chrome
   from a different app. Scrolling itself is untouched — only the painted track/thumb is gone
   (scrollbar-width:none on the rule above covers Firefox). */
.feed::-webkit-scrollbar{width:0;height:0}
/* The classic desktop WebUI is a mouse-sized reading surface too.  The OS window had this override,
   but the same Social feed outside PosterChan OS still inherited the global zero-width scrollbar,
   leaving desktop Firefox with neither position feedback nor a draggable fallback.  Keep phones
   edge-to-edge; expose the themed bar only when the viewport and primary pointer are desktop-like. */
@media (min-width:821px) and (hover:hover) and (pointer:fine){
  body:not(.os-on) .app > .main > .feed{scrollbar-width:thin;scrollbar-color:var(--line) transparent}
  body:not(.os-on) .app > .main > .feed::-webkit-scrollbar{width:10px;height:10px}
  body:not(.os-on) .app > .main > .feed::-webkit-scrollbar-track{background:transparent}
  body:not(.os-on) .app > .main > .feed::-webkit-scrollbar-thumb{background:var(--line);border-radius:8px;
    border:2px solid transparent;background-clip:padding-box}
  body:not(.os-on) .app > .main > .feed::-webkit-scrollbar-thumb:hover{background:var(--muted);background-clip:padding-box}
}

/* ===== note card ===== */
.note{display:flex;gap:12px;padding:16px 20px;border-bottom:1px solid var(--line);transition:background .15s;animation:fade .25s;cursor:pointer}
.note .txt{cursor:text}
@keyframes fade{from{opacity:0;transform:translateY(6px)}to{opacity:1}}
.note:hover{background:rgba(var(--accent-rgb),.06)}
.note .av{width:52px;height:52px;border-radius:50%;object-fit:cover;flex-shrink:0;border:1px solid var(--line);background:#1a1230;cursor:pointer}
.note .body{flex:1;min-width:0}
/* EVERY CARD'S HEADER IS ONE LINE. It used to wrap, and a wrapped header is a different card: name
   on top, handle and time dropped onto a second row 43px tall instead of 19px. Measured on one real
   timeline at 390px, 11 of 63 posts drew the tall version — the same anatomy in two shapes, decided
   by how long somebody's display name happens to be ("posts on the timeline need uniformity").
   So: never wrap, and let the two flexible parts give way instead. The name truncates first (it is
   the longest and the least load-bearing once the avatar is beside it), the handle after it, and the
   time never shrinks — it is three characters and it is the one thing every card must line up. */
.note .hd{display:flex;gap:8px;align-items:baseline;flex-wrap:nowrap;min-width:0}
.note .name{font-weight:700;cursor:pointer;font-size:17px;min-width:0;flex:0 1 auto;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
/* THE HANDLE DOES NOT SHRINK; IT IS CAPPED. Two earlier rules were measured and rejected against
   planted worst cases, and both failed the same way — by leaving a stub nobody can read:
     shrink 2 (give way faster than the name)  -> `Jay Blue Ribbon, Spiritual …` beside `s…`
     shrink 1 (give way in proportion)         -> `@17mugz59` crushed to 29px beside an npub name,
                                                  because proportional means "in proportion to how
                                                  long you already are", and a 63-character name
                                                  next to a 9-character handle is not a fair fight.
   So the overflow goes entirely to the NAME, which is the field that has room to lose some, and the
   handle is bounded by a CEILING instead: 45% of the row, which never touches a handle that fits and
   holds back only one long enough to crowd the name out (`bitcoinl…` beside
   `bitcoinlimit@verified-nost…` — the identity truncated so the address could be spelled in full).
   Worst case the row needs 45% + the timestamp + two gaps, so it cannot overflow. */
.note .handle{color:var(--muted);font-size:15px;min-width:0;max-width:45%;flex:0 0 auto;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.note .time{color:var(--muted);font-size:17px;margin-inline-start:auto;cursor:pointer;flex:none;white-space:nowrap}
.note .time:hover{color:var(--cyan)}
.note .pin-badge{font-size:15px;filter:drop-shadow(0 0 5px rgba(0,240,255,.6))}   /* marks YOUR pinned posts */
.note .txt{margin:6px 0;line-height:1.6;white-space:pre-wrap;word-wrap:break-word;overflow-wrap:anywhere;font-size:20px;
  color:#eceefb;font-weight:500;letter-spacing:.2px;
  /* Zalgo guard: stacked combining marks overflow the line box vertically and, without this, bleed OUT
   * of the note to corrupt the posts above/below it in the feed. Clip paint to the text box (a small
   * margin keeps legit descenders/emoji) so a zalgo bomb stays inside its own note. */
  overflow:clip;overflow-clip-margin:.5em}
/* Wall-of-text guard: clamp long posts (noteCard adds .clamp) and fade the bottom; the "Show more"
 * button toggles .clamp off to reveal the rest. Keeps the feed scannable when people post essays. */
.note .txt.clamp{max-height:15em;overflow:hidden;-webkit-mask-image:linear-gradient(180deg,#000 72%,transparent);mask-image:linear-gradient(180deg,#000 72%,transparent)}
.note .txt-more{display:inline-block;margin:2px 0 4px;padding:4px 14px;background:rgba(var(--accent-rgb),.07);border:1px solid var(--cyan);
  color:var(--cyan);border-radius:13px;cursor:pointer;font-family:inherit;font-size:13px;font-weight:600;letter-spacing:.3px}
.note .txt-more:hover{background:rgba(var(--accent-rgb),.14);box-shadow:var(--glow-cy)}
/* NIP-36 content warning: blur the body + media behind a reveal button until tapped */
.cw-wrap{position:relative;margin:4px 0}
.cw-inner{transition:filter .15s}
.cw-wrap.cw-on .cw-inner{filter:blur(26px);pointer-events:none;user-select:none;min-height:64px;max-height:260px;overflow:hidden}
.cw-reveal{display:none}
/* the overlay sits over blurred content, so it uses a fixed dark scrim + light text — readable on
   every theme (a theme --text would go dark-on-dark over the scrim on light themes) */
.cw-wrap.cw-on>.cw-reveal{position:absolute;inset:0;z-index:2;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:8px;
  background:rgba(8,5,18,.55);border:1px dashed rgba(255,255,255,.3);border-radius:14px;color:#f4f4ff;font-weight:700;text-align:center;padding:12px;cursor:pointer}
.cw-show{padding:6px 16px;border:1px solid #fff;border-radius:20px;color:#fff;font-size:13px;font-weight:700}
.cw-wrap.cw-on>.cw-reveal:hover .cw-show{background:rgba(255,255,255,.16)}
/* composer 🔞 toggle + reason row */
#cmp-cw-btn.on{border-color:var(--neon2);color:var(--neon2);box-shadow:0 0 0 1px var(--neon2) inset}
.cmp-cw-row{margin:6px 0 2px}
.cmp-cw-row.hidden{display:none}
/* ALL post-content media flows horizontally (inline-block) and is height-capped, so multiple
 * attachments form a wrapping gallery row instead of a giant vertical stack. */
/* ===== Layout-stable feed media =================================================================
   Media in a note carries its aspect RATIO and natural WIDTH as inline custom properties (--arn / --nw,
   emitted by app.js _dimAttrs from NIP-92 imeta or a remembered measurement), and each context declares
   its own height cap as --mh. The shared rule below turns those into

       width: min(100%, natural, cap x ratio)

   which is resolvable with NO image loaded — so the browser reserves the final box in the very first
   layout pass. And because that width can never make height exceed the cap, the cap is never reached by
   clamping, so the ratio is never violated.

   What this replaced: `width:auto` + `max-height`, which reserves NOTHING. A note laid out with a 2px-tall
   image and then jumped to full height on decode, shoving everything below it down — 100-700px of shift
   per image, on every image, at every breakpoint. That was most of why the timeline looked like a jumpy
   mess, and worst on a phone, where the short viewport means nearly every image is loading="lazy" and so
   pops in one at a time as you scroll. Measured in tests/client/test_media_reserve.py.

   Two deliberate safety properties, both load-bearing:
   1. NO HINT → the var() is invalid, `width` falls back to `auto`, and the element renders exactly as it
      did before. Every surface not converted here keeps working untouched.
   2. NO --mh in some context → `calc(var(--mh) * var(--arn))` is invalid, so again width falls back to
      `auto`. A cap I missed degrades to the old behaviour rather than to a stretched image.
   max-height is kept alongside as a backstop, with object-fit:contain so that if one ever does clamp, the
   image letterboxes instead of distorting. Do not remove either.
   ================================================================================================ */
.media-row img,.media-row video,
.mc-item img,.mc-item video,
.note .txt img:not(.emoji-inline),.note .txt video,.note .media img,.note .media video,
.bubble img:not(.emoji-inline),.bubble video,
.quoted .txt img:not(.emoji-inline),.quoted .txt video{
  aspect-ratio:var(--arn);
  width:min(100%, calc(var(--nw) * 1px), calc(var(--mh) * var(--arn)));
  height:auto; object-fit:contain;
}
/* `:not(.emoji-inline)` on every `img` above and below: these are ATTACHMENT rules, and a NIP-30 custom
   emoji is an <img> that lives INSIDE .txt too. At (0,2,1) they outspecified the (0,1,0) `.emoji-inline`
   sizing, so a :shortcode: in a name or an @mention rendered at its natural size — a 300px-tall image
   dropped mid-sentence — with a media border and an 8px margin around it. Same specificity trap as the
   `.notif img` avatar rule below; excluding the class keeps it from coming back with the next media rule. */
.note .txt img:not(.emoji-inline),.note .txt video,.note .media img,.note .media video{--mh:300px;height:auto;max-width:100%;max-height:300px;border-radius:12px;margin:8px 6px 0 0;border:1px solid var(--line);display:inline-block;vertical-align:top;background:#0d0a1a}
/* media gallery (pulled out of the text so newlines can't stack them) — capped height */
/* ===== multi-attachment carousel (2+ media) =====
   Horizontal scroll-snap does the paging, so touch swipe / trackpad / shift-wheel all work natively and
   the JS only has to move scrollLeft for the arrows and mirror the index into the dots + counter. */
.media-car{position:relative;margin-top:8px;border-radius:12px;overflow:hidden;
  border:1px solid var(--line);background:#0d0a1a}
.mc-track{display:flex;overflow-x:auto;scroll-snap-type:x mandatory;scroll-behavior:smooth;
  scrollbar-width:none;-webkit-overflow-scrolling:touch}
.mc-track::-webkit-scrollbar{display:none}
.mc-item{flex:0 0 100%;min-width:100%;scroll-snap-align:center;display:flex;align-items:center;justify-content:center}
.mc-item img,.mc-item video{--mh:420px;max-height:420px;max-width:100%;height:auto;display:block;cursor:pointer;
  border:0;border-radius:0;background:transparent}
.mc-nav{position:absolute;top:50%;transform:translateY(-50%);z-index:2;
  width:34px;height:34px;padding:0;border-radius:50%;border:1px solid var(--line);
  background:rgba(0,0,0,.55);color:#fff;font-size:20px;line-height:1;cursor:pointer;
  display:flex;align-items:center;justify-content:center;opacity:0;transition:opacity .15s}
.media-car:hover .mc-nav{opacity:1}
.mc-nav:hover{background:rgba(0,0,0,.8);border-color:var(--cyan)}
.mc-prev{inset-inline-start:8px} .mc-next{inset-inline-end:8px}
.mc-nav[disabled]{opacity:0!important;pointer-events:none}   /* at either end there's nowhere to go */
.mc-count{position:absolute;top:8px;inset-inline-end:8px;z-index:2;padding:2px 10px;border-radius:11px;
  background:rgba(0,0,0,.62);color:#fff;font-size:12px;font-weight:600;letter-spacing:.3px}
.mc-dots{position:absolute;inset-inline-start:0;inset-inline-end:0;bottom:8px;z-index:2;display:flex;gap:6px;justify-content:center}
.mc-dots i{width:6px;height:6px;border-radius:50%;background:rgba(255,255,255,.45);cursor:pointer;transition:.15s}
.mc-dots i.on{background:var(--neon);box-shadow:0 0 6px var(--neon)}
/* A carousel inside a quoted/embedded note is secondary content — keep it from dominating the card. */
.quoted .mc-item img,.quoted .mc-item video{--mh:240px;max-height:240px}
@media(max-width:820px){
  .mc-nav{opacity:1;width:30px;height:30px;font-size:20px}   /* no hover on touch — always show the arrows */
  .mc-item img,.mc-item video{--mh:340px;max-height:340px}
}
.media-row{display:flex;flex-wrap:wrap;gap:6px;margin-top:8px}
.media-row img,.media-row video{--mh:300px;max-height:300px;max-width:100%;height:auto;border-radius:12px;border:1px solid var(--line);background:#0d0a1a;cursor:pointer;flex:0 1 auto}
.thread-hl .media-row img,.thread-hl .media-row video{--mh:420px;max-height:420px}
/* A LONE image gets far more room than the 300px thumbnail cap. That cap is sized for a row of several
   attachments; applied to a single image it was the binding constraint on how big anything could ever
   appear — a text card was scaled down until its type was ~8px. With one attachment there is nothing to
   sit beside it, so let it take the column: width is bound by max-width:100%, height follows the aspect.
   Multi-image rows and carousels keep the compact size. */
.media-row img:only-child,.media-row video:only-child{--mh:720px;max-height:720px}
/* profile Media tab — square thumbnail gallery (media only, no post chrome) */
.media-grid{display:grid;grid-template-columns:repeat(3,1fr);gap:4px;padding:4px}
/* height:auto is LOAD-BEARING. _dimAttrs puts width=/height= ATTRIBUTES on every media element to
   reserve its box before the bytes land, and Chrome maps those to presentation-attribute width/height.
   This rule overrode the width one with 100% but left the HEIGHT attribute standing — so the box was
   100% wide by the source image's pixel height, aspect-ratio:1/1 was ignored (it only applies when a
   dimension is auto), and a 720x960 phone photo rendered as a 298x691 tower. That is the "super long
   rectangles" in the profile Media tab. */
.media-grid img,.media-grid video{width:100%;height:auto;aspect-ratio:1/1;object-fit:cover;border-radius:8px;border:1px solid var(--line);background:#0d0a1a;cursor:pointer;display:block}
.note .txt audio{width:100%;max-width:420px;margin-top:8px;display:block}
.note .txt a.mention{color:var(--cyan);font-weight:600}
.note .repost-tag{font-size:15px;color:var(--green);margin-bottom:4px;display:flex;align-items:center;gap:6px;min-width:0}
/* The reposter's name is a `.name[data-prof]` so decorateProfiles can fill it in when the kind-0
   lands (it read "someone reposted" for ever before). It truncates rather than wrapping, for the
   same reason the header above does not wrap. */
.note .repost-tag .name{font-weight:600;color:inherit;cursor:pointer;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
/* WHEN it was reposted — the repost's own timestamp, beside the original's on the card below. */
.note .repost-tag .rt-when{margin-inline-start:auto;color:var(--muted);font-size:13px;flex:none;white-space:nowrap}
/* repost/boost glyph (SVG, themed via currentColor) — replaces the off-theme orange 🔁 emoji */
/* The five note-action icons are sprite <use> now, not hand-drawn fill art, so they inherit
   svg.ic (20px, fill:none, stroke:currentColor) and match every other icon on the card — they used to
   be visibly heavier than their neighbours. These rules keep only what is genuinely per-icon: the sizes
   the old constants set inline, which svg.ic would otherwise flatten to a uniform 20px. */
/* The five status glyphs (WoT / online / on relay / live / in call) are sized ONLY by their container:
   `.usercount .x-ico` for the sidebar, `.more-stats .x-ico` for the mobile More sheet.
   There used to be bare `.wot-ico{width:13px}`-style defaults here as well. They never applied to
   anything: `svg.ic{width:20px;height:20px;fill:none;stroke:currentColor}` is (0,1,1) and a lone class
   is (0,1,0), so svg.ic won every time. That was invisible while both containers listed all five —
   until the More sheet listed only three, and WoT + online quietly rendered at 20px beside three 13px
   glyphs, which is what knocked that row's baselines out of line. Size them per container, or not at all. */
.rt-ico{vertical-align:-3px;flex:none}
.react-ico,.zap-ico{flex:none}
.repost-tag .rt-ico{filter:drop-shadow(0 0 5px rgba(57,255,139,.45))}
.act.rt .rt-ico{transition:filter .12s;width:22px;height:22px}   /* flat: no drop-shadow glow on hover/active */
.quoted{border:1px solid var(--line);border-radius:12px;padding:10px 12px;margin-top:8px;background:rgba(8,5,18,.4)}
/* ---- webxdc mini apps ------------------------------------------------------------------------
   A CARTRIDGE, not an auto-running frame: an app is code somebody else wrote, and it starts when the
   reader presses Play. Everything here is theme tokens — a card in the timeline has to sit on nine
   different palettes, which the desktop widgets learned the hard way. */
.xdc-card{display:flex;align-items:center;gap:11px;margin-top:8px;padding:10px 12px;max-width:520px;
  border:1px solid var(--line);border-radius:12px;background:var(--panel2);cursor:pointer;
  transition:border-color .15s,transform .12s}
.xdc-card:hover{border-color:var(--neon);transform:translateY(-1px)}
.xdc-ico{flex:none;width:38px;height:38px;border-radius:10px;display:flex;align-items:center;
  justify-content:center;background:rgba(var(--accent-rgb),.14)}
.xdc-ico .ic{width:20px;height:20px;color:var(--neon)}
.xdc-meta{flex:1;min-width:0;display:flex;flex-direction:column;gap:1px}
.xdc-meta b{font-size:13.5px;color:var(--text);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.xdc-meta span{font-size:11.5px;color:var(--muted);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.xdc-play{flex:none}
/* The way out of an app that has saved itself into a state it cannot start from. Muted and beside
   Play rather than hidden in a menu — it is only ever wanted when something is already wrong. */
.xdc-reset{flex:none;opacity:.75}
.xdc-reset:hover{opacity:1}
/* ---- Games → Webxdc: the directory --------------------------------------------------------------
   A gallery, not a list, because these apps ship real cover art (Ditto's kind-1063 carries an
   `image` tag) and a wall of covers is the difference between "some files" and "things to play".
   The cover box has a FIXED aspect-ratio and the tile a fixed row template, so a grid of 30 tiles
   settles at its final size in the first paint — art arrives lazily and nothing below it moves,
   which is the same rule the timeline follows. */
.xdc-gal-top{display:flex;align-items:flex-start;gap:14px;padding:16px 18px 4px}
.xdc-gal-top .muted{flex:1;min-width:0;line-height:1.45}
.xdc-gal-top .btn{flex:none}
.xdc-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(190px,1fr));gap:16px;padding:14px 18px 24px}
.xdc-tile{display:flex;flex-direction:column;background:var(--panel2);border:1px solid var(--line);
  border-radius:16px;overflow:hidden;cursor:pointer;transition:border-color .15s,transform .12s;animation:fade .25s}
.xdc-tile:hover{border-color:var(--neon);transform:translateY(-2px)}
.xdc-cover{aspect-ratio:1/1;background:#0b0716;display:flex;align-items:center;justify-content:center;overflow:hidden}
.xdc-cover img{width:100%;height:100%;object-fit:cover;display:block}
/* No art: the glyph, big and dimmed, so a coverless tile reads as "a mini app" and not as an image
   that failed to load. */
.xdc-cover-none{background:radial-gradient(circle at 50% 40%,rgba(var(--accent-rgb),.20),transparent 70%)}
.xdc-cover-none .ic{width:44%;height:44%;color:var(--neon);opacity:.55}
.xdc-tmeta{padding:10px 12px 4px;display:flex;flex-direction:column;gap:2px;min-width:0}
.xdc-tmeta b{font-size:14px;color:var(--text);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.xdc-tmeta span{font-size:11.5px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
/* The byline carries `.name` so decorateProfiles fills it — which also brings that class's timeline
   styling with it. Put back the muted byline weight it is meant to have inside a tile. */
.xdc-tmeta .name{font-size:inherit;font-weight:500;color:var(--muted)}
.xdc-tfoot{color:var(--neon);opacity:.85}
.xdc-tacts{display:flex;gap:8px;padding:8px 12px 12px;margin-top:auto}
.xdc-tacts .btn{flex:1;min-width:0}
@media(max-width:820px){
  .xdc-grid{gap:10px;padding:10px 12px 20px}
  .xdc-gal-top{padding:12px 12px 2px;flex-direction:column;gap:8px}
  .xdc-gal-top .btn{align-self:flex-start}
  .xdc-tmeta{padding:8px 10px 2px}
  .xdc-tacts{padding:6px 10px 10px;gap:6px}
}
/* Two up on a PHONE only. `minmax(190px,1fr)` alone gives ONE column at 390px, which turns nine
   games into nine full-width banners nobody scrolls to the end of — so it is forced here.
   Measured, and this is why the breakpoint is 600 and not the 820 the rest of the mobile rules use:
   forcing two columns at 820 gave 391px covers, i.e. two enormous tiles on a tablet that has room
   for four. Above 600px auto-fill already does the right thing at every width. */
@media(max-width:600px){
  .concord-hub{margin:0 10px 12px;padding:12px;align-items:flex-start;flex-wrap:wrap}.concord-hub .concord-copy{width:calc(100% - 58px)}.concord-hub .btn{width:100%;min-height:44px}.concord-actions{position:sticky;bottom:0;background:var(--panel);padding-top:10px}.concord-actions .btn{flex:1;min-height:46px}
  .xdc-grid{grid-template-columns:1fr 1fr}
}
/* The app itself. It fills whatever it is given — a desktop window's body, or a full-screen sheet on
   a phone — and the frame is the only thing in it, because everything else is the app's own chrome. */
.xdc-host{position:relative;width:100%;height:100%;min-height:220px;background:#000}
.xdc-frame{position:absolute;inset:0;width:100%;height:100%;border:0;display:block;background:#000;cursor:auto}
/* A game fills its window. The slot is styled for a folder's icons — padded, and deliberately
   translucent so the desktop reads through it — and both are wrong for a viewport: the padding
   letterboxes the canvas and the opacity makes a dark game look broken. */
.osw-slot.xdc-slot{opacity:1;padding:0;overflow:hidden;display:block}
.xdc-sheet{position:fixed;inset:0;z-index:1200;display:flex;flex-direction:column;background:var(--bg)}
.xdc-bar{flex:none;display:flex;align-items:center;gap:10px;padding:8px 10px;
  padding-top:calc(8px + env(safe-area-inset-top));border-bottom:1px solid var(--line);
  background:var(--panel2)}
.xdc-name{flex:1;min-width:0;font-size:13px;font-weight:600;color:var(--text);
  overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.xdc-x{flex:none;width:30px;height:30px;border-radius:50%;border:1px solid var(--line);
  background:transparent;color:var(--muted);cursor:pointer;font-size:12px}
.xdc-x:hover{color:var(--neon);border-color:var(--neon)}
.xdc-body{flex:1;min-height:0;position:relative}

.link-card{display:block;border:1px solid var(--line);border-radius:12px;overflow:hidden;margin-top:8px;background:rgba(8,5,18,.45);cursor:pointer;max-width:520px;transition:.15s}
.link-card:hover{border-color:var(--cyan)}
.link-card:empty{display:none}
/* aspect-ratio, NOT max-height: the preview image's real shape is unknown until it decodes, so a
   max-height box grew from 0 to up to 260px underneath whatever the user was reading. A fixed 16:9
   banner is reserved the instant the card exists and never moves again — and cover-cropping an OG
   image is what it is designed for (they are banners, not photographs). */
.link-card .lc-img{width:100%;aspect-ratio:16/9;height:auto;object-fit:cover;display:block;border:0;border-radius:0;margin:0;background:rgba(0,0,0,.25)}
.link-card .lc-body{padding:10px 12px}
.link-card .lc-site{color:var(--cyan);font-size:12px;text-transform:uppercase;letter-spacing:.5px}
.link-card .lc-title{font-weight:700;font-size:15px;margin:2px 0;line-height:1.45}
.link-card .lc-desc{color:var(--muted);font-size:15px;line-height:1.45}
.quoted .hd{font-size:15px;display:flex;align-items:center;gap:6px;flex-wrap:wrap}
.quoted .qav{width:22px;height:22px;border-radius:50%;object-fit:cover;flex:none;border:1px solid var(--line)}
.quoted .hd .name{font-weight:700;color:var(--text)}
.quoted .hd .handle,.quoted .hd .time{color:var(--muted);font-size:13px}
.quoted .txt{font-size:17px;line-height:1.45;white-space:pre-wrap;overflow-wrap:anywhere;overflow:clip;overflow-clip-margin:.5em}   /* was missing both: newlines collapsed and a long npub/URL blew out the card; overflow:clip = zalgo guard (see .note .txt) */
.quoted .txt img:not(.emoji-inline),.quoted .txt video{--mh:280px;max-width:100%;max-height:280px;border-radius:10px;margin-top:6px;display:block;border:1px solid var(--line)}
.quoted .txt audio{width:100%;max-width:340px;margin-top:6px}
.modal .quoted{max-height:46vh;overflow:auto}
.cmp-parent{cursor:pointer;transition:border-color .15s ease,background .15s ease}
.cmp-parent:hover,.cmp-parent:focus-visible{border-color:var(--neon);background:rgba(var(--accent-rgb),.06);outline:none}
/* profile Replies tab: show the parent post as "↩ replying to" context above each reply */
.reply-pair{border-bottom:1px solid var(--line)}
.reply-pair>.note{border-bottom:none}
.reply-ctx{padding:14px 20px 0 20px}
.reply-ctx-lbl{display:inline-block;font-size:12px;letter-spacing:.6px;text-transform:uppercase;font-weight:700;
  color:var(--neon);text-shadow:0 0 6px rgba(var(--accent-rgb),.45);margin:0 0 4px 2px}
.reply-ctx .quoted{margin-top:0;opacity:.9}
.modal img,.modal video{max-width:100%}
.fld{display:block;margin:12px 0 2px;font-size:13px;color:#c2c4e6;text-transform:uppercase;letter-spacing:.7px;font-weight:600}
.fld .input,.fld textarea{margin-top:4px;text-transform:none;letter-spacing:0}
.note{position:relative}
/* Congrats confetti / gm sunrise (see _playCelebration). Purely decorative: absolutely positioned inside
   the already-relative .note, clipped to the card, and pointer-events:none so it can never block a tap or
   shift a single pixel of layout. The element is removed once the animation is done. */
.celeb{position:absolute;inset:0;overflow:hidden;pointer-events:none;z-index:2}
.celeb i{position:absolute;top:-16px;width:7px;height:12px;border-radius:2px;opacity:0;
  animation-name:celeb-fall;animation-timing-function:cubic-bezier(.25,.6,.5,1);animation-fill-mode:forwards}
@keyframes celeb-fall{
  0%{opacity:0;transform:translateY(0) rotate(0)}
  12%{opacity:1}
  85%{opacity:1}
  100%{opacity:0;transform:translateY(var(--fall,340px)) rotate(var(--rot,360deg))}
}
/* gm: a warm wash over the card with the sun climbing through it */
.celeb-gm{background:linear-gradient(180deg,rgba(255,186,72,.20),rgba(255,186,72,.04) 55%,transparent 75%);
  animation:celeb-glow 2.6s ease-out forwards}
.celeb-sun{position:absolute;inset-inline-start:50%;bottom:-34px;font-size:30px;line-height:1;animation:celeb-rise 2.6s ease-out forwards}
@keyframes celeb-glow{0%{opacity:0}18%{opacity:1}70%{opacity:1}100%{opacity:0}}
@keyframes celeb-rise{0%{transform:translate(-50%,0);opacity:0}22%{opacity:1}75%{opacity:1}100%{transform:translate(-50%,-72px);opacity:0}}
/* sob: 😭 reactions make tears drift UP out of the card and fade. The `i` here is an emoji glyph, not a
   confetti chip, so the shared .celeb i box/animation is overridden wholesale (no width/height/radius,
   its own keyframes) — otherwise it inherits celeb-fall and rains 7x12px rectangles. */
.celeb-sob i{width:auto;height:auto;border-radius:0;background:none;top:auto;bottom:-18px;line-height:1;
  animation-name:celeb-sob;animation-timing-function:ease-out}
@keyframes celeb-sob{
  0%{opacity:0;transform:translate(0,0) scale(.7)}
  15%{opacity:.95;transform:translate(0,calc(var(--rise,340px) * -.12)) scale(1)}
  80%{opacity:.75}
  100%{opacity:0;transform:translate(var(--drift,0),calc(var(--rise,340px) * -1)) scale(1.05)}
}
@media(prefers-reduced-motion:reduce){ .celeb{display:none} }   /* JS skips it too — this is the backstop */
.note::before{content:"";position:absolute;inset-inline-start:0;top:0;bottom:0;width:2px;background:transparent;transition:.15s}
.note:hover::before{background:linear-gradient(var(--neon),var(--neon2));box-shadow:0 0 10px var(--neon)}
/* space-between spread six icons across the ENTIRE card width, so the gap between them was a function
   of how wide the viewport happened to be — 150px apart on desktop. Nothing about that reads as a
   grouped toolbar; it reads as six unrelated things that drifted apart, and it makes the eye travel the
   full card to go from reply to zap. Group them at a fixed rhythm and let bookmark hold the right edge,
   which is the arrangement every reader app converged on for a reason. */
.acts{display:flex;justify-content:flex-start;align-items:center;gap:var(--sp-2);margin-top:var(--sp-3)}
.acts > :last-child{margin-inline-start:auto}
/* the "☰ more" hamburger sits at the bottom-right; the 5 primary actions spread evenly across */
/* Post action buttons ARE left-navbar items — the SAME declarations as .nav-item, not an approximation.
   Idle = uniform var(--muted) (NO per-type amber/green/cyan — uniform colour is what makes a row read as
   the navbar). Hover = .nav-item:hover verbatim. Engaged (.on) = .nav-item.active verbatim + the left
   accent bar. Themed modes fall back to neutral panels, exactly like the navbar's :root[data-theme]
   overrides. Keep this block in lockstep with .nav-item / .nav-item:hover / .nav-item.active above. */
.act.actm{font-size:20px}
.act{display:flex;align-items:center;gap:8px;background:transparent;border:none;color:var(--muted);cursor:pointer;
  font-size:20px;padding:12px 16px;border-radius:11px;transition:.15s;line-height:1;position:relative}   /* geometry + idle colour = .nav-item */
/* hover — verbatim .nav-item:hover */
.act:hover{color:var(--text);background:rgba(var(--accent-rgb),.12);text-shadow:0 0 6px rgba(var(--accent-rgb),.35);transform:none}
/* engaged — verbatim .nav-item.active + the left accent bar */
.act.on{color:#fff;background:linear-gradient(135deg,rgba(var(--accent-rgb),.22),rgba(var(--accent2-rgb),.14));
  box-shadow:inset 0 0 0 1px var(--line);text-shadow:0 0 8px rgba(var(--accent-rgb),.6)}
.act.on::before{content:"";position:absolute;inset-inline-start:0;top:18%;height:64%;width:3px;border-radius:3px;background:var(--neon);box-shadow:var(--glow)}
/* themed modes — neutral panels, exactly like :root[data-theme] .nav-item */
:root[data-theme] .act:hover{background:var(--panel);color:var(--text);text-shadow:none}
:root[data-theme] .act.on{background:var(--panel2);color:var(--text);box-shadow:inset 0 0 0 1px var(--line);text-shadow:none}
:root[data-theme] .act.on::before{box-shadow:none}
.act .react-ic,.act .tipbolt{display:inline-flex;align-items:center;flex:0 0 auto}   /* centre the react/zap glyphs like the direct-child svg icons */
.act .n{font-variant-numeric:tabular-nums;font-size:13px;font-weight:600}

/* compose / modal */
.modal-bg.modal-sub{z-index:200}   /* a modal opened OVER another modal (the Blossom picker) */
.modal-bg{position:fixed;inset:0;background:rgba(4,2,12,.72);backdrop-filter:blur(4px);z-index:100;display:grid;place-items:center;padding:24px 16px;overflow:auto}
.modal{width:min(720px,96vw);max-height:92vh;overflow:auto;border-radius:18px;padding:22px}
/* …and in DYNAMIC viewport units where they exist. On a phone `vh` is the viewport with the browser
   chrome RETRACTED, so 92vh is taller than what you can actually see whenever the URL bar is showing:
   the last section of a long modal sits under the chrome, and because the modal is what scrolls (not
   the page) there is nothing to swipe to reach it. It reads as "the bottom of this dialog does not
   exist". Declared as a second max-height so browsers without dvh keep the vh value. */
.modal{max-height:92dvh}
.modal.cmp-drop{outline:2px dashed var(--neon,#0ff);outline-offset:-8px;background:rgba(0,240,255,.06)}
.modal h3{margin:0 0 12px;letter-spacing:1px;font-size:20px}
/* A SHEET THAT REFUSES THE BACKDROP MUST SHOW ITS OWN WAY OUT. `.modal-sticky` (the composer) does
   not close when the backdrop is clicked, so the ✕ in its title row is the only pointer-driven exit
   on a phone with no hardware Back — it is part of the same change, not decoration. */
.cmp-hd{display:flex;align-items:center;gap:10px}
.modal-x{margin-inline-start:auto;flex:0 0 auto;background:none;border:1px solid transparent;
  color:var(--muted);font-size:24px;line-height:1;width:34px;height:34px;border-radius:10px;
  cursor:pointer;display:inline-flex;align-items:center;justify-content:center}
.modal-x:hover{color:var(--text);border-color:var(--line);background:rgba(255,255,255,.05)}
/* THE FLINCH. A backdrop click that is silently ignored reads as a frozen app — worse than the
   dismissal it replaces — so the sheet says it heard you and stayed. Short, and it never moves the
   box far enough to disturb a cursor in the textarea. */
@keyframes modalNudge{0%,100%{transform:translateX(0)}
  20%{transform:translateX(-5px)}45%{transform:translateX(4px)}70%{transform:translateX(-2px)}}
.modal-nudge{animation:modalNudge .22s ease-in-out 1}
@media (prefers-reduced-motion: reduce){
  .modal-nudge{animation:none;outline:2px solid var(--accent);outline-offset:-2px}
}
.modal textarea{min-height:220px;resize:vertical;font-size:15px}
.cmp-tabs{display:flex;gap:6px;margin-bottom:6px}
/* inline-flex, not the default block: the Preview tab carries a label icon, and the icon-gap rule is a
   flex `gap` — which does nothing on a block box, so the glyph sat flush against the P. Same fix as the
   composer's Draft button. The tabs sit in a flex row either way, so this changes no layout. */
.cmp-tab{display:inline-flex;align-items:center;gap:var(--sp-2);background:transparent;border:1px solid var(--line);color:var(--muted);border-radius:9px 9px 0 0;padding:var(--sp-2) var(--sp-4);cursor:pointer;font-weight:600;font-size:var(--fs-2)}
.cmp-tab.active{color:#fff;border-color:var(--neon);background:rgba(var(--accent-rgb),.12)}
.note-preview{min-height:120px;border:1px solid var(--line);border-radius:11px;padding:12px 14px;margin:6px 0;background:rgba(8,5,18,.5)}
.note-preview .txt{line-height:1.45;white-space:pre-wrap;word-wrap:break-word;overflow-wrap:anywhere}
.note-preview img,.note-preview video{max-width:100%;max-height:400px;border-radius:12px;margin-top:8px;border:1px solid var(--line);display:block}
.note-preview audio{width:100%;max-width:420px;margin-top:8px}
.note-preview a.mention{color:var(--cyan)}
.row{display:flex;gap:8px;align-items:center}
.spacer{flex:1}
/* Composer footer: tools on one wrapping row, then Draft/Schedule left and Post anchored RIGHT — the
   same shape as the inline timeline composer (.tl-cmp-tools) and as every other client.
   It replaces a CENTRED layout that three separate media queries were still trying to rescue, and each
   of those rescues is now deleted rather than re-tuned:
     - `justify-content:center` on both rows left the primary action floating in mid-air with nothing
       to anchor it, and Post reading as just another pill.
     - `.cmp-actions .btn{min-width:120px}` is what pushed Draft and Post onto two lonely centred rows
       on a phone — the "distorted" look the ≤520px block below was written to undo.
     - `.cmp-left > .btn{flex:1 1 calc(50% - 4px)}` (≤820px) stretched whatever button landed alone on
       a line: that is how 🔞 became a 298px-wide pill with one small glyph floating in the middle.
   Buttons size to their own label here and never stretch, at any width. The tools are also all
   LABELLED now — a row that mixed "Attach"/"React" pills with two bare icon pills read as broken. */
.cmp-tools{display:flex;flex-wrap:wrap;align-items:center;gap:var(--sp-2);margin-bottom:var(--sp-2)}
/* The modal composer's tools are the SAME borderless round icons as the timeline composer's, not a
   row of labelled .btn pills. That row was 8 labelled buttons over THREE lines at 360px — the exact
   thing this file already says about the inline row: "the .btn outline boxed each one in and made
   the row look like a toolbar". Two patterns for one control, and the chunky one cost three rows of
   chrome above the text you were writing. Now: four icons + ⋯, one line at every width.
   .cmp-of are the overflow actions. They stay in the DOM and are only HIDDEN, because the .on class
   on Sensitive and Background IS the composer's state — four other places read it — so moving them
   into a menu would mean re-homing that state. ⋯ clicks them instead: same handlers, same state.
   It wraps them in a SPAN rather than hiding each button, because `button:has(> svg.ic.b-ic:
   only-child)` at the top of this file sets display on every icon-only button in the app and its
   :has() argument gives it ~(0,4,2) — a class on the button loses to it outright, and the buttons
   stayed visible. The rule matches `button`, so it cannot reach the wrapper. Same lesson as the
   .rv-acts note above: these global icon rules beat anything selector-shaped. */
.cmp-of{display:none}   /* a SPAN, not the buttons — see below */
.cmp-tools .cmp-ico{font-size:22px}
/* Only Schedule + Post live here — Draft sits with the tools, because it saves rather than submits.
   Two buttons read as a submit bar, so they are CENTRED rather than split left/right; on a phone that
   leaves Draft on the middle tool row and the two submit buttons alone across the bottom. */
.cmp-actions{display:flex;flex-wrap:wrap;align-items:center;justify-content:center;gap:var(--sp-2);margin-top:var(--sp-3)}
.cmp-tools .btn,.cmp-actions .btn{flex:0 0 auto;margin:0;white-space:nowrap;vertical-align:middle}
#cmp-send{min-width:120px;justify-content:center}   /* the primary stays the widest control in its row */

/* ---- The compose sheet is a COLUMN, not one long scroll box ----------------------------------
   `.modal{overflow:auto}` means a composer taller than the viewport loses its BOTTOM — and the
   bottom is Post. A QUOTE POST is the case that breaks it: it stacks a preview of the quoted note
   on top of a composer that already wants 220px of textarea plus two button rows, so on a laptop
   (and on any phone, portrait or landscape) you had to scroll a dialog to reach the button that
   submits it. Nothing about a dialog says "scroll me", so it read as broken.
   Two things were wrong and both are fixed here:
     - the quoted card was sized by `.modal .quoted{max-height:46vh}` (further up this file), which
       beats `.cmp-ctx .quoted{max-height:230px}` on SOURCE ORDER at equal specificity — so the
       documented 230px cap, and the ≤820px 190px one, had never applied inside the composer at
       all. 46vh of a 900px screen is a 414px reference card sitting above an empty text box.
     - the sheet was one scroll box, so any overflow came off the end you need.
   The sheet is now a flex column in which only the two REFERENCE panes give up space: the quoted
   card shrinks first and hardest (it is something you look at, not type in) and scrolls inside its
   own border; the textarea shrinks next, to a floor that is still writable. Title, tabs, tools and
   actions are flex:none and therefore always on screen. Measured by scripts/check_quote_modal.py —
   which fails on the pre-fix stylesheet at every phone size and in a short desktop window. */
.modal.cmp-modal{display:flex;flex-direction:column}
.cmp-modal > *{flex:0 0 auto}
/* min-height:0 is what lets a flex item go below its content height at all; without it the column
   is exactly as tall as its contents and nothing above it shrinks. */
.cmp-modal .cmp-ctx{display:flex;flex-direction:column;flex:0 3 auto;min-height:0}
/* The floor is deliberately at the natural height of a ONE-LINE quote: a min-height binds in
   ordinary layout too, not only under shrink, so a taller one would pad every short quote out with
   dead space — the same complaint from the other direction (check_quote_modal.py asserts it). */
.cmp-modal .cmp-ctx .quoted{flex:0 1 auto;min-height:72px;max-height:230px}
/* `height`, not a flex-basis LENGTH: same 220px preferred size, but the resize grip still works —
   a basis length wins over the inline height a drag writes, and silently freezes it. */
.modal.cmp-modal #cmp{flex:1 1 auto;height:220px;min-height:96px}
.modal.cmp-modal #cmp-preview{flex:1 1 auto;min-height:96px;overflow:auto}
/* A phone in LANDSCAPE, or a short desktop window, has less height than those floors add up to.
   Tighten the fixed chrome rather than let the sheet start scrolling off the bottom again. */
@media (max-height:600px){
  .cmp-modal h3{font-size:17px;margin-bottom:6px}
  .cmp-modal .cmp-ctx{margin-bottom:6px}
  .cmp-modal .cmp-ctx .quoted{min-height:44px;padding:6px 10px}
  .modal.cmp-modal #cmp{height:120px;min-height:64px}
  .modal.cmp-modal #cmp-preview{min-height:64px}
  .cmp-modal .cmp-tools{margin-bottom:4px}
  .cmp-modal .cmp-tools .cmp-ico{width:38px;height:38px;font-size:19px}
  .cmp-modal .cmp-actions{margin-top:6px}
}
/* YouTube inline embed (click the thumbnail facade to load the player) */
/* WIDTH IS A LENGTH, NOT A PERCENTAGE, and the cap is the percentage — the reverse of how these two
   were written. In a container of known width the two spellings are identical (480 capped by the
   container, or the container capped at 480 — same number either way). In a SHRINK-TO-FIT one they
   are not: a percentage cannot be resolved against a width that is still being computed from the
   contents, so it contributes nothing and the box collapses onto whatever else is inside.
   A DM bubble is width:fit-content, and .yt-frame's only child is position:absolute — i.e. nothing
   else is inside — so pressing play on a YouTube link in a DM replaced a correctly-sized facade with
   a frame that resolved to about zero: "video gets big then shrinks to a tiny tiny square". The
   facade survived only because .yt-thumb is a real <img> with an intrinsic size to fall back on. */
.yt-embed{position:relative;display:block;width:480px;max-width:100%;margin-top:8px;cursor:pointer;border-radius:12px;overflow:hidden;border:1px solid var(--line)}
.yt-thumb{display:block;width:100%;aspect-ratio:16/9;object-fit:cover}
.yt-play{position:absolute;inset:0;display:flex;align-items:center;justify-content:center;font-size:40px;color:#fff;background:rgba(0,0,0,.28);transition:background .15s}
.yt-embed:hover .yt-play{background:rgba(0,0,0,.12)}
/* Data-saver YouTube chip: compact tappable label, no external thumbnail fetch. */
.yt-embed.yt-ds{display:inline-block;width:auto;max-width:none;margin-top:8px;padding:6px 12px;color:var(--cyan);font-size:13px;background:rgba(var(--accent-rgb),.06)}
.yt-frame{position:relative;width:480px;max-width:100%;aspect-ratio:16/9;margin-top:8px;border-radius:12px;overflow:hidden}
.yt-frame iframe{position:absolute;inset:0;width:100%;height:100%;border:0}
/* NIP-05 verified blue check */
.vchk{display:inline-flex;vertical-align:middle}
.verified{display:inline-flex;align-items:center;justify-content:center;width:16px;height:16px;margin:0 0 1px 4px;
  border-radius:50%;background:#1d9bf0;color:#fff;font-size:11px;font-weight:900;line-height:1;flex:none}
.pbody h2 .verified{width:20px;height:20px;font-size:12px;margin-inline-start:6px}
/* profile Notes / Replies tabs */
.prof-tabs{display:flex;border-bottom:1px solid var(--line)}
.prof-tab{flex:1;background:transparent;border:none;color:var(--muted);cursor:pointer;padding:14px 0;font-size:15px;
  font-weight:600;letter-spacing:.5px;border-bottom:2px solid transparent;transition:color .15s,border-color .15s}
.prof-tab:hover{color:var(--text)}
.prof-tab.active{color:var(--neon);border-bottom-color:var(--neon)}
/* drafts */
.draft-card{flex-direction:column;gap:10px}
/* clamp the draft preview so a long post / embedded image doesn't fill the page (tap Edit for full) */
.draft-body{white-space:pre-wrap;word-break:break-word;width:100%;font-size:17px;line-height:1.45;max-height:9em;overflow:hidden;-webkit-mask-image:linear-gradient(180deg,#000 74%,transparent);mask-image:linear-gradient(180deg,#000 74%,transparent)}
.draft-body img,.draft-body video{max-height:130px;max-width:100%;width:auto;border-radius:8px;object-fit:cover}
.draft-foot{display:flex;align-items:center;gap:8px;width:100%;flex-wrap:wrap}
/* Keep Edit/Delete/Send together on one line: the buttons are a non-shrinking group, and the "saved …"
   meta shrinks with an ellipsis instead of shoving Send onto a second line on a narrow phone. */
.draft-actions{display:flex;gap:8px;flex:0 0 auto;flex-wrap:nowrap}
.draft-foot .df-meta{min-width:0;flex:0 1 auto;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
/* ⏰ Schedule composer row — wraps cleanly on narrow/mobile/app widths */
/* Left-aligned with the confirm button on the right — the same rhythm as the tools/actions rows above,
   so the panel doesn't switch to centred halfway down the sheet. */
.cmp-sched-row{display:flex;align-items:center;gap:8px;flex-wrap:wrap;margin:8px 0 2px}
.cmp-sched-row.hidden{display:none}
.cmp-sched-row .input{margin:0;max-width:230px;flex:0 1 auto}
.cmp-sched-row .btn{margin:0}
#cmp-sched-go,#tl-cmp-sched-go{margin-inline-start:auto}
.sched-chips{display:flex;gap:6px;flex-wrap:wrap}
.sched-chip{cursor:pointer;border:1px solid var(--line);background:var(--panel2);color:var(--neon);border-radius:999px;padding:4px 12px;font-size:13px;line-height:1}
.sched-chip:hover{background:color-mix(in srgb, var(--neon) 20%, var(--panel2))}
.sched-when{width:100%;margin-top:4px}
#cmp-sched-btn.on{background:color-mix(in srgb, var(--neon) 30%, var(--panel2));border-color:var(--neon)}
/* Scheduled-posts section in the Drafts view */
.sched-head{font-weight:700;color:var(--neon);margin:2px 4px 8px;font-size:15px}
.sched-card{border-inline-start:3px solid var(--neon)}
.sched-card.sched-failed{border-inline-start-color:var(--danger,#ff5a7d)}
.gif-grid{display:grid;grid-template-columns:repeat(3,1fr);gap:6px;max-height:55vh;overflow:auto;margin-top:10px}
.gif-item{width:100%;height:110px;object-fit:cover;border-radius:8px;cursor:pointer;border:1px solid var(--line);background:#120c24}
.gif-item:hover{border-color:var(--neon)}
.emoji-pick{display:flex;gap:6px;flex-wrap:wrap;margin-top:6px}
.emoji-pick button{font-size:20px;background:transparent;border:1px solid var(--line);border-radius:9px;cursor:pointer;padding:4px 8px}
.emoji-pick button:hover{border-color:var(--neon)}

/* dms */
.dm-wrap{display:grid;grid-template-columns:320px 1fr;height:100%}
.dm-list{border-inline-end:1px solid var(--line);overflow-y:auto}
.dm-peer{display:flex;gap:10px;align-items:center;padding:12px 14px;cursor:pointer;border-bottom:1px solid var(--line)}
.dm-peer:hover,.dm-peer.active{background:rgba(var(--accent-rgb),.1)}
.dm-peer img{width:36px;height:36px;border-radius:50%;border:1px solid var(--line)}
.dm-peer b{font-size:20px}.dm-peer .muted.small{font-size:15px}
/* NOTHING HERE STYLES #dm-new's INSIDES, and that is the fix rather than an omission.
   These two rules described markup that no longer exists. "New message" used to be a ROW in the
   peer list, where its .ic sat in the 36px slot the other rows fill with an avatar — so it was
   painted as a solid --cyan disc to keep the column aligned, and its label was a <b> tinted to
   match. It is now a pill BUTTON in the header (`btn btn-neon small dm-newbtn`) with an inline
   `svg.ic.b-ic` and a bare text label: no avatar column to line up with, and no <b> at all.
   The disc survived the move and became a 36px cyan circle inside a 42px themed pill — the one
   element on the button that ignored the theme entirely, which is exactly how it was reported
   ("there is cyan still in the Messages New button on professional", whose accent is blue).
   With the rules gone, `.b-ic` sizes the glyph and it inherits .btn-neon's white — the same
   contract every other primary button in the app is already built on, on every theme. */
.dm-thread{display:flex;flex-direction:column;height:100%;min-height:0}
/* When the feed holds the DM thread, the feed must NOT scroll too — otherwise the unsquished
   (flex:none) bubbles overflow and the inner .dm-msgs never gets to scroll. Make .dm-msgs the
   sole, bounded scroller. (:has supported in current Firefox/Chrome.) */
.feed:has(.dm-wrap){overflow:hidden;padding:0;min-height:0}
.dm-msgs{flex:1;min-height:0;overflow-y:auto;-webkit-overflow-scrolling:touch;padding:14px;display:flex;flex-direction:column;gap:8px}
.bubble{flex:none;width:-moz-fit-content;width:fit-content;max-width:74%;min-width:0;padding:12px 16px;border-radius:14px;font-size:20px;line-height:1.45;white-space:pre-wrap;overflow-wrap:anywhere;word-break:break-word}
.dm-older{flex:none;align-self:center;margin:2px 0 8px;padding:6px 16px;border-radius:12px;background:rgba(0,240,255,.1);border:1px solid var(--line);color:var(--cyan);font-size:13px;cursor:pointer}
.dm-older:hover{background:rgba(0,240,255,.18)}
/* own messages: simple cyberpunk cyan (no magenta gradient) */
/* NOT `.me` — that class belongs to the sidebar identity cluster (display:flex, text-align:center,
   margin:auto auto 0). A bubble carrying it inherited all of that: margin-top:auto soaked up the
   thread's free space (100px gaps between groups on short threads) and the flex/centering pushed the
   timestamp onto its own centred line. `out`/`in` collide with nothing. */
.bubble.out{align-self:flex-end;background:rgba(var(--accent-rgb),.12);border:1px solid var(--neon);color:var(--text);border-end-end-radius:4px;box-shadow:0 0 10px rgba(var(--accent-rgb),.16)}
.bubble.in{align-self:flex-start;background:var(--panel2);border:1px solid var(--line);border-end-start-radius:4px}
/* keep media + long links INSIDE the bubble so they can't overflow / clip on mobile */
/* DM media is in the layout-stable block above (--arn/--nw → a box reserved with nothing loaded), and
   here that is not a nicety, it is the whole bug. A bubble is width:fit-content, so its media's size
   decides the WHOLE bubble's — and with `width:auto` that size came from the video's INTRINSIC
   dimensions, which do not exist until the media loads. In the Android WebView preload is ignored, so
   they arrive the moment you press play: a DM video laid out as a 300x150 default box and then
   re-sized the bubble around the real clip on the first frame ("I click play and it shrinks").
   Portrait clips were also DISTORTED, because the width= presentational attribute _dimLearn writes
   is not overridden here (unlike .media-row/.note .txt, which set an author `width`) — measured, a
   720x1280 clip drew at 270x340. Reserved from the ratio hint instead: same box before and after the
   first frame, and object-fit:contain (inherited from the shared rule) letterboxes rather than squashes.
   `:not(.emoji-inline)` for the reason the shared rule uses it — .bubble img at (0,1,1) outspecified
   .emoji-inline, so a :shortcode: in a DM rendered as a 340px block image mid-sentence. */
.bubble img:not(.emoji-inline),.bubble video,.bubble .m{--mh:340px;max-width:100%;max-height:340px;height:auto;border-radius:10px;display:block;margin-top:4px;
  /* The shared width caps at the media's NATURAL width — right for a feed row, wrong for a bubble that
     shrink-wraps it. `width:auto` on a replaced element means "draw at natural size and never scale up",
     so a low-resolution clip (a transcoded/240p send, a gif-to-mp4) sized the whole bubble to its own
     pixel count: 270px of grey box before the first frame, then a ~130px SQUARE the instant playback
     supplied the real dimensions. A chat bubble should fill its width; only the ceiling is a real cap.
     260px is the floor a small clip is allowed to grow to — beyond it, soft beats tiny.
     NO `100%` TERM, unlike the shared rule: a bubble is width:fit-content, so a percentage in here is
     circular — the browser resolves it by falling back to the media's intrinsic width, which is the very
     thing this is trying to stop, and `min(100%, …)` measured EXACTLY the natural 128px on a 128px clip.
     max-width:100% above is the cap instead, and with aspect-ratio set, clamping the width recomputes
     the height, so the shape survives it. */
  width:min(calc(var(--mh) * var(--arn)), max(calc(var(--nw) * 1px), 260px))}
.bubble img:not(.emoji-inline){cursor:zoom-in}
.bubble a{overflow-wrap:anywhere;word-break:break-all}
.dm-compose{display:flex;flex-direction:column;gap:8px;padding:12px;border-top:1px solid var(--line)}
.dm-compose .input{margin:0;width:100%;font-size:17px;padding:12px 14px;min-height:54px;resize:vertical;font-family:inherit}
/* Attachment preview strip (pasted/attached images) above the DM input — horizontally scrollable so many
   attachments never push the input off-screen; thumbnails are a comfortable tap target on mobile. */
.dm-atts{display:flex;gap:8px;overflow-x:auto;-webkit-overflow-scrolling:touch;padding:2px 0;scrollbar-width:none}
.dm-atts::-webkit-scrollbar{display:none}
.dm-atts[hidden]{display:none}
.dm-att{position:relative;flex:0 0 auto;border:1px solid var(--line);border-radius:9px;overflow:hidden;background:var(--panel2)}
.dm-att img{display:block;width:60px;height:60px;object-fit:cover}
.dm-att.ds{display:inline-flex;align-items:center;gap:6px;padding:8px 10px;color:var(--cyan);font-size:13px}
.dm-att-x{position:absolute;top:2px;inset-inline-end:2px;width:20px;height:20px;border:none;border-radius:50%;background:rgba(0,0,0,.65);color:#fff;font-size:12px;line-height:1;cursor:pointer;display:flex;align-items:center;justify-content:center;padding:0}
.dm-att.ds .dm-att-x{position:static;width:18px;height:18px;background:rgba(255,255,255,.15)}
.dm-tools{display:flex;align-items:center;gap:8px}
.dm-tools .mini{flex-shrink:0;font-size:20px;padding:8px 12px;line-height:1}
.dm-tools .btn{padding:10px 20px;font-size:17px}

/* blossom files */
/* Narrower cells now the art is 96px rather than 140px — an icon view shows MANY files. */
.files-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(126px,1fr));gap:10px;padding:16px}
/* Leftover artifacts from deleted chats: a size + a one-tap clean-up. Wraps on a phone. */
.files-orphans{display:flex;align-items:center;gap:10px;flex-wrap:wrap;margin:14px 18px 0;padding:10px 14px;
  border:1px dashed var(--line);border-radius:12px;font-size:13px;background:rgba(var(--accent2-rgb),.05)}
.files-orphans .btn{margin-inline-start:auto}
@media(max-width:600px){ .files-orphans{margin:12px 10px 0} .files-orphans .btn{margin-inline-start:0;width:100%} }
/* Multi-select toolbar above the drive grid. Sticky so "Delete selected" stays reachable while you
   scroll a long folder picking files — otherwise you select, scroll down, and the buttons are gone. */
.files-selbar{display:flex;align-items:center;gap:8px;flex-wrap:wrap;padding:8px 4px;position:sticky;top:0;z-index:3;background:var(--bg)}
.files-selbar:empty{display:none}
/* NO BOX AROUND A FILE. A bordered, filled card per item is a web design; a file manager draws the
   ICON and the NAME on the desktop background and nothing else. The border and the panel fill were
   the "box around folders and files".
   The border stays as `transparent` rather than being removed: hover and selection colour it, and a
   border that appears on hover shifts every tile in the row by a pixel. Only what is SELECTED or
   under the pointer gets a surface, which is exactly when an OS draws one. */
.file-card{border:1px solid transparent;border-radius:10px;overflow:hidden;background:none;position:relative}
.files-grid:not(.details) .file-card:hover{background:rgba(var(--accent-rgb),.07)}
/* The checkbox sits ON the thumbnail, so it needs its own contrast rather than the page's — a bare
   checkbox over a light photo is invisible. Bigger than default: this is a touch target on mobile. */
/* Holds the checkbox column open on a row that has no checkbox (a folder). Inert everywhere else:
   in the tile view it is an empty inline element with no box of its own. */
.selbox-gap{display:block;width:20px;height:1px}
.files-grid:not(.details) .selbox-gap{display:none}
.file-card .selbox{position:absolute;top:6px;inset-inline-start:6px;z-index:2;width:20px;height:20px;cursor:pointer;
  appearance:none;-webkit-appearance:none;display:grid;place-items:center;padding:0;margin:0;
  border:1px solid rgba(255,255,255,.78);border-radius:4px;color:transparent;font:800 13px/1 system-ui;
  box-shadow:0 0 0 2px rgba(0,0,0,.55);background:rgba(10,12,20,.68)}
/* One checkmark implementation for both sources. Blossom rows use a real checkbox; This Computer
   uses an aria-pressed button because its selection is keyed by a path. Native checkbox artwork
   plus a button's text glyph produced two different, off-centre controls. */
.file-card input.selbox::before{content:'';width:9px;height:5px;border-inline-start:2px solid #071018;
  border-bottom:2px solid #071018;transform:translateY(-1px) rotate(-45deg) scale(0);transform-origin:center}
.file-card input.selbox:checked,.file-card button.selbox[aria-pressed=true]{
  color:#071018;border-color:var(--cyan);background:var(--cyan);box-shadow:0 0 0 2px rgba(0,0,0,.55)}
.file-card input.selbox:checked::before{transform:translateY(-1px) rotate(-45deg) scale(1)}
/* Selection is a FILL plus the border, the way every file manager marks it — a bare glow around a
   transparent tile reads as a hover, not a choice. */
.file-card.selected{border-color:var(--neon);background:rgba(var(--accent-rgb),.16)}
/* While a selection is active the grid is in SELECT mode — a tap picks rather than opens, so show a
   pointer on the whole card instead of the link's "this will open" affordance. */
.files-grid.selmode .file-card a{cursor:pointer}
/* THE SAME BOX a glyph gets — a photograph and a document are one cell tall. */
.file-card img,.file-card video{width:100%;height:96px;object-fit:cover;display:block;border-radius:8px;background:#120c24}
/* THE NAME GETS ITS OWN LINE. Exactly the music-row bug (see .track below): one flex line held the
   filename and a row of flex:none icon buttons, so the NAME absorbed the entire squeeze. Measured on
   a synced folder at every width: a 150px tile, ~110px of buttons, and 10-25px left for the name —
   one character and an ellipsis ("B…"), which is not a file manager.

   So the meta block wraps: the name takes a full row of the tile and up to two lines, the actions sit
   underneath it, right-aligned. `flex-basis:100%` is what forces the break — the buttons cannot fit
   beside a full-width item, so they start line two on their own. */
.file-card .meta{padding:8px;font-size:11px;color:var(--muted);display:flex;flex-wrap:wrap;
  justify-content:space-between;align-items:center;gap:4px 6px}
/* Centred under the icon — a desktop label, not a list row. */
.files-grid:not(.details) .file-card .meta{justify-content:center;text-align:center}
.files-grid:not(.details) .file-card .meta .fc-acts{margin:0 auto}
.file-card .meta .fname{flex:1 1 100%;min-width:0;overflow:hidden;line-height:1.35;
  white-space:normal;overflow-wrap:anywhere;word-break:break-word;
  display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical}
.file-card .meta .fc-acts{flex:none;margin-inline-start:auto;display:flex;align-items:center;gap:2px}
.file-card .meta .dlbtn{background:none;border:none;color:var(--muted);cursor:pointer;font-size:15px;padding:4px 6px;line-height:1}
.file-card .meta .dlbtn:hover{color:var(--cyan,#5ff)}
/* AN ICON VIEW, THE WAY EVERY FILE MANAGER DRAWS ONE.
   UNIFORM — a folder, a document and a photograph occupy the same cell. Mixed heights are the one
   thing no OS does, and doing it here (a short cell for anything with no preview) is what turned a
   tidy grid into a ragged one.
   AND SMALLER. 140px with a 42px glyph is a billboard with a dot in the middle: that is the "wasted
   icon space". 96px with a 56px glyph is the proportion an OS icon has — the art fills its box, and
   a row holds more files. */
.file-icon{display:flex;flex-direction:column;align-items:center;justify-content:center;height:96px;gap:2px;font-size:56px;background:none}
.fx-file-ic{width:58px;height:58px;fill:none;stroke:currentColor;stroke-width:1.55;stroke-linecap:round;stroke-linejoin:round;
  color:var(--cyan,#5ff);filter:drop-shadow(0 0 8px rgba(0,240,255,.22));pointer-events:none}
.fx-file-image{color:#72e6ff}.fx-file-video{color:#c592ff}.fx-file-audio{color:#ff70c8}
.fx-file-pdf{color:#ff657f}.fx-file-archive{color:#ffc65c}.fx-file-document{color:#9db9ff}
.fx-file-folder{color:#ffc65c}.fx-file-file{color:var(--muted)}
.fx-ic .fx-file-ic{width:22px;height:22px;filter:none}
/* NO TYPE CAPTION UNDER THE ICON. No OS prints "PDF" beneath a document; the glyph says what it is
   and the filename carries the extension. It cost a line of every tile and said nothing new. */
.files-grid:not(.details) .file-icon span{display:none}
.file-icon span{font-size:11px;color:var(--muted);text-transform:uppercase;letter-spacing:1px}
.file-card .del{position:absolute;top:6px;inset-inline-end:6px;background:rgba(8,5,18,.7);border:1px solid var(--line);border-radius:8px;color:var(--danger);cursor:pointer;padding:2px 8px}
.file-card .copy{position:absolute;top:6px;inset-inline-end:38px;background:rgba(8,5,18,.7);border:1px solid var(--line);border-radius:8px;color:var(--cyan,#5ff);cursor:pointer;padding:2px 8px;font-size:13px}
.file-card .copy:hover{border-color:var(--cyan,#5ff)}
.file-card.enc{border-color:rgba(255,210,90,.45)}
.file-card.enc .file-icon{background:radial-gradient(ellipse at center,rgba(255,210,90,.10),#120c24)}
/* Encryption is already communicated by the lock glyph. In icon view a gold rim + dark radial
   panel recreates the exact boxed-card UI removed above, and on an encrypted drive that means every
   single file is boxed. Details rows keep their subtle encrypted separator; tiles stay clean. */
.files-grid:not(.details) .file-card.enc{border-color:transparent}
.files-grid:not(.details) .file-card.enc .file-icon{background:none}
.file-card.enc .enc-open{cursor:pointer;display:block}
.nf-opt{display:block;margin:8px 0;padding:10px 12px;border:1px solid var(--line);border-radius:10px;cursor:pointer;font-size:15px;text-transform:none;letter-spacing:0;font-weight:500}
.nf-opt:hover{border-color:var(--cyan)}
.nf-opt input{margin-inline-end:8px}
.uploader{padding:18px;border-bottom:1px solid var(--line)}
/* Files: folder bar + drag-drop upload zone (cyberpunk) */
.folder-bar{display:flex;gap:8px;flex-wrap:wrap;padding:12px 16px 0}
.folder-chip{display:inline-flex;align-items:center;gap:6px;padding:6px 14px;border-radius:13px;border:1px solid var(--line);background:rgba(0,240,255,.06);color:var(--muted);font-size:13px;font-weight:600;cursor:pointer;transition:.12s;white-space:nowrap}
/* A synced folder and the button that forgets its shared record, as one unit — so the ✕ cannot be
   mistaken for part of the chip next to it. `fx-syncx` is deliberately quiet until hovered: removing
   a folder's shared history is not a thing to invite with a bright control. */
.fx-syncwrap{display:inline-flex;align-items:center;gap:2px;margin:0 2px}
.fx-syncx{border:1px solid transparent;background:none;color:var(--muted);opacity:.45;cursor:pointer;
  font-size:12px;line-height:1;padding:6px 7px;border-radius:11px;transition:.12s}
.fx-syncwrap:hover .fx-syncx{opacity:.85}
.fx-syncx:hover{color:var(--danger,#ff5c7a);border-color:var(--danger,#ff5c7a);background:rgba(255,92,122,.10);opacity:1}
.folder-chip:hover{color:var(--text);border-color:var(--cyan);background:rgba(0,240,255,.13)}
.folder-chip.active{color:#04121a;background:linear-gradient(135deg,var(--neon,#0ff),var(--neon2,#0cf));border-color:transparent;text-shadow:none;box-shadow:0 0 12px rgba(0,240,255,.4)}
.folder-chip.newfolder{color:var(--neon2,#0cf);border-style:dashed}
.folder-chip.drop{border-color:#ffd25a;box-shadow:0 0 12px rgba(255,210,90,.5)}
.drop-zone{margin:12px 16px;border:2px dashed rgba(0,240,255,.4);border-radius:16px;background:radial-gradient(ellipse at center,rgba(0,240,255,.05),transparent);padding:18px;text-align:center;transition:.15s}
.drop-zone.over{border-color:var(--neon,#0ff);background:rgba(0,240,255,.12);box-shadow:inset 0 0 26px rgba(0,240,255,.18)}
.dz-inner{display:flex;flex-direction:column;align-items:center;gap:8px;color:var(--muted);font-size:15px}
.dz-ic{font-size:24px;color:var(--cyan);filter:drop-shadow(0 0 7px rgba(0,240,255,.6))}
.up-queue{display:flex;flex-direction:column;gap:4px;margin-top:10px;text-align:start}
.up-queue:empty{display:none}
.up-item{display:flex;justify-content:space-between;gap:10px;font-size:12px;background:rgba(0,240,255,.05);border-radius:8px;padding:4px 10px}
.up-name{color:#cfe9ff;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.up-stat{color:var(--muted);flex:none}.up-stat.ok{color:#5dffb0}.up-stat.err{color:#ff6b8b}
.up-summary{padding:10px 12px;border-radius:9px;background:rgba(0,240,255,.1);color:var(--cyan,#0ff);font-weight:700;font-size:13px;text-align:center;letter-spacing:.3px}
/* persistent floating upload badge (survives view changes; bottom-LEFT, clear of the player) */
#upload-badge.upbadge{position:fixed;inset-inline-start:18px;bottom:88px;z-index:121;display:flex;align-items:center;gap:10px;padding:10px 14px;border-radius:24px;background:linear-gradient(160deg,#150a2c,#0a0418);border:1px solid var(--cyan,#0ff);box-shadow:0 0 22px rgba(0,240,255,.35);color:#eaf0ff;font-size:12px;font-weight:600;max-width:min(320px,90vw);cursor:grab;-webkit-user-select:none;user-select:none}
#upload-badge.upbadge.done{border-color:#5dffb0;box-shadow:0 0 18px rgba(93,255,176,.4)}
#upload-badge .upbadge-ic{font-size:15px;color:var(--cyan,#0ff)}
#upload-badge .upbadge-txt{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
#upload-badge .upbadge-x{cursor:pointer;color:#ff6b8b;font-weight:700;margin-inline-start:2px}
@media(max-width:820px){#upload-badge.upbadge{inset-inline-start:8px;bottom:76px}}
.file-card[draggable]{cursor:grab}.file-card .movebtn{position:absolute;top:6px;inset-inline-end:70px;background:rgba(8,5,18,.7);border:1px solid var(--line);border-radius:8px;color:var(--muted);cursor:pointer;font-size:13px;padding:2px 8px}.file-card .movebtn:hover{color:var(--cyan);border-color:var(--cyan)}
.folder-chip.delfolder{color:var(--danger);border-color:rgba(255,107,139,.5)}.folder-chip.delfolder:hover{background:rgba(255,107,139,.14);border-color:var(--danger)}
/* Music folder: encrypted track list (cyberpunk) */
.music-list{display:flex;flex-direction:column;gap:8px;padding:16px}
.track{display:flex;align-items:center;gap:12px;padding:12px 14px;border-radius:13px;background:linear-gradient(180deg,#160a28,#100420);border:1px solid rgba(0,240,255,.3);transition:.12s}
.track:hover{border-color:var(--cyan);box-shadow:0 0 14px rgba(0,240,255,.18)}
.track-play{flex:none;width:40px;height:40px;border-radius:50%;border:2px solid var(--cyan);background:rgba(0,240,255,.1);color:var(--cyan);font-size:15px;cursor:pointer;display:inline-flex;align-items:center;justify-content:center;transition:.12s}
.track-play:hover{background:var(--cyan);color:#04121a;box-shadow:0 0 14px rgba(0,240,255,.5)}
/* The playing row. `.playing` replaces a textContent assignment that deleted the button's inline
   <svg> — the icon never came back, so a library you had played from was half icons and half bare
   glyphs. Swapping which icon SHOWS keeps both in the DOM and costs one class toggle. */
.track-play .b-ic{display:block}
.track-play::after{content:'⏸';display:none;font-size:13px;line-height:1}
.track-play.playing .b-ic{display:none}
.track-play.playing::after{display:block}
/* The rest of a large library waits behind this. Full width so it reads as the end of the list
   rather than as one more row's worth of controls. */
.mus-more{display:block;width:100%;margin:8px 0 2px;padding:10px;border-radius:10px;
  border:1px dashed var(--line);background:var(--panel2);color:var(--muted);font-size:12.5px;
  cursor:pointer}
.mus-more:hover{border-color:var(--cyan,#0ff);color:var(--text)}
.track-name{flex:1;min-width:0;color:#eaf0ff;font-weight:600;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.track-meta{flex:none;color:var(--muted);font-size:12px}
.track-del{flex:none;background:none;border:none;color:var(--danger);cursor:pointer;font-size:15px;padding:2px 6px;opacity:.7}.track-del:hover{opacity:1}
.track-dl{flex:none;background:none;border:none;color:var(--muted);cursor:pointer;font-size:15px;padding:4px 6px;opacity:.8}.track-dl:hover{opacity:1;color:var(--cyan,#5ff)}
/* Kept on this device. `on` is a state, not an action — it has to read as "this one is already
   here", or the only way to tell is to turn the radio off and find out. */
.track-keep{flex:none;background:none;border:none;color:var(--muted);cursor:pointer;font-size:15px;padding:4px 6px;opacity:.8}
.track-keep:hover{opacity:1;color:var(--cyan,#5ff)}
.track-keep.on{color:var(--neon2,#5f8);opacity:1}
.track-keep.working{animation:mppulse 1.1s infinite}
.track-keep[disabled]{opacity:.4;cursor:default}
/* THE SONG NAME IS THE ROW. On a phone it was the only thing that gave way.
 *
 * One line held a 40px play button, the name, the size/offline meta and three icon buttons, every one
 * of them flex:none — so the name absorbed the whole squeeze. Measured at 390px: 60px of width for a
 * title needing 355px, and 18px (two characters) on any row whose meta says "· offline". A music
 * library where you cannot read the song names is not a music library.
 *
 * So on a phone the row is two lines — play + title, then meta and the three actions underneath. The
 * DOM order is unchanged (this is `order` + flex-wrap), because the JS builds one markup for every
 * width, and the title wraps to two lines instead of ellipsing at the first word.
 *
 * The break is made by the title's BASIS, not by a spacer: `flex-basis:calc(100% - 52px)` is exactly
 * what is left beside the play button, so the next item cannot fit and starts line two. Basis `auto`
 * does not work here — flex wraps on the HYPOTHETICAL size, so a long title's own content width
 * (355px, measured) pushed it onto a line of its own and the buttons onto a third. */
@media(max-width:820px){
  .track{flex-wrap:wrap;gap:8px 10px;padding:10px 12px}
  .track-play{order:0}
  .track-name{order:1;flex:1 1 calc(100% - 52px);white-space:normal;overflow-wrap:anywhere;
    display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden}
  .track-meta{order:2;flex:0 1 auto;margin-inline-start:50px}   /* 40px button + the 10px gap → under the title */
  .track-keep{order:3;margin-inline-start:auto}.track-dl{order:4}.track-del{order:5}
  .track-keep,.track-dl{padding:8px 8px}   /* a finger-sized target */
}
/* ── Floating cyberpunk music player (hovers over every view; minimizable; draggable) ── */
#music-player.hidden{display:none}
#music-player.mp{position:fixed;inset-inline-end:18px;bottom:88px;width:338px;z-index:120;background:linear-gradient(160deg,#1a0c36,#0a0418);border:1.5px solid var(--cyan,#0ff);border-radius:18px;box-shadow:0 0 32px rgba(0,240,255,.35),inset 0 0 38px rgba(var(--accent2-rgb),.07);overflow:hidden;font-size:13px;-webkit-user-select:none;user-select:none}
#music-player.mp.playing{animation:mpglow 2.6s ease-in-out infinite, mpwiggle .55s ease-in-out infinite}
@keyframes mpglow{0%,100%{box-shadow:0 0 30px rgba(0,240,255,.4),inset 0 0 36px rgba(var(--accent2-rgb),.07);border-color:var(--cyan,#0ff)}50%{box-shadow:0 0 50px rgba(255,0,230,.5),0 0 80px rgba(0,240,255,.28),inset 0 0 42px rgba(0,240,255,.1);border-color:#ff2bd6}}
@keyframes mpwiggle{0%,100%{transform:rotate(-1deg) translateY(0)}25%{transform:rotate(1.2deg) translateY(-2px)}50%{transform:rotate(-0.7deg) translateY(0)}75%{transform:rotate(1deg) translateY(-2px)}}
#music-player.mp-mini.playing{animation:mpglow 2.6s ease-in-out infinite, mpwiggle .55s ease-in-out infinite}
@media(prefers-reduced-motion:reduce){#music-player.mp.playing,#music-player.mp-mini.playing{animation:mpglow 2.6s ease-in-out infinite}}
.mp-scan{position:absolute;inset:0;pointer-events:none;z-index:3;background:repeating-linear-gradient(0deg,rgba(0,240,255,.06) 0 1px,transparent 1px 3px);mix-blend-mode:screen;opacity:.5;animation:mpscan 8s linear infinite}
@keyframes mpscan{to{background-position:0 240px}}
.mp-viz{display:block;width:100%;height:56px;background:radial-gradient(ellipse at bottom,rgba(0,240,255,.1),transparent 70%);border-bottom:1px solid rgba(0,240,255,.15)}
.mp-head{display:flex;align-items:center;gap:8px;padding:10px 14px;cursor:grab;background:linear-gradient(90deg,rgba(0,240,255,.12),rgba(255,0,230,.08));border-bottom:1px solid rgba(0,240,255,.3);position:relative;z-index:4}
.mp-logo{flex:1;font-weight:900;letter-spacing:2px;color:var(--cyan,#0ff);text-shadow:0 0 9px rgba(0,240,255,.8);font-size:12px;animation:mpflicker 4s infinite}
@keyframes mpflicker{0%,19%,21%,55%,57%,100%{opacity:1}20%,56%{opacity:.45}}
.mp-head button,#music-player.mp-mini button{background:none;border:none;color:var(--muted);cursor:pointer;font-size:15px;padding:2px 6px}
.mp-head button:hover{color:var(--cyan,#0ff)}
.mp-now{padding:12px 14px 6px;font-weight:700;color:#eaf0ff;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
/* The EXPANDED player has room for the whole name: wrap to two lines before ellipsizing, and break
 * inside a long unbroken name rather than clipping it ("the track name gets cut off" — mobile,
 * where names are longest relative to the panel). The mini bar above keeps its single line. */
#music-player.mp:not(.mp-mini) .mp-now{white-space:normal;display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;line-clamp:2;overflow-wrap:anywhere;line-height:1.3}
.mp-seek{height:7px;margin:6px 14px;border-radius:4px;background:rgba(0,240,255,.14);cursor:pointer;overflow:hidden}
.mp-seek-fill{height:100%;width:0;background:linear-gradient(90deg,var(--neon,#0ff),var(--magenta,#f0f));box-shadow:0 0 9px var(--cyan,#0ff)}
.mp-time{display:flex;justify-content:space-between;padding:1px 14px;font-size:11px;color:var(--muted)}
.mp-controls{display:flex;align-items:center;justify-content:center;gap:16px;padding:8px 0 12px}
.mp-controls button{background:none;border:none;color:var(--cyan,#0ff);cursor:pointer;font-size:20px;transition:.12s}
.mp-controls button:hover{filter:drop-shadow(0 0 7px var(--cyan,#0ff));transform:scale(1.15)}
.mp-controls .mp-big{width:46px;height:46px;border-radius:50%;border:2px solid var(--cyan,#0ff);background:rgba(0,240,255,.12);font-size:17px;display:inline-flex;align-items:center;justify-content:center}
.mp-controls .mp-big:hover{background:var(--cyan,#0ff);color:#04121a}
.mp-controls .mp-shuffle.on{color:var(--magenta,#f0f);filter:drop-shadow(0 0 7px var(--magenta,#f0f))}
.mp-search-row{padding:2px 10px 4px}
.mp .mp-search{width:100%;box-sizing:border-box;background:rgba(0,240,255,.06);border:1px solid rgba(0,240,255,.3);border-radius:14px;color:#eaf0ff;font-size:12px;padding:6px 12px;outline:none}
.mp .mp-search:focus{border-color:var(--cyan,#0ff);box-shadow:0 0 10px rgba(0,240,255,.3)}
.mp .mp-search::placeholder{color:rgba(234,240,255,.45)}
.mp-list{max-height:172px;overflow-y:auto;border-top:1px solid rgba(0,240,255,.2);padding:6px}
.mp-track{display:flex;align-items:center;gap:8px;width:100%;text-align:start;background:none;border:none;color:#cfe0f0;cursor:pointer;padding:8px 10px;border-radius:9px;font-size:12px}
.mp-track:hover{background:rgba(0,240,255,.1)}.mp-track.on{background:rgba(0,240,255,.16);color:#fff}
.mp-track .mp-tnum{flex:none;color:var(--cyan,#0ff);width:14px;text-align:center}
.mp-track span:last-child{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
#music-player.mp-mini{position:fixed;inset-inline-end:18px;bottom:88px;width:auto;max-width:240px;z-index:120;display:flex;align-items:center;gap:8px;padding:8px 12px;background:linear-gradient(160deg,#150a2c,#0a0418);border:1px solid var(--cyan,#0ff);border-radius:26px;box-shadow:0 0 22px rgba(0,240,255,.35);cursor:grab;-webkit-user-select:none;user-select:none}
#music-player.mp-mini .mp-eq{font-size:15px}
#music-player.mp-mini .mp-eq.on{animation:mppulse 1.1s infinite}
@keyframes mppulse{0%,100%{filter:drop-shadow(0 0 2px var(--cyan,#0ff))}50%{filter:drop-shadow(0 0 10px var(--cyan,#0ff))}}
#music-player.mp-mini .mp-title{flex:1;min-width:60px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;color:#eaf0ff;font-size:12px}
#music-player.mp-mini button{color:var(--cyan,#0ff);font-size:15px;min-width:40px;min-height:40px;padding:0;display:inline-grid;place-items:center;border-radius:50%}
/* THE PHONE'S MUSIC APP — the same player, at the size of the screen.
 *
 * EVERY rule here excludes .mp-mini, because the minimised player carries BOTH classes
 * (`class="mp mp-mini"`). Written as bare `#music-player.mp` these rules matched the mini bar too —
 * same specificity, later in the file, so they beat the .mp-mini rules above and blew the floating
 * pill up to the whole screen. Minimising then looked like it opened the player again.
 *
 * Expanded, this was a 290px card floating over the feed: a scaled-down desktop window on a device
 * where the app IS the screen. Nothing about it is different here except the size — same controls,
 * same list, same everything — because a second design is a second thing to keep in step.
 *
 * And it holds still. `mpwiggle` rocks the whole box while a track plays, which is a nice touch on a
 * 338px window parked in a corner and motion sickness on a full screen you are reading a track list
 * on. Also OFF: the drag (a full-screen panel has nowhere to go — see _drag, which is skipped here
 * rather than left to fight the layout with inline left/top) and overscroll chaining, so flicking
 * the track list does not drag the page behind it.
 *
 * The MINI bar stays a floating pill: that one is meant to sit over whatever you are doing. */
@media(max-width:820px){
  #music-player.mp:not(.mp-mini){
    position:fixed;inset:0;inset-inline-start:0;inset-inline-end:0;top:0;bottom:0;
    width:auto;max-width:none;border-radius:0;border:0;z-index:200;
    display:flex;flex-direction:column;overflow:hidden;
    padding-bottom:env(safe-area-inset-bottom);
    overscroll-behavior:contain;
    animation:none}
  #music-player.mp:not(.mp-mini).playing{animation:none}          /* no bounce on a full screen */
  #music-player.mp:not(.mp-mini) .mp-head{cursor:default;padding-top:calc(10px + env(safe-area-inset-top))}
  #music-player.mp:not(.mp-mini) .mp-viz{height:84px}
  /* Full screen = room for the whole name. The 2-line clamp above is for the 338px desktop card;
   * kept here it ellipsized real track names on the one surface with nothing else competing for the
   * space — reported from a car mount as "the song title is cut off" on the player that appears
   * when a head-unit press starts playback. */
  #music-player.mp:not(.mp-mini) .mp-now{font-size:17px;padding:16px 16px 8px;display:block;-webkit-line-clamp:none;line-clamp:none;overflow:visible}
  #music-player.mp:not(.mp-mini) .mp-controls{gap:26px;padding:14px 0 18px}
  #music-player.mp:not(.mp-mini) .mp-controls .mp-big{width:64px;height:64px;font-size:22px}
  #music-player.mp:not(.mp-mini) .mp-controls button{font-size:24px}
  /* The list takes whatever is left, and scrolls inside itself — the panel never scrolls. */
  #music-player.mp:not(.mp-mini) .mp-list{flex:1 1 auto;max-height:none;overflow-y:auto;
    overscroll-behavior:contain;-webkit-overflow-scrolling:touch;padding:6px 6px 10px}
  #music-player.mp:not(.mp-mini) .mp-track{font-size:15px;padding:12px 12px}   /* a finger, not a mouse */
  #music-player.mp:not(.mp-mini) .mp-seek{height:12px;margin:10px 16px}        /* …and a scrubber it can hit */
  #music-player.mp:not(.mp-mini) .mp-time{padding:1px 16px;font-size:12px}
  #music-player.mp:not(.mp-mini) .mp-search{font-size:15px;padding:10px 14px}
  /* The mini pill was pinned to 240px on screens where 240px is most of the width anyway — let it
   * use the row, so its single-line title truncates far later. */
  #music-player.mp-mini{inset-inline-end:8px;bottom:76px;max-width:calc(100vw - 16px)}
}
/* Signup captcha (anti-spam) */
.captcha-box{margin:12px 0;padding:12px;border:1px solid rgba(0,240,255,.3);border-radius:12px;background:rgba(0,240,255,.05)}
.captcha-lbl{font-size:12px;color:var(--cyan,#0ff);font-weight:700;margin-bottom:8px;letter-spacing:.5px}
.captcha-row{display:flex;align-items:center;gap:8px;margin-bottom:8px}
.captcha-img{height:56px;border-radius:8px;border:1px solid rgba(0,240,255,.4);background:#0a0418}
.captcha-box .input{text-transform:uppercase;letter-spacing:4px;text-align:center;font-weight:700}
.blossom-locked{margin:16px;padding:18px 20px;border-radius:14px;text-align:center}
.blossom-locked b{font-size:17px}
.blossom-locked p{margin:6px 0 12px}

/* profile */
.prof{padding:0 0 20px}
.prof .banner{height:210px;background:linear-gradient(135deg,#1a0f33,#2a1158);border-bottom:1px solid var(--line);overflow:hidden}
.prof .banner img{width:100%;height:100%;object-fit:cover;display:block}
.prof .phead{display:flex;gap:16px;align-items:flex-end;padding:0 22px;margin-top:-44px}
.prof .pav{width:92px;height:92px;border-radius:50%;border:3px solid var(--bg);object-fit:cover;background:#1a1230}
.prof .pbody{padding:14px 22px}
.prof .phead .btn{font-size:15px;padding:10px 18px;border-radius:12px}
.prof .prof-actions{margin-inline-start:auto;display:flex;flex-wrap:wrap;gap:8px;align-items:center;justify-content:flex-end}
.prof .prof-actions .btn{white-space:nowrap}
/* :not(.prof-menu-btn) — this hides the icon so the LABELLED profile buttons show their full word on
   desktop, but the ☰ overflow button has no label: hiding its icon left an empty oval with nothing in
   it. An icon-only button must keep its icon whatever the row's label policy is. */
.prof .prof-actions .btn:not(.prof-menu-btn) .ic{display:none}   /* Settings shows full label on desktop */
.prof h2{margin:0} .prof .about{color:var(--text);margin:8px 0;line-height:1.45;white-space:pre-wrap}
.npubrow{display:flex;align-items:center;gap:8px;margin:8px 0}
.npubrow code{color:var(--cyan);font-size:12px}
.prof-joined{display:inline-flex;align-items:center;gap:7px;margin:5px 0 7px;padding:7px 10px;
  border:1px solid var(--line);border-radius:999px;background:var(--panel2);color:var(--muted);font-size:12px}
.prof-joined[hidden]{display:none}
.prof-joined .ic{width:14px;height:14px;color:var(--neon)}
.prof-joined b{color:var(--text);font-weight:650}
.ln-addr{background:transparent;border:1px solid var(--line);color:var(--amber);cursor:pointer;border-radius:9px;padding:6px 12px;font-size:13px;margin:6px 0;display:inline-flex;align-items:center;gap:4px;transition:.13s}
.ln-addr:hover{border-color:var(--amber);box-shadow:0 0 12px rgba(255,207,43,.35)}
/* Monero tips — Monero orange (#f60). ɱ action button + the QR box + the profile address pill. */
.act.actxmr{color:#f60} .act.actxmr:hover{color:#ff8a33;background:rgba(255,102,0,.14);box-shadow:inset 0 0 0 1px var(--line)}
.ln-addr.xmr{color:#f60} .ln-addr.xmr:hover{border-color:#f60;box-shadow:0 0 12px rgba(255,102,0,.35)}
/* Bitcoin Cash tips — BCH green (#0ac18e). Profile address pill + tip-modal preset chips. */
.ln-addr.bch{color:#0ac18e} .ln-addr.bch:hover{border-color:#0ac18e;box-shadow:0 0 12px rgba(10,193,142,.35)}
.bch-preset:active{background:rgba(10,193,142,.16)}
/* Monero tip note badge — shows the amount prominently; when a proof is attached, a one-tap button
   copies a check_tx_proof command so anyone can verify the payment in their own wallet. */
.xmr-tip-badge{display:flex;flex-wrap:wrap;align-items:center;gap:8px;margin:8px 0;padding:8px 12px;border-radius:12px;background:rgba(255,102,0,.10);border:1px solid rgba(255,102,0,.35)}
.xt-amt{font-weight:800;color:var(--amber);font-size:15px}   /* --amber is themed → readable on light AND dark themes */
.xt-verify{margin-inline-start:auto;background:rgba(255,102,0,.16);color:var(--amber);border:1px solid rgba(255,102,0,.4);border-radius:9px;padding:6px 12px;font-size:12px;font-weight:600;cursor:pointer;min-height:34px}
.xt-verify:active{background:rgba(255,102,0,.26)}
.xt-txid{margin-inline-start:auto;color:var(--muted);font-size:12px;font-family:var(--mono)}
/* Monero tip amount presets — one-tap common amounts in the tip modal. */
.xmr-presets{display:flex;flex-wrap:wrap;gap:6px;margin:-2px 0 4px}
.xmr-preset{background:var(--panel2);color:var(--text);border:1px solid var(--line);border-radius:11px;padding:8px 12px;font-weight:600;font-size:13px;cursor:pointer;min-height:38px}
.xmr-preset:active{background:rgba(255,102,0,.14)}
/* Monero tx-proof (optional): collapsible txid + proof-signature inputs in the tip modal. */
.xmr-proof{margin-top:8px}
.xmr-proof>summary{cursor:pointer;font-size:13px;color:var(--cyan);padding:6px 0;list-style:none}
.xmr-proof>summary::-webkit-details-marker{display:none}
.xmr-proof>summary::before{content:'▸ '}
.xmr-proof[open]>summary::before{content:'▾ '}
.xmr-proof .input{margin-top:6px;width:100%}
.xmr-proof textarea.input{resize:vertical;min-height:52px;font-family:var(--mono);font-size:12px;word-break:break-all}
.xmr-qr{display:flex;justify-content:center;align-items:center;min-height:170px;margin:10px 0}
.xmr-qr img{width:208px;height:208px;background:#fff;border-radius:10px;padding:8px;box-sizing:border-box}
.follow-stats{display:flex;gap:18px;margin-top:10px}
.statbtn{background:transparent;border:none;color:var(--muted);cursor:pointer;font-size:15px;padding:0}
.statbtn:hover{color:var(--cyan)} .statbtn b{color:var(--text);font-size:15px}
.people-list{max-height:60vh;overflow:auto;margin-top:8px}
/* Threaded conversation view: root post first, replies nested (indented) under their parent, with the
   post you clicked highlighted. A faint guide line on nested replies shows the indent depth. */
.thread-node{position:relative}
.thread-node[style]{border-inline-start:2px solid var(--line)}
.thread-hl>.note{background:rgba(var(--accent-rgb),.07);box-shadow:0 0 0 2px var(--neon) inset;border-radius:12px}
/* Opening a thread from a notification scrolls THIS node to the top of the feed; the margin leaves a
   sliver of the post above it visible, so you land on the reply with a hint of what it answers rather
   than flush against the edge. (_revealThreadNode) */
.thread-node.thread-hl{scroll-margin-top:76px}
/* compact "parent not loadable" banner above an orphaned reply (overrides .empty's big padding) */
.thread-missing .empty{padding:10px 12px;font-size:13px;opacity:.75;border:1px dashed var(--line);border-radius:10px;margin-bottom:8px}

/* notifications */
/* align-items:flex-start, NOT center: the row grew (header + the reply text + the referenced-post
   card), so centring left the icon and avatar floating beside the MIDDLE of the block instead of
   next to the name. Top-aligned is how ditto/Mastodon place them. */
.notif{display:flex;gap:12px;padding:14px 20px;border-bottom:1px solid var(--line);align-items:flex-start;font-size:17px}
.notif > .ic,.notif > .notif-av{margin-top:1px;flex:0 0 auto}
/* Grouped reaction/repost rows stack the actors' avatars so you can see WHO acted without
   expanding. Overlapped left-to-right; the ring matches the row background so they read as
   separate faces. Each carries a title= with the person's name (the collapsed "and N others"
   does too), because a bare count told you nothing about who it was. */
.notif > .notif-avs{display:flex;flex:0 0 auto;margin-top:1px}
.notif > .notif-avs .notif-av{margin-inline-start:-11px;box-shadow:0 0 0 2px var(--bg)}
.notif > .notif-avs .notif-av:first-child{margin-inline-start:0}
.notif > div{flex:1;min-width:0}
.notif .ic{font-size:20px;width:26px;text-align:center}
/* In-app updater row (pinned atop Notifications when a new build is ready). */
.upd-notif{cursor:pointer;background:rgba(var(--accent-rgb),.08);border-inline-start:3px solid var(--cyan)}
.upd-notif:hover{background:rgba(var(--accent-rgb),.14)}
.notif .muted.small{font-size:13px}
.notif.like .ic{color:var(--neon)} .notif.rt .ic{color:var(--green)} .notif.reply .ic{color:var(--cyan)} .notif.mention .ic{color:var(--amber)} .notif.zap .ic{color:var(--amber)}
/* The AVATAR, not every image in the row. As `.notif img` this was specificity (0,1,1) — a tie with
   `.media-row img` / `.note .txt img`, declared later, so it won: every image in a notification's context
   preview was forced to a 34px CIRCLE, and so was any inline NIP-30 custom emoji in the sender's name.
   Videos were never matched by it, which is why notification media looked broken for photos and merely
   oversized for video. Avatars all carry .notif-av (see notifHtml), so scope it to them. */
.notif .notif-av{width:34px;height:34px;border-radius:50%;border:1px solid var(--line)}
/* infinite scroll-back loading sentinel (appended to the feed/profile while a page loads) */
.load-sentinel{display:flex;justify-content:center;padding:18px 0}

/* mention autocomplete */
.mention-box{background:var(--panel2);border:1px solid var(--neon);border-radius:10px;margin-top:-2px;max-height:230px;overflow:auto;box-shadow:var(--glow);position:relative;z-index:5}
.mention-opt{display:flex;gap:10px;align-items:center;padding:10px 12px;cursor:pointer;font-size:15px}
.mention-opt:hover{background:rgba(var(--accent-rgb),.15)}
.mention-opt img{width:28px;height:28px;border-radius:50%;border:1px solid var(--line);object-fit:cover}
.prof .phead{flex-wrap:wrap}

.empty{text-align:center;color:var(--muted);padding:60px 20px;font-size:17px}
.spinner{width:26px;height:26px;border:3px solid var(--line);border-top-color:var(--neon);border-radius:50%;animation:spin .7s linear infinite;margin:40px auto}
/* The same spinner, sitting IN a line of text rather than owning a screen — the thread's "loading the
 * rest of this conversation" label, where the post above it has already been painted. The base rule's
 * `margin:40px auto` is what makes the block version centre itself, and it has to go here or the
 * label is pushed a screen and a half down the page away from the post it belongs to. */
.spinner-inline{width:13px;height:13px;border-width:2px;margin:0 8px 0 0;display:inline-block;vertical-align:-2px}
@keyframes spin{to{transform:rotate(360deg)}}
.badge{display:inline-grid;place-items:center;min-width:21px;height:21px;padding:0 6px;border-radius:11px;background:var(--neon);color:#fff;font-size:12px;font-weight:700;margin-inline-start:6px;box-shadow:0 0 8px rgba(var(--accent-rgb),.5)}

.lightbox{position:fixed;inset:0;z-index:100000;background:rgba(4,2,12,.92);backdrop-filter:blur(6px);display:flex;align-items:center;justify-content:center;padding:16px;box-sizing:border-box;cursor:zoom-out;animation:fade .15s}
/* leave ~16vh at the bottom for the toolbar + counter so they sit BELOW the media (and clear of a
   video's own controls) instead of overlapping it */
.lightbox img,.lightbox video{max-width:100%;max-height:84vh;width:auto;height:auto;border-radius:10px;border:1px solid var(--neon);box-shadow:0 0 40px rgba(var(--accent-rgb),.4)}
.lightbox img{transform-origin:center center;will-change:transform;touch-action:none}   /* transform-driven zoom/pan (see _lbZoom); touch-action:none so pinch doesn't page-zoom */
.lightbox audio{width:min(560px,100%)}
/* Toolbar sits BELOW the media, centered along the bottom — not the top-right corner (it covered the
   image and was an awkward reach). A row of pills on the bottom edge is thumb-reachable on phones and
   reads as "controls for what's above". Wraps if a narrow screen can't fit the row. */
.lb-bar{position:fixed;bottom:calc(16px + env(safe-area-inset-bottom));inset-inline-start:50%;transform:translateX(-50%);inset-inline-end:auto;top:auto;z-index:401;display:flex;gap:12px;flex-wrap:wrap;justify-content:center;max-width:calc(100vw - 24px);transition:opacity .18s ease}
/* Filled, not a dark disc with a hairline — these read as actions now instead of dissolving into the
   image behind them. Close is the tertiary accent so it is not one of four identical circles. */
.lb-btn{width:46px;height:46px;border-radius:50%;border:none;background:linear-gradient(135deg,var(--neon),var(--neon2));color:#fff;font-size:20px;line-height:1;cursor:pointer;display:flex;align-items:center;justify-content:center;padding:0;box-shadow:0 2px 12px rgba(0,0,0,.5)}
.lb-btn:last-child{background:linear-gradient(135deg,var(--accent-tertiary,#ff3366),#b3123f)}
.lb-btn:active{transform:scale(.9)}
.lb-btn:focus-visible{outline:2px solid #fff;outline-offset:3px}
/* Where there is a real pointer, keep the image clean and bring the actions in when you reach for them.
   NOT on touch: there is no hover, and a full-screen image leaves no backdrop to tap, so the bar is the
   only way to close it — hiding it there would trap the user. :focus-within and .lb-keys cover the
   keyboard, which never hovers. */
@media (hover:hover) and (pointer:fine){
  /* .lightbox fills the viewport, so :hover on IT is always true and would never hide anything — the
     reveal is driven by pointer MOVEMENT instead (.lb-hot, set in JS and cleared after a pause), the way
     a video player shows its controls. Hovering the bar itself, tabbing to it, or using the keys hold it. */
  .lb-bar{opacity:0}
  .lightbox.lb-hot .lb-bar,.lightbox.lb-keys .lb-bar,.lb-bar:hover,.lb-bar:focus-within{opacity:1}
}
/* tap-to-zoom: show the image at natural size and let the backdrop scroll */
/* Lightbox gallery paging (multi-attachment posts): arrows + an n / N counter. */
.lb-nav{position:fixed;top:50%;transform:translateY(-50%);z-index:401;
  width:52px;height:52px;padding:0;border-radius:50%;border:1px solid var(--neon);
  background:rgba(4,2,12,.72);color:#fff;font-size:30px;line-height:1;cursor:pointer;
  display:flex;align-items:center;justify-content:center;
  -webkit-backdrop-filter:blur(4px);backdrop-filter:blur(4px)}
.lb-nav:active{transform:translateY(-50%) scale(.9)}
.lb-nav[disabled]{opacity:.25;pointer-events:none}
.lb-prev{inset-inline-start:calc(10px + env(safe-area-inset-left))}
.lb-next{inset-inline-end:calc(10px + env(safe-area-inset-right))}
/* the n / N counter sits just ABOVE the bottom toolbar so the two don't stack on the same spot */
.lb-count{position:fixed;inset-inline-start:50%;transform:translateX(-50%);bottom:calc(74px + env(safe-area-inset-bottom));
  z-index:401;padding:6px 14px;border-radius:13px;background:rgba(4,2,12,.72);border:1px solid var(--line);
  color:#fff;font-size:13px;font-weight:600;letter-spacing:.4px}
.lightbox.lb-zoom .lb-nav,.lightbox.lb-zoom .lb-count{display:none}   /* zoomed = panning, not paging */
@media(max-width:820px){ .lb-nav{width:44px;height:44px;font-size:24px} }
.lightbox.lb-zoom{cursor:grab}   /* the image itself is scaled/panned via transform (_lbZoom); nav hidden while zoomed */
/* pointer-events:none — toasts are informational (no taps); the layer sits over the bottom-centre
   compose ✏️, so without this a visible toast swallows taps on the New Post button (you had to hit an
   uncovered edge). Toasts never need interaction, so the whole layer stays click-through. */
.ingest-banner{margin:10px 0;padding:12px 14px;border:1px solid var(--danger,#e0245e);border-radius:12px;background:rgba(224,36,94,.12);display:flex;flex-direction:column;gap:8px}
.ingest-banner .ib-row{display:flex;gap:8px;flex-wrap:wrap;align-items:center}
.ingest-banner .ib-row .input{flex:1;min-width:160px}
#toast-root{position:fixed;bottom:20px;inset-inline-start:50%;transform:translateX(-50%);z-index:500;display:flex;flex-direction:column;gap:8px;pointer-events:none}
.toast{background:var(--panel2);border:1px solid var(--neon);border-radius:11px;padding:12px 18px;font-size:17px;box-shadow:var(--glow);animation:fade .2s}
/* Ctrl+F find bar (Electron desktop shell only) — a small floating bar, top-right. */
.find-bar{position:fixed;top:10px;inset-inline-end:14px;z-index:9000;display:flex;align-items:center;gap:4px;background:var(--panel2);border:1px solid var(--neon);border-radius:10px;padding:6px 6px;box-shadow:var(--glow)}
.find-input{background:var(--bg);border:1px solid var(--line);border-radius:7px;color:var(--text);font-size:15px;padding:6px 10px;width:200px;outline:none}
.find-input:focus{border-color:var(--neon)}
.find-input.nofind{border-color:var(--danger,#d61f4e);color:var(--danger,#d61f4e)}
.find-btn{background:transparent;border:none;color:var(--muted);cursor:pointer;font-size:15px;padding:6px 8px;border-radius:6px;line-height:1}
.find-btn:hover{color:#fff;background:rgba(var(--accent-rgb),.16)}
/* Background-agent live progress pill (updates in place with the step count) */
.agent-prog{display:flex;align-items:center;gap:8px;margin:6px 0;padding:8px 12px;border:1px solid var(--line);border-inline-start:3px solid var(--neon);border-radius:10px;background:rgba(var(--accent-rgb),.07);color:var(--muted);font-size:13px}
.agent-prog code{color:var(--cyan);font-family:var(--mono)}
.ap-spin{width:13px;height:13px;border:2px solid rgba(var(--accent-rgb),.3);border-top-color:var(--neon);border-radius:50%;display:inline-block;animation:apspin .8s linear infinite;flex:none}
@keyframes apspin{to{transform:rotate(360deg)}}
/* pointer-events:auto — #toast-root is click-through (so plain toasts don't block the compose button), but
   the notification toast IS interactive (tap → open Notifications), so it must opt back into pointer events. */
.notif-toast{display:flex;align-items:center;gap:10px;cursor:pointer;padding:10px 14px;pointer-events:auto}
.notif-toast img{width:30px;height:30px;border-radius:50%;border:1px solid var(--line);object-fit:cover;flex-shrink:0}

/* iOS "Add to Home Screen" hint — only ever injected on iPhone Safari (see _maybeIosInstallHint).
   Sits above the bottom nav, clears the home indicator, and matches the panel styling. */
.ios-install-hint{position:fixed;inset-inline-start:8px;inset-inline-end:8px;bottom:calc(70px + env(safe-area-inset-bottom));z-index:60;
  display:flex;align-items:center;gap:10px;padding:12px 12px;border-radius:14px;
  background:var(--panel2);border:1px solid var(--line);box-shadow:0 6px 22px rgba(0,0,0,.4);
  font-size:13px;color:var(--text);animation:fade .2s}
.ios-install-hint .iih-txt{flex:1 1 auto;line-height:1.45}
.ios-install-hint .iih-ic{width:16px;height:16px;vertical-align:-3px;margin:0 1px;color:var(--neon)}
.ios-install-hint .iih-x{flex:0 0 auto;display:inline-grid;place-items:center;width:28px;height:28px;
  border:1px solid var(--line);border-radius:8px;background:transparent;color:var(--muted);cursor:pointer;font-size:15px}
.ios-install-hint .iih-x:active{background:var(--panel)}

.mobilenav{display:none}
/* ===== responsive ===== */
@media(max-width:820px){
  .app{grid-template-columns:1fr;padding:0}   /* no outer inset on phones — the feed needs the full width */
  .sidebar{display:none}
  /* Density on mobile (no global zoom — Firefox's zoom leaves a black border). Scale down the main
     content via font-size so the app reads denser without breaking the viewport. */
  .note .txt{font-size:15px;line-height:1.45} .note .name{font-size:15px}
  .note .handle,.note .time{font-size:13px} .act{font-size:var(--fs-3);padding:var(--sp-2)} .act.actq{font-size:20px}   /* 8px sides: six actions must still fit a 320px phone */
  .bubble{font-size:15px;padding:10px 12px}
  .dm-peer b{font-size:15px} .dm-peer .muted.small{font-size:13px}
  .prof .pbody h2{font-size:20px}
  .topbar h2{font-size:15px} .nav-item{font-size:15px}
  #btn-compose{display:none}   /* topbar compose hidden on mobile — the bottom nav has ✏️ */
  .feed{padding-bottom:80px}
  /* Messages on mobile: ONE pane at a time, full-height flex so the thread actually scrolls and
     the conversation list (with its "＋ New message") can't bleed over the thread's back button */
  .dm-wrap{display:flex;flex-direction:column;height:100%;min-height:0}
  .dm-list{flex:1;min-height:0;width:100%;border-inline-end:none}
  .dm-thread{min-height:0;width:100%;display:none}
  .dm-list.has-active{display:none}              /* a chat is open → hide the list */
  /* Open thread = full-screen FIXED overlay so the message list scrolls reliably (no nested-scroll
     fight with .feed) and the compose bar/back button are pinned. Sits above the bottom nav. */
  .dm-list.has-active + .dm-thread{display:flex;flex-direction:column;position:fixed;inset:0;z-index:45;background:var(--bg)}
  .dm-list.has-active + .dm-thread .dm-msgs{flex:1;min-height:0;overflow-y:auto}
  .dm-list.has-active + .dm-thread .dm-compose{padding-bottom:calc(12px + env(safe-area-inset-bottom))}
  .mobilenav{display:flex;flex-wrap:nowrap;position:fixed;bottom:0;inset-inline-start:0;inset-inline-end:0;justify-content:space-around;
    align-items:center;padding:6px env(safe-area-inset-right) calc(6px + env(safe-area-inset-bottom)) env(safe-area-inset-left);
    /* var(--bg), not var(--rail): this bar is FIXED and the feed scrolls underneath it, so unlike the
       desktop rails it cannot be transparent. The canvas colour keeps it the same tone as everything
       else; it used to be a hardcoded near-black bar even on the light themes. */
    border-top:1px solid var(--line);z-index:40;background:var(--bg)}
  /* flex:1 1 0 + min-width:0 → all items shrink to equal widths and stay on ONE line, so the ☰ never
     wraps to a second row on narrow phones. Icon over a 10px label: the label is what turns a row of
     ambiguous glyphs into a bar people can actually navigate, and it fits because the icons are line
     art at a fixed 23px instead of emoji at whatever size the phone's font decides. */
  .mobilenav .nav-item{flex:1 1 0;min-width:0;display:flex;flex-direction:column;align-items:center;justify-content:center;
    gap:4px;height:48px;font-size:24px;line-height:1;color:var(--muted);background:transparent;border:none;padding:0;position:relative}
  .mobilenav .nav-item b{font-size:11px;font-weight:600;letter-spacing:.2px;line-height:1;
    max-width:100%;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
  .mobilenav svg.ic{width:23px;height:23px;stroke-width:1.75}
  .mobilenav .nav-item.active{color:var(--neon)}
  .mobilenav .nav-item.active svg.ic{stroke-width:2.1}
  /* Compose is the one ACTION in a row of destinations, so it gets a tinted accent pill instead of a
     plain glyph. Tint + accent stroke (not a filled accent with a dark glyph) because half the themes
     are light and would need the inverse foreground — this reads on all of them. */
  .mobilenav .compose{color:var(--neon)}
  .mobilenav .compose svg.ic{width:21px;height:21px;position:relative;z-index:1}
  .mobilenav .compose::before{content:"";position:absolute;top:5px;width:46px;height:33px;border-radius:11px;
    background:rgba(var(--accent-rgb),.16);border:1px solid rgba(var(--accent-rgb),.45)}
  .mobilenav .compose:active::before{background:rgba(var(--accent-rgb),.3)}
  .mobilenav .badge{position:absolute;top:4px;inset-inline-start:56%;margin:0}
  /* width / horizontal-overflow guards on mobile */
  html,body,.app,.main,.feed{max-width:100vw;overflow-x:hidden}
  .note{padding:12px 12px}
  .note .txt img,.note .txt video,.note .txt audio,.link-card,.quoted,.note-preview{max-width:100%}
  /* Action row must FIT the card on ONE line: posts with reaction/boost COUNTS widen the buttons, which
     used to either push ☰ off the right edge (taps then hit Zap) or — with flex-wrap — drop actions onto
     a second row. Instead the six actions share the width evenly (flex:1 1 0 + min-width:0, the same
     trick as .mobilenav) and the count truncates inside its own button, so the row can neither wrap nor
     overflow no matter how big the numbers get. */
  .acts{gap:2px;flex-wrap:nowrap}
  .act{font-size:15px;padding:8px 4px;gap:2px}
  /* each button keeps its natural width and shrinks only when the row would overflow (min-width:0 lets
     it), so the six actions stay evenly spread and only the biggest count gives way on a tiny screen */
  .acts .act{flex:0 1 auto;min-width:0;overflow:hidden}
  .act > svg,.act .tipbolt,.act .react-ic{flex:0 0 auto}   /* icons keep their size; only the count gives way */
  .act .n{min-width:0;font-size:11px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
  .act.actq{font-size:24px;padding-top:6px;padding-bottom:4px}
  .note.poll .acts .act{flex:0 1 auto}   /* a poll has just reply + ☰ — leave those at natural size */
  /* shrink the messages view so the compose bar + list clear the fixed bottom nav */
  /* lift the DM composer above the fixed bottom nav (reliable, no :has/height-calc) */
  .dm-compose{padding-bottom:calc(14px + 70px + env(safe-area-inset-bottom))}
  .dm-list{padding-bottom:74px}
  /* Concord drills through one pane at a time: communities/channels first, conversation second. */
  .cc-app{display:flex;position:relative;height:100%;min-height:0}.cc-communities{flex:0 0 62px;width:62px;padding:9px 5px}.cc-brand,.cc-server{width:44px;height:44px;flex-basis:44px}.cc-channels{flex:1;min-width:0;border:0}.cc-conversation{display:none;position:fixed;inset:0;z-index:45;background:var(--bg)}.cc-app.show-chat .cc-communities,.cc-app.show-chat .cc-channels{display:none}.cc-app.show-chat .cc-conversation{display:flex}.cc-mobile-back{display:grid}.cc-conversation>header{padding-top:env(safe-area-inset-top);min-height:54px;height:auto}.cc-conversation>header .cc-topic{display:none}.cc-messages{padding:14px 12px}.cc-compose{margin:0 8px calc(8px + env(safe-area-inset-bottom));border-radius:11px}.cc-compose .btn{min-height:42px}.cc-join{position:fixed;z-index:60;padding:calc(12px + env(safe-area-inset-top)) 12px calc(12px + env(safe-area-inset-bottom));place-items:end center}.cc-join-card{padding:20px 16px;border-radius:18px}.cc-join-actions .btn{flex:1;min-height:46px}.cc-channel{min-height:46px;font-size:16px}.cc-head-btn,.cc-compose-btn,.cc-mobile-back{min-width:44px;width:44px;height:44px}
  body.modal-open .mobilenav{display:none}   /* don't let the fixed nav overlap an open modal */
  /* Clear the notch / status bar (safe-area inset) so the sticky topbar isn't cut off; opaque bg
     fills the notch strip and keeps content from showing through on scroll. */
  .topbar{padding:calc(10px + env(safe-area-inset-top)) calc(14px + env(safe-area-inset-right)) 10px calc(14px + env(safe-area-inset-left));gap:8px;background:var(--bg)}
  /* Title and search share ONE row here, and the title used to be flex-shrink:0 — so every pixel the
     title wanted came out of the search box. On a 390px phone that left the search 161px on Alerts /
     186px on Messages against the 257px its placeholder needs, i.e. the search was visibly cut off on
     every view that HAS a title (the three timeline views hide theirs, which is why only those looked
     right). The title truncates now and the search keeps a floor, so neither can squeeze the other out. */
  .topbar h2{font-size:17px;flex:0 1 auto;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
  /* 140px, not 120: the floor only binds on a long USER-SUPPLIED title (a chat room name, a long
     hashtag), and "🔍 Search" measures 115px in Inter — a 5px margin that a wider fallback font on
     someone's phone eats, putting the clipping straight back on exactly the surface this fixed. The
     title has min-width:0, so it absorbs the extra by truncating; the row still cannot overflow. */
  .searchbox{margin:0 4px;min-width:140px}
  /* A view that ships its OWN search does not also get the topbar's. Measured: Messages, Notes,
     Passwords and Git each render one inside #feed, so on a phone those screens showed TWO identical
     pills — reported the moment the topbar one became legible enough to notice. Driven off the
     rendered DOM rather than a list of view names, so a view that grows (or loses) a search needs no
     second edit here, and the paths that bypass switchView (thread, profile, hashtag) can't leave a
     stale flag behind. The title takes the freed width. */
  .main:has(#feed input[type="search"]) .searchbox{display:none}
  .note,.tl-cmp{padding:14px 14px;gap:10px}
  .reply-ctx{padding:12px 14px 0}   /* align reply context with the note body on mobile */
  .note .av,.tl-cmp-av{width:40px;height:40px}
  .acts{max-width:none}
  .prof .phead{padding:0 14px}.prof .pbody{padding:12px 14px}
  /* Phone: the action buttons wrap as one right-aligned group under the avatar and stay compact so
     Edit / Settings / ☰ (and Call / Zap / ☰) always fit — Settings collapses to its ⚙ icon. */
  .prof .phead{flex-wrap:wrap;align-items:flex-start}
  .prof .prof-actions{width:100%;margin-inline-start:0;justify-content:flex-start;margin-top:8px;gap:6px}
  .prof .phead .btn{margin-top:0;font-size:13px;padding:8px 12px}
  .prof .prof-actions .btn .lbl{display:none}   /* collapse "⚙ Settings" → "⚙" on phones */
  .prof .prof-actions .btn .ic{display:inline}
  /* Same trick in a chat room's header: Join / Edit / Delete keep their icons, drop their words, so the
     channel NAME keeps the width instead of being crushed to a few pixels. */
  .chatroom-head .btn .lbl{display:none}
  .chatroom-head .btn{padding:8px 10px}
  /* Files on a phone are an icon gallery, not a stack of landscape preview cards. 92px yields three
     useful columns on a 360px screen; square artwork makes mixed photos/videos/documents line up and
     leaves the filename below instead of spending most of the viewport on each preview. */
  .files-grid{grid-template-columns:repeat(auto-fill,minmax(92px,1fr));gap:7px;padding:8px}
  .files-grid:not(.details) .file-card>img,
  .files-grid:not(.details) .file-card>video,
  .files-grid:not(.details) .file-card>.file-icon,
  .files-grid:not(.details) .file-card>a>.file-icon,
  .files-grid:not(.details) .file-card>a>img,
  .files-grid:not(.details) .file-card>a>video{width:100%;height:auto;aspect-ratio:1;object-fit:cover}
  .files-grid:not(.details) .file-card .meta{padding:6px 4px}
  .media-grid{gap:4px;padding:4px}   /* tighter phone gallery, still 3 across */
  .media-grid img,.media-grid video{border-radius:6px}
  .modal{padding:16px}
  /* mobile: top-align the modal so the on-screen keyboard doesn't shove it off-center */
  .modal-bg{place-items:start center;padding:6vh 12px}
  .auth-card{padding:22px}
}
/* Tiny phones (≤359px: SE, Fold cover screen): buy the action row ~16px by tightening the card so a
   post with four big counts still shows them in full instead of eliding to "2…". */
@media(max-width:359px){
  .note,.tl-cmp{padding:12px 10px;gap:8px}
  .note .av,.tl-cmp-av{width:34px;height:34px}
}

/* ===== More sheet (mobile overflow) — flat icon tiles =====
   The tile surface is var(--panel) + var(--line), NOT the old hardcoded rgba(8,5,18,.5): that dark
   fill sat under var(--text) labels, so on the light themes the sheet was near-black tiles with
   dark-grey text. Panel vars follow the theme, so one rule reads on all seven. */
.more-grid{display:grid;grid-template-columns:repeat(3,1fr);gap:10px;margin-top:6px}
.more-item{display:flex;flex-direction:column;align-items:center;gap:10px;padding:16px 6px 14px;
  background:var(--panel);border:1px solid var(--line);
  border-radius:14px;color:var(--text);cursor:pointer;font-size:12px;font-weight:600;letter-spacing:.2px;
  transition:.16s;position:relative;overflow:hidden}
.more-item::after{content:"";position:absolute;inset:0;opacity:0;transition:.16s;
  background:radial-gradient(circle at 50% 0%,rgba(var(--accent-rgb),.2),transparent 70%)}
.more-item:hover,.more-item:active{border-color:var(--neon);box-shadow:0 0 0 1px rgba(var(--accent-rgb),.25);transform:translateY(-1px)}
.more-item:hover::after{opacity:1}
/* The glyph sits in its own tinted disc — that's what gives the grid a rhythm when every icon is the
   same flat line weight (emoji used to do it with colour). */
.more-ic{display:flex;align-items:center;justify-content:center;width:42px;height:42px;border-radius:12px;
  background:rgba(var(--accent-rgb),.13);color:var(--neon);font-size:24px;line-height:1;position:relative;z-index:1}
.more-ic svg.ic{width:22px;height:22px;stroke-width:1.75}
.more-item > span{position:relative;z-index:1}
/* sheet headings ("🧭 Discover" → icon + word) */
h3 svg.ic.h-ic{width:19px;height:19px;vertical-align:-3px;margin-inline-end:4px;color:var(--neon)}
.more-logout{color:var(--danger);border-color:rgba(255,60,90,.4)}
.more-logout .more-ic{background:rgba(255,60,90,.14);color:var(--danger)}
.more-logout:hover{border-color:var(--danger);box-shadow:0 0 0 1px rgba(255,60,90,.35)}

/* ===== Articles (NIP-23) ===== */
.art-top{padding:14px 18px;border-bottom:1px solid var(--line)}
/* Repos toolbar: announce button + search on one row. Scoped to .repo-top so the shared .art-top
   (articles) keeps its plain block layout. .input is width:100%, hence the explicit flex basis. */
.repo-top{display:flex;gap:10px;align-items:center;flex-wrap:wrap}
.repo-top .repo-search{flex:1 1 220px;min-width:0;max-width:460px;margin:0}
/* Mine / All repos: ONE segmented control, not two loose buttons — the two options are a single
   either/or choice, and a shared track says that at a glance where separate pills read as two actions. */
.repo-scope{display:inline-flex;gap:2px;padding:4px;border:1px solid var(--line);border-radius:999px;
  background:var(--panel);flex:none}
.repo-sc{padding:6px 16px;border:0;border-radius:999px;background:transparent;color:var(--muted);
  font-family:inherit;font-size:13px;font-weight:700;line-height:1.45;cursor:pointer;white-space:nowrap;transition:.15s}
.repo-sc:hover{color:var(--text)}
.repo-sc.on{background:rgba(var(--accent-rgb),.18);color:var(--neon);box-shadow:0 0 0 1px rgba(var(--accent-rgb),.35) inset}
@media(max-width:600px){.repo-top .repo-search{flex-basis:100%}
  .repo-scope{order:3;flex-basis:100%}}   /* chips get their own row on a phone, above the search */
.article-card{display:flex;gap:14px;padding:16px 20px;border-bottom:1px solid var(--line);cursor:pointer;
  transition:background .15s;position:relative;animation:fade .25s}
.article-card::before{content:"";position:absolute;inset-inline-start:0;top:12%;bottom:12%;width:3px;border-radius:3px;background:transparent;transition:.15s}
.article-card:hover{background:rgba(var(--accent-rgb),.06)}
.article-card:hover::before{background:linear-gradient(var(--neon),var(--neon2));box-shadow:0 0 10px var(--neon)}
.art-img{width:128px;height:86px;object-fit:cover;border-radius:12px;border:1px solid var(--line);flex-shrink:0;background:var(--bg2);transition:.15s}
.article-card:hover .art-img{border-color:var(--neon2);box-shadow:0 0 16px rgba(var(--accent2-rgb),.3)}
.art-meta{flex:1;min-width:0}
.art-title{margin:0 0 6px;font-size:20px;line-height:1.2;color:var(--text);font-weight:700}
.art-sum{color:var(--muted);font-size:17px;line-height:1.45;margin-bottom:10px;display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden}
.art-by{display:flex;align-items:center;gap:8px;font-size:15px;color:var(--muted)}
.art-av{width:22px;height:22px;border-radius:50%;object-fit:cover;border:1px solid var(--line)}
/* Discover: torrents (NIP-35) + git repos (NIP-34) cards — themed via vars */
.tor-card{cursor:default}
/* Discover → Git repo cards — a tidy grid of tiles, same family as the Streams grid. */
.repo-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(260px,1fr));gap:14px;padding:14px 0}
.repo-card{display:flex;flex-direction:column;gap:8px;background:var(--panel2);border:1px solid var(--line);
  border-radius:14px;padding:14px 16px;cursor:pointer;transition:.16s;animation:fade .25s}
.repo-card:hover{border-color:var(--neon);box-shadow:var(--glow),0 0 20px rgba(var(--accent2-rgb),.18);transform:translateY(-3px)}
/* keyboard-selected card (j/k navigation) — same affordance as hover, but sticky + a visible ring */
.repo-card.kb-sel{border-color:var(--neon);box-shadow:0 0 0 2px var(--neon),0 0 22px rgba(var(--accent-rgb),.30);transform:translateY(-2px)}
/* inline checkbox row inside a modal form (Create repo → Private) */
.fld-check{display:flex;flex-direction:row;align-items:center;gap:8px;cursor:pointer}
.fld-check input{width:auto;margin:0}
/* first-commit quick-start (Create repo dialog + empty-repo view) */
.qs{display:flex;flex-direction:column;gap:12px}
.qs-clone{display:flex;align-items:center;gap:8px;flex-wrap:wrap}
.qs-block{border:1px solid var(--line);border-radius:10px;overflow:hidden}
.qs-head{display:flex;align-items:center;justify-content:space-between;gap:8px;padding:6px 10px;background:var(--panel2)}
.qs-pre{margin:0;padding:10px 12px;overflow-x:auto;font-family:var(--mono);font-size:.85em;white-space:pre;line-height:1.45}
.repo-kbhint{margin-inline-start:auto;opacity:.6;white-space:nowrap;font-size:.82em}
@media(max-width:620px){.repo-kbhint{display:none}}
.repo-card-hd{display:flex;align-items:baseline;gap:8px;min-width:0}
.repo-card-ico{flex:none}
.repo-card-name{font-weight:800;font-size:17px;letter-spacing:.2px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;
  background:linear-gradient(90deg,var(--cyan),var(--neon));-webkit-background-clip:text;background-clip:text;color:transparent}
.repo-card-desc{color:var(--muted);font-size:13px;line-height:1.45;display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden;min-height:1.4em}
.repo-card-by{display:flex;align-items:center;gap:6px;font-size:12px;color:var(--muted);margin-top:auto}
/* Where the code lives. Muted by default (it's context, not a headline); repos on THIS host get the
   accent, because those are the ones you can browse, clone and push without leaving. */
.repo-host{margin-inline-start:auto;flex:none;max-width:45%;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;
  font-size:11px;font-weight:700;letter-spacing:.2px;padding:2px 8px;border-radius:999px;
  border:1px solid var(--line);background:var(--panel);color:var(--muted)}
.repo-host.here{border-color:rgba(var(--accent-rgb),.5);color:var(--neon);background:rgba(var(--accent-rgb),.12)}
.repo-card-av{width:20px;height:20px;border-radius:50%;object-fit:cover}
/* A profile whose `name` is a raw hex pubkey is common on an open relay, and 64 chars ran straight
   through the host chip and out of the card. Clip it instead. */
.repo-card-by .name{color:var(--text);cursor:pointer;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.repo-card-by .name:hover{color:var(--neon)}
/* One row, always. Three default-size buttons overflowed a 240px card and wrapped "Web" onto a line of
   its own, which made every card in the grid a different height — the single ugliest thing about the
   All-repos wall. They shrink to fit instead. */
.repo-card-acts{display:flex;gap:6px;flex-wrap:nowrap}
.repo-card-acts>.btn{flex:1 1 auto;min-width:0;padding:6px 8px;font-size:12px;
  white-space:nowrap;overflow:hidden;text-overflow:ellipsis;text-align:center}
.tor-title{font-size:20px;font-weight:700;color:var(--text);margin-bottom:6px;word-break:break-word}
.tor-desc{margin:8px 0;line-height:1.45;color:var(--text);white-space:pre-wrap;word-wrap:break-word;font-size:15px}
.tor-tags{display:flex;flex-wrap:wrap;gap:6px;margin:8px 0}
.tor-tag{font-size:12px;padding:2px 10px;border-radius:11px;border:1px solid var(--line);color:var(--muted);background:var(--panel2)}
.tor-actions{gap:8px;margin-top:10px;flex-wrap:wrap}
.art-by .name{cursor:pointer;color:var(--text)}.art-by .name:hover{color:var(--neon)}
.article-view{padding:20px 22px 60px;max-width:none;margin:0 auto;animation:fade .25s}   /* wide format — fill the column, like Markets/News (not a centered reading column) */
/* NIP-34 repo detail (README + issues + patches) */
/* Fill the column like Markets / Streams / Articles (max-width:none), not a 900px reading column.
   A forge view is tables and file lists — code lines, paths, commit rows — and those were wrapping
   inside a narrow box with dead space beside them on a wide screen. */
.repo-view{padding:20px 22px 60px;animation:fade .25s;max-width:none}
.rv-head{border:1px solid var(--line);border-radius:14px;padding:16px 18px;margin:10px 0 6px;
  background:linear-gradient(180deg,rgba(var(--accent2-rgb),.05),transparent)}
.rv-headrow{display:flex;align-items:center;gap:12px}
.rv-avatar{width:46px;height:46px;border-radius:12px;object-fit:cover;border:1px solid var(--line);cursor:pointer;flex:none}
.rv-headmain{min-width:0}
.rv-title{font-size:24px;line-height:1.2;margin:0;font-weight:800;letter-spacing:.3px;word-break:break-word;
  background:linear-gradient(90deg,var(--cyan),var(--neon));-webkit-background-clip:text;background-clip:text;color:transparent}
.rv-by{display:flex;align-items:center;gap:6px;margin-top:4px;font-size:13px;color:var(--muted)}
.rv-by .name{color:var(--text);cursor:pointer}.rv-by .name:hover{color:var(--neon)}
/* Co-maintainers as faces, beside the owner's name. Overlapped, the way every forge draws a
   contributor strip — and negative margin only BETWEEN them (:first-child resets), or the row's
   first avatar slides under the word before it. */
.rv-maints{display:inline-flex;align-items:center;gap:6px;margin-inline-start:8px;flex-wrap:wrap}
.rv-maint-av{width:20px;height:20px;border-radius:50%;object-fit:cover;cursor:pointer;
  border:1px solid var(--line);background:var(--panel);margin-inline-start:-6px}
.rv-maint-av:first-of-type{margin-inline-start:0}
.rv-maint-av:hover{border-color:var(--neon);z-index:1}
@media(max-width:520px){.rv-by{flex-wrap:wrap}}
.rv-desc{margin:12px 0 0;color:var(--text);line-height:1.45}
.rv-clone{display:flex;align-items:center;gap:8px;margin-top:14px;flex-wrap:wrap}
/* A sprite branch mark, not the ⎇ character: U+2387 is in no font this ships or can assume, so it
   drew as a tofu box — and once the clone URL took a line of its own on a phone, that box was alone
   on the line above it. */
.rv-clone-ico{color:var(--cyan);font-size:17px;flex:none;display:inline-flex;align-items:center}
.rv-clone-url{flex:1;min-width:180px;font-family:var(--mono);font-size:12px;color:var(--text);
  background:rgba(var(--accent2-rgb),.08);border:1px solid var(--line);border-radius:8px;padding:8px 10px;overflow-x:auto;white-space:nowrap}
/* Copy / Share / Web / Edit / Delete. Their own flex row so they stay TOGETHER when the clone URL is
   long: as loose children of .rv-clone each wrapped independently, leaving a single orphaned button on
   a line of its own. On a phone the URL takes the first line and the buttons take the next.
   min-width, NOT flex-grow: `button:has(> svg.ic.b-ic:only-child)` up at the top of this file sets
   `flex:none` on every one of these (`:only-child` counts ELEMENTS, so a button's text label does not
   exempt it the way that rule's comment assumes), and it outweighs anything selector-shaped like
   `.rv-acts>.btn`. So they sit at their natural width with a floor under the short labels, which keeps
   Edit and Delete the same size instead of Delete being a phone-wide red-hot target. */
.rv-acts{display:flex;align-items:center;gap:8px;flex-wrap:wrap;flex:none}
/* The repo Announce/Edit sheet's description box. `.modal textarea` (0,1,1) forces min-height:220px,
   which is a post composer's height: it buried Save below the fold on a phone for a field whose
   whole job is one or two lines. */
.modal textarea.rp-ta{min-height:76px}
@media(max-width:560px){
  .repo-view{padding:16px 14px 60px}
  .rv-clone-url{flex:1 1 auto;min-width:0}
  .rv-acts{flex:1 1 100%}
  /* One size for all five. Left alone they come out 31/33/34px tall — a plain button, an icon+label
     button and an <a> pad differently — which reads as a wobbly row and gives the shortest one the
     worst tap target of the set. */
  .rv-acts>.btn{min-width:92px;min-height:34px;display:inline-flex;align-items:center;justify-content:center;text-align:center}
}
.rv-tabs{display:flex;gap:4px;margin:20px 0 0;border-bottom:1px solid var(--line);flex-wrap:wrap}
.rv-tab{background:none;border:none;border-bottom:2px solid transparent;color:var(--muted);cursor:pointer;font:inherit;
  font-weight:600;padding:10px 14px;margin-bottom:-1px;border-radius:8px 8px 0 0;transition:color .15s}
.rv-tab:hover{color:var(--text)}
.rv-tab.active{color:var(--neon);border-bottom-color:var(--neon)}
.rv-count{display:inline-block;min-width:18px;padding:0 6px;font-size:11px;font-weight:700;line-height:16px;text-align:center;
  border-radius:9px;background:rgba(var(--accent2-rgb),.15);color:var(--muted)}
.rv-count:empty{display:none}
.rv-tab.active .rv-count{background:var(--neon);color:var(--bg,#000)}
.rv-panel{padding-top:16px;animation:fade .2s}
.rv-readme{border:1px solid var(--line);border-radius:12px;padding:16px 20px;margin:0;overflow-x:auto}
.rv-collab-hd{display:flex;align-items:center;justify-content:space-between;gap:10px;flex-wrap:wrap;margin-bottom:12px}
.rv-collab{display:flex;flex-direction:column;gap:10px}
.rv-empty{padding:18px;text-align:center;border:1px dashed var(--line);border-radius:12px}
.collab-row{border:1px solid var(--line);border-radius:12px;padding:12px 14px;cursor:pointer;
  transition:border-color .15s,transform .05s;background:linear-gradient(180deg,rgba(var(--accent2-rgb),.03),transparent)}
.collab-row:hover{border-color:var(--neon)}
.collab-row:active{transform:translateY(1px)}
.collab-title{font-weight:700;margin-bottom:4px;word-break:break-word;display:flex;gap:8px;align-items:baseline}
.collab-ico{flex:none}
/* Attachment count on an issue/patch row. margin-inline-start:auto parks it at the far edge so it can't be
   pushed off by a long subject (the title wraps; a flex sibling would otherwise get squeezed to 0). */
/* NIP-34 issue state. The badge sits with the title; the actions live in the meta row so a row still
   opens the thread on click — the buttons stopPropagation so acting on an issue doesn't navigate. */
.collab-state{flex:none;font-size:11px;font-weight:700;padding:2px 8px;border-radius:999px;
  border:1px solid var(--line);white-space:nowrap}
.collab-state.st-open{color:#3fd07a;border-color:rgba(63,208,122,.45)}
.collab-state.st-closed{color:#ff6b81;border-color:rgba(255,107,129,.45)}
.collab-state.st-resolved{color:var(--cyan);border-color:rgba(0,229,255,.4)}
.collab-state.st-draft{color:var(--muted)}
.collab-filter{display:flex;gap:6px;margin:0 0 10px}
.cf-tab{font:inherit;font-size:12px;font-weight:600;padding:6px 12px;border-radius:999px;cursor:pointer;
  background:none;border:1px solid var(--line);color:var(--muted);transition:.15s}
.cf-tab:hover{color:var(--text);border-color:var(--neon)}
.cf-tab.on{color:var(--neon);border-color:var(--neon);box-shadow:var(--glow)}
.cf-act{font:inherit;font-size:11px;font-weight:600;margin-inline-start:6px;padding:4px 10px;border-radius:8px;
  cursor:pointer;background:none;border:1px solid var(--line);color:var(--muted);transition:.15s}
.cf-act:hover{color:var(--neon);border-color:var(--neon)}
.cf-act:disabled{opacity:.5;cursor:default}
.collab-att{flex:none;margin-inline-start:auto;font-size:12px;font-weight:600;color:var(--muted);opacity:.85}
.collab-body{color:var(--muted);font-size:13px;line-height:1.45;margin:2px 0 8px;word-break:break-word}
.collab-meta{display:flex;align-items:center;gap:6px;font-size:12px;color:var(--muted)}
.collab-av{width:20px;height:20px;border-radius:50%;object-fit:cover}
.collab-meta .name{color:var(--text);cursor:pointer}.collab-meta .name:hover{color:var(--neon)}
/* Files browser */
.fb-crumbs{display:flex;align-items:center;gap:6px;flex-wrap:wrap;margin-bottom:10px;font-size:13px}
.fb-crumb{color:var(--cyan);cursor:pointer}.fb-crumb:hover{color:var(--neon);text-decoration:underline}
.fb-sep{color:var(--muted)}
.fb-list{border:1px solid var(--line);border-radius:12px;overflow:hidden}
.fb-row{display:flex;align-items:center;gap:10px;padding:10px 14px;cursor:pointer;border-bottom:1px solid var(--line);transition:background .12s}
.fb-row:last-child{border-bottom:none}
.fb-row:hover{background:rgba(var(--accent2-rgb),.07)}
.fb-ico{flex:none}
.fb-name{flex:0 1 auto;min-width:0;max-width:42%;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;word-break:break-all}
.fb-size{color:var(--muted);font-size:12px;font-variant-numeric:tabular-nums}
/* Forge-style file row: name · last commit message · when · size. The message takes the slack so
   the date and size stay right-aligned in a column, like GitHub/Gitea. */
.fb-cmsg{flex:1 1 auto;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;
  color:var(--muted);font-size:12px}
.fb-row:hover .fb-cmsg{color:var(--text)}
.fb-cwhen{flex:none;width:62px;text-align:end;color:var(--muted);font-size:12px;font-variant-numeric:tabular-nums}
.fb-headbar{display:flex;align-items:center;gap:10px;padding:10px 14px;margin:0 0 -1px;
  background:var(--panel2);border:1px solid var(--line);border-radius:12px 12px 0 0;font-size:12px}
.fb-headbar+.fb-list{border-radius:0 0 12px 12px}
.fb-hsha{flex:none;font-family:var(--mono);color:var(--cyan)}
.fb-hmsg{flex:1 1 auto;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.fb-hby,.fb-hwhen{flex:none;color:var(--muted)}
/* Commits list */
.cm-list{border:1px solid var(--line);border-radius:12px;overflow:hidden}
.cm-row{display:flex;align-items:center;gap:12px;padding:10px 14px;border-bottom:1px solid var(--line)}
.cm-row:last-child{border-bottom:none}
.cm-row:hover{background:rgba(var(--accent2-rgb),.07)}
.cm-main{flex:1;min-width:0}
.cm-subj{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-size:15px}
.cm-meta{display:flex;gap:8px;color:var(--muted);font-size:12px;margin-top:2px}
.cm-sha{flex:none;cursor:pointer;background:var(--panel2);border:1px solid var(--line);color:var(--cyan);
  border-radius:8px;padding:4px 10px;font-size:12px;font-family:var(--mono);transition:.15s}
.cm-sha:hover{color:var(--neon);border-color:var(--neon)}
/* Phones: drop the commit-message column, keep name + date (a 360px row can't hold four columns). */
@media(max-width:600px){
  .fb-name{max-width:none;flex:1 1 auto}
  .fb-cmsg,.fb-hby{display:none}
}
.fb-fileview{margin-top:14px;border:1px solid var(--line);border-radius:12px;overflow:hidden}
.fb-fvhd{display:flex;align-items:center;gap:10px;flex-wrap:wrap;padding:10px 14px;border-bottom:1px solid var(--line);
  font-weight:700;font-size:13px;background:rgba(var(--accent2-rgb),.06)}
.fb-fvname{flex:1 1 auto;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;word-break:break-all}
.fb-fvacts{flex:none;display:flex;gap:6px;flex-wrap:wrap}
.fb-fileview .markdown{padding:16px 20px}
.fb-code{margin:0;padding:14px 16px;overflow-x:auto;font-family:var(--mono);font-size:12px;line-height:1.45;white-space:pre;color:var(--text)}
/* A source file: a number column beside the code, both on ONE baseline grid.
   The gutter is its own <pre> so selecting the code selects the CODE — a ::before counter is copied
   as text in some browsers, which is how "paste the snippet" ends up with a number on every line.
   Only the code column scrolls sideways; the numbers stay put, which is the whole reason they are
   readable on a phone. The line-height/font-size are shared so line 200 is level with line 200. */
.fb-codewrap{display:flex;align-items:stretch;min-width:0;background:var(--panel2)}
.fb-gutter{margin:0;padding:14px 10px 14px 14px;flex:none;text-align:right;user-select:none;
  color:var(--muted);opacity:.65;background:var(--panel);border-right:1px solid var(--line);
  font-family:var(--mono);font-size:12px;line-height:1.45;white-space:pre}
.fb-codewrap .fb-code{flex:1 1 auto;min-width:0;padding-left:14px}
.fb-hlnote{padding:8px 14px;border-bottom:1px solid var(--line);background:rgba(var(--accent2-rgb),.05)}
/* Branch / tag switcher */
.rv-refbar{display:flex;align-items:center;gap:10px;margin-top:12px;flex-wrap:wrap}
/* "Source" sits at the far end of the ref bar: it downloads whatever the branch button says, so it
   belongs on that row and nowhere else — and margin-inline-start (not margin-left) so it stays at
   the end in an RTL locale rather than crossing to the other side of its own control. */
.rv-dlsrc{margin-inline-start:auto}
.rv-refbtn{font-family:var(--mono);font-size:12px}
.fb-tools{display:flex;gap:8px;flex-wrap:wrap;align-items:center;margin-bottom:10px}
/* File finder: the search box owns the row and the New file button keeps its natural width, so on a
   phone the button wraps under a full-width box instead of squeezing it to nothing. */
.fb-find{flex:1 1 220px;min-width:0;margin:0;font-size:13px}
.fb-findout{border:1px solid var(--line);border-radius:12px;overflow:hidden;background:var(--panel2);
  max-height:60vh;overflow-y:auto}
.fb-hit{display:flex;align-items:baseline;gap:8px;padding:8px 12px;cursor:pointer;min-width:0;
  border-bottom:1px solid var(--line)}
.fb-hit:last-child{border-bottom:0}
.fb-hit:hover,.fb-hit.on{background:rgba(var(--accent-rgb),.14)}
.fb-hit.on{box-shadow:inset 2px 0 0 var(--neon)}
.fb-hitname{font-weight:700;flex:none;max-width:60%;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
/* The DIRECTORY is the part that disambiguates two files with the same name, so when it does not fit
   it is trimmed from the LEFT — the tail (`.../services/git.py`) is what tells them apart. */
.fb-hitdir{color:var(--muted);font-size:12px;font-family:var(--mono);min-width:0;overflow:hidden;
  text-overflow:ellipsis;white-space:nowrap;direction:rtl;text-align:left}
.rv-scope{padding:8px 12px;margin-bottom:10px;border:1px dashed var(--line);border-radius:10px}
/* Web editor: one monospace box, sized so a phone keyboard still leaves usable text visible. */
.fb-edit{padding:12px 14px;display:flex;flex-direction:column;gap:10px}
.fb-editor{width:100%;min-height:46vh;resize:vertical;font-family:var(--mono);font-size:12px;
  line-height:1.45;white-space:pre;overflow-wrap:normal;overflow-x:auto;tab-size:4}
.fb-editacts{display:flex;align-items:center;justify-content:space-between;gap:10px;flex-wrap:wrap}
.fb-editbtns{display:flex;gap:8px;flex:none}
/* Commit view (one commit + its diff) */
.cmv-top{display:flex;align-items:center;justify-content:space-between;gap:8px;margin-bottom:10px;flex-wrap:wrap}
.cmv-subj{margin:0 0 6px;font-size:17px;line-height:1.45;word-break:break-word}
.cmv-body{margin:0 0 10px;padding:10px 12px;background:rgba(var(--accent2-rgb),.06);border-radius:10px;
  font-family:var(--mono);font-size:12px;line-height:1.45;white-space:pre-wrap;color:var(--muted)}
.cmv-meta{margin-bottom:14px}
.df{border:1px solid var(--line);border-radius:12px;margin-bottom:12px;overflow:hidden}
.df-hd{display:flex;align-items:center;gap:10px;padding:10px 14px;cursor:pointer;background:var(--panel2);font-size:12px}
.df-hd::-webkit-details-marker{display:none}
.df-path{flex:1 1 auto;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;word-break:break-all;
  font-family:var(--mono)}
.df-stat{flex:none;display:flex;gap:8px;font-variant-numeric:tabular-nums}
.df-add{color:#3fb950}.df-del{color:#f85149}.df-bin{color:var(--muted)}
.df-body{overflow-x:auto;font-family:var(--mono);font-size:12px;line-height:1.45;background:var(--panel)}
/* One diff line = [old no][new no][text]. Grid keeps the gutters aligned without a table, and the text
   column sets its own width so a long line scrolls the file box rather than the page. */
.dl{display:grid;grid-template-columns:44px 44px 1fr;min-width:max-content}
.dl .dn{padding:0 6px;text-align:end;color:var(--muted);opacity:.65;user-select:none;
  border-inline-end:1px solid var(--line);font-variant-numeric:tabular-nums}
.dl .dt{padding:0 12px;white-space:pre}
.dl-add{background:rgba(63,185,80,.12)}.dl-add .dt{color:#7ee787}
.dl-del{background:rgba(248,81,73,.12)}.dl-del .dt{color:#ffa198}
.dl-hunk{background:rgba(var(--accent2-rgb),.12)}.dl-hunk .dt{color:var(--cyan)}
.dl-meta .dt{color:var(--muted)}
@media(max-width:600px){
  /* Phones: one gutter (the new-file line numbers) — two 44px columns eat a third of a 360px screen. */
  .dl{grid-template-columns:34px 1fr}
  .dl .dn:first-child{display:none}
  .df-body{font-size:11px}
  .fb-editor{min-height:38vh;font-size:12px}
  .fb-fvacts{width:100%}
}
.av-banner{width:100%;max-height:340px;object-fit:cover;border-radius:16px;border:1px solid var(--line);margin:16px 0;box-shadow:0 0 30px rgba(var(--accent2-rgb),.18)}
.av-title{font-size:30px;line-height:1.2;margin:12px 0;font-weight:800;letter-spacing:.3px;
  background:linear-gradient(90deg,var(--cyan),var(--neon));-webkit-background-clip:text;background-clip:text;color:transparent}
.av-by{display:flex;align-items:center;gap:8px;margin-bottom:16px;font-size:15px;color:var(--muted)}
.av-by .name{color:var(--text);cursor:pointer}.av-by .name:hover{color:var(--neon)}
.av-actions{display:flex;gap:8px;margin:8px 0 20px;border-bottom:1px solid var(--line);padding-bottom:16px}
.av-body{font-size:20px;line-height:1.6}
/* article comments (NIP-22) under the reader — header wraps on narrow phones so the button never overflows */
.av-comments{margin-top:30px;border-top:1px solid var(--line);padding-top:6px}
.av-comments-hd{display:flex;align-items:center;justify-content:space-between;gap:10px;flex-wrap:wrap;margin-bottom:4px}
.av-comments-hd .search-section-title{padding:8px 0}
.av-comments .empty{padding:18px 4px}
/* threaded comment cards (nested replies indent via inline margin-left, capped for phones) */
.ac-item{padding:10px 0 6px;border-top:1px solid var(--line)}
.ac-hd{display:flex;align-items:center;gap:8px;font-size:13px;flex-wrap:wrap}
.ac-av{width:26px;height:26px;border-radius:50%;object-fit:cover;flex:none}
.ac-hd .name{font-weight:700}
.ac-hd .handle{color:var(--muted);font-size:12px}
.ac-hd .time{color:var(--muted);font-size:12px;margin-inline-start:auto}
.ac-body{margin:6px 0 4px;font-size:15px;line-height:1.45;white-space:pre-wrap;word-break:break-word}
.ac-body img{max-width:100%;border-radius:10px}
.ac-reply{padding:4px 12px;font-size:12px}
.article-editor{padding:18px 20px 60px;max-width:none;margin:0 auto;display:flex;flex-direction:column;gap:12px;animation:fade .2s}   /* wide format (also used by New/Edit listing) — fill the column, not centered */
.article-body{width:100%;resize:none;font-family:inherit;font-size:15px;line-height:1.6}

/* ===== Article editor: write and preview, side by side ==========================================
   Two panes of equal weight on a desktop, one at a time behind the Write/Preview tabs on a phone.
   Both panes are always in the DOM — the tabs only decide which is SHOWN — so switching is instant
   and the preview never has to be rebuilt. Every colour is a theme token, so all seven themes get
   this without a second pass. */
.ae-split{display:grid;grid-template-columns:1fr 1fr;gap:var(--sp-3, 10px);align-items:stretch;min-width:0}
.ae-pane{display:flex;flex-direction:column;min-width:0;border:1px solid var(--line);border-radius:12px;
  background:var(--panel);overflow:hidden}
/* A quiet label on each half. Without it the two columns read as one wrapped box, and on a narrow
   desktop window it is genuinely unclear which side is which. */
.ae-pane-hd{flex:0 0 auto;padding:7px 12px;font-size:11px;font-weight:700;letter-spacing:1px;
  text-transform:uppercase;color:var(--muted);background:var(--panel2);border-bottom:1px solid var(--line)}
/* THE HEIGHT IS ON THE PANE, not on the textarea, and both panes scroll their own content. Sizing the
   textarea instead lets the two halves grow to different heights, which is what makes a split view
   look broken the moment one side has more in it than the other. */
.ae-pane > .article-body, .ae-pane > .article-preview{
  flex:1 1 auto;height:clamp(320px, 52dvh, 720px);overflow-y:auto;margin:0;border:0;border-radius:0;
  background:transparent}
.ae-pane > .article-body{padding:12px 14px}
.ae-pane > .article-body:focus{outline:none;box-shadow:none}
.ae-pane > .article-preview{padding:12px 14px;overflow-wrap:anywhere}
/* The preview renders real article markup — keep the first heading from adding a big empty gap at
   the top of a pane that already has a label. */
.ae-pane > .article-preview > :first-child{margin-top:0}
.ae-pane:focus-within{border-color:var(--neon);box-shadow:0 0 0 1px rgba(var(--accent-rgb),.35)}
/* Above 820px both panes are visible, so a Write/Preview switch would mean nothing. */
.ae-tabs{display:none}

@media(max-width:820px){
  /* One column, and the tabs come back as the way to choose. Whichever pane is hidden is hidden
     completely — `display:none`, not width:0 — so nothing of it can push the page sideways. */
  .ae-split{grid-template-columns:1fr}
  .ae-tabs{display:flex}
  .ae-split .ae-pane-hd{display:none}                    /* the tab already says which one this is */
  .ae-split:not(.show-preview) .ae-pane:last-child{display:none}
  .ae-split.show-preview .ae-pane:first-child{display:none}
  .ae-pane > .article-body, .ae-pane > .article-preview{height:clamp(280px, 48dvh, 560px)}
  .ae-pane > .article-body{font-size:16px}               /* 16px or iOS zooms on focus and never zooms back */
  .cmp-tab{min-height:38px}                              /* thumb-sized, like every other control here */
}

/* shared markdown typography (articles + previews) */
.markdown{font-size:17px;line-height:1.6}
.markdown h2{font-size:24px;margin:24px 0 10px;font-weight:800;color:var(--text);border-bottom:1px solid var(--line);padding-bottom:6px}
.markdown h3{font-size:20px;margin:20px 0 8px;color:var(--cyan)}
.markdown h4,.markdown h5,.markdown h6{font-size:17px;margin:16px 0 6px;color:var(--neon)}
.markdown p{margin:0 0 16px}
.markdown a{color:var(--cyan);text-decoration:underline;text-underline-offset:2px;word-break:break-word}
.markdown a:hover{color:var(--neon);text-shadow:0 0 8px rgba(var(--accent-rgb),.5)}
.markdown img{max-width:100%;border-radius:12px;border:1px solid var(--line);margin:10px 0;cursor:zoom-in}
/* An article may contain an original phone photo several thousand pixels tall.  It remains
   available at full size through the existing lightbox, but the reading surface must not turn one
   inline image into several screens of scrolling. Preserve its aspect ratio and fit it inside a
   useful reading viewport instead of cropping it. */
.article-view .av-body img{display:block;width:auto;max-width:100%;max-height:min(60vh,560px);
  object-fit:contain;margin:14px auto}
.markdown ul,.markdown ol{margin:0 0 16px;padding-inline-start:24px}
.markdown li{margin:6px 0}
.markdown li::marker{color:var(--neon2)}
.markdown blockquote{margin:0 0 16px;padding:8px 16px;border-inline-start:3px solid var(--neon);color:var(--muted);
  background:linear-gradient(90deg,rgba(var(--accent-rgb),.08),transparent);border-radius:0 10px 10px 0}
.markdown pre{background:var(--bg2);border:1px solid var(--line);border-radius:12px;padding:14px 16px;overflow-x:auto;margin:0 0 16px;box-shadow:inset 0 0 20px rgba(0,0,0,.4)}
.markdown code{background:rgba(var(--accent-rgb),.16);color:var(--cyan);padding:2px 6px;border-radius:6px;font-size:.92em;font-family:var(--mono)}
.markdown pre code{background:none;padding:0;color:var(--text)}
.markdown hr{border:none;height:1px;background:linear-gradient(90deg,transparent,var(--neon2),transparent);margin:24px 0}

/* ===== Streams (NIP-53) ===== */
.stream-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(232px,1fr));gap:16px;padding:18px}
.stream-card{background:var(--panel2);border:1px solid var(--line);border-radius:16px;overflow:hidden;cursor:pointer;transition:.16s;animation:fade .25s}
.stream-card:hover{border-color:var(--neon);box-shadow:var(--glow),0 0 24px rgba(var(--accent2-rgb),.2);transform:translateY(-3px)}
.prof-streams .stream-card{animation:none}   /* profile Streams tab re-renders on lazy-fetch; suppress fade re-run to avoid flicker */
.stream-thumb{position:relative;aspect-ratio:16/9;background:radial-gradient(circle at 50% 40%,#1a0f33,var(--bg2));display:flex;align-items:center;justify-content:center;overflow:hidden}
.stream-thumb img{width:100%;height:100%;object-fit:cover;display:block;transition:.3s}
.stream-card:hover .stream-thumb img{transform:scale(1.05)}
.tl-notes>.community-card{display:grid;grid-template-columns:min(180px,28%) minmax(0,1fr);
  width:100%;border-radius:0;border-width:0 0 1px;animation:none}
.tl-notes>.community-card:hover{transform:none}
.tl-notes>.community-card .stream-thumb{aspect-ratio:auto;min-height:112px;max-height:156px}
.tl-notes>.community-card .stream-meta{min-width:0;align-self:center}
.stream-thumb.noimg img{display:none}
.stream-play{font-size:36px;color:var(--neon2);filter:drop-shadow(0 0 10px rgba(var(--accent2-rgb),.6))}
.stream-meta{padding:12px 14px}
.stream-title{font-weight:700;font-size:17px;margin-bottom:8px;line-height:1.45;display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden}
.live-badge{position:absolute;top:9px;inset-inline-start:9px;background:linear-gradient(135deg,#ff2b4d,var(--neon));color:#fff;font-size:11px;font-weight:800;
  padding:4px 10px;border-radius:7px;letter-spacing:.6px;box-shadow:0 0 14px rgba(255,43,77,.7);animation:livepulse 1.6s ease-in-out infinite}
@keyframes livepulse{0%,100%{box-shadow:0 0 10px rgba(255,43,77,.5)}50%{box-shadow:0 0 20px rgba(255,43,77,.95)}}
.av-title .live-badge{position:static;font-size:13px;vertical-align:middle;display:inline-block;-webkit-text-fill-color:#fff}
.ended-badge,.planned-badge{position:absolute;top:9px;inset-inline-start:9px;background:rgba(8,5,18,.75);border:1px solid var(--line);color:var(--muted);font-size:11px;padding:4px 10px;border-radius:7px;backdrop-filter:blur(4px)}
.planned-badge{color:var(--amber);border-color:rgba(255,207,43,.4)}
.stream-view{padding:20px 22px 60px;max-width:900px;margin:0 auto;animation:fade .25s}
.stream-player{width:100%;max-height:62vh;background:#000;border-radius:14px;border:1px solid var(--neon2);margin:14px 0;display:block;box-shadow:0 0 28px rgba(var(--accent2-rgb),.25)}

/* ===== Market / classified listings (NIP-99) ===== */
.mkt-filter{padding:12px 18px;border-bottom:1px solid var(--line);display:flex;flex-direction:column;gap:10px}
.mkt-cats{display:flex;flex-wrap:wrap;gap:8px}
.mkt-chip{background:var(--panel2);border:1px solid var(--line);color:var(--muted);font-size:12px;font-weight:600;
  padding:6px 12px;border-radius:20px;cursor:pointer;transition:.14s;white-space:nowrap}
.mkt-chip:hover{border-color:var(--neon);color:#fff}
.mkt-chip.on{background:linear-gradient(135deg,var(--neon),var(--neon2));color:#06010f;border-color:transparent;box-shadow:0 0 12px rgba(var(--accent2-rgb),.35)}
.mkt-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(218px,1fr));gap:16px;padding:18px}
.mkt-card{background:var(--panel2);border:1px solid var(--line);border-radius:16px;overflow:hidden;cursor:pointer;transition:.16s;animation:fade .25s;display:flex;flex-direction:column}
.mkt-card:hover{border-color:var(--neon);box-shadow:var(--glow),0 0 24px rgba(var(--accent2-rgb),.2);transform:translateY(-3px)}
.mkt-card.sold{opacity:.62}
.mkt-thumb{position:relative;aspect-ratio:4/3;background:radial-gradient(circle at 50% 40%,#1a0f33,var(--bg2));display:flex;align-items:center;justify-content:center;overflow:hidden}
.mkt-thumb img{width:100%;height:100%;object-fit:cover;display:block;transition:.3s}
.mkt-card:hover .mkt-thumb img{transform:scale(1.05)}
.mkt-noimg{font-size:40px;opacity:.5}
.mkt-sold-badge{background:linear-gradient(135deg,#ff2b4d,var(--neon));color:#fff;font-size:11px;font-weight:800;padding:4px 10px;border-radius:7px;letter-spacing:.6px;box-shadow:0 0 14px rgba(255,43,77,.6)}
.mkt-thumb .mkt-sold-badge{position:absolute;top:9px;inset-inline-start:9px}
.mkt-info{padding:12px 14px;display:flex;flex-direction:column;gap:6px;flex:1}
.mkt-price{font-size:20px;font-weight:800;color:var(--neon);text-shadow:0 0 10px rgba(var(--accent-rgb),.4)}
.mkt-title{font-weight:650;font-size:15px;line-height:1.45;display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden}
.mkt-loc{font-size:12px;color:var(--muted)}
.mkt-info .art-by{margin-top:auto}
.listing-view{padding:18px 22px 60px;max-width:860px;margin:0 auto;animation:fade .25s}
.li-gallery{margin:14px 0}
.li-main{width:100%;max-height:460px;object-fit:contain;background:#06010f;border-radius:16px;border:1px solid var(--line);box-shadow:0 0 28px rgba(var(--accent2-rgb),.18);cursor:zoom-in;display:block}
.li-thumbs{display:flex;gap:8px;flex-wrap:wrap;margin-top:10px}
.li-th{width:72px;height:54px;object-fit:cover;border-radius:9px;border:2px solid var(--line);cursor:pointer;transition:.14s}
.li-th.on,.li-th:hover{border-color:var(--neon2);box-shadow:0 0 12px rgba(var(--accent2-rgb),.4)}
.li-head{margin:6px 0}
.li-price{font-size:30px;font-weight:800;color:var(--neon);text-shadow:0 0 14px rgba(var(--accent-rgb),.4);display:flex;align-items:center;gap:10px}
.li-title{font-size:24px;margin:6px 0;line-height:1.2}
.li-cats{display:flex;flex-wrap:wrap;gap:8px;margin-top:8px}
.li-by{display:flex;align-items:center;gap:10px;margin:12px 0;cursor:pointer}
.li-actions{display:flex;flex-wrap:wrap;gap:10px;margin:14px 0;padding-bottom:6px;border-bottom:1px solid var(--line)}
.li-body{margin-top:14px}
.le-pricerow{display:grid;grid-template-columns:1fr 1fr 1fr;gap:10px}
.le-imgs{display:flex;flex-wrap:wrap;gap:10px}
.le-img{position:relative;width:90px;height:90px}
.le-img img{width:100%;height:100%;object-fit:cover;border-radius:10px;border:1px solid var(--line)}
.le-img-x{position:absolute;top:-7px;inset-inline-end:-7px;width:22px;height:22px;border-radius:50%;border:none;background:#ff2b4d;color:#fff;cursor:pointer;font-size:12px;line-height:1;box-shadow:0 0 8px rgba(255,43,77,.6)}

@media(max-width:820px){
  .article-card{padding:14px}
  .art-img{width:98px;height:68px}
  .av-title{font-size:24px}
  .article-view,.article-editor,.stream-view{padding:16px 14px 60px}
  .stream-grid{grid-template-columns:1fr 1fr;gap:10px;padding:12px}
  .tl-notes>.community-card{grid-template-columns:108px minmax(0,1fr)}
  .tl-notes>.community-card .stream-thumb{min-height:96px;max-height:124px}
  .av-body{font-size:17px}
  .mkt-grid{grid-template-columns:1fr 1fr;gap:12px;padding:12px}
  .mkt-filter{padding:10px 12px}
  .mkt-price{font-size:17px}
  .mkt-title{font-size:15px}
  .listing-view{padding:14px 14px 60px}
  .li-title{font-size:24px}
  .li-price{font-size:24px}
  .le-pricerow{grid-template-columns:1fr 1fr}
  .le-pricerow .fld:first-child{grid-column:1/-1}
  .li-actions .btn{flex:1 1 auto}
}

/* ===== floating mini stream player (desktop only) ===== */
.mini-player{position:fixed;inset-inline-end:18px;bottom:18px;width:330px;z-index:350;background:#000;
  border:1px solid var(--neon);border-radius:14px;overflow:hidden;display:none;animation:fade .2s;
  resize:both;min-width:220px;min-height:150px;max-width:92vw;max-height:88vh;   /* drag the bar, resize the corner */
  box-shadow:0 0 0 1px rgba(var(--accent-rgb),.35),0 0 34px rgba(var(--accent2-rgb),.4)}
.mini-player.on{display:flex;flex-direction:column}
.mini-player video{flex:1;width:100%;min-height:0;height:auto;display:block;background:#000;object-fit:contain}
.mini-bar{display:flex;align-items:center;gap:6px;padding:6px 6px 6px 12px;flex:none;cursor:move;touch-action:none;background:linear-gradient(90deg,rgba(var(--accent2-rgb),.28),var(--panel2))}
.mini-title{flex:1;font-size:12px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;color:var(--text);letter-spacing:.3px}
.mini-x{background:transparent;border:none;color:var(--muted);cursor:pointer;font-size:15px;padding:2px 8px;line-height:1;transition:.13s}
.mini-x:hover{color:var(--neon);text-shadow:0 0 8px rgba(var(--accent-rgb),.6)}
@media(max-width:820px){ .mini-player{display:none!important} }

/* ===== emoji reaction popover (anchored under the react button) ===== */
/* Emoji picker. A COLUMN (search box + scrolling grid) rather than one scrolling wrap: with the
   instance's custom packs the list runs to thousands, so the search has to stay pinned while the
   grid under it scrolls. The grid — not the popover — owns the scrolling and the height cap. */
/* NO backdrop-filter here, unlike the other popovers. This one opens OVER the composer modal, which
   is itself two blur layers (.modal-bg blur(4) + .glass blur(7)) — a third one on top means every
   scroll of a grid holding hundreds of images re-blurs the whole stack, which is exactly why the
   picker crawled in "New post" while the identical picker on the timeline (no modal behind it) was
   fine. A solid panel behind it costs nothing and reads better over a busy backdrop anyway. */
.emoji-pop{position:fixed;z-index:360;display:flex;flex-direction:column;width:312px;max-width:92vw;padding:10px;
  background:var(--bg2);border:1px solid var(--neon);border-radius:14px;contain:layout paint;
  box-shadow:0 0 0 1px rgba(var(--accent-rgb),.3),0 0 26px rgba(var(--accent-rgb),.3);animation:pop .14s ease-out}
@keyframes pop{from{opacity:0;transform:translateY(-6px) scale(.96)}to{opacity:1;transform:none}}
.ep-head{padding:0 0 8px;flex:0 0 auto}
.ep-q{width:100%;box-sizing:border-box;background:rgba(var(--accent-rgb),.06);color:var(--text);
  border:1px solid var(--line);border-radius:9px;padding:8px 10px;font:inherit;font-size:15px;outline:none}
.ep-q:focus{border-color:var(--neon);box-shadow:0 0 0 1px rgba(var(--accent-rgb),.3)}
/* Pack tabs (Pleroma-style): recents, the unicode set, then one chip per instance pack. Scrolls
   sideways rather than wrapping — a node with a dozen packs must not push the grid off the sheet. */
.ep-tabs{display:flex;gap:4px;overflow-x:auto;overflow-y:hidden;padding:8px 0 0;scrollbar-width:none}
.ep-tabs::-webkit-scrollbar{display:none}
.emoji-pop .ep-tab{display:inline-flex;align-items:center;gap:6px;flex:0 0 auto;
  font-size:12px;line-height:1;padding:6px 10px;border-radius:8px;color:var(--muted);
  border:1px solid transparent;max-width:132px}
.emoji-pop .ep-tab:hover{transform:none;background:rgba(var(--accent-rgb),.12)}
.emoji-pop .ep-tab.on{color:var(--text);background:rgba(var(--accent-rgb),.16);border-color:var(--neon)}
.emoji-pop .ep-tab img{width:16px;height:16px;object-fit:contain;display:block}
.emoji-pop .ep-tab span{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.ep-grid{display:flex;flex-wrap:wrap;align-content:flex-start;gap:4px;overflow-y:auto;
  max-height:min(52vh,308px);overscroll-behavior:contain;padding-top:8px}
.emoji-pop button{background:transparent;border:none;font-size:24px;cursor:pointer;padding:6px 8px;border-radius:9px;line-height:1;transition:.1s}
.emoji-pop button:hover{background:rgba(var(--accent-rgb),.18);transform:scale(1.18)}
/* Custom (image) emoji: a fixed box so a 4 MB source and a 32px sticker occupy the same cell and
   the grid stays a grid. `loading=lazy` + the server's 72px thumbnails keep scrolling cheap. */
.emoji-pop button.ce{padding:4px}
.emoji-pop button.ce img{display:block;width:30px;height:30px;object-fit:contain}
/* Off-screen cells are skipped entirely by the compositor — with a 3000-emoji pack the grid holds
   hundreds of <img> and painting them all on every scroll frame was the other half of the New-post
   slowdown. The intrinsic size keeps the scrollbar honest while they're skipped. */
.ep-grid>button{content-visibility:auto;contain-intrinsic-size:36px 36px}
.ep-empty{padding:14px 6px;color:var(--muted);font-size:13px;width:100%;text-align:center}
/* `:shortcode` autocomplete — the drop-down every composer shares (attachEmojiAutocomplete). Fixed
   position on <html> like the popovers (body carries zoom on desktop), and it opens ABOVE the box by
   default because on a phone the keyboard owns the space below the caret. */
.em-ac{position:fixed;z-index:370;display:flex;flex-direction:column;max-height:min(46vh,260px);overflow-y:auto;
  padding:6px;background:var(--bg2);border:1px solid var(--neon);border-radius:12px;
  box-shadow:0 0 0 1px rgba(var(--accent-rgb),.28),0 6px 26px rgba(0,0,0,.45);overscroll-behavior:contain}
.em-ac-row{display:flex;align-items:center;gap:8px;width:100%;background:transparent;border:none;cursor:pointer;
  color:var(--text);font:inherit;font-size:13px;text-align:start;padding:6px 8px;border-radius:8px}
.em-ac-row img{width:22px;height:22px;object-fit:contain;flex:0 0 auto}
.em-ac-row span{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.em-ac-row:hover,.em-ac-row.on{background:rgba(var(--accent-rgb),.18)}
@media(max-width:820px){ .em-ac-row{font-size:15px;padding:10px 10px} .em-ac-row img{width:26px;height:26px} }

/* per-post "☰ more" menu popover (bookmark / copy id / delete / block)
   --pop-scale: these menus are children of <html>, not <body> (fixed-position math — see
   openMenuPopover), so the desktop `body{zoom:.67-.77}` tiers never reached them: a 15.5px menu hung
   off a UI scaled to 0.72, measured 1.22x-1.40x everything around it. _placePop passes the body's
   zoom down as --pop-scale for the ANCHORED desktop menus only (never the .sheet below, and never
   mobile, where zoom is 1 anyway). It SIZES rather than transforms: `transform` belongs to the `pop`
   keyframe, `zoom`'s effect on a fixed element's left/top is browser-specific, and both would throw
   off _placePop's real-pixel clamping — intrinsic size doesn't. Every scaled value reads the var
   DIRECTLY (no em chain off the container): with the property set on the popover, Chrome resolves
   the container but leaves an em-inheriting child at the unscaled size. */
.menu-pop{position:fixed;z-index:360;display:flex;flex-direction:column;min-width:calc(178px * var(--pop-scale,1));max-width:92vw;padding:calc(6px * var(--pop-scale,1));max-height:min(70vh,440px);overflow-y:auto;
  background:var(--panel2);backdrop-filter:blur(14px);-webkit-backdrop-filter:blur(14px);border:1px solid var(--neon);border-radius:14px;
  box-shadow:0 0 0 1px rgba(var(--accent-rgb),.3),0 0 26px rgba(var(--accent-rgb),.3);animation:pop .14s ease-out}
.menu-pop button{display:block;width:100%;text-align:start;background:transparent;border:none;color:var(--text);
  cursor:pointer;font-size:calc(15px * var(--pop-scale,1));padding:calc(12px * var(--pop-scale,1)) calc(14px * var(--pop-scale,1));border-radius:9px;transition:.12s;font-family:inherit;white-space:nowrap}
.menu-pop button:hover{background:rgba(var(--accent-rgb),.16);color:var(--cyan)}
.menu-pop button.danger{color:var(--danger)}
.menu-pop button.danger:hover{background:rgba(255,77,109,.16);color:var(--danger)}

/* Popover menus become a full-width bottom action-sheet on phones (always attached + readable,
   never floating off to the side of a tiny right-edge button) with a dim backdrop; the anchored
   dropdown is kept on desktop. */
.pop-backdrop{position:fixed;inset:0;z-index:355;background:rgba(4,2,12,.5);-webkit-backdrop-filter:blur(2px);backdrop-filter:blur(2px)}
@keyframes sheetup{from{opacity:0;transform:translateY(22px)}to{opacity:1;transform:none}}
@media(max-width:1179px){
  .menu-pop.sheet,.emoji-pop.sheet{inset-inline-start:0;inset-inline-end:0;bottom:calc(8px + env(safe-area-inset-bottom));top:auto;min-width:0;
    width:min(480px,calc(100vw - 16px));margin-inline:auto;max-height:calc(82vh - env(safe-area-inset-bottom));border-radius:18px;
    animation:sheetup .2s ease-out}
  .menu-pop.sheet button{font-size:17px;padding:16px 16px}
  /* Bottom sheet on phones: bigger touch targets, and the GRID takes the sheet's height (the sheet
     is a column, so its own max-height must not be what scrolls). */
  .emoji-pop.sheet .ep-grid{justify-content:center;max-height:calc(78vh - 118px - env(safe-area-inset-bottom))}
  .emoji-pop.sheet .ep-tab{font-size:13px;padding:8px 12px}
  .emoji-pop.sheet .ep-tab img{width:18px;height:18px}
  .emoji-pop.sheet button{font-size:24px;padding:8px 10px}
  .emoji-pop.sheet button.ce{padding:6px}
  .emoji-pop.sheet button.ce img{width:38px;height:38px}
}

/* ===== trending hashtag chips + inline #hashtags — cyberpunk, spaced out ===== */
.tag-cloud{display:flex;flex-wrap:wrap;gap:10px 12px;padding:4px 0 2px}
/* Topics: a 2-column GRID rather than a wrap. Variable-width pills wrapped raggedly — one long tag
   claimed a whole row while two short ones shared the next, so a third of the block was dead space.
   Equal cells fill every row; the label ellipsizes and `title` carries the full tag. */
#rb-topics .tag-cloud{display:grid;grid-template-columns:1fr 1fr;gap:var(--sp-1);padding:2px 0}
#rb-topics .tag-chip{min-width:0;gap:var(--sp-1);justify-content:space-between;
  padding:var(--sp-2) var(--sp-2);font-size:var(--fs-1);letter-spacing:0;
  clip-path:polygon(5px 0,100% 0,100% calc(100% - 5px),calc(100% - 5px) 100%,0 100%,0 5px)}
#rb-topics .tag-chip:hover{transform:none}   /* a lift reads as jitter in a dense grid */
#rb-topics .tag-chip .tg{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
#rb-topics .tag-chip .tag-n{flex:0 0 auto}
/* A hashtag is a LINK, and it was drawn as a control: Orbitron 700, uppercased, letter-spaced, on a
   two-colour gradient inside an accent border with an inset glow — five decorations competing on an
   element whose whole content is one short word. Uppercasing also destroys the shape of the word, which
   is the thing you actually scan for in a list of twenty. Now: the UI font at its natural case, a quiet
   surface, one hairline. The angular corner survives at 6px because that clip IS the cyberpunk signature
   — it just doesn't need four other effects helping it. All theme vars, so all seven inherit. */
.tag-chip{display:inline-flex;align-items:center;gap:var(--sp-2);cursor:pointer;
  background:var(--panel2);
  border:1px solid var(--line);color:var(--text);
  padding:var(--sp-2) var(--sp-3);font-size:var(--fs-1);font-weight:600;
  letter-spacing:0;transition:.16s;position:relative;
  clip-path:polygon(6px 0,100% 0,100% calc(100% - 6px),calc(100% - 6px) 100%,0 100%,0 6px)}
.tag-chip:hover{color:var(--neon);border-color:rgba(var(--accent-rgb),.55);
  background:color-mix(in srgb, var(--neon) 10%, var(--panel2))}
/* The count is secondary information, so it reads as secondary: no accent colour, no glow. */
.tag-chip .tag-n{color:var(--muted);font-size:var(--fs-0);font-weight:600;letter-spacing:0;
  font-variant-numeric:tabular-nums}
a.hashtag{color:var(--cyan);font-weight:600}
a.hashtag:hover{text-shadow:0 0 8px rgba(var(--accent-rgb),.5)}

/* ===== profile info + search text sized to match posts ===== */
.prof .about{font-size:20px;line-height:1.6}
.prof .pbody h2{font-size:24px}
.prof .pbody .muted.small{font-size:17px}
.npubrow code{font-size:13px}
.statbtn{font-size:15px}.statbtn b{font-size:17px}
.psearch b{font-size:17px}.psearch .muted.small{font-size:15px}

/* extra breathing room (+ a divider) between the Topics chips and the post list below them */
#rb-topics{padding-bottom:18px;margin-bottom:4px;border-bottom:1px solid var(--line)}

/* font consistency: compose/reply textarea, preview + tool buttons larger */
#cmp,.note-preview .txt{font-size:20px;line-height:1.6;letter-spacing:.2px}
.cmp-tools .btn{font-size:15px}

/* live "mentioning @name" hint under the composer */
.mention-hint{color:var(--cyan);font-size:13px;margin:2px 2px 8px;letter-spacing:.2px}
.mention-hint,#cmp-status{overflow-wrap:anywhere}   /* a long @handle / URL must not widen the modal */

/* ===== settings view ===== */
.settings{padding:22px 32px 70px;max-width:none;margin:0;animation:fade .25s;font-size:17px}
.signer-app{max-width:920px;margin:0 auto}
.signer-hero{display:flex;align-items:center;gap:20px;padding:22px 24px;margin-bottom:18px;border:1px solid rgba(var(--accent-rgb),.35);border-radius:22px;background:linear-gradient(135deg,rgba(var(--accent-rgb),.15),rgba(8,5,18,.35));box-shadow:0 16px 44px rgba(0,0,0,.18)}
.signer-hero h2{font:700 25px/1.2 'Orbitron',sans-serif;color:var(--neon);margin:0 0 7px;text-shadow:0 0 14px rgba(var(--accent-rgb),.35)}
.signer-hero p{margin:0;max-width:58ch;color:var(--muted);line-height:1.5}
.signer-mark{position:relative;display:grid;place-items:center;flex:0 0 76px;width:76px;height:76px;border-radius:24px;background:rgba(var(--accent-rgb),.13);color:var(--neon);box-shadow:inset 0 0 0 1px rgba(var(--accent-rgb),.32),0 0 28px rgba(var(--accent-rgb),.16)}
.signer-mark>.ic{width:48px;height:48px}.signer-mark>.signer-mark-key{position:absolute;width:24px;height:24px;color:#fff;filter:drop-shadow(0 2px 5px rgba(0,0,0,.55))}
.signer-app .set-card{border-radius:18px;background:linear-gradient(145deg,rgba(8,5,18,.52),rgba(var(--accent-rgb),.045))}
.signer-app .set-body{margin-top:14px}.signer-app #signer-apps>.row{padding:8px 0;border-top:1px solid var(--line)}
@media(max-width:600px){.signer-hero{align-items:flex-start;padding:18px;gap:14px}.signer-mark{flex-basis:58px;width:58px;height:58px;border-radius:18px}.signer-mark>.ic{width:38px;height:38px}.signer-mark>.signer-mark-key{width:19px;height:19px}.signer-hero h2{font-size:20px}}
.set-card{background:rgba(8,5,18,.4);border:1px solid var(--line);border-radius:var(--r);padding:20px 20px 18px;margin-bottom:18px}
.set-head{display:flex;align-items:flex-start;justify-content:space-between;gap:16px}
.set-head .muted.small{margin-top:6px;line-height:1.45;max-width:50ch;font-size:15px}
.set-title{font-family:'Orbitron',sans-serif;font-size:24px;letter-spacing:.5px;color:var(--neon);text-shadow:0 0 8px rgba(var(--accent-rgb),.55),0 0 16px rgba(var(--accent-rgb),.3)}
.set-body{margin-top:16px;transition:opacity .15s}
/* The stay-connected opt-in. Set apart from the buttons above it because it is a DIFFERENT KIND of
   choice — push is a capability you switch on, this is a trade you accept — and because the sentence
   explaining the battery cost has to be read, not skimmed past as button chrome. */
.set-stay{margin-top:12px;padding-top:12px;border-top:1px solid var(--line);
  display:flex;flex-direction:column;gap:5px}
.set-stay label{display:flex;align-items:center;gap:8px;font-size:13px;cursor:pointer}
.set-stay input{flex:none}
.set-body.disabled{opacity:.4;pointer-events:none;filter:grayscale(.4)}
.set-body .muted.small{font-size:15px;line-height:1.45}
.set-body code{color:var(--cyan);font-size:15px}
.set-body .input{font-size:17px;padding:14px 16px}
.set-body .btn.small{font-size:15px;padding:10px 16px}
/* Settings → Sidebar: one switch per nav row, boxed so ~35 of them read as one control rather than
   as thirty-five settings. It is the whole tab, so the box scrolls with the pane rather than owning
   a scroller of its own — a short inner scroller inside a scrolling pane is the shape nobody can
   drive on a phone. The rows carry the sidebar's own indenting, so a group and its children are
   recognisable as the group being looked at on the left. */
.nav-hide-list{margin:10px 0 4px;padding:6px 10px;border:1px solid var(--line);border-radius:var(--r);
  background:rgba(8,5,18,.35)}
.nav-hide-row{margin:2px 0;text-transform:none;letter-spacing:0;font-size:15px;font-weight:500;color:var(--text);gap:12px}
.nav-hide-row>span{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.nav-hide-row.sub{padding-inline-start:18px}
.nav-hide-row.grp{margin-top:8px;font-weight:700;color:var(--neon)}
.nav-hide-row .switch{flex:none}
.set-actions{display:flex;gap:10px;align-items:center;flex-wrap:wrap;margin:10px 0}
.set-actions .input{flex:1;min-width:180px;margin:0}
.settings #set-save,.settings #us-save{font-size:17px;padding:14px 24px;display:block;width:fit-content;max-width:100%;margin:18px auto 6px}
.set-foot{text-align:center;margin-top:12px;font-size:15px}
.relay-row{display:flex;align-items:center;gap:10px;margin:8px 0}
.relay-row .input{margin:0;flex:1}
.rr-dot{width:8px;height:8px;border-radius:50%;flex-shrink:0;background:var(--green);box-shadow:0 0 8px var(--green)}
.rr-del{flex-shrink:0;line-height:1}
.rr-del:hover{color:var(--neon2);border-color:var(--neon2)}
/* toggle switch (matches the cyan→magenta neon accent) */
.switch{position:relative;display:inline-block;width:46px;height:26px;flex-shrink:0;cursor:pointer}
.switch input{opacity:0;width:0;height:0}
/* The real checkbox is 0×0 and invisible, so Tab reached every toggle in Settings with NOTHING to show
   for it — you could not tell which one you were on, or that focus was in the pane at all. Put the ring
   on the visible slider instead. (Space still toggles it; that part always worked.) */
.switch input:focus-visible+.slider{outline:2px solid var(--neon);outline-offset:3px}
.switch .slider{position:absolute;inset:0;border-radius:26px;background:rgba(8,5,18,.8);border:1px solid var(--line);transition:.2s}
.switch .slider::before{content:"";position:absolute;width:18px;height:18px;inset-inline-start:3px;top:3px;border-radius:50%;background:var(--muted);transition:.2s}
.switch input:checked+.slider{background:linear-gradient(135deg,var(--neon),var(--neon2));border-color:transparent;box-shadow:var(--glow)}
.switch input:checked+.slider::before{transform:translateX(20px);background:#fff}
@media(max-width:820px){ .settings{padding:16px 14px 90px} .set-head .muted.small{max-width:none} }

/* ===== AI view + nav button (the AI app, merged into the client) ===== */
.nav-item.nav-ai{color:var(--neon)}   /* accent the AI item without making it look selected */
.nav-item.nav-ai.active{color:#04020c;background:linear-gradient(135deg,var(--neon),var(--neon2));box-shadow:var(--glow)}
.ai-view{padding:26px 24px 80px;max-width:760px;margin:0 auto;animation:fade .25s}
.ai-gate{text-align:center;display:flex;flex-direction:column;align-items:center;gap:12px;padding-top:48px}
.ai-gate h2{font-family:'Orbitron',sans-serif;font-size:24px;color:#fff;margin:0}
.ai-gate .muted{max-width:46ch;line-height:1.45}
.ai-gate .btn{margin-top:6px}

/* ===== AI chat (merged web UI) ===== */
.ai-chat{display:flex;flex-direction:column;height:100%;min-height:0;position:relative}
.ai-chat.ai-drop::after{content:"⬆ Drop files to attach";position:absolute;inset:8px;z-index:5;display:flex;align-items:center;justify-content:center;font-size:20px;font-weight:700;color:var(--neon,#0ff);border:2px dashed var(--neon,#0ff);border-radius:16px;background:rgba(0,240,255,.10);pointer-events:none}
.ai-bar{display:flex;gap:8px;align-items:center;padding:10px 16px;border-bottom:1px solid var(--line)}
.ai-bar .input{margin:0;flex:1;max-width:420px;font-size:15px;padding:8px 12px}
.ai-msgs{flex:1;min-height:0;overflow-y:auto;padding:18px 16px 8px;display:flex;flex-direction:column;gap:12px}
.ai-msg{display:flex}
.ai-msg.user{justify-content:flex-end}
.ai-bubble{max-width:80%;padding:12px 16px;border-radius:14px;line-height:1.6;font-size:17px;
  white-space:normal;word-wrap:break-word;overflow-wrap:anywhere;border:1px solid var(--line)}
.ai-msg.user .ai-bubble{background:linear-gradient(135deg,rgba(var(--accent-rgb),.16),rgba(var(--accent2-rgb),.12));color:#fff;white-space:pre-wrap}
.ai-msg.assistant .ai-bubble{background:rgba(8,5,18,.5)}
.ai-bubble p:first-child{margin-top:0} .ai-bubble p:last-child{margin-bottom:0}
.ai-bubble pre{background:rgba(0,0,0,.4);border:1px solid var(--line);border-radius:8px;padding:10px;overflow-x:auto}
.ai-userfiles{margin-top:6px;font-size:13px;color:var(--cyan)}
.ai-err{color:#ff6b8b}
.ai-media img,.ai-media video{max-width:100%;border-radius:12px;border:1px solid var(--line);margin-top:6px}
.ai-media audio{width:100%;margin-top:6px}
.ai-imggrid{display:grid;grid-template-columns:repeat(auto-fill,minmax(120px,1fr));gap:6px;margin-top:8px}
.ai-imggrid img{width:100%;height:120px;object-fit:cover;border-radius:10px;border:1px solid var(--line);cursor:zoom-in}
.ai-search{display:flex;flex-direction:column;gap:8px;margin-top:6px}
.ai-files{display:flex;flex-wrap:wrap;gap:8px;margin-top:6px}
.ai-file{border:1px solid var(--line);border-radius:9px;padding:6px 12px;font-size:15px}
/* attach · mic · box · send, all sitting on the same line.
   The row is align-items:flex-end so the controls stay level with the LAST line of the box as it
   grows — but `.mini{align-self:center}` opted the attach and mic buttons out of exactly that, so
   they floated to the middle while Send stayed at the bottom, and the gap widened with every line
   typed. Same round shapes and sizes as the DM composer, which is the same row in a different view. */
.ai-compose{display:flex;gap:8px;align-items:flex-end;padding:12px 16px;border-top:1px solid var(--line)}
.ai-compose .input{margin:0;flex:1 1 auto;min-width:0;min-height:44px;max-height:200px;resize:none;
  font-size:17px;padding:11px 14px;border-radius:22px;line-height:1.35}
.ai-compose .mini{align-self:flex-end}
/* One line, clipped — never wrapped. A textarea placeholder wraps like any other text, and at phone
   width the box is ~174px: the hint broke across two lines and the second was sliced in half by the
   40px min-height, which is most of what "ugly" was. */
.ai-compose .input::placeholder{white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.ai-attachbar{display:flex;flex-wrap:wrap;gap:6px;padding:0 16px 10px}
.ai-attachbar:empty{display:none}
.ai-chip{font-size:12px;color:var(--cyan);border:1px solid var(--line);border-radius:8px;padding:4px 8px}
.ai-chip-x{background:none;border:none;color:var(--muted);cursor:pointer}
/* new-chat welcome splash */
.ai-welcome{margin:auto;max-width:560px;width:100%;text-align:center;padding:22px 16px;display:flex;flex-direction:column;align-items:center;gap:10px;animation:fade .25s}
.ai-welcome .aw-logo{width:74px;height:74px;border-radius:50%;object-fit:cover;border:1px solid var(--neon);box-shadow:var(--glow)}
.ai-welcome h3{margin:0;font-family:'Orbitron',sans-serif;font-size:24px;letter-spacing:.5px;background:linear-gradient(135deg,var(--neon),var(--neon2));-webkit-background-clip:text;background-clip:text;color:transparent}
.ai-welcome p{margin:0;line-height:1.45}
.aw-cmds{display:flex;flex-direction:column;gap:8px;width:100%;margin:6px 0}
.ai-eg{display:flex;align-items:center;gap:10px;flex-wrap:wrap;text-align:start;background:rgba(8,5,18,.5);border:1px solid var(--line);border-radius:12px;padding:12px 14px;color:var(--text);cursor:pointer;font-size:15px;transition:border-color .15s,background .15s}
.ai-eg:hover{border-color:var(--neon);background:rgba(var(--accent-rgb),.08)}
.ai-eg b{color:var(--cyan);font-family:var(--mono)}
.ai-eg span{color:var(--muted);font-size:13px;margin-inline-start:auto}
.ai-welcome .ai-cmd{color:var(--cyan);cursor:pointer;border-bottom:1px dashed var(--cyan)}
@media(max-width:820px){
  .ai-welcome{padding:14px 8px;gap:8px}
  .ai-welcome .aw-logo{width:62px;height:62px}
  .ai-welcome h3{font-size:20px}
  .ai-eg{font-size:15px;padding:10px 12px;gap:6px}
  .ai-eg span{margin-inline-start:0;flex-basis:100%}   /* description drops to its own line */
  .ai-bubble{max-width:90%;font-size:17px}
  .ai-bar{padding:8px 12px;flex-wrap:wrap}
  /* The conversation <select> shares one flex row with four labelled buttons (Home/New/🔊/🗑). With
     flex:1 (basis 0) it collapsed into whatever sliver the buttons left — a tiny unusable button on a
     phone. Give it its OWN full-width row (basis 100% wraps it) and put the buttons on the row above,
     so the selector is a proper full-width dropdown you can actually tap. */
  .ai-bar .input{flex:1 1 100%;max-width:none;min-width:0;order:2;min-height:42px}
  .ai-bar .btn{order:1}
  .ai-msgs{padding:12px 12px 8px}
  /* lift the compose bar above the fixed bottom nav (≈62px) + the notch */
  .ai-compose{padding:10px 12px;padding-bottom:calc(10px + 62px + env(safe-area-inset-bottom))}
}

/* ---- Live Translate: full-height, big-text, thumb-reachable mic (built for holding up in a room) ---- */
.lt-wrap{display:flex;flex-direction:column;height:100%;min-height:0}
.lt-bar{display:flex;align-items:center;gap:8px;padding:12px 16px;border-bottom:1px solid var(--line);flex:0 0 auto}
.lt-lang{flex:1;min-width:0;margin:0;font-size:17px;padding:10px 12px;border:1px solid var(--line);border-radius:12px;background:var(--panel2);color:var(--text)}
.lt-swap{flex:0 0 auto;width:42px;height:42px;border-radius:50%;border:1px solid var(--neon);background:transparent;color:var(--neon);font-size:20px;cursor:pointer}
.lt-swap:active{background:rgba(var(--accent-rgb),.14)}
.lt-log{flex:1;min-height:0;overflow-y:auto;padding:16px;display:flex;flex-direction:column;gap:14px}
.lt-hint{text-align:center;margin:auto;max-width:520px;line-height:1.45;font-size:15px}
.lt-turn{display:flex;flex-direction:column;gap:6px;animation:fade .2s}
.lt-tag{display:inline-block;font-size:12px;font-weight:700;opacity:.7;margin-inline-end:8px;vertical-align:middle}
.lt-src{font-size:15px;color:var(--muted);border-inline-start:2px solid var(--line);padding-inline-start:12px}
.lt-tr{font-size:24px;line-height:1.45;font-weight:600;color:var(--text);border-inline-start:3px solid var(--neon);padding-inline-start:12px}
.lt-replay{float:inline-end;margin-inline-start:8px;background:none;border:none;color:var(--cyan);font-size:20px;cursor:pointer;opacity:.7;padding:0 2px;line-height:1}
.lt-replay:active{opacity:1;transform:scale(.9)}
.lt-foot{flex:0 0 auto;display:flex;align-items:center;justify-content:center;gap:18px;position:relative;padding:14px 16px calc(14px + env(safe-area-inset-bottom));border-top:1px solid var(--line)}
.lt-mic{width:76px;height:76px;border-radius:50%;border:2px solid var(--neon);background:rgba(var(--accent-rgb),.06);color:var(--neon);font-size:30px;cursor:pointer;box-shadow:var(--glow);transition:transform .1s,background .15s}
.lt-mic:active{transform:scale(.94)}
.lt-mic.rec{border-color:#ff4d6d;color:#ff4d6d;background:rgba(255,77,109,.12);animation:lt-pulse 1.2s ease-in-out infinite}
@keyframes lt-pulse{0%,100%{box-shadow:0 0 0 0 rgba(255,77,109,.45)}50%{box-shadow:0 0 0 12px rgba(255,77,109,0)}}
.lt-speak{width:46px;height:46px;border-radius:50%;border:1px solid var(--line);background:transparent;color:var(--text);font-size:20px;cursor:pointer}
.lt-status{position:absolute;bottom:calc(2px + env(safe-area-inset-bottom));inset-inline-start:0;inset-inline-end:0;text-align:center;font-size:12px}
@media(max-width:820px){
  .lt-bar{padding:10px 12px;gap:6px}
  .lt-log{padding:12px}
  .lt-tr{font-size:20px}
  /* lift the mic/foot above the fixed bottom nav (≈62px) + the notch */
  .lt-foot{padding-bottom:calc(14px + 62px + env(safe-area-inset-bottom))}
  .lt-status{bottom:calc(52px + env(safe-area-inset-bottom))}
}

/* curated "Discover" hashtag chips (rightbar, for newcomers) */
.tag-chip.disc{background:linear-gradient(135deg,rgba(var(--accent-rgb),.10),rgba(var(--accent2-rgb),.08));border-color:var(--line)}
.tag-chip.disc:hover{border-color:var(--neon2);color:#fff}
.ai-magnet{display:inline-block;margin:4px 6px 4px 0;background:linear-gradient(135deg,var(--neon),var(--neon2));color:#fff;border:none;border-radius:9px;padding:8px 14px;font-weight:600;cursor:pointer;font-size:15px}
.ai-cmd{display:inline-block;margin:4px 6px 4px 0;background:var(--panel2);color:var(--text);border:1px solid var(--neon);border-radius:9px;padding:8px 14px;font-weight:600;cursor:pointer;font-size:15px;font-family:inherit}
/* Tab has to be VISIBLE to be usable. The AI view is all buttons — the starter cards, the chips, the
   compose row — and none of them showed where the focus was, so tabbing to one was a guess. */
.ai-chat button:focus-visible,.ai-chat select:focus-visible,.ai-chat textarea:focus-visible,
.ai-welcome button:focus-visible,.ai-cmd:focus-visible,.aw-card:focus-visible{
  outline:2px solid var(--neon);outline-offset:3px}
/* 🎬 Effects studio chips — bigger tap targets (mobile-friendly), prefill (don't auto-send) */
.fx-cmd{display:inline-flex;align-items:center;background:var(--panel2);color:var(--text);border:1px solid var(--neon);border-radius:11px;padding:10px 14px;font-weight:600;cursor:pointer;font-size:15px;min-height:44px}
.fx-cmd:active{background:rgba(var(--accent-rgb),.14)}
.fx-mot,.fx-char{display:inline-flex;align-items:center;background:transparent;color:var(--cyan);border:1px dashed var(--cyan);border-radius:11px;padding:10px 14px;font-weight:600;cursor:pointer;font-size:15px;min-height:42px}
.fx-mot:active,.fx-char:active{background:rgba(var(--accent-rgb),.1)}
.fx-char{color:var(--neon);border-color:var(--neon)}
.fx-grid{display:flex;flex-wrap:wrap;gap:6px;max-height:30vh;overflow:auto;padding:4px;border:1px solid var(--line);border-radius:12px;background:rgba(8,5,18,.4)}
/* The Effects catalog is the studio's primary control (shown inline, not collapsed), so it gets a taller
   scroll window than the small Enhance grid, with a bottom fade cueing that more chips scroll into view. */
.fx-grid-eff{max-height:40vh;-webkit-mask-image:linear-gradient(to bottom,#000 calc(100% - 20px),transparent);mask-image:linear-gradient(to bottom,#000 calc(100% - 20px),transparent)}
.fx-grid-eff:hover,.fx-grid-eff:focus-within{-webkit-mask-image:none;mask-image:none}
.fx-guide{line-height:1.45}
.fx-guide code{background:var(--panel2);border:1px solid var(--line);border-radius:6px;padding:1px 6px;font-size:13px}
/* The action row under a chat result. Up to eight buttons — Send the Reply, Convert to MP3, Post,
   Save to Blossom, Save to Notes, Meme Builder, Download, Copy link — and it used to carry its
   layout as an inline style on two separate template literals, so a change had to be made twice or
   the two rows silently diverged. Here, once, and it wraps with a width floor per button: a plain
   flex row squeezes eight labels into slivers on a phone. */
.fx-reply-row{margin-top:8px;display:flex;gap:8px;flex-wrap:wrap}
.fx-reply-row>*{flex:1 1 auto;min-width:136px;justify-content:center;white-space:nowrap}
@media (max-width:460px){ .fx-reply-row>*{min-width:calc(50% - 4px)} }
/* Android app associations on a vault entry. Chips, because they are a set you add to and remove
   from, not a text field — a package name is not something anyone should have to type. */
.pv-applist{display:flex;flex-wrap:wrap;gap:6px;margin-top:6px}
.pv-app{display:inline-flex;align-items:center;gap:6px;padding:5px 8px;border-radius:8px;
  border:1px solid var(--line);background:rgba(var(--accent-rgb),.08);font-size:12px}
.pv-app b{font-weight:600;word-break:break-all}
/* 32px floor, same as every other tap target here — these are thumb-sized buttons on a phone, and
   the mobile checker's scan list did not cover .pv-applist, so 22px passed unnoticed. */
.pv-app .pv-app-del{min-height:32px;min-width:32px;padding:0 8px;line-height:1}
.pv-app-add{min-height:32px;max-width:100%;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
/* An entry with no associations must not leave a gap: .pv-ed-body is a flex column with a 12px gap,
   so an always-emitted empty slot costs 12px on every desktop entry. */
.pv-apps:empty{display:none}
.fx-reply-row .ai-reply-fx{min-height:42px;font-weight:700}
/* Section header + inline hint + optional-add rows. */
.fx-sec{font-size:13px;font-weight:700;color:var(--cyan);margin:12px 0 4px}
.fx-sec:first-of-type{margin-top:6px}
.fx-hint{opacity:.6;font-weight:400;font-size:11px}
.fx-row{display:flex;flex-wrap:wrap;gap:6px}

/* ---- Effects studio (modal sheet) ---------------------------------------------------------
   Replaces the in-chat guide. The catalog is ~70 effects, so the layout is built around finding
   one: a sticky search at the top, ONE scrolling body (the old version nested a 32dvh scrollbar
   inside the scrolling chat), and a sticky footer showing the exact command that will be sent. */
.modal.fxs-modal{ padding:0; overflow:hidden; display:flex; flex-direction:column; max-height:88vh; max-height:88dvh; }
.fxs{ display:flex; flex-direction:column; min-height:0; flex:1; }
.fxs-hd{ padding:16px 18px 12px; border-bottom:1px solid var(--line); flex:0 0 auto; }
.fxs-title{ display:flex; align-items:center; gap:12px; }
.fxs-title h3{ margin:0; font-size:20px; letter-spacing:.5px; }
.fxs-title > div{ flex:1; min-width:0; }
.fxs-thumb{ width:46px; height:46px; border-radius:10px; object-fit:cover; border:1px solid var(--line); flex:0 0 auto; }
.fxs-x{ flex:0 0 auto; background:transparent; border:0; color:var(--muted); font-size:20px; cursor:pointer;
  width:38px; height:38px; border-radius:10px; }
.fxs-x:hover{ background:var(--panel2); color:var(--text); }
.fxs-search{ margin:12px 0 0; }
.fxs-body{ flex:1 1 auto; min-height:0; overflow:auto; padding:12px 18px 18px; }
.fxs-sec{ font-size:13px; font-weight:700; color:var(--cyan); margin:14px 0 8px; }
.fxs-sec:first-child{ margin-top:2px; }
.fxs-hint{ opacity:.6; font-weight:400; font-size:11px; }
.fxs-grid{ display:flex; flex-wrap:wrap; gap:8px; }
.fxs-empty{ color:var(--muted); font-size:15px; padding:6px 2px; }
/* Command sheet (`help`) — rows, not chips: a command is a name PLUS what it does, and 39 of them as
   bare chips would be a wall of jargon. Reuses the .fxs sheet chrome so the two sheets feel like one
   thing. */
.cmds-sec{margin-top:16px}
.cmds-grid{display:flex;flex-direction:column;gap:6px}
.cmds-item{display:flex;align-items:baseline;gap:10px;width:100%;text-align:start;min-height:40px;
  padding:10px 12px;background:var(--panel2);border:1px solid var(--line);border-radius:10px;
  color:var(--text);font-family:inherit;font-size:15px;cursor:pointer;transition:.12s}
.cmds-item:hover{border-color:var(--neon);background:rgba(var(--accent-rgb),.10)}
.cmds-name{flex:0 0 auto;font-weight:700;color:var(--neon);font-family:var(--mono)}
.cmds-desc{flex:1 1 auto;min-width:0;color:var(--muted);font-size:13px;
  overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
@media(max-width:820px){
  .cmds-item{flex-direction:column;align-items:flex-start;gap:2px;min-height:46px}
  .cmds-desc{white-space:normal;overflow:visible;font-size:12px}
}
/* Chips are ≥40px tall so they're comfortably tappable — the old ones were 13px text in a 38px box. */
.fxs-chip{ display:inline-flex; align-items:center; min-height:40px; padding:10px 14px; cursor:pointer;
  background:var(--panel2); color:var(--text); border:1px solid var(--line); border-radius:11px;
  font-weight:600; font-size:15px; transition:.12s; }
.fxs-chip:hover{ border-color:var(--neon); color:var(--neon); }
.fxs-chip.on{ background:rgba(var(--accent-rgb),.16); border-color:var(--neon); color:var(--neon);
  box-shadow:0 0 0 1px var(--neon) inset; }
/* Modifier the picked effect can't take (see _fxModBlock): still readable, clearly not pickable —
   greying it out entirely made the row look broken. The reason rides along in the title + .fxs-why. */
.fxs-chip.off{ opacity:.42; cursor:not-allowed; text-decoration:line-through; }
.fxs-chip.off:hover{ border-color:var(--line); color:var(--text); }
.fxs-why{ font-weight:400; font-size:11px; color:var(--amber); margin-inline-start:6px; }
.fxs-ft{ flex:0 0 auto; border-top:1px solid var(--line); padding:12px 18px;
  display:flex; align-items:center; gap:12px; flex-wrap:wrap; background:var(--panel2); }
.fxs-cmd{ flex:1 1 190px; min-width:0; font-size:13px; padding:8px 12px; border-radius:9px;
  background:var(--bg); border:1px solid var(--line); color:var(--neon);
  overflow:auto; white-space:nowrap; }
.fxs-cmd.muted{ color:var(--muted); }
.fxs-acts{ display:flex; gap:10px; align-items:center; margin-inline-start:auto; }
.fxs-acts .btn{ margin:0; }
.fxs-acts .btn:disabled{ opacity:.45; cursor:not-allowed; }
/* Full-bleed backdrop: the default .modal-bg padding would shrink an intentionally tall sheet. */
.modal-bg.fxs-bg{ padding:16px; }
@media(max-width:820px){
  /* Near-fullscreen on a phone: the point is to see as many effects at once as possible, and the
     sticky footer keeps Apply reachable without scrolling back down the list. */
  .modal-bg.fxs-bg{ padding:0; place-items:end center; }
  .modal.fxs-modal{ width:100vw; max-width:100vw; height:92dvh; max-height:92dvh; border-radius:16px 16px 0 0; }
  .fxs-hd{ padding:14px 14px 10px; }
  .fxs-body{ padding:10px 14px 16px; }
  .fxs-ft{ padding:10px 14px; gap:10px; }
  .fxs-title h3{ font-size:17px; }
  .fxs-thumb{ width:40px; height:40px; }
  .fxs-chip{ min-height:42px; font-size:15px; padding:10px 14px; }   /* bigger, not smaller, on touch */
  .fxs-cmd{ flex:1 1 100%; order:2; }
  .fxs-acts{ order:1; width:100%; margin-inline-start:0; }
  .fxs-acts .btn{ flex:1; justify-content:center; }
}
/* Mobile: smaller chips + shorter scroll grids so the studio isn't a cluttered wall. Grid height uses
   dvh (dynamic viewport) so it SHRINKS when the on-screen keyboard opens, keeping chips + input reachable.
   The primary Effects grid stays a bit taller than Enhance so the catalog is browsable without over-scroll. */
@media(max-width:820px){
  .fx-cmd{padding:8px 12px;font-size:13px;min-height:40px}
  .fx-mot,.fx-char{padding:8px 10px;font-size:13px;min-height:38px}
  .fx-grid{max-height:22vh;max-height:22dvh;gap:6px}
  .fx-grid-eff{max-height:32vh;max-height:32dvh}
  .fx-sec{margin:10px 0 4px}
}
/* Interactive flashcard deck (study quiz) */
.flashcard-deck{border:1px solid var(--neon);border-radius:14px;padding:14px;margin-top:6px;background:rgba(8,5,18,.55);box-shadow:0 0 18px rgba(var(--accent-rgb),.12)}
.fc-head{display:flex;justify-content:space-between;align-items:center;gap:10px;margin-bottom:10px}
.fc-title{font-weight:800;color:var(--neon)}
.fc-progress{font-size:13px;color:var(--cyan);opacity:.85}
.fc-q{font-size:17px;font-weight:700;line-height:1.45;margin-bottom:12px}
.fc-options{display:flex;flex-direction:column;gap:8px}
.fc-opt{display:block;width:100%;text-align:start;background:var(--panel2);color:var(--text);border:1px solid var(--line);border-radius:11px;padding:12px 14px;font-size:15px;cursor:pointer;min-height:44px;transition:border-color .12s,background .12s}
.fc-opt:hover:not(:disabled){border-color:var(--neon)}
.fc-opt:disabled{cursor:default;opacity:.95}
.fc-opt.fc-correct{border-color:var(--green);background:rgba(0,255,136,.14);color:var(--green);font-weight:700}
.fc-opt.fc-wrong{border-color:var(--accent-tertiary,#ff3366);background:rgba(255,51,102,.14);color:var(--accent-tertiary,#ff3366)}
.fc-explain{margin-top:12px;padding:10px 12px;border-inline-start:3px solid var(--neon);background:rgba(var(--accent-rgb),.07);border-radius:8px;font-size:15px;line-height:1.45}
.fc-controls{display:flex;align-items:center;justify-content:space-between;gap:8px;margin-top:14px;flex-wrap:wrap}
.fc-controls button{background:transparent;color:var(--cyan);border:1px solid var(--cyan);border-radius:10px;padding:10px 14px;font-weight:600;cursor:pointer;min-height:42px}
.fc-controls button:hover:not(:disabled){background:rgba(var(--accent-rgb),.12)}
.fc-controls button:disabled{opacity:.35;cursor:default}
.fc-score{font-size:15px;color:var(--amber);font-weight:700}
.ai-cmd:hover{background:var(--neon);color:#04020c}
.ai-cmd-danger{border-color:var(--accent-tertiary,#ff3366)}
.ai-cmd-danger:hover{background:var(--accent-tertiary,#ff3366);color:#fff}
.ai-pins{display:flex;flex-direction:column;gap:10px;margin-top:8px}
.ai-pin{border:1px solid var(--line);border-radius:11px;padding:10px 12px;background:var(--panel)}
.ai-pin-q{margin-bottom:8px;font-size:15px}
.ai-pin-btns{display:flex;gap:8px;flex-wrap:wrap}
/* Interactive budget card buttons (Telegram-parity): wrap nicely on mobile. */
/* Profile → Relays modal: one relay per row, theme-safe. (Distinct class from Settings' .relay-row
   input rows so the two don't collide.) */
.prof-relays{display:flex;flex-direction:column;gap:8px;margin-top:6px}
.prof-relay{display:flex;align-items:center;justify-content:space-between;gap:10px;padding:10px 12px;border:1px solid var(--line);border-radius:10px;background:var(--panel2)}
.prof-relay code{word-break:break-all}
.ai-budget-btns{display:flex;gap:8px;flex-wrap:wrap;margin-top:8px}
.ai-addbill{display:inline-block;background:var(--panel2);color:var(--text);border:1px solid var(--neon);border-radius:9px;padding:8px 14px;font-weight:600;cursor:pointer;font-size:15px}
/* hover keeps neon TEXT on the panel fill (not a dark label on the accent) so it stays readable on
   themes where --neon is dark (Professional/Win98/Dark) — same approach as .btn-ghost:hover. */
.ai-addbill:hover{border-color:var(--neon);color:var(--neon)}
.us-tabs{display:flex;flex-wrap:wrap;gap:6px;margin:4px 0 14px}
.us-tab{background:var(--panel2);border:1px solid var(--line);color:var(--text);border-radius:9px;padding:8px 14px;cursor:pointer;font-size:15px;font-weight:600}
.us-tab.active{background:var(--neon);color:#04020c;border-color:var(--neon)}
.us-pane{display:none;flex-direction:column;gap:12px}
.us-pane.active{display:flex}
.us-conn{border:1px solid var(--line);border-radius:11px;padding:12px;display:flex;flex-direction:column;gap:10px}
.set-title.small{font-size:15px}
.us-mail{display:grid;grid-template-columns:1.4fr 1.4fr .6fr 1.4fr .6fr 1.2fr auto;gap:6px;align-items:center;margin-bottom:8px}
.us-mail .input{font-size:15px;padding:8px 10px}
@media(max-width:820px){ .us-mail{grid-template-columns:1fr 1fr;}}
.us-key{display:flex;justify-content:space-between;align-items:center;border:1px solid var(--line);border-radius:9px;padding:10px 12px;margin-bottom:8px;gap:8px}
.us-key .mini{margin-inline-start:6px}
/* The linked/OK status text. `--green` rather than a hardcoded #4ade80 so it matches the icon
   beside it and stays readable on win98/winxp/professional, where green is a dark tone and a
   fixed mint sat on the light background almost invisibly. */
.us-ok{color:var(--green);font-weight:600}
@keyframes reminderPulse{0%,100%{box-shadow:0 0 40px 6px rgba(var(--accent-rgb),.55)}50%{box-shadow:0 0 70px 16px rgba(255,0,128,.7)}}
.reminder-card{max-width:520px;width:100%;text-align:center;background:var(--bg2);border:2px solid var(--neon);border-radius:16px;padding:34px 26px;animation:reminderPulse 1.4s ease-in-out infinite}
.feed.feed-admin{padding:0;overflow:hidden}
.admin-frame{width:100%;height:100%;border:none;display:block;background:var(--bg);transition:opacity .2s}
#admin-host{flex:1;min-height:0;overflow:hidden;display:none}   /* persistent admin iframe host (sibling of #feed) */
.draft-art{display:flex;align-items:center;gap:8px;padding:10px 14px;margin:0 16px 6px;border:1px solid var(--line);border-radius:10px;background:rgba(8,5,18,.4)}
.draft-art .da-title{font-weight:600;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}

.uploader{font-size:15px;display:flex;align-items:center;gap:10px;flex-wrap:wrap;padding:12px 16px}
.uploader .muted.small{font-size:15px}

/* ---- Desktop scaling --------------------------------------------------------
   The UI's sizing is px-based and tuned for a large / 4K display; on smaller desktops (1080p and
   OS-scaled laptops) those fixed sizes read as oversized. Scale the whole app down with `zoom` in
   steps, compensating the full-height containers (height:100dvh/zoom) so they still fill the screen
   with no blank strip. dvh (not vh) so on a TABLET with visible browser chrome the app fits the visible
   viewport and the sidebar footer (username → connection → Logout) isn't pushed off the bottom; on
   desktop dvh==vh so nothing changes. Bounded to >820px so the mobile layout (≤820px, sidebar hidden)
   is left completely alone; true 4K (>1920px CSS px) gets no zoom. */
@media (min-width:821px) and (max-width:1920px){ body{ zoom:.77; --zf:.77 } .app{ height:calc(100dvh / .77) } .main{ height:100% } }
@media (min-width:821px) and (max-width:1600px){ body{ zoom:.72; --zf:.72 } .app{ height:calc(100dvh / .72) } .main{ height:100% } }
@media (min-width:821px) and (max-width:1366px){ body{ zoom:.67; --zf:.67 } .app{ height:calc(100dvh / .67) } .main{ height:100% } }
/* 4K / very large screens: native 1.0 (no shrink).
   `--zf` IS SET HERE, and forgetting it is what cut the top off the desktop on Windows.
   These rules only override `zoom`; the variable kept whatever an earlier tier set, so a screen
   matching both got `zoom:1` with `--zf:.72` — and every container sized `calc(100dvh / var(--zf))`
   then computed 139% of the viewport. The desktop (.os-root, line ~6037) is one of them, so it grew
   taller than the window and its top went off screen; resizing narrow enough to leave this tier made
   the two agree again, which is exactly how it was reported: "i have to resize the entire windows
   app to see the desktop normally". `zoom` and `--zf` are one fact and must be written together. */
@media (min-width:1921px){ body{ zoom:1; --zf:1 } .app{ height:100dvh } .main{ height:100% } }
/* OS display scaling shrinks a screen's CSS width, so a high-DPI DESKTOP panel (e.g. 1868px CSS at
   devicePixelRatio 1.25 = Windows 125%) lands in the laptop-shrink tiers above and renders everything
   tiny. Key off device-pixel-ratio: give these screens native scale. A genuine ~1.0-dPR laptop reports
   a true small width AND low dPR, so it still gets the shrink tiers. Placed AFTER them to win the cascade.
   MUST be gated to real MOUSE desktops (hover + fine pointer): a high-DPI TOUCH tablet (Galaxy Tab) is also
   ≥1400px at dPR≥1.2, and native desktop scale is far too big on it — those stay in the shrink tiers. */
@media (min-width:1400px) and (min-resolution:1.2dppx) and (hover:hover) and (pointer:fine){ body{ zoom:1; --zf:1 } .app{ height:100dvh } .main{ height:100% } }
@media (min-width:1400px) and (-webkit-min-device-pixel-ratio:1.2) and (hover:hover) and (pointer:fine){ body{ zoom:1; --zf:1 } .app{ height:100dvh } .main{ height:100% } }
/* Feed post cards are intrinsically sized for a big 4K panel (avatar 52px, body 18.5px), so at laptop
   widths they still read oversized next to the now well-scaled rails — even under the zoom above. Trim
   the post card itself here only. 4K (>1920) keeps the full size; mobile (≤820) has its own block. */
@media (min-width:821px) and (max-width:1920px){
  .note,.tl-cmp{padding:12px 18px;gap:12px}
  .note .av,.tl-cmp-av{width:46px;height:46px}
  .note .name,.note .time{font-size:15px}
  .note .handle{font-size:15px}
  .note .txt{font-size:17px;line-height:1.6}
  .quoted .txt{font-size:15px}
}

/* Flashcards' card and its question title. These two selectors were shared with the 4chan
 * browser that used to live here — the browser is gone, these are still drawn. */
.fc-card{display:flex;flex-direction:column;border:1px solid var(--line);border-radius:12px;overflow:hidden;background:var(--panel);cursor:pointer;transition:.15s}
.fc-card:hover{border-color:var(--cyan);box-shadow:0 0 12px rgba(var(--accent-rgb),.18);transform:translateY(-2px)}
.fc-title{font-size:13px;line-height:1.45;color:var(--text);max-height:3.6em;overflow:hidden;word-break:break-word}
@media(max-width:820px){
  .fc-title{font-size:12px}
}

/* ---------- NIP-88 polls ---------- */
.note.poll .poll-q{margin:6px 0 10px;font-size:17px;line-height:1.45}
.poll-opts{display:flex;flex-direction:column;gap:8px}
.poll-opt{position:relative;display:flex;align-items:center;gap:8px;width:100%;text-align:start;
  padding:10px 12px;border:1px solid var(--line);border-radius:12px;background:var(--panel);
  color:var(--text);font-family:inherit;font-size:15px;cursor:pointer;overflow:hidden;transition:border-color .15s}
.poll-opt:hover{border-color:var(--cyan)}
.poll-opt.voted{border-color:var(--magenta);box-shadow:0 0 0 1px var(--magenta) inset}
.poll-opt:disabled{cursor:default}
.poll-bar{position:absolute;inset-inline-start:0;top:0;bottom:0;width:0;background:rgba(var(--accent-rgb),.14);z-index:0;transition:width .4s ease}
.poll-opt.voted .poll-bar{background:rgba(255,0,170,.18)}
.poll-label{position:relative;z-index:1;flex:1;min-width:0}
.poll-pct{position:relative;z-index:1;color:var(--muted);font-size:13px;white-space:nowrap}
.poll-foot{margin-top:8px}
.poll-build{margin:8px 0;padding:10px;border:1px solid var(--line);border-radius:12px;background:var(--panel);display:flex;flex-direction:column;gap:8px}
.poll-build .poll-opt-in{margin:0}
#cmp-poll.active,#cmp-poll-add{color:var(--cyan)}

/* ---------- Files view tabs (Public / AI Chat) ---------- */
.files-tabs{display:flex;gap:8px;padding:14px 16px 6px;border-bottom:1px solid var(--line)}
.feed.feed-files{padding:0;overflow:hidden;display:flex;flex-direction:column}
.feed-files .files-tabs{flex:0 0 auto}
.feed-files #files-pane{flex:1;min-height:0;overflow:auto;display:flex;flex-direction:column}
.feed-files #files-pane>.fx-explorer{flex:1;min-height:0;border-radius:0;border-width:0}
.feed-files #files-pane>.fx-explorer>.fx-side{max-height:none;min-height:0}
.feed-files #files-pane>.fx-explorer>.fx-main{min-height:0;overflow:auto}
.ftab{padding:8px 16px;border:1px solid var(--line);border-radius:16px;background:var(--panel);
  color:var(--muted);cursor:pointer;font-family:inherit;font-size:15px;font-weight:600}
.ftab:hover{border-color:var(--cyan);color:var(--text)}
.ftab.active{background:var(--cyan);color:#06121a;border-color:var(--cyan)}
@media(max-width:820px){
  /* ONE VERTICAL SCROLLER ON A PHONE.  The desktop explorer gives `.fx-main` its own overflow box,
     inside the already-scrollable `#files-pane`. Android WebView can hand the touch gesture to that
     inner box while its block-layout height is unconstrained, so neither box moves: Files looks
     complete but cannot scroll. Let the pane own vertical touch scrolling and allow the explorer
     and its main column to take their natural height. Horizontal folder chips remain independent. */
  .feed-files #files-pane{padding-bottom:calc(62px + env(safe-area-inset-bottom));box-sizing:border-box;
    overflow-y:auto;-webkit-overflow-scrolling:touch;touch-action:pan-y}
  .feed-files #files-pane>.fx-explorer{flex:0 0 auto;overflow:visible}
  .feed-files #files-pane>.fx-explorer>.fx-main{overflow:visible}
}

/* ---------- Chess hub (#chesstr) ---------- */
.chess-hub{padding:16px;display:flex;flex-direction:column;gap:18px}
.chess-splash{padding:18px 20px;border-radius:16px}
.chess-splash h2{margin:0 0 4px;font-size:24px;color:var(--cyan)}
.chess-how{margin:12px 0;padding-inline-start:22px;line-height:1.6;color:var(--text)}
.chess-how code{background:rgba(var(--accent-rgb),.1);padding:1px 6px;border-radius:6px;color:var(--cyan);font-size:13px}
.chess-invite{margin-top:14px}
.chess-lbl{display:block;font-weight:700;margin-bottom:6px;color:var(--magenta)}
.chess-inv-res{margin-top:8px;display:flex;flex-direction:column;gap:6px;max-height:340px;overflow:auto}
.chess-inv-row{display:flex;align-items:center;gap:10px;padding:8px 10px;border:1px solid var(--line);border-radius:12px;background:var(--panel)}
.chess-inv-row img{width:40px;height:40px;border-radius:50%;object-fit:cover;flex:none}
.chess-inv-row .ci-meta{flex:1;min-width:0;display:flex;flex-direction:column;overflow:hidden}
.chess-inv-row .ci-meta b{white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.chess-inv-row button{flex:none}
.chess-games h3{margin:0 0 10px;font-size:20px;color:var(--cyan)}
.chess-game-card{padding:14px;border-radius:16px;margin-bottom:14px}
.chess-card-hd{display:flex;align-items:center;gap:10px;margin-bottom:10px}
.cc-av{width:46px;height:46px;border-radius:50%;object-fit:cover;flex:none;border:1px solid var(--line)}
.cc-meta{flex:1;min-width:0;display:flex;flex-direction:column}
.cc-badge{flex:none;padding:4px 10px;border-radius:12px;font-size:12px;font-weight:700}
.cc-badge.you{background:var(--cyan);color:#06121a}
.cc-badge.wait{background:rgba(255,255,255,.08);color:var(--muted);border:1px solid var(--line)}
.cc-badge.done{background:rgba(255,0,170,.18);color:var(--magenta);border:1px solid var(--magenta)}
/* Game-over result banner — make WHO WON unmistakable (shared by all game cards). */
.chess-result{display:flex;align-items:center;gap:8px;flex-wrap:wrap;margin:0 0 12px;padding:10px 14px;border-radius:12px;font-weight:800;font-size:17px}
.chess-result.win{background:linear-gradient(90deg,rgba(0,255,170,.22),rgba(0,255,170,.06));color:#7dffce;border:1px solid rgba(0,255,170,.5);box-shadow:0 0 16px rgba(0,255,170,.18)}
.chess-result.loss{background:rgba(255,0,90,.12);color:#ff8aa8;border:1px solid rgba(255,0,90,.4)}
.chess-result.draw{background:rgba(255,255,255,.06);color:var(--muted);border:1px solid var(--line)}
.chess-result .small{font-weight:600}
/* Blackjack table */
.bj-table{padding:6px 0 2px}
.bj-hand{margin:10px 0}
.bj-lbl{display:block;font-weight:700;color:var(--muted);margin-bottom:6px;font-size:15px}
.bj-betrow{display:flex;align-items:center;gap:8px;justify-content:center;margin:0 0 12px;flex-wrap:wrap}
.bj-betbar{margin-top:12px;padding:12px;border-radius:15px;background:linear-gradient(180deg,#16092b,#100420);border:2px solid rgba(255,210,90,.45)}
.bj-bethdr{display:flex;align-items:center;justify-content:center;gap:8px;font-size:20px;margin-bottom:10px}
.bj-bethdr .bj-betnum{color:#ffd25a;font-size:24px;font-weight:900;text-shadow:0 0 9px rgba(255,210,90,.5)}
.bj-chiprow{display:flex;gap:12px;justify-content:center;flex-wrap:wrap;align-items:center;margin-bottom:10px}
.bj-pchip{width:54px;height:54px;border-radius:50%;border:4px dashed #999;background:radial-gradient(circle at 50% 36%,#2a1450,#140824);color:#fff;font-weight:900;font-size:15px;cursor:pointer;transition:.12s;flex:none;display:inline-flex;align-items:center;justify-content:center;box-shadow:0 2px 7px rgba(0,0,0,.45)}
.bj-pchip:hover{transform:translateY(-3px)} .bj-pchip:active{transform:scale(.92)}
.bj-pchip.v5{border-color:#5dffb0;color:#5dffb0} .bj-pchip.v25{border-color:#21e6ff;color:#7df0ff}
.bj-pchip.v100{border-color:#ff5a8a;color:#ff8aa8} .bj-pchip.v500{border-color:#ffd25a;color:#ffd25a}
.bj-pchip.vmax{border-color:#ff3cd2;color:#ff8ae6;font-size:12px}
.bj-betbar .bj-betinp{display:block;width:7em;margin:0 auto 10px;text-align:center;font-weight:800;color:#ffd25a}
.bj-myval{text-align:center;font-weight:900;font-size:20px;color:#ffd25a;margin-top:8px;letter-spacing:.5px}
.bj-myval.bust{color:#ff6b8b} .bj-myval.win{color:#5dffb0;text-shadow:0 0 9px rgba(93,255,176,.5)}
.pk-seat .bj-cards{display:inline-flex;gap:4px;vertical-align:middle}
.pk-seat .bj-cards .pk-card{min-width:24px;height:34px;font-size:13px;border-width:2px;margin:0}
.bj-cards{display:flex;gap:8px;flex-wrap:wrap}
.bj-card{display:inline-flex;align-items:center;justify-content:center;min-width:46px;height:64px;padding:0 10px;border-radius:9px;background:#160a28;border:2px solid var(--cyan);color:#eaf0ff;font-weight:800;font-size:20px}
.bj-card.red{color:#ff8aa8;border-color:#ff5a8a}
.bj-card.back{background:#28124a;border-color:var(--magenta);color:var(--magenta)}

/* ── Texas Hold'em (mobile-first cyberpunk felt) ─────────────────────── */
.pk-card{display:inline-flex;align-items:center;justify-content:center;min-width:42px;height:60px;padding:0 8px;border-radius:8px;background:#160a28;border:2px solid var(--cyan);color:#eaf0ff;font-weight:800;font-size:20px;margin:2px;box-shadow:0 0 6px rgba(0,240,255,.25)}
.pk-card.red{color:#ff8aa8;border-color:#ff5a8a;box-shadow:0 0 6px rgba(255,90,138,.3)}
.pk-card.back{background:#28124a;border-color:var(--magenta);color:var(--magenta);box-shadow:none}
.pk-card.empty{background:transparent;border:2px dashed rgba(120,110,150,.4);box-shadow:none}
.pk-card.big{min-width:64px;height:90px;font-size:30px;border-radius:11px}
.pk-felt{margin:10px 0;padding:18px 14px;border-radius:22px;background:radial-gradient(ellipse at center,#0e2a24,#081513);border:2px solid rgba(0,240,255,.35);text-align:center;box-shadow:inset 0 0 34px rgba(0,240,255,.08)}
.pk-pot{color:#ffd25a;font-weight:800;letter-spacing:.5px;margin-bottom:10px;font-size:15px;text-shadow:0 0 8px rgba(255,210,90,.4)}
.pk-street{color:var(--cyan);font-weight:800;font-size:12px;letter-spacing:2px;margin-bottom:8px;opacity:.85}
.pk-board{display:flex;justify-content:center;gap:8px;flex-wrap:wrap}
.pk-log{display:flex;flex-direction:column;gap:4px;align-items:center;margin-top:12px}
.pk-logln{color:#9fcfc8;font-size:11px;line-height:1.45;opacity:.7;background:rgba(0,240,255,.05);border-radius:9px;padding:1px 10px;max-width:100%;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
/* the LATEST action stands out: bigger, dark-on-neon pill with a glowing pulse + a ▶ marker */
.pk-logln:last-child{position:relative;color:#04141f;font-weight:800;font-size:15px;letter-spacing:.3px;opacity:1;background:linear-gradient(90deg,var(--neon,#0ff),#9df3ff);border:1px solid rgba(255,255,255,.35);border-radius:11px;padding:6px 14px 6px 26px;text-shadow:none;white-space:normal;overflow:visible;text-overflow:clip;text-align:center;animation:pklast 1.5s ease-in-out infinite}
.pk-logln:last-child::before{content:"▶";position:absolute;inset-inline-start:10px;top:50%;transform:translateY(-50%);font-size:11px;color:#04141f}
.pk-logln:only-child{margin-top:0}
@keyframes pklast{0%,100%{box-shadow:0 0 10px rgba(0,240,255,.45)}50%{box-shadow:0 0 20px rgba(0,240,255,.8),0 0 30px rgba(255,0,230,.25)}}
@media(prefers-reduced-motion:reduce){.pk-logln:last-child{animation:none;box-shadow:0 0 14px rgba(0,240,255,.55)}}
.pk-mychips{margin:10px 0;padding:12px 14px;border-radius:13px;text-align:center;background:linear-gradient(180deg,#1a1030,#120622);border:2px solid rgba(255,210,90,.5);color:#ffe9a8;font-size:17px;font-weight:600;box-shadow:0 0 14px rgba(255,210,90,.18)}
.pk-mychips b{color:#ffd25a;font-size:24px;font-weight:900;text-shadow:0 0 8px rgba(255,210,90,.5)}
.pk-last{margin:8px 0 0;padding:10px 14px;border-radius:11px;text-align:center;background:#150826;border:1px solid rgba(120,110,150,.4);color:#b9a9d6;font-weight:700}
.pk-last.win{background:linear-gradient(180deg,#10301f,#0b2417);border-color:rgba(93,255,176,.6);color:#5dffb0;box-shadow:0 0 14px rgba(93,255,176,.25)}
/* Big win/loss announcement — a deliberate visual pause between hands so the result is unmissable */
.pk-bigresult{position:fixed;inset:0;z-index:420;display:grid;place-items:center;padding:24px;background:rgba(4,2,12,.74);backdrop-filter:blur(5px);-webkit-backdrop-filter:blur(5px);animation:fade .18s;cursor:pointer}
.pk-bigresult.out{animation:fade .28s reverse}
.pk-br-card{text-align:center;padding:30px 34px;border-radius:20px;background:linear-gradient(180deg,#1a1030,#120622);min-width:240px;max-width:min(90vw,420px);animation:pop .3s}
.pk-bigresult.win .pk-br-card{border:2px solid rgba(93,255,176,.7);box-shadow:0 0 44px rgba(93,255,176,.4),inset 0 0 30px rgba(93,255,176,.08);animation:pop .3s,pkwinglow 1.4s ease-in-out infinite .3s}
.pk-bigresult.loss .pk-br-card{border:2px solid rgba(255,60,120,.6);box-shadow:0 0 40px rgba(255,0,90,.3),inset 0 0 26px rgba(255,0,90,.06)}
.pk-br-emoji{font-size:58px;line-height:1;margin-bottom:6px}
.pk-br-title{font-size:30px;font-weight:900;letter-spacing:1px}
.pk-bigresult.win .pk-br-title{color:#5dffb0;text-shadow:0 0 14px rgba(93,255,176,.6)}
.pk-bigresult.loss .pk-br-title{color:#ff8aa8;text-shadow:0 0 14px rgba(255,0,90,.45)}
.pk-br-amt{font-size:24px;font-weight:900;color:#ffd25a;text-shadow:0 0 10px rgba(255,210,90,.5);margin-top:4px}
.pk-br-sum{margin-top:12px;color:var(--muted);font-size:13px;font-weight:600;line-height:1.45}
.pk-br-tap{margin-top:16px;font-size:11px;color:var(--muted);opacity:.7;text-transform:uppercase;letter-spacing:1px}
@keyframes pkwinglow{0%,100%{box-shadow:0 0 44px rgba(93,255,176,.4),inset 0 0 30px rgba(93,255,176,.08)}50%{box-shadow:0 0 64px rgba(93,255,176,.65),0 0 90px rgba(255,210,90,.2),inset 0 0 36px rgba(93,255,176,.12)}}
@media(prefers-reduced-motion:reduce){.pk-bigresult.win .pk-br-card{animation:pop .3s}}
.pk-seats{display:flex;flex-direction:column;gap:6px;margin:8px 0}
.pk-seat{display:flex;align-items:center;justify-content:space-between;gap:8px;padding:8px 12px;border-radius:11px;background:#150826;border:2px solid rgba(255,60,210,.38)}
.pk-seat.me{border-color:var(--cyan)}
.pk-seat.turn{border-color:#ffd25a;box-shadow:0 0 12px rgba(255,210,90,.45)}
.pk-seat.out{opacity:.45}
.pk-who{display:flex;align-items:center;gap:10px;min-width:0}
.pk-av{width:34px;height:34px;border-radius:50%;object-fit:cover;flex:none;border:2px solid rgba(0,240,255,.4);background:#0d0a1a}
.pk-seat.me .pk-av{border-color:var(--cyan)}
.pk-nm{font-weight:700;color:#eaf0ff;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.pk-stk{color:#b9a9d6;font-size:13px;display:flex;align-items:center;gap:6px}
.pk-hole{display:inline-flex;gap:4px}
.pk-myhand{display:flex;flex-direction:column;gap:12px;margin:12px 0;padding:12px 14px;border-radius:15px;background:linear-gradient(180deg,#16092b,#100420);border:2px solid var(--cyan);box-shadow:0 0 16px rgba(0,240,255,.18)}
.pk-myinfo{display:flex;align-items:center;gap:12px;min-width:0;align-self:flex-start}
.pk-myav{width:52px;height:52px;border-radius:11px;object-fit:cover;flex:none;border:2px solid var(--cyan);background:#0d0a1a;box-shadow:0 0 10px rgba(0,240,255,.3)}
.pk-mymeta{display:flex;flex-direction:column;gap:4px;min-width:0}
.pk-myname{font-weight:800;color:#eaf0ff;font-size:17px}
.pk-mychipline{color:#ffe9a8;font-size:15px;white-space:nowrap}
.pk-mychipline b{color:#ffd25a;font-size:20px;font-weight:900;text-shadow:0 0 7px rgba(255,210,90,.5)}
.pk-mycards{display:flex;gap:10px;justify-content:center}
.pk-controls{display:flex;flex-wrap:wrap;gap:8px;align-items:center;margin-top:12px}
.pk-controls .btn{flex:1 1 auto;min-width:72px}
.pk-raise{display:inline-flex;gap:6px;align-items:center;flex:2 1 9em}
.pk-amt{padding:8px 10px;flex:1 1 auto}
.btn-magenta{background:linear-gradient(135deg,#ff3cd2,#a01e8c);color:#fff;border:none}
@media(max-width:520px){
  .pk-card{min-width:32px;height:48px;font-size:17px;padding:0 4px;margin:1px}
  .pk-card.big{min-width:46px;height:66px;font-size:24px}
  .pk-board{gap:6px}
  .pk-felt{padding:14px 8px}
  .pk-pot{font-size:15px} .pk-street{font-size:11px;letter-spacing:1.5px}
  .pk-myhand{padding:12px;gap:10px}
  .pk-myav{width:46px;height:46px} .pk-av{width:30px;height:30px}
  .pk-myname{font-size:15px} .pk-mychipline{font-size:13px}
  .pk-mychipline b{font-size:17px}
  .pk-seat{padding:6px 10px} .pk-nm{font-size:15px}
  .pk-seat .bj-cards .pk-card{min-width:20px;height:30px;font-size:11px}
  .pk-controls{gap:8px} .pk-controls .btn{min-width:60px;font-size:13px;padding:10px 6px;flex:1 1 30%}
  .bj-betrow{font-size:13px;gap:6px}
  .bj-myval{font-size:17px}
  .chess-game-card{padding:12px}
}
.bj-controls{display:flex;gap:10px;margin-top:12px}
.bj-seatchips{display:flex;flex-wrap:wrap;gap:6px;margin:8px 0}
.bj-chip{display:inline-flex;align-items:center;gap:4px;background:rgba(60,230,255,.12);border:1px solid var(--cyan);border-radius:14px;padding:4px 6px 4px 10px;font-size:13px}
.bj-chip button{background:none;border:none;color:var(--muted);cursor:pointer;font-size:15px;padding:0}
.bj-hand.mine .bj-lbl{color:var(--cyan)}
.bj-hand.dealer .bj-lbl{color:var(--magenta)}
.bj-out{font-weight:800;padding:1px 6px;border-radius:8px;font-size:12px}
.bj-out.win{background:rgba(0,255,170,.2);color:#7dffce}
.bj-out.loss{background:rgba(255,0,90,.15);color:#ff8aa8}
.chess-board-img{display:block;width:100%;max-width:520px;height:auto;border-radius:12px;margin:0 auto;border:1px solid var(--line)}
.chess-board-link{display:block}
.chess-move-row{display:flex;gap:8px;margin-top:12px;max-width:520px;margin-inline-start:auto;margin-inline-end:auto}
.chess-move-in{flex:1;min-width:0}
.chess-move-row button{flex:none}
@media(max-width:820px){
  .chess-hub{padding:10px}
  .chess-splash{padding:14px}
  .chess-splash h2{font-size:24px}
  .chess-how{font-size:15px;padding-inline-start:18px}
  .chess-board-img,.chess-move-row{max-width:100%}
  .cc-badge{font-size:11px;padding:4px 8px}
}

/* interactive chessboard (tap-to-move) */
.chess-board-wrap{max-width:460px;margin:0 auto}
.chess-board-wrap.flip .chessboard{transform:rotate(180deg)}
.chess-board-wrap.flip .cpc{display:inline-block;transform:rotate(180deg)}
.chessboard{display:grid;grid-template-columns:repeat(8,1fr);grid-template-rows:repeat(8,1fr);aspect-ratio:1/1;width:100%;
  border:2px solid var(--cyan);border-radius:10px;overflow:hidden;box-shadow:0 0 16px rgba(var(--accent-rgb),.25)}
.csq{display:flex;align-items:center;justify-content:center;position:relative;cursor:pointer;user-select:none;aspect-ratio:1/1;min-width:0;min-height:0}
.csq.lt{background:#26124a}
.csq.dk{background:#120826}
.csq.sel{box-shadow:inset 0 0 0 3px var(--magenta);background:rgba(255,0,170,.18)}
.csq.pending{opacity:.55}
.cpc{font-size:min(7.2vw,30px);line-height:1}
.cpc.cw{color:#3ce6ff;text-shadow:0 0 6px rgba(60,230,255,.9)}
.cpc.cb{color:#ff3cd2;text-shadow:0 0 6px rgba(255,60,210,.9)}
@media(max-width:820px){ .chess-board-wrap{max-width:100%} .cpc{font-size:8.5vw} }

.chessboard.locked{opacity:.55;pointer-events:none}

.chess-quit{flex:none;background:none;border:1px solid var(--line);color:var(--muted);border-radius:8px;
  cursor:pointer;font-size:13px;padding:2px 8px;margin-inline-start:6px}
.chess-quit:hover{border-color:var(--magenta);color:var(--magenta)}

/* ---------- Tic-Tac-Toe ---------- */
.ttt-wrap{max-width:360px;margin:0 auto}
.ttt-board{display:grid;grid-template-columns:repeat(3,1fr);grid-template-rows:repeat(3,1fr);aspect-ratio:1/1;width:100%;
  gap:6px;background:var(--cyan);border:2px solid var(--cyan);border-radius:12px;overflow:hidden;box-shadow:0 0 16px rgba(var(--accent-rgb),.25)}
.ttt-cell{display:flex;align-items:center;justify-content:center;background:#120826;position:relative;cursor:default;min-width:0;min-height:0}
.ttt-cell.empty{cursor:pointer}
.ttt-cell.empty:hover{background:#1c0f3a}
.ttt-cell.pending{opacity:.5}
.ttt-num{position:absolute;top:4px;inset-inline-start:7px;font-size:13px;color:#5a3b80}
.ttt-mark{font-size:min(16vw,64px);font-weight:800;line-height:1}
.ttt-mark.tx{color:#3ce6ff;text-shadow:0 0 8px rgba(60,230,255,.9)}
.ttt-mark.to{color:#ff3cd2;text-shadow:0 0 8px rgba(255,60,210,.9)}

/* ---------- Hangman ---------- */
.hm-word{font-family:var(--mono);font-size:24px;letter-spacing:5px;text-align:center;
  color:#3ce6ff;text-shadow:0 0 8px rgba(60,230,255,.7);margin:10px 0 14px;word-break:break-word}
.hm-keys{display:grid;grid-template-columns:repeat(9,1fr);gap:6px;max-width:420px;margin:0 auto}
.hm-key{aspect-ratio:1/1;border:1px solid var(--line);border-radius:8px;background:var(--panel);color:var(--text);
  font-weight:700;font-size:15px;cursor:pointer;font-family:inherit;display:flex;align-items:center;justify-content:center}
.hm-key:hover:not(:disabled){border-color:var(--cyan);color:var(--cyan)}
.hm-key.used,.hm-key:disabled{opacity:.3;cursor:default}
@media(max-width:820px){ .ttt-wrap{max-width:300px} .hm-keys{grid-template-columns:repeat(7,1fr)} .hm-word{font-size:24px;letter-spacing:3px} }

/* ---------- Connect Four ---------- */
.c4-wrap{max-width:420px;margin:0 auto}
.c4-board{display:grid;grid-template-columns:repeat(7,1fr);grid-template-rows:repeat(6,1fr);gap:6px;
  background:rgba(var(--accent-rgb),.07);border:2px solid var(--cyan);border-radius:12px;padding:6px;
  box-shadow:0 0 16px rgba(var(--accent-rgb),.22)}
.c4-cell{aspect-ratio:1/1;display:flex;align-items:center;justify-content:center;cursor:pointer;min-width:0}
.c4-cell.pending{opacity:.6}
.c4-disc{width:84%;height:84%;border-radius:50%;background:#160a30;border:1px solid #2a1850}
.c4-disc.d1{background:#3ce6ff;border-color:#3ce6ff;box-shadow:0 0 8px rgba(60,230,255,.8)}
.c4-disc.d2{background:#ff3cd2;border-color:#ff3cd2;box-shadow:0 0 8px rgba(255,60,210,.8)}
@media(max-width:820px){ .c4-wrap{max-width:100%} }

/* Hangman in-app gallows */
.hm-game{display:flex;align-items:center;justify-content:center;gap:18px;margin:8px 0 14px;flex-wrap:wrap}
.hm-svg{width:120px;height:150px;flex:none;filter:drop-shadow(0 0 5px rgba(var(--accent-rgb),.4))}
.hm-game .hm-word{margin:0}

/* ===== Messages: DM/Email tabs + Nostr-mailbox Email client (themed via vars; mobile-responsive) ===== */
.feed.feed-dm{display:flex;flex-direction:column;min-height:0}
.feed.feed-dm>.dm-wrap{flex:1 1 auto;min-height:0;height:auto}
.messages-tabs{flex:0 0 auto;display:flex;gap:4px;padding:7px 10px;border-bottom:1px solid var(--line);background:var(--panel)}.messages-tabs button{min-height:34px;padding:6px 13px;border:0;border-radius:8px;background:transparent;color:var(--muted);font:inherit;font-weight:650;cursor:pointer}.messages-tabs button:hover{background:var(--panel2);color:var(--text)}.messages-tabs button.on{background:rgba(var(--accent-rgb),.14);color:var(--text)}

.mail-root{flex:1 1 auto;min-height:0;display:flex;overflow:hidden}
.mail-loading,.mail-empty{flex:1;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:12px;text-align:center;padding:24px}
.mail-empty .me-ico{font-size:44px}
.mail-empty p{max-width:430px}
.mail-wrap{flex:1;display:grid;grid-template-columns:210px 330px 1fr;height:100%;min-height:0;width:100%}
.mail-side{border-inline-end:1px solid var(--line);padding:12px 10px;display:flex;flex-direction:column;gap:10px;overflow-y:auto;background:var(--panel)}
.mail-side .mail-acct{width:100%;font-size:13px}
.mail-compose{width:100%}
.mail-folders{display:flex;flex-direction:column;gap:4px;margin-top:4px}
.mail-folder{text-align:start;background:transparent;border:none;color:var(--text);border-radius:9px;padding:9px 11px;cursor:pointer;font-size:14px}
.mail-folder:hover{background:var(--panel2)}
.mail-folder.on{background:var(--panel2);color:var(--cyan);font-weight:600}
.mail-list{border-inline-end:1px solid var(--line);display:flex;flex-direction:column;min-height:0;overflow:hidden}
.mail-list-top{flex:0 0 auto;display:flex;gap:6px;padding:8px;border-bottom:1px solid var(--line)}
.mail-search{flex:1;font-size:14px}
.mail-refresh{flex:0 0 auto}
.mail-items{flex:1;overflow-y:auto;min-height:0}
.mail-item{padding:11px 13px;border-bottom:1px solid var(--line);cursor:pointer}
.mail-item:hover{background:var(--panel2)}
.mail-item.active{background:var(--panel2);box-shadow:inset 3px 0 0 var(--cyan)}
.mail-item.unread{background:rgba(0,240,255,.045)}
.mail-item.unread .mi-from,.mail-item.unread .mi-subj{font-weight:700}
.mi-row{display:flex;justify-content:space-between;gap:8px;align-items:baseline}
.mi-from{font-size:13.5px;color:var(--text);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.mi-date{font-size:11.5px;color:var(--muted);flex:0 0 auto}
.mi-subj{font-size:13.5px;color:var(--text);margin-top:2px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.mi-prev{white-space:nowrap;overflow:hidden;text-overflow:ellipsis;margin-top:2px}
.mail-read{display:flex;flex-direction:column;min-width:0;min-height:0;overflow-y:auto}
.mail-read>.empty{margin:auto;color:var(--muted)}
.mail-read-hd{flex:0 0 auto;display:flex;gap:10px;align-items:flex-start;padding:16px 18px 8px;border-bottom:1px solid var(--line)}
.mail-back{display:none}
.mr-meta{min-width:0}
.mr-subj{font-size:18px;font-weight:700;color:var(--text);line-height:1.3;word-break:break-word}
.mr-from{margin-top:4px;color:var(--text)}
.mr-to{margin-top:2px;word-break:break-all}
/* The reader actions are icon buttons, not seven equal-width content columns. Keep their desktop
   footprint compact and stable; flex-wrap only matters when a genuinely narrow split pane cannot
   fit the seven 40px controls. The phone reader overrides this with a full-width touch grid below. */
.mail-actions{flex:0 0 auto;display:flex;flex-wrap:wrap;
  gap:6px;padding:10px 18px;border-bottom:1px solid var(--line);align-items:stretch;max-width:100%;overflow:hidden}
.mail-actions .btn{flex:0 0 40px;width:40px;min-height:36px;min-width:0;justify-content:center;padding:7px 6px;
  overflow:hidden;text-overflow:ellipsis;white-space:nowrap;
  background:color-mix(in srgb, var(--neon) 16%, var(--panel2));
  border-color:color-mix(in srgb, var(--neon) 42%, var(--line));color:var(--text)}
.mail-actions .btn-cyan{background:linear-gradient(135deg,#0bb6cf,var(--cyan));border:none;color:#04222a}
.mail-actions .btn-red{background:linear-gradient(135deg,#ff3366,#ff5a7d);border:none;color:#fff}
/* the ✨ Summarize result: model text, so it arrives as plain lines — keep them */
.mail-ai-summary{white-space:pre-wrap;overflow-wrap:anywhere;max-height:60vh;overflow-y:auto;
  font-size:14px;line-height:1.5;background:var(--bg-2,rgba(255,255,255,.04));
  border:1px solid var(--line,rgba(255,255,255,.08));border-radius:10px;padding:12px}

/* The bulk actions are the same family of control, so they get the same fill. */
.mail-bulk-act .btn{background:color-mix(in srgb, var(--neon) 16%, var(--panel2));
  border-color:color-mix(in srgb, var(--neon) 42%, var(--line));color:var(--text)}
.mail-bulk-act .btn-red{background:linear-gradient(135deg,#ff3366,#ff5a7d);border:none;color:#fff}

/* The BULK bar under Select. Its buttons were appended inline after the "n selected" label, so they
   sat hard against it and wrapped under the checkbox at any narrow width. The label holds the left,
   the actions hold the right, and they stay on one line together. */
.mail-bulk-act .btn{min-height:34px;white-space:nowrap;padding:5px 8px;font-size:12.5px}
.mail-atts{flex:0 0 auto;display:flex;flex-wrap:wrap;gap:8px;padding:10px 18px}
.mail-att{display:inline-flex;align-items:center;gap:6px;background:var(--panel2);border:1px solid var(--line);border-radius:10px;padding:6px 10px;color:var(--cyan);text-decoration:none;font-size:13px}
.mail-att:hover{border-color:var(--neon)}
.mail-body{padding:16px 18px;color:var(--text);line-height:1.5;overflow-wrap:anywhere}
/* HTML mail is already a complete document. Let it use the reader edge-to-edge instead of putting
   a decorative card gutter around a second document viewport; plain text keeps the reading inset. */
.mail-body:has(>.mail-html){padding:0}
.mail-body .mail-text{white-space:pre-wrap}
/* THE MESSAGE FILLS THE PANE. A fixed 55vh left a band of empty pane under every short mail and
   made every long one scroll inside a letterbox — two scrollbars, one inside the other.
   Divided by --zf for the reason .ws-frame is: desktop scales the page with body{zoom:.67-.77} and
   viewport units are NOT rescaled by it, so a raw vh paints at two thirds of what it asked for. */
.mail-html{width:100%;border:none;background:#fff;border-radius:0;
  height:calc((100dvh / var(--zf, 1)) - 300px);min-height:420px}
/* When a thread holds ONE message (the common case) that message owns the pane, so the body grows
   instead of the pane keeping slack under it. `.mail-thread` itself is never :only-child: the reader
   header and action bar are its siblings, so only test the message count inside the thread. */
.mail-thread>.mail-msg:only-child{display:flex;flex-direction:column;min-height:0;flex:1}
.mail-thread>.mail-msg:only-child>.mail-msg-body{flex:1;min-height:0;display:flex;flex-direction:column}
.mail-thread>.mail-msg:only-child .mail-body{flex:1;min-height:0;display:flex;flex-direction:column}
.mail-thread>.mail-msg:only-child .mail-html{flex:1}
.mail-acct-row{display:flex;justify-content:space-between;align-items:center;gap:10px;padding:10px 0;border-bottom:1px solid var(--line)}
.mar-meta{min-width:0}
.mar-meta b{word-break:break-all}

@media(max-width:820px){
  .mail-wrap{display:flex;flex-direction:column;height:100%;min-height:0}
  /* The three panes become ONE flow. Without this the grid keeps its `210px 330px 1fr` tracks, so a
     360px phone renders a 210px sidebar, a 330px list and a reading pane squeezed to nothing — and
     every rule below (which assumes flex children) styles a grid that is still three columns. */
  .mail-wrap{display:flex;flex-direction:column;grid-template-columns:none}
  .mail-side{flex:0 0 auto;flex-direction:row;flex-wrap:wrap;align-items:center;gap:8px;border-inline-end:none;border-bottom:1px solid var(--line);padding:8px}
  /* 16px on every text field: below it iOS zooms on focus and never zooms back, and mail is search
     boxes and a compose form — nothing but text fields. */
  .mail-search, .mail-list-top .input{font-size:16px}
  #cm-to, #cm-cc, #cm-subj, #cm-body{font-size:16px}
  .mail-side .mail-acct{flex:1;min-width:130px;width:auto}
  .mail-compose{width:auto;flex:0 0 auto}
  .mail-folders{flex-direction:row;width:100%;margin-top:0;gap:6px}
  .mail-folder{flex:1;text-align:center}
  .mail-list{flex:1;min-height:0;border-inline-end:none}
  .mail-items{padding-bottom:calc(62px + env(safe-area-inset-bottom))}
  .mail-read{display:none}
  .mail-read.has-open{display:flex;position:fixed;inset:0;z-index:46;background:var(--bg);padding-bottom:calc(62px + env(safe-area-inset-bottom))}
  .mail-read-hd{position:sticky;top:0;z-index:3;background:color-mix(in srgb,var(--panel) 96%,transparent);backdrop-filter:blur(12px);border-bottom:1px solid var(--line)}
  .mail-html{height:auto;min-height:62dvh;border-radius:0}
  .mail-back{display:inline-flex}
  /* A reader toolbar, not seven giant message cards. All actions stay visible in one compact row;
     40px remains a comfortable touch target and fits even the 360px WebView without scrolling. */
  .mail-actions{padding:6px 8px;gap:4px;display:grid;grid-template-columns:repeat(7,minmax(0,1fr));
    overflow:visible;background:color-mix(in srgb,var(--panel) 96%,transparent);
    border-bottom:1px solid var(--line)}
  .mail-actions .btn{width:auto;min-width:0;min-height:40px;height:40px;font-size:13px;padding:7px;
    border-radius:9px;box-shadow:none}
  .mail-read-hd{padding:14px 12px 11px;gap:10px;align-items:flex-start}
  .mr-subj{font-size:20px;line-height:1.25;font-weight:750;white-space:normal;overflow:visible}
  .mail-msg-hd{padding:14px 14px;min-height:62px;align-items:center}.mm-who b{font-size:15px}.mm-who .muted{margin-top:3px;font-size:12px}
  .mail-atts{padding:12px 14px}.mail-att{min-height:44px;padding:9px 11px;max-width:100%}
  .mail-body{padding:18px 16px 30px;font-size:16px;line-height:1.68;background:color-mix(in srgb,var(--panel) 74%,var(--bg));word-break:normal;overflow-wrap:anywhere}
  .mail-body .mail-text{max-width:72ch;margin:0 auto;width:100%}
}
/* Compose → 👤 Contacts: the same encrypted addressbooks the Contacts screen and your phone sync. */
/* Stacks OVER the composer: closeModal() empties #modal-root, so a picker opened as a plain modal
   takes the half-written email with it when it closes. Above .modal-bg (100), below uiConfirm (500). */
.sheet-bg{z-index:220}
/* WRITING AN EMAIL IS NOT FILLING IN A DIALOG. The composer inherited the app's default modal —
   720px wide and sized to its content — which gave a nine-row box for something people write pages
   in. It is now a proper window: wide, tall, and the message body takes every pixel the headers do
   not. On a phone it is the whole screen, because there is nothing else you are doing while writing
   an email.
   Divided by --zf like every other full-height thing here: desktop scales the page with
   body{zoom:.67-.77} and viewport units are not rescaled by it. */
.mail-compose-modal{width:min(calc(94vw / var(--zf, 1)), 1600px);max-width:none;
  /* Tall on purpose: writing an email is the one thing in this app people write pages in, so the
     composer gets the screen. Divided by --zf like every other viewport-sized thing here, because
     desktop scales the page with body{zoom:.67-.77} and viewport units are not rescaled by it. */
  height:calc((88dvh / var(--zf, 1)));max-height:none;min-height:340px;
  display:flex;flex-direction:column;gap:0}
/* Title and Send share the header row; the body below it is the only thing that grows. */
.mail-compose-modal .cm-head{flex:0 0 auto;display:flex;align-items:center;gap:10px;margin-bottom:10px}
.mail-compose-modal .cm-head h3{margin:0;flex:0 1 auto;min-width:0;overflow:hidden;
  text-overflow:ellipsis;white-space:nowrap}
.mail-compose-modal .cm-head .spacer{flex:1}
.mail-compose-modal .cm-head .btn{flex:0 0 auto}
.mail-compose-modal h3{flex:0 0 auto;margin-bottom:10px}
/* One footer row, wrapping. Two rows of buttons with a spacer between them never lined up. */
.mail-compose-modal .cm-actions{flex:0 0 auto;display:flex;flex-wrap:wrap;align-items:center;
  gap:8px;margin-top:10px}
.mail-compose-modal .cm-actions .btn{flex:0 0 auto}
.cm-atts{flex:1 1 100%;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.mail-compose-modal .input,.mail-compose-modal .mail-from{flex:0 0 auto}
.mail-compose-modal #cm-body{flex:1 1 auto;min-height:160px;resize:none;margin-top:8px}
.mail-compose-modal .cmp-tools,.mail-compose-modal .cm-actions{flex:0 0 auto}
@media(max-width:820px){
  /* Full screen. A 96vw sheet floating on a dimmed page wastes the only space a phone has, and the
     Send button ended up under the keyboard. */
  .modal-bg:has(.mail-compose-modal){padding:0}
  .mail-compose-modal{width:100vw;height:100dvh;border-radius:0;padding:14px 12px
    calc(14px + env(safe-area-inset-bottom))}
  .mail-compose-modal #cm-body{min-height:120px}
}

/* Recipient type-ahead. Anchored to the field it completes, above the modal's own stacking so it is
   not clipped by the composer's scroll container. */
.mc-auto{position:relative;z-index:5;margin:-6px 0 8px;border:1px solid var(--line);border-radius:10px;
  background:var(--panel);overflow:hidden;max-height:210px;overflow-y:auto}
.mc-auto-item{display:flex;flex-direction:column;align-items:flex-start;gap:0;width:100%;text-align:start;
  padding:8px 10px;border:0;background:transparent;color:inherit;cursor:pointer;min-height:44px}
.mc-auto-item.on,.mc-auto-item:hover{background:var(--panel2)}
/* Settings → Mail → 📂 Folders */
.usf-grid{display:flex;flex-direction:column;gap:8px;margin-top:10px}
.usf-row{display:flex;align-items:center;gap:10px}
.usf-lbl{flex:0 0 132px;font-size:13px;color:var(--muted)}
.usf-sel{flex:1 1 auto;min-width:0}
@media(max-width:820px){
  .usf-row{flex-direction:column;align-items:stretch;gap:3px}
  .usf-lbl{flex:none}
  .usf-sel{font-size:16px}      /* 16px or iOS zooms on focus and never zooms back */
}
.mc-list{max-height:46vh;overflow:auto;margin-top:10px;display:flex;flex-direction:column;gap:2px}
.mc-item{display:flex;flex-direction:column;align-items:flex-start;gap:1px;width:100%;text-align:start;
  padding:9px 10px;border:1px solid transparent;border-radius:10px;background:transparent;
  color:inherit;cursor:pointer;min-height:44px}
.mc-item:hover{border-color:var(--neon);background:var(--panel)}
.mc-name{font-weight:600;font-size:14px}
.mc-mail{overflow-wrap:anywhere}
@media(max-width:820px){
  /* 16px or iOS zooms the page on focus and never zooms back — the search box is the first thing
     tapped in this picker. */
  #mc-q{font-size:16px}
}
.mi-acct{display:inline-block;background:var(--panel2);border:1px solid var(--line);border-radius:7px;padding:0 5px;font-size:10.5px;color:var(--cyan);margin-inline-end:3px;vertical-align:middle}
.mail-from{display:block;margin:-2px 0 8px}
.mail-from select{margin-top:3px}
/* mail bulk-select */
.mail-item{display:flex;align-items:flex-start;gap:9px}
/* The keyboard cursor. Distinct from .active (the message actually OPEN) — with j/k you move the
   cursor without opening anything, and the two must not look the same. */
.mail-item.cursor{outline:2px solid var(--neon);outline-offset:-2px;border-radius:8px}
.mi-chk{margin-top:3px;flex:0 0 auto;cursor:pointer;width:16px;height:16px}
.mi-content{flex:1;min-width:0;cursor:pointer}
/* "still working" for a folder pull that legitimately takes minutes — a bare spinner over an empty
   list is indistinguishable from a hung screen. */
.mail-busy{flex:0 0 auto;display:flex;align-items:center;gap:8px;padding:7px 10px;font-size:12.5px;
  color:var(--muted);border-bottom:1px solid var(--line);background:var(--panel)}
.mail-busy-dot{width:8px;height:8px;border-radius:50%;background:var(--cyan);flex:0 0 auto;
  animation:mailPulse 1.1s ease-in-out infinite}
@keyframes mailPulse{0%,100%{opacity:.25}50%{opacity:1}}
/* The bulk bar lives in the LIST column, which is ~220px on a desktop pane. A label plus three
   actions cannot share one line there, so it WRAPS — my previous attempts kept it on one row and
   then either clipped Delete off the edge or hid it inside a scroll container, which is the same
   thing from the user's side: an action that is not there. */
.mail-bulk{flex:0 0 auto;display:flex;align-items:center;gap:6px 8px;padding:6px 10px;border-bottom:1px solid var(--line);font-size:12.5px;color:var(--muted);flex-wrap:wrap;min-height:40px}
.mail-selall{display:inline-flex;align-items:center;gap:5px;cursor:pointer;white-space:nowrap}
.mail-bulk-act{display:flex;align-items:center;gap:6px;flex-wrap:wrap;flex:1 1 100%}
.mail-bulk-act .btn{flex:1 1 auto;min-width:0;justify-content:center}
.mail-bulk-n{color:var(--cyan);font-weight:600}
/* mail conversation thread + Blossom attach picker */
.mail-thread{display:flex;flex-direction:column;flex:1;min-height:0}
.mail-msg{border-bottom:1px solid var(--line)}
.mail-msg-hd{display:flex;justify-content:space-between;gap:10px;align-items:flex-start;padding:12px 18px;cursor:pointer}
/* From/To are unbounded foreign strings; without this a long one pushes the date out of the pane. */
.mm-who{overflow:hidden}
.mm-who b,.mm-who .muted{display:block;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.mail-msg-hd:hover{background:var(--panel2)}
.mail-msg.open>.mail-msg-hd{background:var(--panel2)}
.mm-who{min-width:0}
.mm-date{flex:0 0 auto}
.mail-msg-body{display:none}
.mail-msg.open>.mail-msg-body{display:block}
.bp-list{max-height:50vh;overflow-y:auto;display:flex;flex-direction:column;gap:5px;margin-top:6px}
.bp-item{text-align:start;background:var(--panel2);border:1px solid var(--line);color:var(--text);border-radius:9px;padding:9px 11px;cursor:pointer}
.bp-item:hover{border-color:var(--neon)}
@media(max-width:820px){ .hm-svg{width:100px;height:124px} }

/* ============================================================================================
   Premium polish v2 — motion + interaction feedback that makes the client feel alive/modern.
   All theme-var based (safe across the 8 themes), hover-only where it should be (so touch feeds
   aren't affected), and disabled under prefers-reduced-motion. Purely additive.
   ============================================================================================ */

/* Feed cards rise in as they load/stream (replaces the plain fade) — a busy feed feels alive. */
.note{ animation: noteRise .3s cubic-bezier(.22,.7,.3,1) both; }
@keyframes noteRise{ from{opacity:0; transform:translateY(9px)} to{opacity:1; transform:none} }

/* Desktop pointer only: a subtle lift + neon accent edge + avatar glow on hover. */
@media(hover:hover){
  .note{ transition: background .18s, box-shadow .18s, transform .18s; }
  .note:hover{ transform:translateY(-1px); box-shadow: inset 3px 0 0 -1px var(--neon), 0 8px 26px -16px var(--neon); }
  .note .av{ transition: box-shadow .2s, transform .2s; }
  .note:hover .av{ box-shadow: var(--glow-cy); transform: scale(1.03); }
}

/* Buttons: springy press + a touch brighter on hover. */
.btn{ transition: transform .14s cubic-bezier(.3,1.4,.5,1), box-shadow .18s, filter .18s; }
.btn:hover{ filter:brightness(1.07); }
.btn:active{ transform: translateY(1px) scale(.975); }

/* A slow travelling sheen across the primary CTAs on hover — the "premium" tell. */
.btn-neon, .btn-cyan{ position:relative; overflow:hidden; }
.btn-neon::after, .btn-cyan::after{
  content:''; position:absolute; inset:0; pointer-events:none; transform:translateX(-130%);
  background:linear-gradient(120deg,transparent 32%,rgba(255,255,255,.4) 50%,transparent 68%); }
.btn-neon:hover::after, .btn-cyan:hover::after{ animation: sheen .85s ease; }
@keyframes sheen{ to{ transform:translateX(130%); } }

/* Action taps (reply / repost / like / zap): satisfying pop on press, glow burst when toggled ON. */
.act{ transition: transform .16s cubic-bezier(.3,1.5,.5,1), color .15s, box-shadow .15s; }
.act:active{ transform: scale(1.25); }
.act.on{ animation: actPop .34s cubic-bezier(.3,1.5,.5,1); }
@keyframes actPop{ 0%{transform:scale(1)} 40%{transform:scale(1.42)} 70%{transform:scale(.9)} 100%{transform:scale(1)} }

/* The login card gently breathes so the very first screen feels alive. Battery-safe: a static glow
   layer whose OPACITY pulses (compositor-only, no per-frame paint), login-screen only, and it stops
   with the app's anim-off-when-hidden system. */
.auth-card.neon-border{ position:relative; }
.auth-card.neon-border::before{
  content:''; position:absolute; inset:-2px; border-radius:inherit; pointer-events:none; z-index:-1;
  box-shadow:0 0 46px -8px var(--neon); animation: authGlow 5.5s ease-in-out infinite; will-change:opacity; }
@keyframes authGlow{ 0%,100%{opacity:.22} 50%{opacity:.6} }

/* Skeleton shimmer utility for loading placeholders (opt-in via .skel). */
.skel{ background:linear-gradient(100deg,var(--panel) 38%,var(--panel2) 50%,var(--panel) 62%);
  background-size:200% 100%; animation: shimmer 1.3s linear infinite; border-radius:8px; }
@keyframes shimmer{ to{ background-position:-200% 0; } }
/* Skeleton timeline card — deliberately the same geometry as a real .note (46px avatar, 12px gutter,
   the same vertical rhythm) so the content lands WHERE the placeholder was instead of shifting the
   page under the reader. A skeleton that doesn't match its content is worse than a spinner. */
.skel-note{display:flex;gap:var(--sp-3);padding:var(--sp-4) var(--sp-4) var(--sp-5);
  border-bottom:1px solid var(--line)}
.skel-av{width:46px;height:46px;border-radius:50%;flex:none}
.skel-body{flex:1 1 auto;min-width:0;display:flex;flex-direction:column;gap:var(--sp-2);padding-top:4px}
.skel-line{height:11px;width:100%}
.skel-name{width:38%;height:13px}
/* Respect the OS setting: a shimmer is decoration, and for someone who asked for less motion it is the
   kind that provokes symptoms. The placeholder still shows, it just holds still. */
@media(prefers-reduced-motion:reduce){ .skel{ animation:none } }
@media(max-width:820px){ .skel-note{padding:var(--sp-3) var(--sp-3) var(--sp-4);gap:var(--sp-2)}
  .skel-av{width:40px;height:40px} }

@media(prefers-reduced-motion:reduce){
  .note{ animation:none; }
  .btn-neon::after,.btn-cyan::after{ display:none; }
  .act.on{ animation:none; }
  .auth-card.neon-border::before{ animation:none; opacity:.3; }
  .skel{ animation:none; }
}

/* ============================================================================================
   Landing redesign — a striking, theme-adaptive first impression. The grid + glows use ACCENT
   VARS, so they glow blue/purple on the Dark default and go full cyan/magenta on Cyberpunk.
   Battery-safe: the grid drifts via TRANSFORM only (GPU compositor, zero per-frame paint), the
   glows are STATIC, it only exists on the login screen, and it stops under anim-off/reduced-motion.
   Mobile-first: fixed layers (no scroll jank) + chips wrap + hidden on short phones to keep the CTA up.
   ============================================================================================ */
.auth-gate::before{   /* drifting neon grid, faded from the top */
  content:''; position:fixed; inset:-48px; z-index:-1; pointer-events:none;
  background-image:
    linear-gradient(var(--neon) 1px, transparent 1px),
    linear-gradient(90deg, var(--neon) 1px, transparent 1px);
  background-size:46px 46px; opacity:.10; will-change:transform;
  -webkit-mask-image:radial-gradient(135% 90% at 50% -12%, #000 24%, transparent 72%);
  mask-image:radial-gradient(135% 90% at 50% -12%, #000 24%, transparent 72%);
  animation:gridDrift 9s linear infinite;
}
@keyframes gridDrift{ from{transform:translateY(0)} to{transform:translateY(46px)} }
.auth-gate::after{    /* static ambient glows for depth (accent-tinted, theme-adaptive) */
  content:''; position:fixed; inset:0; z-index:-2; pointer-events:none; opacity:.4;
  background:
    radial-gradient(55% 42% at 50% -4%, var(--neon2), transparent 68%),
    radial-gradient(48% 38% at 100% 102%, var(--cyan), transparent 70%);
}

/* Feature chips — sell the superpowers right on the landing (why THIS client). */
.feat-chips{display:flex;flex-wrap:wrap;justify-content:center;gap:6px;margin:2px 0 20px}
.feat-chips span{font-size:11px;line-height:1;padding:6px 10px;border-radius:999px;
  background:var(--panel2);border:1px solid var(--line);color:var(--text);white-space:nowrap;
  transition:transform .14s cubic-bezier(.3,1.4,.5,1)}
@media(hover:hover){ .feat-chips span:hover{ transform:translateY(-2px); } }

/* Cyberpunk (bare :root) gets the amped, louder version — brighter grid, stronger glow, chip halos. */
:root:not([data-theme]) .auth-gate::before{ opacity:.17; }
:root:not([data-theme]) .auth-gate::after{ opacity:.6; }
:root:not([data-theme]) .feat-chips span{ box-shadow:0 0 12px -5px var(--neon); border-color:var(--neon); }
:root:not([data-theme]) .logo-img{ box-shadow:0 0 26px -6px var(--neon); }

/* Short / landscape phones: drop the chips so the login buttons stay above the fold. */
@media(max-height:640px){ .feat-chips{display:none} }
@media(max-width:480px){ .feat-chips span{font-size:11px;padding:6px 10px} }

@media(prefers-reduced-motion:reduce){ .auth-gate::before{ animation:none } }

/* Combined tip (⚡) — a small Monero mark on the bolt when the author also has an XMR address, and the
   Lightning/Monero chooser modal. ɱ orange is Monero's brand colour (fixed, reads on every theme). */
.tipbolt{ white-space:nowrap; }                                   /* keep ⚡ and its ɱ mark together */
.xmr-mark{ font-size:.58em; color:#f60; font-weight:800; vertical-align:super; margin-inline-start:-1px; }
.bch-mark{ font-size:.5em; vertical-align:super; margin-inline-start:-1px; }   /* 🟢 already carries its own colour */
.tip-choices{ display:flex; flex-direction:column; gap:8px; margin-top:10px; }
.tip-choices .btn{ text-align:start; white-space:normal; }         /* full-width chooser buttons wrap cleanly on narrow phones */
/* The description follows the BUTTON's own text colour (white on neon, dark on cyan) at reduced opacity —
   NOT var(--muted), which is a low-contrast grey that's unreadable on the coloured gradient buttons
   (worst on cyberpunk). */
.tip-choices .btn .muted{ margin-inline-start:4px; color:inherit; opacity:.82; font-weight:400; }

/* ===== 🎨 Background posts (Facebook-style): a scrollable swatch strip + a live composer preview.
   Mobile-first — the strip scrolls horizontally, swatches are touch-sized, and the tools row wraps so
   the extra 🎨 button never overflows a narrow phone. The final post is a rendered IMAGE (canvas). ===== */
.cmp-bg-strip{ display:flex; gap:8px; overflow-x:auto; -webkit-overflow-scrolling:touch; padding:8px 2px; margin:2px 0 0; scrollbar-width:thin; }
.cmp-swatch{ flex:0 0 auto; width:38px; height:38px; border-radius:50%; border:2px solid transparent; cursor:pointer; padding:0;
  display:flex; align-items:center; justify-content:center; font-size:20px; line-height:1; box-shadow:0 1px 4px rgba(0,0,0,.25); transition:transform .12s; }
@media(hover:hover){ .cmp-swatch:hover{ transform:scale(1.1); } }
.cmp-swatch.on{ border-color:var(--neon); box-shadow:0 0 0 2px var(--bg), 0 0 9px var(--neon); }
.cmp-swatch-none{ background:var(--panel2); color:var(--text); border:2px solid var(--line); font-weight:800; font-size:15px; }
/* Live preview of a background/framed post. NOT the textarea dressed up as a card: the card is a derived
   subset of the draft (links stripped, trimmed to the opening), so a card-styled textarea showed a URL the
   card would never contain. This is the REAL rendered card (see makeCardPreview), sitting under the
   swatches, with the box above it staying an ordinary editor. */
.cmp-cardprev{ display:flex; flex-direction:column; gap:6px; margin:8px 0 0; }
.cmp-cardprev.hidden{ display:none; }
.cmp-cardprev-img{ width:190px; max-width:56%; height:auto; border-radius:12px;
  border:1px solid var(--line); background:var(--panel2); }   /* card is 1:1, 4:5 or 2:3 — let it be */
/* 🖼️ Framed card (AI menu): preview the border the rendered PNG will carry. currentColor, not a fixed
   white — the textarea's colour is already the background's own fg, so this stays visible on the light
   swatches (`gold` has a dark fg, where a white ring would disappear). */
@media(max-width:480px){
  .cmp-swatch{ width:34px; height:34px; font-size:17px; }
  .cmp-cardprev-img{ width:148px; }
}

/* Data saver — tap-to-load image placeholder (User Settings → "don't auto-load images"). Nothing is
   downloaded until tapped. Touch-friendly + theme-safe. */
.img-hold{ display:flex; align-items:center; justify-content:center; gap:6px; width:100%; height:100%; min-height:80px;
  box-sizing:border-box; border:1px dashed var(--line); border-radius:12px; background:var(--panel2); color:var(--muted);
  font-size:13px; cursor:pointer; text-align:center; padding:14px; -webkit-user-select:none; user-select:none; overflow:hidden; }
/* full-size contexts (feed + inline text media) get a taller tap target than small card thumbnails */
.media-row .img-hold, .media-grid .img-hold, .txt .img-hold, .note-preview .img-hold{ min-height:130px; }
.img-hold:active{ opacity:.75; }
@media(hover:hover){ .img-hold:hover{ border-color:var(--neon); color:var(--text); } }

/* ===== Budget (bills + plans; data lives in an encrypted Nostr event, see budget.js) =====
   Every colour here is a THEME VARIABLE, never a literal — that's what makes the view work in all
   nine themes (including the light ones and win98) without a single per-theme override. */
.bg-wrap{padding:14px 16px calc(28px + env(safe-area-inset-bottom));max-width:760px;margin:0 auto}
.bg-monthrow{display:flex;align-items:center;gap:10px;margin-bottom:10px;flex-wrap:wrap}
.bg-month{font-size:15px;font-weight:700;color:var(--muted);letter-spacing:.4px;text-transform:uppercase}
.bg-monthrow .btn{margin-inline-start:auto}
/* auto-fit keeps four tiles on a desktop row and folds to 2x2 on a phone with no media query */
.bg-tiles{display:grid;grid-template-columns:repeat(auto-fit,minmax(140px,1fr));gap:10px}
.bg-tile{background:var(--panel2);border:1px solid var(--line);border-radius:14px;padding:12px 14px;display:flex;flex-direction:column;gap:4px;min-width:0}
.bg-tl{font-size:12px;color:var(--muted);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.bg-tv{font-size:20px;font-weight:800;color:var(--text);font-variant-numeric:tabular-nums;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.bg-tile.in .bg-tv{color:var(--green)}
.bg-tile.due .bg-tv{color:var(--amber)}
.bg-tile.ok .bg-tv{color:var(--neon)}
.bg-tile.neg .bg-tv{color:var(--danger)}

.bg-tabs{display:flex;align-items:center;gap:8px;margin:16px 0 10px;border-bottom:1px solid var(--line);padding-bottom:8px}
.bg-tab{cursor:pointer;border:1px solid transparent;background:transparent;color:var(--muted);font:inherit;font-weight:700;
  padding:8px 14px;border-radius:10px;display:inline-flex;align-items:center;gap:6px}
.bg-tab:hover{color:var(--text)}
.bg-tab.on{color:var(--neon);border-color:var(--line);background:color-mix(in srgb, var(--neon) 12%, var(--panel2))}
.bg-tab .bg-n{font-style:normal;font-size:11px;background:var(--amber);color:var(--bg);border-radius:9px;padding:1px 8px;font-weight:800}
.bg-tab.ghost{font-size:17px;padding:6px 12px}

.bg-sec{font-size:12px;font-weight:800;letter-spacing:.6px;text-transform:uppercase;color:var(--muted);margin:14px 0 6px}
.bg-row{display:flex;align-items:center;gap:10px;padding:12px 12px;border:1px solid var(--line);border-radius:12px;
  background:var(--panel2);margin-bottom:8px;min-width:0}
.bg-row.done{opacity:.62}
.bg-row.done .bg-name{text-decoration:line-through}
.bg-row.hid{border-style:dashed;opacity:.55}
.bg-check,.bg-more,.bg-idel{cursor:pointer;background:transparent;border:none;color:var(--muted);font:inherit;padding:4px;
  border-radius:8px;flex-shrink:0;line-height:1}
.bg-check{font-size:20px;min-width:38px;min-height:38px}   /* comfortable touch target on a phone */
.bg-more{font-size:15px;min-width:34px;min-height:34px}
.bg-more:hover,.bg-idel:hover{color:var(--neon)}
.bg-name{flex:1;min-width:0;color:var(--text);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.bg-flag{font-style:normal;font-size:11px;color:var(--muted);border:1px solid var(--line);border-radius:7px;padding:1px 6px;margin-inline-start:6px}
.bg-amt{font-variant-numeric:tabular-nums;font-weight:700;white-space:nowrap;flex-shrink:0}
.bg-amt.in{color:var(--green)}
.bg-amt.out{color:var(--text)}
.bg-empty{padding:12px 2px}

.bg-plan{border:1px solid var(--line);border-radius:14px;padding:10px;margin-bottom:10px;background:var(--panel)}
.bg-plan.done{opacity:.62}
.bg-plan.hid{border-style:dashed;opacity:.55}
.bg-plan-hd{margin-bottom:4px;background:transparent;border:none;padding:4px 2px}
.bg-items{padding:0 4px 4px}
.bg-item{display:flex;align-items:center;gap:10px;padding:6px 8px;border-bottom:1px solid var(--line);min-width:0}
.bg-item:last-child{border-bottom:none}
.bg-iname{flex:1;min-width:0;color:var(--muted);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.bg-iamt{font-variant-numeric:tabular-nums;color:var(--text);white-space:nowrap}
.bg-additem{margin:2px 4px 4px}

/* Add-row: wraps to its own line on a phone rather than squeezing the name field to nothing */
.bg-add{display:flex;flex-wrap:wrap;gap:8px;align-items:center;margin-top:14px;padding-top:14px;border-top:1px solid var(--line)}
.bg-add .input{flex:1 1 190px;min-width:0;width:auto}
.bg-add .bg-amtin{flex:0 1 120px}
.bg-chk{display:inline-flex;align-items:center;gap:6px;color:var(--muted);font-size:13px;white-space:nowrap;cursor:pointer}
.bg-menu{display:flex;flex-direction:column;gap:8px;margin-top:10px}
/* Add Bill with AI — the two source buttons stack on a phone and sit side by side on a desktop */
.bg-ai{flex:0 0 auto;white-space:nowrap}
.bg-aipick{display:flex;gap:10px;margin:14px 0 4px;flex-wrap:wrap}
.bg-aibtn{flex:1 1 160px;min-height:52px;font-size:15px}   /* thumb-sized targets, not text links */
.bg-aibtn:disabled{opacity:.5;cursor:default}
.bg-aistat{min-height:22px;margin-top:8px;display:flex;align-items:center;gap:8px;color:var(--muted);font-size:13px}
.bg-aistat .spinner{width:16px;height:16px;margin:0;flex-shrink:0}
@media(max-width:480px){
  .bg-ai{flex:1 1 100%}          /* full-width under the Add row rather than squeezed beside it */
  .bg-aibtn{flex:1 1 100%}
}
.bg-foot{margin-top:18px;text-align:center}
.bg-link{background:none;border:none;color:var(--neon);cursor:pointer;font:inherit;text-decoration:underline;padding:0 4px}
.bg-err{padding:22px;text-align:center;color:var(--danger);display:flex;flex-direction:column;gap:10px;align-items:center}
@media(max-width:480px){
  .bg-wrap{padding:12px 12px calc(28px + env(safe-area-inset-bottom))}
  .bg-tv{font-size:20px}
  .bg-add .input{flex:1 1 100%}
  .bg-add .bg-amtin{flex:1 1 100%}
}

/* ===== Voice/Video calls ===== */
@keyframes callpulse{0%,100%{opacity:1}50%{opacity:.45}}
.call-overlay{position:fixed;inset:0;z-index:400;background:radial-gradient(circle at 50% 28%,#1a1a2e,#08040e);display:flex;flex-direction:column;align-items:center;color:#fff;padding:calc(26px + env(safe-area-inset-top)) 18px calc(30px + env(safe-area-inset-bottom))}
.call-remote{position:absolute;inset:0;width:100%;height:100%;object-fit:cover;background:#000;z-index:0}
.call-overlay.rd .call-remote{object-fit:contain;image-rendering:auto}
.call-overlay.rd.control-on .call-remote{cursor:none}
.call-overlay.aud .call-remote{display:none}
.call-head{position:relative;z-index:2;text-align:center;display:flex;flex-direction:column;align-items:center;gap:12px;margin:auto 0}
.call-overlay.vid .call-head{position:absolute;top:calc(18px + env(safe-area-inset-top));inset-inline-start:0;inset-inline-end:0;margin:0;flex-direction:row;justify-content:center;align-items:center;gap:10px;background:linear-gradient(#000a,transparent);padding:12px}
.call-head img{width:118px;height:118px;border-radius:50%;object-fit:cover;border:2px solid var(--neon,#0ff);box-shadow:0 0 30px rgba(0,240,255,.4)}
.call-overlay.vid .call-head img{width:44px;height:44px;box-shadow:none}
.call-name{font-size:24px;font-weight:800}
.call-overlay.vid .call-name{font-size:17px}
.call-status{font-size:15px;color:#9fb3c8}
.call-overlay.ring .call-status{color:var(--neon,#0ff);animation:callpulse 1.2s infinite}
/* Draggable (see _dragSelfView): touch-action:none so a drag on a phone is a drag and not a scroll/
   zoom gesture, and cursor:grab so it reads as movable on a desktop. It is positioned with left/top
   once moved — never transform, which is spent on the scaleX(-1) mirror. */
.call-local{position:absolute;bottom:calc(100px + env(safe-area-inset-bottom));inset-inline-end:16px;width:104px;height:150px;object-fit:cover;border-radius:12px;border:1px solid #ffffff33;z-index:3;background:#000;transform:scaleX(-1);touch-action:none;cursor:grab}
.call-local.dragging{cursor:grabbing;opacity:.9;border-color:var(--neon,#0ff);box-shadow:0 0 18px rgba(0,240,255,.35)}
.call-overlay.aud .call-local{display:none}
.call-actions{position:relative;z-index:3;margin-top:auto;display:flex;gap:16px;align-items:center;justify-content:center}
.call-btn{width:60px;height:60px;border-radius:50%;border:none;font-size:24px;cursor:pointer;background:#2a2a3e;color:#fff;display:flex;align-items:center;justify-content:center;transition:filter .15s;-webkit-tap-highlight-color:transparent}
.call-btn:hover{filter:brightness(1.15)}
.call-btn.accept{background:#1db954;width:72px;height:72px}
.call-btn.decline,.call-btn.hang{background:#e0245e;width:72px;height:72px}
/* Minimized call/room → a floating thumbnail so the rest of the app is usable during a call. Purely
   visual (the media rides the PeerConnection, not the DOM). Mirrors the live-stream .pl-mini thumbnail. */
.call-overlay.call-mini{inset:auto;inset-inline-end:14px;bottom:20px;width:132px;height:196px;padding:0;z-index:9600;
  border-radius:12px;border:2px solid var(--neon,#0ff);box-shadow:0 8px 26px rgba(0,0,0,.55);overflow:hidden;cursor:pointer}
.call-overlay.call-mini .call-remote{border-radius:0}
.call-overlay.call-mini .call-local,.call-overlay.call-mini .call-actions,.call-overlay.call-mini .room-hd,
.call-overlay.call-mini .call-status{display:none}
.call-overlay.call-mini .call-head{position:absolute;top:6px;inset-inline-start:0;inset-inline-end:0;margin:0;padding:4px;background:linear-gradient(#000a,transparent);flex-direction:row;gap:6px;z-index:2}
.call-overlay.call-mini .call-head img{width:26px;height:26px;box-shadow:none}
.call-overlay.call-mini .call-name{font-size:11px}
.call-overlay.call-mini.room .call-grid{height:100%;gap:1px}
.call-overlay.call-mini::after{content:"● LIVE";position:absolute;top:5px;inset-inline-start:5px;background:#e0245e;color:#fff;font-size:11px;font-weight:700;padding:2px 6px;border-radius:10px;z-index:3;letter-spacing:.5px}
@media (max-width:820px){
  .call-overlay.call-mini{bottom:calc(env(safe-area-inset-bottom,0) + 76px);width:112px;height:168px}   /* clear the mobile nav */
}
.calls-view{max-width:none;margin:0 auto;padding:22px 18px}
.call-start{margin:14px 0 22px}
/* Missed calls sit above the contact grid and need to read as a separate section, not as more
   contacts — same tile shape, its own spacing and a rule under it. */
.call-missed{margin:12px 0 4px;padding-bottom:10px;border-bottom:1px solid var(--line)}
.call-missed .call-contact{position:relative}
.call-contacts{display:grid;grid-template-columns:repeat(auto-fill,minmax(84px,1fr));gap:10px}
.call-contact{display:flex;flex-direction:column;align-items:center;gap:6px;background:var(--panel2);border:1px solid var(--line);border-radius:12px;padding:10px 6px;cursor:pointer;color:var(--text);font-size:12px}
.call-contact img{width:48px;height:48px;border-radius:50%;object-fit:cover}
.call-contact span{white-space:nowrap;overflow:hidden;text-overflow:ellipsis;max-width:78px}
.call-act{display:flex;flex-direction:column;align-items:center;gap:6px}
.call-act span{font-size:12px;color:#cdd6e4;font-weight:600}
.room-hd{position:relative;z-index:2;text-align:center;font-weight:700;margin:6px 0 auto;color:#fff}
.call-grid{position:relative;z-index:2;display:grid;gap:8px;width:100%;max-width:640px;margin:auto;padding:8px}
.call-grid[data-n="1"],.call-grid[data-n="2"]{grid-template-columns:1fr}
.call-grid[data-n="3"],.call-grid[data-n="4"]{grid-template-columns:1fr 1fr}
.call-grid[data-n="5"],.call-grid[data-n="6"]{grid-template-columns:1fr 1fr 1fr}
.call-tile{position:relative;aspect-ratio:1;background:#141422;border-radius:14px;overflow:hidden;display:flex;align-items:center;justify-content:center}
.call-tile video{position:absolute;inset:0;width:100%;height:100%;object-fit:cover}
.call-tile.me video{transform:scaleX(-1)}
.call-tile img{width:64px;height:64px;border-radius:50%;object-fit:cover}
.call-tile .tl-name{position:absolute;inset-inline-start:6px;bottom:5px;font-size:11px;background:#000a;padding:2px 8px;border-radius:8px;color:#fff}

/* Group-call picker (Calls view) */
.call-group{margin:10px 0;padding:10px;border:1px solid var(--line,#2a2a3a);border-radius:10px}
.grp-list{display:flex;flex-direction:column;gap:2px;max-height:46vh;overflow-y:auto}
.grp-contact{display:flex;align-items:center;gap:10px;padding:6px 8px;border-radius:8px;cursor:pointer}
.grp-contact:hover{background:rgba(255,255,255,.05)}
.grp-contact img{width:32px;height:32px;border-radius:50%;object-fit:cover;flex:none}
.grp-contact span{flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.grp-contact .grp-pick{flex:none;width:18px;height:18px}

/* Go Live (OBS streaming) controls */
/* .live-badge is position:absolute (top:9px/inset-inline-start:9px) for the stream CARD, whose .stream-thumb is
   position:relative. In this row there was no positioned ancestor, so it escaped all the way up to
   .main{position:relative} and rendered at the top-left of the whole column — i.e. floating above the
   "Streams" heading whenever you were live. .av-title already had this override; this row never did. */
.streams-top{display:flex;align-items:center;gap:10px;margin:0 0 12px;min-height:34px;position:relative}
.streams-top .live-badge{position:static;font-size:12px;display:inline-flex;align-items:center;-webkit-text-fill-color:#fff}
.copyrow{display:flex;gap:6px;align-items:center}
.copyrow .input{flex:1;min-width:0;font-family:var(--mono);font-size:.86em}

/* Phone live-stream overlay (WHIP go-live from the camera) */
.phone-live{position:fixed;inset:0;z-index:9600;background:#000;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:16px}
.phone-live video{max-width:100%;max-height:78vh;transform:scaleX(-1);border-radius:10px}
.phone-live video.rear{transform:none}   /* only the front (selfie) camera is mirrored */
.phone-live .pl-badge{position:absolute;top:calc(env(safe-area-inset-top,0) + 14px);inset-inline-start:16px;background:#e0245e;color:#fff;font-weight:700;padding:6px 12px;border-radius:20px;letter-spacing:.5px}
.phone-live .pl-actions{position:absolute;bottom:calc(env(safe-area-inset-bottom,0) + 24px);display:flex;gap:10px;align-items:center;flex-wrap:wrap;justify-content:center;padding:0 10px}
/* Live viewer count — mirrors the LIVE badge on the opposite corner. */
.phone-live .pl-viewers{position:absolute;top:calc(env(safe-area-inset-top,0) + 14px);inset-inline-end:16px;background:rgba(0,0,0,.55);
  color:#fff;font-weight:600;padding:6px 12px;border-radius:20px;border:1px solid rgba(255,255,255,.18)}
.phone-live.pl-mini .pl-viewers{top:5px;inset-inline-end:5px;font-size:11px;padding:2px 8px;border-radius:12px}
/* Native screen share: the capture happens outside the WebView, so there's no preview to show — this placard
   stands in for the video element (which is hidden), including in the minimized thumbnail. */
.phone-live .pl-note{color:var(--neon,#0ff);font-weight:600;font-size:20px;text-align:center;padding:0 20px}
.phone-live.pl-mini .pl-note{font-size:11px;padding:0 6px}
/* Minimized: a floating thumbnail so the rest of the app stays usable while you're still broadcasting.
   inset:auto is what un-blocks the page — the full-screen overlay no longer covers (and eats clicks on) it. */
.phone-live.pl-mini{inset:auto;inset-inline-end:14px;bottom:20px;width:132px;height:196px;padding:0;gap:0;border-radius:12px;
  border:2px solid var(--neon,#0ff);box-shadow:0 8px 26px rgba(0,0,0,.55);overflow:hidden;cursor:pointer}
.phone-live.pl-mini video{width:100%;height:100%;max-width:none;max-height:none;object-fit:cover;border-radius:0}
.phone-live.pl-mini .pl-actions{display:none}
.phone-live.pl-mini .pl-badge{top:5px;inset-inline-start:5px;font-size:11px;padding:2px 8px;border-radius:12px}
@media (max-width:820px){
  .phone-live.pl-mini{bottom:calc(env(safe-area-inset-bottom,0) + 76px);width:112px;height:168px}   /* clear the mobile nav */
}

/* Go Live divider */
.gl-or{text-align:center;margin:12px 0;color:var(--muted,#8a8db3);font-size:13px}
/* Go Live source picker. These are CHOICES, not launch buttons — nothing is captured until the user
   confirms, so the wrong camera can never be broadcast before they can change it. */
.gl-src{margin:10px 0 4px}
.gl-opt{display:flex;align-items:center;gap:10px;padding:10px 12px;margin:6px 0;border-radius:9px;
  border:1px solid var(--line);cursor:pointer;font-size:15px}
.gl-opt:hover{border-color:var(--neon,#6ee7ff)}
.gl-opt input{margin:0;flex:none;width:16px;height:16px}   /* flex:none — the radio must never squash */
.gl-sub{margin:2px 0 6px 22px;padding-inline-start:10px;border-inline-start:2px solid var(--line)}
.gl-actions .btn[disabled]{opacity:.45;cursor:not-allowed}
/* The Go Live entry while its sheet is being fetched. Opening it is two round trips, and an entry that
   looks untouched for the length of them is one you click again — which used to stack a second sheet. */
.gl-busy{opacity:.55;pointer-events:none}
#gl-need{text-align:center;margin:8px 0 0}

/* Live stream chat (NIP-53 kind-1311) — beside the player on desktop, below on mobile */
/* Chat sits BELOW the player, not beside it: the 330px side rail squeezed the video into the leftover
   width. Stacking gives the player the full column and the chat the full width too. */
.stream-layout{display:flex;flex-direction:column;gap:14px;align-items:stretch}
.stream-layout.chat-hidden .stream-chat{display:none}
.stream-main{min-width:0}
/* Your own live stream, on your own stream page. Takes the PLAYER's place — it is the same shape and
   the same weight, so landing here after Go Live does not look like a page that failed to load a
   video. See the note beside `selfLive` in app.js for why there is no player here at all. */
.st-self{border:1px solid var(--line);border-radius:14px;padding:16px 16px 14px;
  background:linear-gradient(180deg,rgba(255,0,80,.10),transparent 70%);
  display:flex;flex-direction:column;gap:10px}
/* .live-badge is position:absolute for the stream CARD's thumbnail (top:9px/inset-inline-start:9px). Dropped into
   flow it would fly to the nearest positioned ancestor — the same correction .av-title and
   .streams-top each had to make. */
.st-selfhd{display:flex;align-items:center;gap:9px;font-size:15px;font-weight:700}
/* Its OWN class, not the generic `.row`. `body.popout .stream-main > .row{display:none}` is an
   UNQUALIFIED selector that exists to hide the pop-out/open-URL buttons, and a second .row in
   .stream-main would silently be hidden by it too — which tests/test_stream_popout.py pins for
   exactly this reason, and caught this. */
.st-self-actions{display:flex;gap:8px;flex-wrap:wrap;align-items:center}
.st-selfhd .live-badge{position:static;font-size:12px;display:inline-flex;align-items:center;
  -webkit-text-fill-color:#fff}
.stream-chat{width:100%;flex:none;display:flex;flex-direction:column;border:1px solid var(--line,#2a2a3a);border-radius:12px;overflow:hidden;height:44vh}
.st-chat-hd{padding:10px 12px;font-weight:700;border-bottom:1px solid var(--line,#2a2a3a);background:rgba(255,255,255,.03)}
.scm-list{flex:1;overflow-y:auto;padding:8px 10px;display:flex;flex-direction:column;gap:8px}
.scm{display:flex;gap:8px;align-items:flex-start}
.scm-av{width:26px;height:26px;border-radius:50%;object-fit:cover;flex:none;cursor:pointer}
.scm-body{min-width:0;font-size:15px;line-height:1.45;word-break:break-word}
.scm-name{cursor:pointer;color:var(--neon,#0ff)}
.scm-txt{color:var(--text)}
/* A reaction is chat, but it is not a sentence — dimmed so a burst of 🤙 during a busy stream reads
   as background noise instead of shouting over the people actually typing. A custom `:shortcode:`
   reaction carries its image in an `emoji` tag, so it renders as that image rather than as the
   literal text ":pepe:". */
.scm-react .scm-txt{color:var(--muted)}
.scm-emoji{height:1.15em;width:auto;vertical-align:-.18em}
.scm-input{display:flex;gap:6px;padding:8px;border-top:1px solid var(--line,#2a2a3a)}
.scm-input .input{flex:1;min-width:0}
@media (max-width:820px){
  .stream-chat{height:50vh}   /* base is already stacked + full width */
}

/* ---- 📊 Server Stats -------------------------------------------------------------------------
   Every chart is a static SVG the module rebuilds on refresh (once a minute at most) — no canvas,
   no rAF, no chart library. The only motion here is a hover transition, so an open Stats page is
   ~0% CPU when idle. Per-card colour arrives as --acc, set inline by stats.js. */
.st-wrap{max-width:none;margin:0 auto;padding:0 10px 40px}
.st-head{display:flex;align-items:flex-start;justify-content:space-between;gap:12px;margin:4px 0 12px}
.st-h1{margin:0 0 2px;font-size:24px}
.st-sec{margin:26px 0 10px;font-size:15px;letter-spacing:.04em;text-transform:uppercase;color:var(--muted)}

/* tabs — Activity / Uptime. Same pill language as the range buttons, one notch louder so the two
   rows read as a hierarchy (tab picks the page, range filters what's on it). */
.st-tabs{display:flex;gap:8px;margin:0 0 14px;border-bottom:1px solid var(--line);padding-bottom:10px}
.st-tab{flex:0 0 auto;background:transparent;border:1px solid var(--line);color:var(--muted);
  border-radius:999px;padding:8px 18px;font-size:15px;font-weight:600;cursor:pointer;transition:.15s}
.st-tab:hover{border-color:var(--cyan);color:var(--text)}
.st-tab.on{color:#04121a;background:var(--cyan);border-color:var(--cyan);box-shadow:var(--glow-cy)}

.st-ranges{display:flex;gap:8px;margin-bottom:14px}
.st-range{flex:0 0 auto;background:var(--panel2);border:1px solid var(--line);color:var(--muted);
  border-radius:999px;padding:8px 16px;font-size:15px;cursor:pointer;transition:.15s}
.st-range:hover{border-color:var(--cyan);color:var(--text)}
.st-range.on{color:#04121a;background:var(--cyan);border-color:var(--cyan);font-weight:700;box-shadow:var(--glow-cy)}

/* hero — the headline series, drawn as bars because individual buckets are worth reading */
.st-hero{background:var(--panel);border:1px solid var(--line);border-radius:var(--r);padding:16px 16px 0;
  position:relative;overflow:hidden}
.st-hero::before{content:'';position:absolute;inset:0;pointer-events:none;
  background:radial-gradient(120% 90% at 50% 100%, rgba(var(--accent-rgb),.10), transparent 70%)}
.st-herohd{display:flex;align-items:flex-start;justify-content:space-between;gap:12px;position:relative}
.st-hnum{font-size:34px;font-weight:800;line-height:1.2;color:var(--acc,var(--cyan));
  text-shadow:0 0 18px color-mix(in srgb, var(--acc,var(--cyan)) 45%, transparent)}
.st-herochart{margin-top:10px;height:150px}
.st-peak{white-space:nowrap;padding-top:4px}
.st-lbl{font-size:13px;letter-spacing:.05em;text-transform:uppercase;color:var(--muted)}

/* per-metric cards */
.st-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(190px,1fr));gap:10px;margin-top:12px}
.st-card{background:var(--panel);border:1px solid var(--line);border-radius:var(--r);padding:12px 12px 0;
  position:relative;overflow:hidden;transition:border-color .15s, transform .15s}
.st-card:hover{border-color:var(--acc);transform:translateY(-1px)}
.st-cardhd{display:flex;align-items:center;gap:8px}
.st-ic{font-size:15px}
.st-num{font-size:24px;font-weight:800;line-height:1.2;margin-top:2px;color:var(--acc)}
.st-sub{margin-bottom:6px}
.st-spark{height:46px;margin:0 -12px}
.st-svg{display:block;width:100%;height:100%}

/* plain number tiles */
.st-tiles{display:grid;grid-template-columns:repeat(auto-fill,minmax(140px,1fr));gap:10px}
.st-tile{background:var(--panel2);border:1px solid var(--line);border-radius:12px;padding:12px 14px}
.st-tval{font-size:20px;font-weight:700;color:var(--text)}
.st-tlbl{margin-top:2px}

/* games — a labelled bar per game */
.st-games{margin-top:12px;display:flex;flex-direction:column;gap:8px}
.st-grow{display:flex;align-items:center;gap:10px;font-size:15px}
.st-glbl{flex:0 0 40%;max-width:170px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.st-gbar{flex:1 1 auto;height:9px;border-radius:999px;background:var(--panel2);overflow:hidden}
.st-gbar>i{display:block;height:100%;border-radius:999px;
  background:linear-gradient(90deg, var(--neon), var(--neon2));box-shadow:var(--glow-cy)}
.st-gnum{flex:0 0 auto;min-width:38px;text-align:end;font-weight:700;color:var(--cyan)}

.st-note{margin-top:16px;border-inline-start:2px solid var(--line);padding-inline-start:10px}
.st-foot{margin-top:18px;text-align:center}

/* ---- 🛰️ Relay tab ----------------------------------------------------------------------------
   Health colours (green/amber/red) are hard-coded for the same reason the Uptime tab's are: a
   themed "queue is backing up" would turn into decoration on a light theme. */
.st-qlist{display:flex;flex-direction:column;gap:10px}
.st-qcard{background:var(--panel);border:1px solid var(--line);border-radius:var(--r);padding:12px 14px;
  border-inline-start:3px solid var(--line)}
.st-qcard.bad{border-inline-start-color:#fbbf24}
.st-qhd{display:flex;align-items:center;justify-content:space-between;gap:10px}
.st-qname{font-weight:700;font-size:15px}
.st-qpill{flex:0 0 auto;border-radius:999px;padding:4px 10px;font-size:12px;font-weight:700;
  text-transform:uppercase;letter-spacing:.04em;background:rgba(148,163,184,.15);color:var(--muted)}
.st-qpill.ok{background:rgba(52,211,153,.15);color:#34d399}
.st-qpill.warn{background:rgba(251,191,36,.15);color:#fbbf24}
.st-qpill.down{background:rgba(239,68,68,.18);color:#f87171}
.st-qbar{height:8px;border-radius:999px;background:var(--panel2);overflow:hidden;margin:10px 0 8px}
.st-qbar>i{display:block;height:100%;border-radius:999px;min-width:2px;
  background:linear-gradient(90deg, var(--neon), var(--neon2))}
.st-qmeta{display:flex;flex-wrap:wrap;gap:4px 14px}
.st-qmeta b{color:var(--text)}
.st-bad b{color:#f87171}

/* one row per live upstream subscription */
.st-fh{display:flex;flex-direction:column;gap:6px;margin-top:12px}
.st-fhrow{display:flex;align-items:center;gap:10px;font-size:14px;
  background:var(--panel2);border:1px solid var(--line);border-radius:10px;padding:8px 12px}
.st-fhrow .up-dot{background:#34d399;box-shadow:0 0 8px rgba(52,211,153,.8)}
.st-fhrow.off{opacity:.6}
.st-fhrow.off .up-dot{background:#ef4444;box-shadow:none}
/* some of this relay's subscriptions are live and some are not — its own state, because a single
   dead subscription is invisible in a relay that is otherwise up */
.st-fhrow.part .up-dot{background:#fbbf24;box-shadow:0 0 8px rgba(251,191,36,.7)}
.st-fhurl{flex:1 1 auto;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.st-fhnum{flex:0 0 auto;font-weight:700;color:var(--cyan)}
.st-gday{flex:0 0 auto;min-width:72px;text-align:end}

@media (max-width:820px){
  .st-qmeta{gap:2px 10px}
  .st-gday{min-width:0}
}

/* ---- 📡 Uptime tab ---------------------------------------------------------------------------
   Status colours are hard-coded rather than themed: red/green mean one thing here and must not turn
   into an accent colour on a light theme, where a themed "down" could read as decoration. */
.up-banner{border-radius:var(--r);padding:12px 16px;font-weight:700;margin-bottom:12px;
  border:1px solid var(--line);background:var(--panel)}
.up-banner.ok{border-color:rgba(52,211,153,.45);background:rgba(52,211,153,.10);color:#34d399}
.up-banner.down{border-color:rgba(239,68,68,.55);background:rgba(239,68,68,.12);color:#f87171}
/* "up, but what it is telling you is stale" — not green, not an outage either. */
.up-banner.warn{border-color:rgba(251,191,36,.5);background:rgba(251,191,36,.10);color:#fbbf24}

.up-list{display:flex;flex-direction:column;gap:10px;margin-top:12px}
.up-card{background:var(--panel);border:1px solid var(--line);border-radius:var(--r);padding:12px 14px;
  transition:border-color .15s}
.up-card.up{border-inline-start:3px solid #34d399}
.up-card.down{border-inline-start:3px solid #ef4444}
.up-card.pending{border-inline-start:3px solid var(--muted)}
.up-hd{display:flex;align-items:center;justify-content:space-between;gap:10px}
.up-name{display:flex;align-items:center;gap:8px;min-width:0;font-weight:700;font-size:15px}
.up-name a{color:var(--text);text-decoration:none;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.up-name a:hover{color:var(--cyan)}
.up-dot{flex:0 0 auto;width:9px;height:9px;border-radius:50%;background:var(--muted)}
.up-card.up .up-dot{background:#34d399;box-shadow:0 0 8px rgba(52,211,153,.8)}
.up-card.down .up-dot{background:#ef4444;box-shadow:0 0 8px rgba(239,68,68,.8)}
.up-pill{flex:0 0 auto;border-radius:999px;padding:4px 10px;font-size:12px;font-weight:700;
  text-transform:uppercase;letter-spacing:.04em}
.up-pill.up{background:rgba(52,211,153,.15);color:#34d399}
.up-pill.down{background:rgba(239,68,68,.18);color:#f87171}
.up-pill.pending{background:rgba(148,163,184,.15);color:var(--muted)}
.up-url{margin:2px 0 8px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}

/* heartbeat bar — one <i> per check, newest on the right */
.up-beats{display:flex;align-items:flex-end;gap:2px;height:26px;overflow:hidden}
.up-beat{flex:1 1 3px;min-width:2px;max-width:8px;height:100%;border-radius:2px;
  background:#34d399;opacity:.85}
.up-beat.bad{background:#ef4444;opacity:1}

.up-meta{display:flex;flex-wrap:wrap;gap:4px 14px;margin-top:8px}
.up-meta b{color:var(--text)}
.up-err{margin-top:6px;color:#f87171;word-break:break-word}

@media (max-width:820px){
  .up-beats{height:22px}
  .up-beat{max-width:6px}
  .up-meta{gap:2px 10px}
}

@media (max-width:820px){
  .st-wrap{padding:0 8px 90px}
  .st-h1{font-size:20px}
  .st-hnum{font-size:30px}
  .st-herochart{height:120px}
  .st-grid{grid-template-columns:repeat(auto-fill,minmax(150px,1fr));gap:8px}
  .st-tiles{grid-template-columns:repeat(auto-fill,minmax(120px,1fr));gap:8px}
  .st-glbl{flex-basis:34%}
}
.st-new{font-size:11px;letter-spacing:.06em;text-transform:uppercase;color:var(--acc);
  border:1px solid currentColor;border-radius:999px;padding:1px 6px;opacity:.9}
.st-hint{margin-top:8px}

/* ---- ✨ Make something: guided image/music/video/link actions --------------------------------
   Cards replace "type the command and hope you got the syntax right". Sized for THUMBS first:
   every target is >=44px tall, the grid collapses to one column on a phone, and the sheet reuses
   the Effects-studio shell (.fxs) so it already scrolls correctly inside a fixed viewport. */
.aw-make{display:grid;grid-template-columns:repeat(auto-fit,minmax(190px,1fr));gap:10px;margin:10px 0 4px;width:100%}
.aw-card{display:flex;flex-direction:column;align-items:flex-start;gap:2px;min-height:76px;
  background:var(--panel2);border:1px solid var(--line);border-radius:14px;padding:12px 14px;
  color:var(--text);cursor:pointer;text-align:start;transition:.15s}
.aw-card:hover,.aw-card:active{border-color:var(--cyan);transform:translateY(-1px)}
.aw-card b{font-size:15px}
.aw-card>span:last-child{color:var(--muted);font-size:12px}
/* Capability-card icons. These replaced the emoji that used to carry these cards, so they are the
   card's colour accent, not a grey glyph — the row would otherwise read as 20 identical items.
   SCOPED TO THE CARD on purpose: `svg.ic{width:20px;height:20px}` is (0,1,1), so a bare `.awc-ic`
   (0,1,0) loses to it and the size silently does nothing (see the note above `.nav-item .ic`). */
.aw-card svg.awc-ic,.gen-pick svg.awc-ic{width:24px;height:24px;color:var(--neon);opacity:.95}
.aw-card:hover svg.awc-ic,.gen-pick:hover svg.awc-ic{opacity:1}
/* the generator sheet's heading takes the same icon; `gap` needs the box to be flex */
.fxs-title h3:has(> svg.ic){display:flex;align-items:center;gap:var(--sp-2)}
.fxs-title h3 > svg.ic{width:1em;height:1em;flex:none;color:var(--neon)}
.aw-or{margin:16px 0 2px}

/* picker sheet (the ✨ composer button, mid-chat) */
.gen-picker{display:flex;flex-direction:column;gap:8px;margin-top:10px;max-height:min(60vh,460px);overflow-y:auto}
.gen-pick{display:flex;align-items:center;gap:12px;width:100%;min-height:56px;background:var(--panel2);
  border:1px solid var(--line);border-radius:12px;padding:10px 14px;color:var(--text);cursor:pointer;text-align:start}
.gen-pick:hover{border-color:var(--cyan)}
.gen-pick>span:last-child{display:flex;flex-direction:column;min-width:0}
.gen-pick b{font-size:15px}
.gen-pick .muted{white-space:normal}

/* studio fields */
/* `.modal textarea` (specificity 0,1,1) forces min-height:220px + font-size:15px — it beats a plain
   class, so these are scoped to match it. 220px-tall boxes made the sheet unusable on a phone, and
   <16px makes iOS zoom the whole page the moment the field is focused. */
.modal textarea.gen-prompt,.modal input.gen-prompt,.gen-prompt{width:100%;font-size:17px;min-height:0}
.modal textarea.gen-prompt{min-height:76px}
.modal textarea.gen-lyrics,.gen-lyrics{width:100%;font-size:17px;margin-top:8px;min-height:76px}
.gen-lyrics:disabled{opacity:.45}
.gen-check{display:flex;align-items:center;gap:8px;margin-top:10px;font-size:15px;cursor:pointer;min-height:32px}
.gen-check input{width:18px;height:18px}
.gen-chip.on{color:#04121a;background:var(--cyan);border-color:var(--cyan);font-weight:700}

@media (max-width:820px){
  .aw-make{grid-template-columns:1fr;gap:8px}
  .aw-card{min-height:64px;padding:12px 14px;flex-direction:row;align-items:center;gap:10px}
  .aw-card>div,.aw-card b{flex:0 0 auto}
  .aw-card>span:last-child{flex:1 1 auto}
  .gen-picker{max-height:64vh}
}

/* ---- Go Live: cover picker + centred action row ---------------------------------------------
   The cover used to be a URL box + an Upload button. Both were ways to get it wrong (dead link,
   duplicate upload, or blank → a bare ▶ tile on Discover → Streams), so it's now a single "choose
   from your drive" pick. */
.gl-cover-acts{display:flex;justify-content:center;gap:8px;flex-wrap:wrap;margin:6px 0 2px}
/* .row is left-aligned by default; the sheet's two closing buttons read better centred. */
.row.gl-actions{justify-content:center;margin-top:14px}

.bp-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(104px,1fr));gap:8px;margin-top:10px;
  max-height:min(56vh,420px);overflow-y:auto}
.bp-cell{padding:0;border:1px solid var(--line);border-radius:12px;overflow:hidden;background:var(--panel2);
  cursor:pointer;aspect-ratio:1/1;transition:.15s}
.bp-cell:hover,.bp-cell:active{border-color:var(--cyan);transform:translateY(-1px)}
/* Keyboard cursor in the cover grid (_popKeys), same marker the other grids use. Without it the
   arrow/hjkl cursor moved invisibly across a wall of thumbnails. */
.bp-cell.kb{outline:2px solid var(--neon);outline-offset:2px;border-color:var(--neon)}
.bp-cell img{width:100%;height:100%;object-fit:cover;display:block}

@media (max-width:820px){
  .bp-grid{grid-template-columns:repeat(auto-fill,minmax(92px,1fr));gap:6px;max-height:58vh}
  .row.gl-actions{flex-wrap:wrap}
  .row.gl-actions .btn{flex:1 1 auto;min-width:140px}
}
/* cover picker: folder bar. Reuses .news-chip (incl. its .on state) so it matches the News tab's
   folder/feed switcher rather than inventing a third chip style. Wraps — never scrolls off-screen. */
/* Sticky, ONE ROW, OPAQUE. Three things were wrong with the wrapping chip bar it replaces: it grew
   with the folder count until it filled a phone screen, var(--panel) is TRANSLUCENT so the grid
   scrolled visibly through it ("overlaps the files"), and it stayed sticky at that full height. A
   select is fixed-height whatever the folder count, and opens the OS picker on a phone. The negative
   margins let the opaque strip span the modal's padding so nothing shows past its edges. */
/* The sticky region is the FIRST child and carries the title, so nothing can scroll above or through
   it — a bar placed after the title left a padding-height gap where cards showed above it. */
/* NO negative top margin. Sticky pins the MARGIN box, so a -22px top margin made the header jump by
   exactly that much the moment it pinned — "it moves as you scroll". The modal drops its own top
   padding instead (.bp-modal) and the header supplies it, so the pinned position equals the resting
   position and nothing shifts. */
.bp-modal{padding-top:0}
.bp-file-picker{width:min(960px,calc(100vw - 28px));max-width:960px;height:min(720px,calc(100vh - 28px));
  display:flex;flex-direction:column;background:var(--bg2)}
.bp-head{position:sticky;top:0;z-index:3;background:var(--bg2);margin:0 -22px 10px;padding:16px 22px 0;
  border-bottom:1px solid var(--line)}
.bp-head h3{margin:0 0 6px}
.bp-file-picker .bp-head{position:relative;display:flex;align-items:center;gap:10px;flex:0 0 auto;padding-bottom:10px}
.bp-file-picker .bp-head h3{margin:0;flex:1 1 auto}
.bp-sort{display:flex;align-items:center;gap:5px;color:var(--muted);font-size:12px;white-space:nowrap}
.bp-sort select{min-height:36px;max-width:112px;border:1px solid var(--line);border-radius:8px;
  padding:4px 22px 4px 8px;background:var(--panel2);color:var(--text);font:inherit}
.bp-close{font-size:22px;line-height:1}
.bp-explorer{display:grid;grid-template-columns:220px minmax(0,1fr);min-height:0;flex:1 1 auto;
  border:1px solid var(--line);background:var(--panel);overflow:hidden}
.bp-explorer>.bp-folders{min-width:0;margin:0;padding:10px;border-inline-end:1px solid var(--line);
  background:var(--panel2);overflow:auto}
.bp-explorer>.files-grid{min-width:0;min-height:0;margin:0;padding:12px;overflow:auto;align-content:start}
/* The attachment picker can contain thousands of files.  Do not let the bounded explorer height
   participate in sizing its implicit grid rows: Chromium has compressed those rows to a few pixels
   when a large lazy-image result arrives in one paint, leaving every photo as a horizontal line.
   Each result owns one full thumbnail row and the pane scrolls through those rows. */
.bp-explorer>.files-grid{grid-auto-rows:max-content;overscroll-behavior:contain}
.bp-explorer>.files-grid>.file-card{min-width:0;min-height:96px;align-self:start}
.bp-explorer>.files-grid>.file-card>img,
.bp-explorer>.files-grid>.file-card>video{display:block;width:100%;height:96px;min-height:96px;object-fit:cover}
.bp-explorer .bp-pick-card{display:flex;flex-direction:column;width:100%;padding:0;color:var(--text);
  font:inherit;text-align:start;cursor:pointer}
.bp-explorer .bp-pick-preview{display:block;width:100%;height:96px;flex:0 0 96px}
.bp-explorer .bp-pick-preview>img,.bp-explorer .bp-pick-preview>video,
.bp-explorer .bp-pick-preview>.file-icon{display:flex;width:100%;height:96px;min-height:96px;object-fit:cover}
.bp-explorer .bp-pick-card .meta{display:flex;flex-direction:column;align-items:stretch;gap:2px;width:100%;
  padding:8px;line-height:1.3}
.bp-explorer .bp-pick-card .fname{display:-webkit-box;min-width:0;overflow:hidden;-webkit-box-orient:vertical;
  -webkit-line-clamp:2;overflow-wrap:anywhere;font-size:13px;color:var(--text)}
.bp-explorer .bp-pick-card small{overflow:hidden;color:var(--muted);font-size:10px;white-space:nowrap;
  text-overflow:ellipsis}
.bp-explorer .folder-bar{display:flex;flex-direction:column;gap:2px}
.bp-explorer .folder-chip{width:100%;padding:8px 10px;border:1px solid transparent;border-radius:8px;
  background:none;color:var(--text);text-align:start;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;cursor:pointer}
.bp-explorer .folder-chip:hover{background:rgba(var(--accent-rgb),.10)}
.bp-explorer .folder-chip.active{background:rgba(var(--accent-rgb),.16);border-color:var(--neon)}
.bp-folders{margin:0;padding:0 0 10px}
.bp-folders:empty{display:none}
.bp-folderlbl{display:flex;align-items:center;gap:8px}
.bp-folder-sel{margin:0;flex:1 1 auto;min-width:0;font-size:17px;min-height:44px}  /* 16px: iOS zoom */
@media (max-width:820px){
  .modal-bg.bp-picker-bg{inset:0;padding:0;place-items:stretch;background:var(--bg2);overflow:hidden}
  .bp-picker-bg>.bp-file-picker{box-sizing:border-box;width:100vw;max-width:none;height:100vh;max-height:none;
    height:100dvh;margin:0;border:0;border-radius:0;padding:0 env(safe-area-inset-right) env(safe-area-inset-bottom) env(safe-area-inset-left);overflow:hidden}
  .bp-file-picker .bp-head{box-sizing:border-box;margin:0;padding:calc(8px + env(safe-area-inset-top)) 12px 8px;
    position:relative;min-height:48px}
  .bp-file-picker .bp-head h3{font-size:15px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
  .bp-sort span{display:none}.bp-sort select{width:88px;max-width:88px;min-height:38px}
  .bp-locations{display:inline-flex;flex:0 0 auto;min-height:38px;align-items:center}
  .bp-density{display:flex;flex:0 0 auto;gap:2px}.bp-density .mini{min-width:36px;min-height:38px;padding:5px}
  .bp-density .mini.active{color:var(--neon);background:rgba(var(--accent-rgb),.18)}
  .bp-explorer{display:flex;flex-direction:column;position:relative}
  .bp-explorer>.bp-folders{position:absolute;z-index:5;inset:0 auto 0 0;width:min(88vw,330px);max-height:none;overflow-y:auto;padding:8px;
    border-inline-end:1px solid var(--line);border-bottom:0;transform:translateX(-105%);transition:transform .16s ease;background:var(--bg2);box-shadow:12px 0 28px rgba(0,0,0,.38)}
  .bp-explorer.bp-locations-on::after{content:"";position:absolute;z-index:4;inset:0;background:rgba(0,0,0,.52)}
  .bp-explorer.bp-locations-on>.bp-folders{transform:translateX(0)}
  .bp-explorer .fx-tree-children{padding-inline-start:0;margin-inline-start:0}
  .bp-explorer .folder-bar{flex-direction:column;overflow:visible;padding-bottom:2px}
  .bp-explorer .folder-chip{width:100%;min-width:0;padding:10px 12px;text-align:start;white-space:normal;overflow-wrap:anywhere}
  .bp-explorer>.files-grid{flex:1 1 auto;padding:7px;grid-template-columns:repeat(auto-fill,minmax(92px,1fr));gap:7px}
  .bp-explorer.bp-medium>.files-grid{grid-template-columns:repeat(2,minmax(0,1fr));gap:9px}
  .bp-explorer>.files-grid>.file-card{min-height:0;align-self:start}
  .bp-explorer .bp-pick-preview{width:100%;height:auto;flex:0 0 auto;aspect-ratio:1}
  .bp-explorer .bp-pick-preview>img,.bp-explorer .bp-pick-preview>video,
  .bp-explorer .bp-pick-preview>.file-icon{width:100%;height:100%;min-height:0;aspect-ratio:1;object-fit:cover}
  .bp-explorer .bp-pick-preview>.file-icon{align-items:center;justify-content:center;font-size:34px}
  .bp-explorer .bp-pick-card .meta{padding:6px;min-height:52px}
  .bp-explorer .bp-pick-card .fname{display:block;white-space:nowrap;text-overflow:ellipsis}
  .bp-explorer .bp-pick-card small{display:block}
}
@media (min-width:821px){.bp-locations{display:none}}
/* A 360px picker cannot hold Locations + title + Sort + two density buttons + Close on one row.
   Sort and location are navigation; density is optional presentation, so yield that control before
   either a file operation is pushed off-screen or the picker grows a horizontal overflow strip. */
@media (max-width:520px){.bp-file-picker .bp-density{display:none}}
/* file-mode fields in the Make-something sheet */
.gen-file-btn{margin-top:8px;min-height:46px}
.gen-file-list{margin-top:6px;word-break:break-all}
.modal input.gen-x,.gen-x{width:100%;font-size:17px;min-height:0}   /* 16px: iOS zooms below that */
/* optional argument row in the Make-something sheet (e.g. ytdl clip from/to) */
.gen-optrow{display:flex;gap:10px;flex-wrap:wrap;margin-top:6px}
.gen-opt{flex:1 1 140px;display:flex;flex-direction:column;gap:4px;min-width:0}

/* ---- Messages: messenger-style list + thread -------------------------------------------------
   The list was a flat column of identical name+preview pills — no time, no unread weight, nothing
   to scan ("a long boring list"). The thread was a column of identical bubbles with no time, no
   grouping and no day breaks. Both now follow the conventions people already know from Telegram /
   WhatsApp, sized for thumbs first. */
.dm-listhd{position:sticky;top:0;z-index:2;display:flex;gap:8px;align-items:center;
  padding:10px 12px;background:var(--bg2);border-bottom:1px solid var(--line)}
.dm-search{margin:0;flex:1 1 auto;min-width:0;font-size:17px;min-height:42px;border-radius:999px;padding:10px 14px}
/* ONE hue, not two. The shared .btn-neon fill is a gradient from --neon to --neon2, and on most
   themes those are opposite ends of the wheel — cyan to magenta on the default, blue to green on
   Luna. Across a wide surface that reads as a deliberate wash; across a 90px button it is two
   colours fighting, which is exactly how it was reported. Solid --neon keeps the button primary and
   keeps .btn-neon's white-text contract, which every theme is already built around. */
.dm-newbtn{flex:0 0 auto;min-height:42px;white-space:nowrap;
  background:var(--neon);background-image:linear-gradient(180deg,rgba(255,255,255,.14),rgba(0,0,0,.10))}
.dm-rows{display:block}

.dm-peer{display:flex;gap:12px;align-items:center;padding:12px 14px;cursor:pointer;
  border-bottom:1px solid var(--line);transition:background .12s}
.dm-peer img{width:52px;height:52px;border-radius:50%;border:1px solid var(--line);flex:0 0 auto;object-fit:cover}
.dm-peer-main{flex:1 1 auto;min-width:0;display:flex;flex-direction:column;gap:4px}
.dm-peer-top{display:flex;align-items:baseline;gap:8px}
.dm-peer-top b{font-size:17px;font-weight:600;flex:1 1 auto;min-width:0;overflow:hidden;
  text-overflow:ellipsis;white-space:nowrap}
.dm-when{flex:0 0 auto;font-size:12px;color:var(--muted)}
.dm-peer-bot{display:flex;align-items:center;gap:8px}
.dm-prev{flex:1 1 auto;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;
  font-size:15px;color:var(--muted)}
.dm-you{color:var(--muted);opacity:.8}
.dm-dot{flex:0 0 auto;width:10px;height:10px;border-radius:50%;background:var(--neon);
  box-shadow:0 0 8px rgba(var(--accent-rgb),.7)}
.dm-peer.unread .dm-peer-top b{font-weight:800}
.dm-peer.unread .dm-prev{color:var(--text)}
.dm-peer.unread .dm-when{color:var(--neon)}

/* thread: day separators + grouped bubbles with a time */
.dm-day{align-self:center;margin:10px 0 2px}
.dm-day span{font-size:12px;color:var(--muted);background:var(--panel2);border:1px solid var(--line);
  border-radius:999px;padding:4px 12px}
.bubble.cont{margin-top:-4px}                     /* stacked: tight against the previous one */
.bubble.out.cont{border-end-end-radius:14px}
.bubble.in.cont{border-end-start-radius:14px}
.b-txt{display:inline}
.b-meta{display:inline-flex;align-items:center;gap:4px;float:inline-end;margin:6px 0 -2px 10px;
  font-size:11px;color:var(--muted);opacity:.85;user-select:none}
.b-tick{font-size:11px}

/* composer: one growing row + a round send that lights up when there's something to send */
.dm-row{display:flex;align-items:flex-end;gap:8px}
.dm-compose .input.dm-in{flex:1 1 auto;min-width:0;min-height:44px;max-height:132px;resize:none;
  border-radius:22px;padding:12px 16px;font-size:17px;line-height:1.45}
.dm-sendbtn{flex:0 0 auto;width:44px;height:44px;border-radius:50%;border:1px solid var(--line);
  background:var(--panel2);color:var(--muted);font-size:17px;cursor:pointer;transition:.15s}
.dm-sendbtn.on{background:linear-gradient(135deg,var(--neon),var(--neon2));color:#04121a;
  border-color:transparent;box-shadow:var(--glow-cy)}
.dm-sendstate{flex:0 1 auto;max-width:120px;color:var(--neon);font-size:11px;line-height:1.15;
  text-align:end;white-space:normal}
.dm-sendbtn.busy{opacity:.9;border-color:var(--neon);cursor:wait}
.dm-sendbtn.busy .ic{animation:dm-send-spin .85s linear infinite}
@keyframes dm-send-spin{to{transform:rotate(360deg)}}
.dm-row .mini{flex:0 0 auto;width:40px;height:40px}
/* 🔒 attachment encryption, off by default — it has to read as OFF at a glance, because the whole
   point is that the user chose it for this file. Dimmed when off, lit amber when on. */
.dm-lock{opacity:.5;filter:grayscale(1);transition:.15s;line-height:1}
.dm-lock.on{opacity:1;filter:none;border-color:rgba(251,191,36,.6);background:rgba(251,191,36,.12)}
@media (max-width:820px){
  /* A .mini is a 22px text pill — fine for a label, too small for a toggle you press with a thumb.
     Scoped to the DM thread's own topbar so it lifts the lock AND the mute beside it without
     resizing every other topbar in the app. */
  .dm-thread .topbar .mini{min-height:38px;padding-inline-start:12px;padding-inline-end:12px}
}

/* an encrypted attachment before (and if) it decrypts */
.encatt{display:inline-block;max-width:100%}
.encatt.done{display:block}

@media (max-width:820px){
  .dm-peer{padding:10px 12px;gap:12px}
  .dm-peer img{width:48px;height:48px}
  .dm-peer-top b{font-size:17px}
  .dm-prev{font-size:15px}
  .dm-compose{padding:8px 10px calc(8px + env(safe-area-inset-bottom))}
  .dm-row .mini{width:38px;height:38px}
  .bubble{max-width:82%;font-size:17px}
}
.st-rangelbl{margin-inline-start:8px;font-size:11px;letter-spacing:.04em;text-transform:none;color:var(--cyan);
  border:1px solid var(--line);border-radius:999px;padding:2px 10px;vertical-align:middle}

/* ---- DM message actions: reply banner (long-press / right-click -> Reply) --------------------
   Reply is sent as a quote block rather than a NIP-10 e-tag: the kind-14 rumor's tags are built in
   three separate signer paths, and a tag other clients may not render buys nothing over a quote
   every client already displays. */
.dm-replybar{display:flex;align-items:center;gap:10px;padding:8px 10px;margin-bottom:8px;
  background:var(--panel2);border:1px solid var(--line);border-inline-start:3px solid var(--neon);
  border-radius:10px}
.dm-rb-body{flex:1 1 auto;min-width:0;display:flex;flex-direction:column;gap:1px}
.dm-rb-who{font-size:11px;color:var(--neon);letter-spacing:.03em}
.dm-rb-txt{font-size:15px;color:var(--muted);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.dm-replybar .mini{flex:0 0 auto;width:36px;height:36px}

/* a quoted line inside a bubble (what a reply looks like on arrival, in any client) */
.bubble .b-quote{display:block;border-inline-start:2px solid var(--line);padding-inline-start:8px;margin-bottom:6px;
  color:var(--muted);font-size:.92em;opacity:.9}

/* Messages: hide the painted scrollbars, matching .feed. A 14px bar down the thread and another
   down the conversation list read as chrome from a different app — the rest of the client already
   hides them. Scrolling itself is untouched: only the track/thumb is gone, and both panes are
   vertical (a hidden bar on a HORIZONTAL strip is what made the news feed chips unreachable, which
   is why this is deliberately limited to the vertical panes). */
.dm-msgs,.dm-list,.dm-rows{scrollbar-width:none}
.dm-msgs::-webkit-scrollbar,.dm-list::-webkit-scrollbar,.dm-rows::-webkit-scrollbar{width:0;height:0}

/* ---- Composer on phones (reply/quote especially) --------------------------------------------
   Only a density tweak now — tighter tool buttons so six labelled pills wrap into two neat rows
   instead of three. The row LAYOUT itself is the same at every width (see .cmp-tools/.cmp-actions);
   the ≤520px overrides that used to live here were undoing a centred design that no longer exists.
   The fixed bottom nav is hidden while a modal is open: a dialog owns the screen, and leaving the bar
   visible under the backdrop is what made the hamburger look squashed against the sheet. */
@media (max-width:520px){
  .cmp-tools .btn{padding:8px 10px;font-size:13px}
  .cmp-tools,.cmp-actions{gap:6px}
}
body.modal-open .mobilenav{display:none}
/* …and the same while the LOGIN GATE is up: that bar belongs to the app behind it, and showAuth only
   hides #app — the nav is its SIBLING, so on a phone it stayed painted across the bottom of the login
   card. Keyed off the gate's own class (it sits before .mobilenav in the DOM), so nothing has to
   remember to toggle a body class on every path in and out of the gate. */
.auth-gate:not(.hidden) ~ .mobilenav{display:none}

/* ===== In-app confirm (replaces native confirm(); a native dialog wedges the Electron renderer's focus) ===== */
.btn-danger{background:linear-gradient(135deg,var(--danger,#d61f4e),#a01436);border:none;color:#fff;box-shadow:0 0 12px rgba(214,31,78,.4)}
.uiconfirm-bg{position:fixed;inset:0;z-index:500;display:grid;place-items:center;padding:24px;background:rgba(4,2,12,.72);backdrop-filter:blur(3px)}
.osw-body:has(>.uiconfirm-owned){position:relative}.uiconfirm-bg.uiconfirm-owned{position:absolute}
.uiconfirm{width:min(400px,94vw);border-radius:16px;padding:22px}
.uiconfirm-msg{font-size:17px;line-height:1.45;margin-bottom:18px;text-align:center}
.uiconfirm-btns{display:flex;gap:10px;justify-content:center}
.uiconfirm-btns .btn{min-width:110px}
/* An owned confirmation is constrained by its managed WINDOW, not by the page viewport.  A 360px
   VM window previously asked for a 400px dialog (94vw is still desktop-wide), clipping Delete and
   Cancel outside the window body.  Let the card shrink to the overlay and its actions wrap at the
   genuinely narrow end; body-owned browser dialogs keep their established sizing. */
.uiconfirm-owned .uiconfirm{width:min(400px,100%);max-width:100%}
.uiconfirm-owned .uiconfirm-btns{flex-wrap:wrap}
.uiconfirm-owned .uiconfirm-btns .btn{flex:1 1 110px;min-width:0}

/* ===== Node Control panel (launcher for the agentic `node` command) ===== */
.node-panel{display:flex;flex-direction:column;gap:12px}
.np-intro{margin:0;line-height:1.45}
.np-row{display:flex;align-items:center;gap:10px;flex-wrap:wrap}
.np-lbl{font-weight:600;font-size:15px;color:var(--muted)}
.np-row #np-node{flex:1;min-width:120px}
.np-all{display:flex;align-items:center;gap:6px;font-size:15px;color:var(--muted);white-space:nowrap;cursor:pointer}
.np-modes{display:flex;gap:8px}
.np-mode{flex:1;display:flex;align-items:center;justify-content:center;gap:8px;padding:10px;border:1px solid var(--line);border-radius:11px;cursor:pointer;font-size:15px;font-weight:600;color:var(--muted);transition:.15s}
.np-mode input{display:none}
.np-mode.active{color:#fff;border-color:var(--neon);background:rgba(var(--accent-rgb),.12);box-shadow:inset 0 0 0 1px var(--neon)}
#np-input{min-height:56px;resize:vertical;font-size:15px}
.np-egs{display:flex;flex-wrap:wrap;gap:8px}
.np-eg{background:rgba(var(--accent-rgb),.08);border:1px solid var(--line);color:var(--cyan);border-radius:999px;padding:6px 12px;font-size:12px;cursor:pointer;transition:.15s;font-family:inherit}
.np-eg:hover{border-color:var(--neon);color:#fff;background:rgba(var(--accent-rgb),.16)}
.np-run{display:flex;align-items:center;justify-content:flex-end;gap:12px}
.np-hint{opacity:.7}
.np-run .btn{min-width:120px}
.np-jobs-h{font-weight:700;letter-spacing:.5px;font-size:12px;color:var(--muted);text-transform:uppercase;margin-top:2px;border-top:1px solid var(--line);padding-top:12px}
.node-jobs{display:flex;flex-direction:column;gap:6px;max-height:34vh;overflow:auto}
.node-job{display:flex;align-items:center;gap:10px;flex-wrap:wrap;padding:8px 10px;border:1px solid var(--line);border-radius:10px;background:rgba(8,5,18,.4);font-size:13px}
.nj-ic{font-size:15px;flex:none}
.nj-id{font-variant-numeric:tabular-nums;color:var(--muted);font-weight:600;flex:none}
.nj-node{color:var(--cyan);font-weight:600;white-space:nowrap;flex:none}
.nj-cmd{flex:1;min-width:80px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-family:var(--mono);color:var(--text);opacity:.85}
.nj-status{white-space:nowrap;color:var(--muted);font-size:12px;flex:none}
.nj-status.run{color:var(--amber)}
.nj-acts{display:flex;gap:6px;white-space:nowrap;margin-inline-start:auto;flex:none}
.nj-acts .btn{padding:4px 10px;font-size:12px;min-width:0}
.np-run #np-save-cur{min-width:0}
/* Recent / Saved tabs */
.np-tabs{display:flex;gap:6px;margin-top:4px;border-top:1px solid var(--line);padding-top:12px}
.np-tab{flex:1;background:transparent;border:1px solid var(--line);border-radius:10px;color:var(--muted);font-weight:700;font-size:12px;letter-spacing:.3px;padding:8px;cursor:pointer;transition:.15s;font-family:inherit}
.np-tab:hover{color:#fff;border-color:var(--neon)}
.np-tab.active{color:#fff;border-color:var(--neon);background:rgba(var(--accent-rgb),.12);box-shadow:inset 0 0 0 1px var(--neon)}
.np-tabpanel{margin-top:10px}
.np-tabpanel.hidden{display:none}
.np-saved{display:flex;flex-direction:column;gap:6px;max-height:34vh;overflow:auto}
.np-saved-row{display:flex;align-items:center;gap:10px;padding:8px 10px;border:1px solid var(--line);border-radius:10px;background:rgba(8,5,18,.4);font-size:13px}
.np-saved-name{flex:1;min-width:60px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;color:var(--text);font-weight:600}
.np-saved-tgt{flex:none;white-space:nowrap}
/* Editing one saved task, in place of its row. */
.np-saved-edit{display:flex;flex-direction:column;gap:10px;padding:12px;border:1px solid var(--neon);
  border-radius:10px;background:rgba(var(--accent-rgb),.07)}
.np-saved-edit .fld{display:flex;flex-direction:column;gap:4px;font-size:12px;color:var(--muted)}
.np-saved-edit textarea{resize:vertical;min-height:62px}
.np-saved-edit .np-row.hidden{display:none}
/* phones / APK: full-width Run, bigger tap targets, no keyboard hint (no Ctrl+Enter on touch) */
@media(max-width:600px){
  .np-hint{display:none}
  /* A saved task is name + target + three buttons — too much for one 360px line, so the actions
     drop to their own full-width row (the same treatment .nj-acts gets for job rows). */
  .np-saved-row{flex-wrap:wrap}
  .np-saved-row .nj-acts{width:100%;justify-content:flex-end;margin-top:4px}
  .np-saved{max-height:46vh}
  .np-saved-edit .np-modes{flex-direction:column}
  .np-saved-edit .np-run .btn{flex:1;min-width:0}
  .np-run{justify-content:stretch}
  .np-run .btn{flex:1;min-width:0}
  .np-mode{padding:12px 6px}
  .np-eg{padding:8px 14px;font-size:13px}
  .np-row #np-node{flex:1 1 100%}
  .np-all{margin-inline-start:auto}
  .nj-acts{margin-inline-start:0;width:100%;justify-content:flex-end;margin-top:2px}
  .nj-acts .btn{padding:8px 12px;font-size:12px}
  .uiconfirm-btns .btn{flex:1;min-width:0}
}

/* ===== Meme Builder =====================================================================
   A layered editor: a preview STAGE you drag on, an INSPECTOR, and a TIMELINE of clip bars.
   Every colour is a theme token (no hardcoded dark surfaces), so it themes for free — the same
   rule that the rails and composer now follow.

   ONE structure, three layouts. The markup is always: a single toolbar, the stage, a tab strip and
   four panes (Layer / Timeline / Canvas / Result).
     • Desktop (>1100px) hides the tab strip and lays every pane out at once — stage top-left, the
       layer inspector down the right, timeline and canvas under the stage. Nothing is hidden.
     • Phone portrait: a fixed-height app column. Toolbar, stage, tabs, and ONE pane that scrolls
       inside itself. The page itself never scrolls.
     • Phone landscape (and anything short): the same, but stage on the left and the pane beside it,
       because 300px of height cannot hold both.
   This replaces a single ~1700px-tall column in which the stage, the controls and the timeline were
   never on screen together — on a phone you scrolled past six rows of wrapped toolbar to reach the
   preview, and tapping a clip changed a panel that was off-screen upwards.

   The stage's box IS the project's aspect ratio (every layer is positioned in % of it), so it is
   sized in JS from the space actually left over — see _fitStage. A vh height cannot do that, which
   is exactly why landscape was unusable. */
.mb-wrap{display:flex;flex-direction:column;gap:10px;padding:12px 16px 90px;min-width:0}
/* Drag an image in from the desktop or another tab (bindDrop). The whole builder is the target — the
   dropped file becomes its own layer wherever you let go — so the cue is a frame around it rather than
   a box to aim at, and the ::after banner says which drop it accepts. Pointer-events off so the
   highlight can never eat the drop it is advertising. Touch devices never fire these events. */
.mb-wrap.mb-dropping{outline:2px dashed var(--neon);outline-offset:-6px;border-radius:16px;position:relative}
.mb-wrap.mb-dropping::after{content:'⤵ Drop to add a layer';position:absolute;inset-inline-start:50%;top:14px;transform:translateX(-50%);
  padding:8px 16px;border-radius:999px;background:var(--panel2);border:1px solid var(--neon);color:var(--neon);
  font-size:15px;font-weight:600;letter-spacing:.4px;pointer-events:none;z-index:5;
  box-shadow:0 0 18px rgba(var(--accent-rgb),.35)}
/* The toolbar WRAPS. It used to be two rows of eleven buttons that wrapped into six on a phone, and the
   fix for that was cutting it to six buttons — not making the row un-wrappable. A single nowrap row that
   "fits" only fits the width it was measured at: at 390px it came to 263px inside 262px, so undo/redo
   were clipped on any device with slightly wider glyphs, a larger font setting or a narrower screen.
   Wrapping is self-correcting at every width and can never hide a control.
   Render is outside the wrapping group and never shrinks — it is the one button that must always be
   exactly where you left it. */
.mb-bar{display:flex;align-items:center;gap:8px;flex-wrap:nowrap;min-width:0}
.mb-bar .btn{margin:0}
.mb-barmain{display:flex;align-items:center;flex-wrap:wrap;gap:8px;flex:1 1 auto;min-width:0}
.mb-barmain .btn{flex:0 0 auto}
.mb-e{font-style:normal;margin-inline-end:6px}     /* the leading icon, dropped below 480px (see there) */
/* It is a sprite <svg> now, not an <i> holding an emoji. margin-right is the wrong lever for that —
   the .btn:has(.b-ic) rule already lays the button out as a flex row with a gap — and an svg needs
   its own baseline nudge, so zero the margin and let the flex gap do the spacing. */
svg.mb-e{margin-inline-end:0}
.mb-barend{display:flex;align-items:center;gap:8px;flex:0 0 auto}
.mb-status{flex:0 0 auto;white-space:nowrap}
.mb-spacer{flex:1 1 auto}
/* Desktop: every pane visible at once — the stage top-left, the layer inspector down the right, the
   timeline and canvas settings under the stage. The tab strip is a phone affordance and is hidden. */
.mb-body{display:grid;grid-template-columns:minmax(0,1fr) 330px;gap:14px 16px;align-items:start;min-width:0;
  grid-template-areas:"left layer" "tl layer" "canvas layer" "res res"}
/* Placed by ATTRIBUTE, not by #id: the landscape layout below re-homes whichever pane is open into a
   single `pane` area with `.mb-pane.on`, and an id selector here would outrank it — the open pane kept
   asking for a grid area that layout does not define, got auto-placed into a track of its own, and
   squeezed the stage column to zero. Same specificity → the later rule wins, which is the intent. */
.mb-left{grid-area:left;min-width:0}
.mb-pane[data-pane="layer"]{grid-area:layer}
.mb-pane[data-pane="timeline"]{grid-area:tl;min-width:0}
.mb-pane[data-pane="canvas"]{grid-area:canvas;min-width:0}
.mb-pane[data-pane="result"]{grid-area:res;min-width:0}
.mb-tabs{display:none}
.mb-fit{min-width:0}
/* container-type so a text layer's font-size can be expressed in cqh — the caption then scales with
   the preview instead of being a fixed px size that only looks right at one stage width. */
/* Size from the HEIGHT, not the width: with width:100%+max-height the box stops being the project's
   aspect ratio (a 9:16 project drew as a wide letterbox) and every layer's %-based position lands in the
   wrong place. Fixing the height and letting aspect-ratio derive the width keeps preview geometry equal
   to render geometry, which is the whole contract here. max-width keeps a 16:9 project inside the column.
   Under 1100px _fitStage overrides both axes in px — same contract, measured instead of guessed. */
.mb-stage{position:relative;height:58vh;max-width:100%;margin:0 auto;overflow:hidden;
  border:1px solid var(--line);border-radius:var(--r);container-type:size}
.mb-item{position:absolute;cursor:grab;touch-action:none}   /* touch-action:none → we own the gesture */
.mb-item.sel{outline:2px solid var(--neon);outline-offset:1px}
/* TEXT is its own bounds — the words show you exactly what and where they are, so a big neon rectangle
   around the layout box only mislead: it looked far larger than the glyphs and ran outside the frame on
   a large caption. Selection is shown by the resize handle instead, which is unambiguous and tiny. */
.mb-item.mb-text.sel{outline:none}
.mb-item.mb-text.sel .mb-h{display:block}
.mb-item img,.mb-item video{width:100%;height:100%;object-fit:contain;display:block;pointer-events:none}
/* The layer's EFFECT box, between .mb-item and the media — see stageEl. It carries the free rotate,
   and .crop carries the clip for the effects ffmpeg constrains to a fixed WxH (zoom/shake/pulse/spin).
   The two cannot be one element: the effect must be clipped to the layer box while the rotate must
   spill outside it. Not display:contents either — that box is what does the clipping. */
.mb-fx{position:absolute;inset:0;display:block;pointer-events:none}
.mb-fx.crop{overflow:hidden}
/* The ERASE box, inside .mb-fx and around the media. The mask lives HERE and never on the <img>/
   <video> itself: a mask on a replaced element is the case both engines drop the element for when the
   mask is not usable that frame, which is what made layers and timeline tiles vanish at random.
   Wrapping means the compositor masks an ordinary box instead. It also carries the flip + effect, so
   the paint order is mask -> flip -> effect, matching the renderer. Styles are written inline by
   stageEl too — these are documentation, nothing depends on them. */
.mb-mk{position:absolute;inset:0;display:block;pointer-events:none}
/* width:max-content — the box HUGS the text. It used to be a full 92%-wide box with the text centred
   inside it, so the preview showed the caption centred while ffmpeg's drawtext anchors the text's LEFT
   EDGE at the same x — the caption you placed centre-bottom rendered hard against the left edge. Hugging
   the content makes the layer's x the real left edge in BOTH, so preview == render. */
/* Must match ffmpeg's drawtext EXACTLY, or the caption lands somewhere else in the export than where you
   put it: drawtext never wraps, draws from the layer's x, and left-aligns its lines. So: white-space:pre
   (no wrapping), no max-width (nothing to wrap against), text-align:start, line-height:1 so the box hugs
   the glyphs. With these, the preview's top-left IS the rendered text's top-left. */
/* text-align:CENTRE, not inset-inline-start: the renderer anchors every line on the caption's centre (x = cx - text_w/2,
   or (w-text_w)/2 when the ⇔ Centre flag is on), so a caption with more than one line exports with its
   lines centred on each other. Left-aligning them here made the preview disagree with the export for any
   multi-line caption — which is now every caption long enough to wrap. */
/* line-height 1.18, NOT 1: the renderer draws one drawtext PER LINE and steps each one down by
   `round(size * 1.18)` (see _line_dy in meme_builder_service.py), so a preview at 1.0 packs the lines
   0.18em tighter than the export at every line after the first. That is cumulative and invisible on
   the single-line caption everything was checked against — at size 64 the fourth line of a wrapped
   caption came out 36px lower in the MP4 than on the stage, and captions WRAP automatically now, so
   this hit any caption long enough to need two lines. The number is the renderer's; the export is
   what people have already made, so the preview is the side that moves.
   Cross-check: scripts/check_meme_render_match.py screenshots the stage and renders the same payload
   through ffmpeg, and fails on exactly this. */
.mb-text{font-weight:700;line-height:1.18;white-space:pre;width:max-content;max-width:none;text-align:center;
  padding:0;margin:0;
  /* Match the font ffmpeg draws with (Liberation Sans Bold, via _meme_font_path). The preview used to
     inherit the THEME font — cyberpunk's stylised glyphs meant the caption you saw was not the caption
     that rendered (a different 'e', and different widths, which also shifted where the text sat). */
  /* weight 700, NOT 800: ffmpeg draws with LiberationSans-BOLD (700). At 800 the browser has no
     matching face and SYNTHESISES a heavier one, which is measurably WIDER — so a caption centred
     by eye produced an x for a wider string than ffmpeg would draw, landing it left of centre
     (and negative for a caption wider than the canvas). */
  font-family:'PCMemeFont','Liberation Sans',Helvetica,Arial,sans-serif;
  /* ffmpeg's drawtext places the INK (cap) top exactly at the layer's y — measured, +0px at every size.
     CSS instead places the ASCENT top there, and the ink sits ~0.19em lower (ascender .905 - capHeight
     .716 for Arial/Liberation). So the export always came out ~0.19 x font-size HIGHER than the preview
     showed — 36px on a 189px caption. Lift the preview by the same amount so ink lines up with ink.
     -0.28em, not -0.19em: line-height 1.18 (above) also puts half its extra leading ABOVE the first
     line — (1.18 - 1)/2 = 0.09em — which would push the WHOLE caption down by that much. The two
     corrections are independent and both are needed; 0.19 + 0.09 is the total. */
  transform:translateY(-0.28em)}
/* ONE span per wrapped line. Each is its own inline-block so the optional background box hugs that line's
   own ink — which is what drawtext's box=1 draws. The box itself is a box-SHADOW spread, never padding:
   padding would move the glyphs, and drawtext's boxborderw does not. */
.mb-tline{display:inline-block}
/* Snap guides: shown only while a drag is snapped to that line (JS toggles display). Deliberately thin and
   dashed so they read as a guide rather than as part of the meme. */
.mb-guide{position:absolute;pointer-events:none;z-index:4}
.mb-gv{top:0;bottom:0;width:0;border-inline-start:1px dashed var(--neon);margin-inline-start:-1px}
.mb-gh{inset-inline-start:0;inset-inline-end:0;height:0;border-top:1px dashed var(--neon);margin-top:-1px}
/* Resize handle — only on the selected layer, and finger-sized (18px) rather than a desktop 8px dot. */
.mb-h{display:none;position:absolute;inset-inline-end:-9px;bottom:-9px;width:18px;height:18px;border-radius:50%;
  background:var(--neon);border:2px solid var(--bg);cursor:nwse-resize}
.mb-item.sel .mb-h{display:block}
.mb-playrow{display:flex;align-items:center;gap:10px;margin-top:8px}
.mb-playrow .btn{margin:0}
.mb-scrub{flex:1;min-width:0;accent-color:var(--neon)}
/* ---- the tab strip and its panes ---- */
/* Longhands, not the `font` shorthand: `font:600 13px/1 inherit` is INVALID CSS (a CSS-wide keyword is only
   legal as a property's sole value), so the whole declaration would be dropped — and a <button> does not
   inherit font-family on its own, so it would have fallen back to the UA font at the UA size. */
.mb-tab{flex:1 1 0;min-width:0;padding:10px 6px;cursor:pointer;
  font-family:inherit;font-size:13px;font-weight:600;line-height:1;letter-spacing:.2px;
  color:var(--muted);background:transparent;border:0;border-bottom:2px solid transparent;
  white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.mb-tab.on{color:var(--neon);border-bottom-color:var(--neon)}
.mb-pane{min-width:0}
.mb-inspector{display:flex;flex-direction:column;gap:10px;padding:12px;
  border:1px solid var(--line);border-radius:var(--r)}
/* Collapsible inspector groups. A video layer's panel is ~20 controls; three named groups make it four
   rows plus whatever you opened, and the open/closed state is remembered across repaints (_sec). */
.mb-sec{border:1px solid var(--line);border-radius:var(--r);background:var(--panel2)}
.mb-sec>summary{padding:10px 12px;font-size:13px;font-weight:600;color:var(--text);cursor:pointer;
  list-style:none;user-select:none}
.mb-sec>summary::-webkit-details-marker{display:none}
.mb-sec>summary::after{content:'▾';float:inline-end;color:var(--muted);font-size:11px;transition:transform .15s}
.mb-sec[open]>summary::after{transform:rotate(180deg);display:inline-block}
.mb-sec>summary:hover{color:var(--neon)}
.mb-secbody{display:flex;flex-direction:column;gap:10px;padding:0 12px 12px}
/* A heading inside a pane that is not a collapsible group (the Canvas pane is short enough to stay open). */
.mb-secttl{font-size:12px;font-weight:700;letter-spacing:.6px;text-transform:uppercase;color:var(--muted);margin-top:2px}
/* Canvas shape presets. Buttons, not a dropdown: four shapes with their pixel size under the name read
   at a glance and are thumb-sized, where the <select> they replace was an 88px control in a wrapped bar. */
.mb-sizes{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:6px}
.mb-szb{display:flex;flex-direction:column;align-items:center;gap:2px;padding:8px 2px;cursor:pointer;
  font-family:inherit;color:var(--muted);background:var(--panel2);border:1px solid var(--line);border-radius:9px}
.mb-szb b{font-size:13px;font-weight:700}
.mb-szb i{font-style:normal;font-size:11px;opacity:.75}
.mb-szb:hover{color:var(--text);border-color:var(--neon)}
.mb-szb.on{color:var(--cyan);border-color:rgba(var(--accent-rgb),.55);background:rgba(var(--accent-rgb),.18)}
.mb-bgrow{display:flex;align-items:center;gap:10px;font-size:13px;color:var(--muted)}
.mb-bgrow input[type=color]{flex:0 0 auto;width:46px;height:36px;padding:2px;background:var(--panel2);
  border:1px solid var(--line);border-radius:8px;cursor:pointer}
/* "Add to the meme" sheet: each source gets a line saying what it is — the room the toolbar buttons
   never had. One sheet now holds everything the old 🖼️ Media and ➕ More buttons split between them. */
.mb-addb{display:block;text-align:start;line-height:1.45}
.mb-addb b{display:block;font-size:15px}
.mb-addb i{display:block;font-style:normal;font-size:12px;opacity:.72;font-weight:400}
/* Group headings in the one "Add to the meme" sheet. Seven entries in a flat list is a wall you have to
   read end to end; two labelled runs of three or four you can skip to. Cheap in height (one small line
   each) because that is the whole budget on a phone — the sheet is capped at 92vh and scrolls, and the
   goal is that on a normal handset it does not have to. First one loses its top margin so the heading
   sits under the <h3> at the same rhythm as the buttons. */
.mb-addgrp{margin:14px 2px 6px;font-size:11px;letter-spacing:.8px;text-transform:uppercase;
  font-weight:700;color:var(--neon);opacity:.85}
.mb-addgrp:first-of-type{margin-top:2px}
.mb-insp-hd{display:flex;align-items:center;justify-content:space-between;gap:8px;flex-wrap:wrap}
.mb-insp-hd .btn{margin:0;flex:0 0 auto}   /* wrap to a second line rather than squash a label */
/* Cut + Duplicate + Delete stay together on the right — space-between with loose children would strand
   them in the middle of the header, away from the buttons they belong with. They WRAP as a group: three
   buttons plus the "Video layer" title is more than a 360px phone has in one row, and flex:0 0 auto alone
   would push the last one off the edge instead of dropping it to a second line. */
.mb-insp-acts{display:flex;align-items:center;gap:6px;margin-inline-start:auto;flex:0 1 auto;flex-wrap:wrap;
  justify-content:flex-end}
.mb-f{display:flex;flex-direction:column;gap:4px;font-size:13px;color:var(--muted);min-width:0}
.mb-f .input,.mb-f textarea{margin:0}
.mb-f input[type=color]{width:100%;height:34px;padding:2px;background:var(--panel2);
  border:1px solid var(--line);border-radius:8px}
/* "Where is the mouth?" — the placement control for Make it talk. The picture is capped by VIEWPORT
   height, not a fixed px: on a phone in landscape a tall portrait photo would otherwise push the
   marker, the slider and both buttons off the bottom of the sheet, and you cannot drag what you
   cannot see. touch-action:none because we own the drag — without it the browser scrolls the modal
   instead of moving the marker on the very gesture the control exists for. */
.mb-mouth-wrap{position:relative;display:inline-block;max-width:100%;line-height:0;
  touch-action:none;cursor:crosshair;border-radius:10px;overflow:hidden;border:1px solid var(--line)}
.mb-mouth-wrap img{display:block;max-width:100%;max-height:48vh;width:auto;height:auto}
/* A WIDE, thin marker: it is showing a mouth's position AND its width, and the width is the number
   every length in the renderer scales off. A dot would say nothing about it. */
.mb-mouth-pin{position:absolute;height:0;border-top:3px solid var(--neon);border-radius:3px;
  box-shadow:0 0 0 1px rgba(0,0,0,.65),0 0 10px rgba(var(--accent-rgb),.9);pointer-events:none}
.mb-mouth-pin::before,.mb-mouth-pin::after{content:'';position:absolute;top:-5px;width:3px;height:11px;
  background:var(--neon);border-radius:2px;box-shadow:0 0 0 1px rgba(0,0,0,.65)}
.mb-mouth-pin::before{inset-inline-start:-1px}
.mb-mouth-pin::after{inset-inline-end:-1px}
/* ✂ Erase parts — the same shrink-wrapped, untransformed picture the mouth picker uses, with the mask
   scrim laid over it. `touch-action:none` is LOAD-BEARING on a phone: without it the browser claims the
   drag as a scroll/pinch and the stroke never reaches the canvas — the tool looks dead to a finger while
   working perfectly with a mouse. */
.mb-er-wrap{position:relative;display:inline-block;max-width:100%;line-height:0;
  touch-action:none;cursor:crosshair;border-radius:10px;overflow:hidden;border:1px solid var(--line);
  /* A checkerboard behind the art, so an erased area reads as SEE-THROUGH rather than as a coloured
     patch — the same convention every image editor uses, and the honest preview of what the export does. */
  background-image:linear-gradient(45deg,#2a2a2a 25%,transparent 25%,transparent 75%,#2a2a2a 75%),
                   linear-gradient(45deg,#2a2a2a 25%,transparent 25%,transparent 75%,#2a2a2a 75%);
  background-size:18px 18px;background-position:0 0,9px 9px;background-color:#1b1b1b}
.mb-er-wrap>img,.mb-er-wrap>video{display:block;max-width:100%;max-height:46vh;width:auto;height:auto}
/* Stretched to the element box, NOT sized in pixels: the canvas backing store is the MASK's resolution
   (capped source pixels) while the picture is laid out at whatever fits the screen, and the two only stay
   in register if the browser scales one onto the other. */
.mb-er-wrap>canvas{position:absolute;inset:0;width:100%;height:100%;pointer-events:none}
.mb-er-tools{display:flex;gap:8px;flex-wrap:wrap;margin:10px 0 4px}
.mb-er-tools .btn{flex:1 1 auto;min-width:96px}
.mb-frow{display:flex;gap:8px}
/* The flip toggles are the only mb- buttons with an ON state, and there was no .btn.on rule to
   carry it — an unlit toggle is worse than no toggle, you cannot tell whether the layer is
   mirrored. Same treatment as .tl-cmp-btn.on. */
/* …and the same for a toggle that is NOT in a .mb-frow — ⇔ Centre horizontally is a full-width button, so
   scoping the lit state to .mb-frow left it with no way to show that it is on. */
.mb-frow .btn.on,.mb-inspector .btn.on{background:rgba(var(--accent-rgb),.18);color:var(--cyan);border-color:rgba(var(--accent-rgb),.55)}
.mb-frow .mb-f{flex:1 1 0}
/* Visual clip trimmer (video layer): the source video IS the scrubber. Bright band = kept, dark = trimmed,
   neon bars = drag handles. Theme vars only for the frame; the dim/handles are fixed so they read on any clip. */
.mb-trim{position:relative;width:100%;background:#000;border:1px solid var(--line);border-radius:8px;overflow:hidden;touch-action:none}
.mb-trim-vid{display:block;width:100%;max-height:220px;object-fit:contain;background:#000;pointer-events:none}
.mb-trim-track{position:absolute;inset:0;touch-action:none}
.mb-trim-dim{position:absolute;top:0;bottom:0;background:rgba(0,0,0,.58);pointer-events:none}
.mb-trim-dim-l{inset-inline-start:0;width:0}
.mb-trim-dim-r{inset-inline-end:0;inset-inline-start:100%;width:0}
.mb-trim-sel{position:absolute;top:0;bottom:0;inset-inline-start:0;width:100%;box-sizing:border-box;
  border:2px solid var(--neon);box-shadow:0 0 0 9999px transparent;pointer-events:none}
.mb-trim-h{position:absolute;top:0;bottom:0;width:20px;margin-inline-start:-10px;cursor:ew-resize;z-index:2;
  display:flex;align-items:center;justify-content:center;touch-action:none}
.mb-trim-h::before{content:'';width:5px;height:56%;border-radius:3px;background:var(--neon);
  box-shadow:0 0 0 1px rgba(0,0,0,.65),0 0 6px rgba(0,0,0,.5)}
.mb-trim-lbl{display:flex;justify-content:space-between;align-items:center;font-size:12px;color:var(--muted);margin-top:6px}
.mb-trim-len{color:var(--neon);font-weight:700}
.mb-check{flex-direction:row;align-items:center;gap:8px}
.mb-order{display:flex;gap:8px}
.mb-order .btn{margin:0;flex:1}
/* Nine-position align pad. Square cells in a 3x3 so it reads as the canvas it maps onto, and 34px tall so
   each one is a real touch target rather than a 9-way sliver. */
.mb-align{display:grid;grid-template-columns:repeat(3,1fr);gap:4px}
.mb-ab{height:34px;line-height:1;padding:0;font-size:15px;cursor:pointer;color:var(--muted);
  background:var(--panel2);border:1px solid var(--line);border-radius:7px}
.mb-ab:hover{color:var(--text);border-color:var(--neon)}
/* The timeline is the scroll PORT; .mb-tlinner is the content, and the ruler and playhead are inside it so
   they stay glued to the lanes when a phone scrolls the timeline sideways. */
/* overflow-x at EVERY width, not just on a phone: the timeline zoom makes the content wider than the port
   on a desktop too, and without this a zoomed timeline would simply overflow the panel. */
.mb-timeline{padding:10px;border:1px solid var(--line);border-radius:var(--r);
  overflow-x:auto;-webkit-overflow-scrolling:touch}
/* --mb-pps is PIXELS PER SECOND, and it is what makes the timeline hold still. The lane used to stretch to
   exactly the project length, so a second was worth however much room happened to be left — change ONE
   clip's length and every other bar on the timeline moved and resized. With a fixed scale a bar's position
   and width come from that layer alone, an edit moves nothing but the trailing empty space, and the width
   of the lane (set in meme.js from this and --mb-name-w) is the only thing that grows. Zoom multiplies it.
   --mb-name-w is the row's label column; the lane's calc() has to use the very same value the rows do or
   the ruler ticks drift out of line with the clips underneath them at a breakpoint. */
.mb-tlinner{position:relative;display:flex;flex-direction:column;gap:6px;min-width:100%;--mb-pps:56px;--mb-name-w:72px}
.mb-trackname,.mb-rname{flex:0 0 var(--mb-name-w)}
/* Seconds ruler — same .mb-track/.mb-lane geometry as a clip row, which is what makes a tick line up with
   the clip below it without measuring anything. */
.mb-rrow{align-items:flex-end}
.mb-rlane{height:20px;background:transparent;cursor:pointer;touch-action:none}
.mb-tick{position:absolute;top:6px;bottom:0;width:0;border-inline-start:1px solid var(--line)}
/* Longhands, NOT the `font:` shorthand: `font:600 10px/1 inherit` is invalid (the shorthand's family slot
   cannot be `inherit`) and the whole declaration is dropped, which leaves the labels at the default size.
   font-style:normal because the tick is an <i>. */
.mb-tick b{position:absolute;inset-inline-start:3px;top:-8px;font-weight:600;font-size:11px;line-height:1;font-style:normal;
  color:var(--muted);white-space:nowrap}
.mb-tick b.mb-tickr{inset-inline-start:auto;inset-inline-end:3px}          /* the final label hangs left of its line, or it is clipped */
/* Playhead: one line across every lane, with a grab tab at the top. Positioned in px by paintPlayhead
   (from a real lane's offset), so it cannot drift from the lanes at any breakpoint. */
.mb-ph{position:absolute;top:14px;bottom:2px;width:0;border-inline-start:2px solid var(--danger);
  margin-inline-start:-1px;pointer-events:none;z-index:3}
/* The line itself is pointer-transparent (it lies over every lane and would eat clip drags); only the tab
   is grabbable, which is also the visible "you can drag this" affordance. */
.mb-ph b{position:absolute;inset-inline-start:-7px;top:-9px;width:16px;height:16px;border-radius:3px;background:var(--danger);
  pointer-events:auto;cursor:ew-resize;touch-action:none}
.mb-empty{padding:6px 2px}
.mb-tlhint{margin:0}
/* The timeline's own toolbar: the hint, the crossfade length and the zoom stepper. Sits directly above the
   lanes because all three are about the lanes — putting zoom in the main bar would separate it from the
   thing it zooms. */
.mb-tlbar{display:flex;align-items:center;gap:10px;flex-wrap:wrap;margin:8px 2px 4px}
.mb-tlf{display:inline-flex;align-items:center;gap:6px;font-size:13px;color:var(--muted)}
.mb-tlf .input{width:auto;min-width:74px;margin:0;padding:6px 8px}
.mb-zoom{display:inline-flex;align-items:center;gap:6px}
/* Wide enough for "0.13×" as well as "1×": the label sits BETWEEN the steppers, so a label that
   changes width shoves the buttons sideways under the thumb that is pressing them. */
.mb-zoom b{min-width:44px;text-align:center;font-size:12px;color:var(--cyan)}
.mb-zb{width:30px;height:28px;line-height:1;padding:0;font-size:15px;cursor:pointer;color:var(--muted);
  background:var(--panel2);border:1px solid var(--line);border-radius:7px}
.mb-zb:hover{color:var(--text);border-color:var(--neon)}
/* (The export-format dropdown used to sit beside Render. It is a once-per-project setting and it was the
   85px that pushed ➕ More and undo/redo off a 390px phone's toolbar, so it lives in ⚙︎ Canvas now.) */
/* Voice-over recorder: a big elapsed clock and a pulsing dot, so it is obvious the mic is live. */
/* The "what the speed did to this clip's slot" note, inside the Speed label. */
.mb-slot{font-style:normal;font-size:11px}
.mb-rec{display:flex;align-items:center;justify-content:center;gap:10px;margin:4px 0 12px}
.mb-rec b{font-size:24px;color:var(--cyan)}
.mb-recdot{width:13px;height:13px;border-radius:50%;background:var(--line)}
.mb-recdot.on{background:var(--danger);animation:mbrec 1s ease-in-out infinite}
@keyframes mbrec{0%,100%{opacity:1}50%{opacity:.25}}
/* A layer row is [grip][tile][bar] at every width — see trackEl. The whole point of the geometry is that
   the three columns line up down the list, so it reads as ONE list rather than a stack of per-type widgets. */
.mb-track{display:flex;align-items:center;gap:10px;min-width:0}
/* Selection reads on the TILE as well as the bar. The tile is where you look to find a layer, so it has to
   be where "this is the one you are editing" shows — the bar alone is off to the right, and on a short clip
   it is a 30px sliver somewhere in the middle of the lane. */
.mb-track.sel .mb-clip{box-shadow:inset 0 0 0 2px var(--neon)}
.mb-track.sel .mb-tile{border-color:var(--neon);box-shadow:0 0 0 1px var(--neon)}
/* 132px -> 72px: grip (14) + gap (6) + tile (52). The column used to hold a filename too, which is now in
   the bar, so the 60px it was spending on truncated text goes to the lane. */
.mb-trackname{position:relative;display:flex;align-items:center;gap:6px;min-width:0}
/* Restack grip. touch-action:none is what makes a vertical drag OUR gesture instead of a page scroll —
   scoped to this 14px handle precisely so scrolling the page still works everywhere else in the row. */
/* align-self:stretch, not a fixed height: the grip is the smallest thing a finger has to hit in the row,
   so it takes the row's FULL height as its target rather than the 14px the icon needs. */
.mb-rgrip{flex:0 0 auto;width:14px;align-self:stretch;min-height:24px;display:flex;align-items:center;
  justify-content:center;color:var(--muted);cursor:grab;touch-action:none;user-select:none;-webkit-user-select:none}
.mb-rgrip svg.ic{width:14px;height:14px;stroke-width:2}
.mb-rgrip:hover{color:var(--neon)}
/* Music has no stacking order. The grip still occupies its 14px so that row's tile keeps the same left
   edge as every other one — an inert column beats a ragged list. */
.mb-rgrip-off{visibility:hidden;cursor:default;touch-action:auto}
.mb-track.mb-dragging{opacity:.65}
.mb-track.mb-dragging .mb-rgrip{cursor:grabbing;color:var(--neon)}
/* ONE tile for every layer type — a frame for a picture or a clip, a glyph for text and music. Before this
   the media rows had a thumbnail and the text/music rows had a truncated string, so no two rows were the
   same shape and the list looked broken rather than mixed. */
.mb-tile{position:relative;flex:0 0 auto;width:52px;height:34px;border-radius:7px;overflow:hidden;
  border:1px solid var(--line);background:var(--panel2);display:flex;align-items:center;
  justify-content:center;font-weight:800;font-size:15px;line-height:1;color:var(--muted)}
.mb-tile img,.mb-tile video{width:100%;height:100%;object-fit:cover;display:block}
/* Text and music have no frame to show, so their tile is the type icon on the same panel the picture
   tiles sit on — same box, same border, so the column still lines up. */
.mb-tile-gl{color:var(--text)}
.mb-tile-gl svg.ic{width:17px;height:17px}
/* Theme vars only — a hardcoded rgba() here would be invisible or garish in the light themes. */
.mb-tile-aud{color:var(--cyan)}
/* Corner badge marking a tile as a VIDEO, inside the tile it belongs to (it used to be positioned against
   the whole name column and floated over the middle of the picture). */
.mb-tvid{position:absolute;inset-inline-end:2px;bottom:1px;display:flex;color:#fff;pointer-events:none;
  filter:drop-shadow(0 1px 2px #000)}
.mb-tvid svg.ic{width:11px;height:11px;stroke-width:2.4}
/* Music beds: tinted so the soundtrack reads as a different KIND of track at a glance, not another clip. */
/* Theme vars only — a hardcoded rgba() here would be invisible or garish in the light themes. */
.mb-track-aud .mb-clip{background:linear-gradient(135deg,rgba(var(--accent-rgb),.22),rgba(var(--accent-rgb),.10));
  box-shadow:inset 3px 0 0 var(--cyan),inset -3px 0 0 var(--cyan)}
.mb-track-aud .mb-lane{height:26px}
/* Music that runs past the end of the meme: the bar is clamped to the lane, so say it is cut, not short. */
.mb-clip.mb-cut{border-inline-end:2px dashed var(--danger)}
/* Preview-only <audio> elements — audible, never visible (hidden would also stop them in some engines). */
.mb-audios{position:absolute;width:0;height:0;overflow:hidden;opacity:0;pointer-events:none}
/* Fixed width from --mb-lane-w (set per render in meme.js), NOT flex:1 1 auto. Absorbing the leftover
   room is what tied a second's width to the panel's width, so the same project drew a different
   timeline in a wide window than a narrow one and every edit that changed the length re-laid-out every
   bar. The fallback keeps a lane on any markup rendered before the property is set. */
.mb-lane{position:relative;flex:0 0 var(--mb-lane-w,100%);height:34px;border-radius:9px;background:var(--panel2);min-width:0}
/* The bar carries the layer's NAME on the left and its duration on the right. The padding clears the 14px
   grip at each end so neither label ever sits under one. */
/* user-select:none — the bar carries real text now AND is the drag surface, so without it dragging a clip
   sweeps a text selection across the name and leaves it highlighted. */
.mb-clip{position:absolute;top:3px;bottom:3px;display:flex;align-items:center;gap:8px;padding:0 18px;
  border-radius:7px;background:linear-gradient(135deg,rgba(var(--accent-rgb),.45),rgba(var(--accent2-rgb),.35));
  color:var(--text);font-size:12px;font-weight:700;cursor:grab;touch-action:none;overflow:hidden;
  user-select:none;-webkit-user-select:none}
.mb-cname{flex:1 1 auto;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;
  font-weight:600;opacity:.92}
/* tabular-nums so the durations down the list don't jitter as the digits change while you drag a trim. */
.mb-cdur{flex:0 0 auto;font-size:11px;opacity:.82;font-variant-numeric:tabular-nums}
/* Too short for a name (trackEl decides, zoom included): centre the duration and drop the padding, or the
   grips would cover the only thing left in the bar. */
.mb-clip-sm{justify-content:center;padding:0 2px;gap:0}
.mb-grip{position:absolute;top:0;bottom:0;width:14px;cursor:ew-resize;background:rgba(var(--accent-rgb),.5)}
.mb-grip-l{inset-inline-start:0;border-radius:7px 0 0 7px}
.mb-grip-r{inset-inline-end:0;border-radius:0 7px 7px 0}
.mb-result{display:flex;flex-direction:column;gap:10px;margin-top:12px;padding:12px;
  border:1px solid var(--line);border-radius:var(--r)}
.mb-resvid{width:100%;max-height:52vh;object-fit:contain;border-radius:10px;background:#000}
/* An <img> defaults to object-fit:FILL, and `width:100%` + `max-height` is exactly the pair that makes
   that visible: the width stretches to the panel, then the height is clamped independently, so a GIF or
   a still came out squashed and blown up — while the same file posted to the timeline looked right,
   because nothing there forces its width. Let a picture size itself inside the panel instead: never
   wider or taller than the box, never upscaled past its own pixels, never distorted. */
img.mb-resvid{width:auto;height:auto;max-width:100%;margin:0 auto;display:block;background:none}
.mb-resacts{display:flex;gap:8px;flex-wrap:wrap}
.mb-resacts .btn{margin:0}
.mb-err{margin-top:10px;padding:10px 12px;border:1px solid var(--danger);border-radius:var(--r);
  color:var(--danger);font-size:15px}
/* ---- tablet / phone: a fixed-height app, not a page ----
   #feed stops scrolling and the builder fills it exactly: toolbar, stage, tabs, and ONE pane that scrolls
   inside itself. Nothing below the fold, so the stage can never be scrolled away from the controls that
   act on it. The `.feed-meme` class is set by renderView (and by PCMeme.render, for the direct entries). */
@media(max-width:1100px){
  /* padding:0 so the builder gets the whole box — .feed's default 90px bottom pad is for a scrolling
     timeline and would just be dead space here. The mobile nav's own clearance is added back at ≤820px. */
  .feed.feed-meme{overflow:hidden;padding:0}
  .mb-wrap{height:100%;overflow:hidden;padding-bottom:0;gap:8px}
  /* align-items:stretch, explicitly: the desktop rule above is a GRID with align-items:start, and in a flex
     COLUMN that same declaration means "shrink every child to its content width" — the tab strip and the
     stage box came out as wide as their text instead of as wide as the screen. */
  .mb-body{display:flex;flex-direction:column;align-items:stretch;flex:1 1 auto;min-height:0;gap:8px}
  /* The split is a FIXED fraction, and neither side's basis comes from its content. That is deliberate:
     _fitStage writes the stage's height from the space this box ends up with, so if the box's own size also
     depended on content the two would need more than one layout pass to settle — and they did. A pane sized
     from its content (`flex:0 1 auto` + max-height) grew when the trim widget's <video> reported its
     metadata, which shrank .mb-fit AFTER the stage had already been sized into the taller box, leaving the
     preview overflowing by 2x until a ResizeObserver happened to catch up. One pass, no catching up.
     It is also the better behaviour: the preview stays exactly the same size while you work, instead of
     jumping every time you open an inspector group or switch tabs. */
  .mb-left{display:flex;flex-direction:column;flex:1 1 0;min-height:0}
  .mb-fit{flex:1 1 0;min-height:0;min-width:0;display:flex;align-items:center;justify-content:center;overflow:hidden}
  .mb-stage{height:auto;margin:0}                 /* _fitStage writes both axes in px */
  .mb-tabs{display:flex;flex:0 0 auto;gap:2px;border-bottom:1px solid var(--line)}
  .mb-pane{display:none}                          /* only the open pane exists */
  .mb-pane.on{display:block;flex:0 0 52%;min-height:0;overflow-y:auto;overscroll-behavior:contain}
  .mb-inspector{border:0;padding:0 0 8px}         /* the pane is the frame now */
  .mb-result{margin-top:0}
  .mb-resvid{max-height:36vh}
}
/* Side by side instead of stacked, in the two cases where stacking wastes the screen. The comma is an OR:
   (a) a SHORT window — landscape phone. Half of a 330px-tall viewport is not a panel, and stacking is
       what made the builder unusable "even sideways".
   (b) a WIDE-ish one under the desktop breakpoint — a tablet. Stacked, a 768×1024 iPad gave the stage 293px
       and the panel 531px; side by side the stage gets the full height of the column. */
@media (max-width:1100px) and (max-height:560px),
       (max-width:1100px) and (min-width:700px){
  .mb-body{display:grid;align-items:stretch;grid-template-columns:minmax(0,1fr) minmax(230px,40%);
    grid-template-rows:auto minmax(0,1fr);grid-template-areas:"left tabs" "left pane";gap:6px 10px}
  .mb-left{grid-area:left}
  .mb-tabs{grid-area:tabs}
  .mb-pane.on{grid-area:pane;flex:none;max-height:100%;height:100%}
  .mb-tab{font-size:12px;padding:8px 4px}
  /* The pane is ~40% of the window here, so it is NARROW however wide the viewport is — the ≤820px
     rules key off viewport width and never fire. Same trade as there: the lane gets the width, the row
     is identified by its thumbnail, z-order lives in the layer panel. Without this a landscape phone
     spent 132 of its 300 panel pixels on a name column and left a 135px lane. */
  .mb-tlinner{--mb-name-w:66px;--mb-pps:44px}
  /* 46 -> 64: the restack grip (14px + its gap) lives INSIDE this column. .mb-rname moves with it or
     the ruler's ticks stop lining up with the clips below them, which is the whole point of the two
     sharing a geometry. The 18px comes off the lane, which is flex:1 1 auto with min-width:0 — so the
     timeline still fits the screen at 1x rather than spilling into a horizontal scroll. */
}
@media(max-width:820px){
  /* the fixed .mobilenav overlays the bottom ~62px — reserve it or the open pane runs under the nav */
  .feed.feed-meme{padding-bottom:calc(62px + env(safe-area-inset-bottom))}
  .mb-wrap{padding:8px 10px 0;gap:8px}
  .mb-bar{gap:6px}
  .mb-stage{height:auto}
  /* NO horizontal scroll at 1x. The lane used to be kept usable by forcing the content to 520px inside a
     ~368px box, which meant the timeline ALWAYS spilled off the right edge: every project, even a
     two-clip one, had to be scrolled sideways to reach its own clips — and scrolling a box sideways
     while trying to drag a clip inside it fights itself.
     The width it was buying back went to the 104px NAME COLUMN, not to the lane. So take that instead:
     on a phone the row is identified by its thumbnail alone (46px), the z-order buttons come off the row
     (they are in the layer panel as ⬇︎ Send back / ⬆︎ Bring front), and the lane gets the rest. A 1x
     timeline now fits the screen exactly, and scrolling means you deliberately zoomed in. */
  .mb-tlinner{--mb-name-w:66px;--mb-pps:44px}
  /* see the note in the landscape rules above — grip width, and .mb-rname must stay identical. */
  .mb-tlbar{gap:8px}
  .mb-tlhint{flex:1 1 100%}                       /* the hint takes its own line; controls stay reachable */
  .mb-zb{width:38px;height:34px}                  /* thumb-sized zoom stepper */
  .mb-bar .btn.mb-icon{min-width:44px;padding-inline-start:10px;padding-inline-end:10px}
  /* Tighter, so the six buttons usually still make ONE row on a phone — and wrap to a second by
     themselves on a narrow one, or when the user's font settings make them wider, instead of the last
     two disappearing off the edge. */
  /* align-items:flex-start — when the buttons do wrap, Render stays on the FIRST row instead of floating
     vertically centred against a half-empty second one, which reads as a mistake rather than a layout. */
  .mb-bar{gap:6px;align-items:flex-start}
  /* Tight enough that a 390px phone keeps all six on ONE row with a few px to spare, without shrinking a
     tap target. This is an optimisation, not a constraint — anything narrower (or a larger system font)
     simply wraps, which is why the row can afford to be tuned at all. */
  .mb-barmain{gap:4px}
  .mb-barmain .btn{padding-inline-start:8px;padding-inline-end:8px}
  .mb-ab{height:38px;font-size:15px}              /* thumb-sized align pad */
  /* Thumbnail only — the width is the lane's (see .mb-tlinner above). */
  .mb-trackname{font-size:12px;gap:4px}
  .mb-rgrip{width:18px}                           /* wider drag target for a thumb (column basis follows) */
  .mb-tile{width:42px;height:30px;border-radius:6px}
  .mb-tile-gl svg.ic{width:15px;height:15px}
  /* Inspector: full-width controls and finger-sized rows rather than desktop two-ups squeezed to slivers. */
  .mb-frow{flex-wrap:wrap}
  .mb-frow .btn{flex:1 1 46%}
  .mb-f input[type=range]{height:28px}
  .mb-dbg{word-break:break-word}
  .mb-lane{height:40px}                           /* taller = easier to grab with a thumb */
  .mb-grip{width:18px}
  .mb-clip{padding:0 22px}                        /* clear the wider grips, or the duration sits under one */
  .mb-h{width:22px;height:22px;inset-inline-end:-11px;bottom:-11px}
  /* ✂ Erase: the picture has to share a phone screen with four tool buttons, a slider and Apply, and it
     is the one control here you cannot use if it is scrolled off — you are drawing on it. dvh, not vh:
     on iOS Safari 46vh is measured against the tall layout viewport, so with the URL bar showing the
     real estimate is short by the bar's height and Apply lands under the fold. */
  .mb-er-wrap>img,.mb-er-wrap>video{max-height:38dvh}
  .mb-er-tools .btn{flex:1 1 46%;min-width:0;padding-top:10px;padding-bottom:10px}
}
/* A real phone (360–430px). Emoji + word + two arrows + Render is 58px more than the row has, so the three
   labelled buttons drop their emoji and keep the WORD — Undo staying on screen is worth more than a
   pictogram, and hiding the label instead would leave "🅣" and "➕" to be guessed at. */
@media(max-width:480px){
  .mb-e{display:none}
  /* Project is the fourth labelled button on that row and the one that can afford to lose its word:
     Media and Text are the two things you came here to do, and a folder is legible without reading.
     Dropping the WORD here rather than the icon is the opposite of the rule above, on purpose. */
  #mb-project .mb-lbl{display:none}
  #mb-project svg.ic{margin-inline-end:0}
}
/* (The Meme Builder's Blossom grid used to live here. It now opens the shared blossomPicker — folders
   and all — so .mb-picker/.mb-pick are gone rather than left as a second, folderless look.) */
.mb-projrow{text-align:start}
/* A centred caption spans the canvas and centres its own text — matching drawtext's (w-text_w)/2, so the
   preview and the exported frame put the words in the same place. Not draggable horizontally by design. */
/* Centre WITHOUT stretching the box: a full-width box made the caption's invisible hit area span the
   whole canvas and parked the resize dot at the far right. Hug the text and shift it by half its own
   width instead — same visual centre, but the box (and its handle) stay on the words. */
.mb-text.centred{width:max-content;max-width:none;text-align:center;
  transform:translate(-50%,-0.28em)}   /* the -0.28em is the same pair of corrections — see .mb-text */

/* ===== 🧾 Raw event viewer (post ☰ → Raw event) =========================================
   A label/value grid of the decoded header, then the pretty-printed event. The <pre> WRAPS
   (pre-wrap + break-word) rather than scrolling sideways: an event is mostly 64- and 128-char
   hex and a base64 image URL, so a non-wrapping block is a horizontal scrollbar on every post
   and unreadable on a phone. Colours are theme tokens, so it reads in the light themes too. */
.rawev-meta{display:grid;grid-template-columns:auto minmax(0,1fr);gap:4px 12px;align-items:baseline;
  margin:10px 0 12px;font-size:13px}
.rawev-meta>span{color:var(--muted);text-transform:uppercase;font-size:11px;letter-spacing:.6px}
.rawev-meta>b{min-width:0;font-weight:600;color:var(--text);overflow-wrap:anywhere}
.rawev-meta>b>i{font-style:normal;font-weight:400;color:var(--muted)}
.rawev-mono{font-family:var(--mono);font-size:12px;font-weight:400}
.rawev-json{max-height:52vh;overflow:auto;overscroll-behavior:contain;margin:0;padding:12px;
  background:var(--panel2);border:1px solid var(--line);border-radius:var(--r);
  font:12px/1.55 ui-monospace,SFMono-Regular,Menlo,monospace;color:var(--text);
  white-space:pre-wrap;overflow-wrap:anywhere;tab-size:2}
/* Four tokens, four different HUES — --neon and --cyan are the same colour on the flagship theme, so
   keys and strings in those two were indistinguishable. Every theme defines all of these, tuned to its
   own background, so this reads on the light ones too. */
.rawev-json .jk{color:var(--cyan)}                                  /* object key */
.rawev-json .js{color:var(--green)}                                 /* string value */
.rawev-json .jn{color:var(--amber);font-weight:600}                 /* number */
.rawev-json .jl{color:var(--neon2);font-weight:600}                 /* true / false / null */
.rawev-acts{justify-content:flex-end;gap:8px;margin-top:12px;flex-wrap:wrap}
.rawev-acts .btn{margin:0}
@media(max-width:820px){
  .rawev-meta{grid-template-columns:minmax(0,1fr);gap:0 0}
  .rawev-meta>span{margin-top:8px}
  .rawev-json{max-height:46vh;font-size:11px;padding:10px}
  .rawev-acts .btn{flex:1 1 auto}
}

/* ⌨️ Keyboard shortcut sheet (Alt+/). Two columns on a desktop modal, one on a phone — a keycap grid that
   wraps mid-row reads as noise, so the breakpoint is a hard switch rather than an auto-fit. */
.ks-grid{display:grid;grid-template-columns:1fr 1fr;gap:8px 18px;margin:12px 0 4px}
.ks-row{display:flex;align-items:center;gap:6px;min-width:0}
.ks-row kbd{display:inline-block;min-width:26px;padding:4px 8px;text-align:center;
  font:600 12px ui-monospace,SFMono-Regular,Menlo,monospace;color:var(--text);
  background:var(--panel2);border:1px solid var(--line);border-bottom-width:2px;border-radius:6px}
.ks-plus{color:var(--muted);font-size:11px}
.ks-lbl{margin-inline-start:6px;color:var(--muted);font-size:13px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.ks-foot{margin-top:10px}
@media(max-width:560px){ .ks-grid{grid-template-columns:1fr} }

/* The updater row in the RIGHT RAIL. The feed version's flush 3px rule collides with the rail's 2px
   gutter, so give it a little room and round the inner edge; everything else it inherits. */
.rightbar .upd-notif{padding-inline-start:10px;padding-inline-end:6px;border-radius:0 8px 8px 0}

/* Keyboard-selected post. An inset ring rather than an outline: .note is a full-bleed feed row with a
   bottom rule, so an outline would draw outside it and clash with the row above. */
.note.sel{background:rgba(var(--accent-rgb),.07);box-shadow:inset 3px 0 0 var(--neon)}
.ks-sec{margin:14px 0 2px;font-weight:700;font-size:13px;color:var(--text)}

/* Keyboard cursor inside the reaction grid / ☰ menu — matches each popover's own hover treatment, so
   arrowing through looks the same as mousing through. */
.emoji-pop button.kb{background:rgba(var(--accent-rgb),.18);transform:scale(1.18)}
.menu-pop button.kb{background:rgba(var(--accent-rgb),.16);color:var(--cyan)}

/* Keyboard-selected conversation. Distinct from .dm-peer.active (the one that is OPEN) — you arrow past
   rows without opening them, so the cursor and the open conversation have to be tellable apart. */
.dm-peer.sel{box-shadow:inset 3px 0 0 var(--neon);background:rgba(var(--accent-rgb),.07)}

/* Cursor on a thread post and on a notification row. .thread-node is a wrapper, so the mark goes on the
   card inside it; .thread-hl (the post you arrived at) keeps its own ring and the two can coexist. */
.thread-node.sel>.note{background:rgba(var(--accent-rgb),.07);box-shadow:inset 3px 0 0 var(--neon)}
.notif.sel{background:rgba(var(--accent-rgb),.07);box-shadow:inset 3px 0 0 var(--neon)}
/* AI Chat transcript rows. The bubble carries its own background, so mark the ROW with a rail rather than
   tinting it — a wash over an assistant bubble is invisible in the light themes. */
.ai-msg.sel{box-shadow:inset 3px 0 0 var(--neon);border-radius:8px}
/* The splash's starter cards are a GRID, so the cursor is an outline round the whole card rather than a
   left rail — a rail on a tile reads as a border glitch, not a selection. */
.aw-card.sel,.ai-welcome .ai-cmd.sel{outline:2px solid var(--neon);outline-offset:2px}
/* The detail views that are now navigable too: a chat room, Markets' tickers. */
.chat-msg.sel{box-shadow:inset 3px 0 0 var(--neon);border-radius:8px}
.mkts-card.sel,.bl-more.sel{outline:2px solid var(--neon);outline-offset:2px}
/* Cursor for the More / Files / Discover / Games sheets, which are arrow/vim navigable grids now. */
.more-item.kb{outline:2px solid var(--neon);outline-offset:2px}
/* A thread thumbnail is an <a> with no href — it only became reachable by being given a tabindex, so it
   needs a ring to show for it. */

/* Keyboard cursor in the Effects studio, matching how a picked chip already reads. */
.fxs-chip.kb{outline:2px solid var(--neon);outline-offset:1px}
/* The footer buttons are on the same arrow/vim walk as the chips, so they need the same cursor — without
   it, stepping down past the last chip looked like the cursor had simply vanished. */
.fxs-ft .btn.kb{outline:2px solid var(--neon);outline-offset:2px}
/* Same cursor for the Blossom picker's grid, which is now arrow/vim navigable. */
.file-card.kb{outline:2px solid var(--neon);outline-offset:2px}

/* Cursor on the other card views (Streams, Articles, Shopping, Communities, Pics). They are
   bordered cards rather than full-bleed rows, so a ring reads better here than the inset rule. */
.stream-card.sel,.article-card.sel,.mkt-card.sel,.community-card.sel,.channel-card.sel,.fc-card.sel,.pic-card.sel{
  outline:2px solid var(--neon);outline-offset:2px}
.news-card.sel{outline:2px solid var(--neon);outline-offset:2px}
/* Web Search results. Without this the cursor IS on the card and nothing shows it, which is
   indistinguishable from the keys not working at all. */
.ws-card.sel{outline:2px solid var(--neon);outline-offset:2px}
.file-card.sel{outline:2px solid var(--neon);outline-offset:2px}

/* Vim cursor in the side columns. They are dense lists, so a left rule reads better than a full ring. */
.sidebar .nav-item.sel,.rightbar .notif.sel{box-shadow:inset 3px 0 0 var(--neon);background:rgba(var(--accent-rgb),.10)}

/* Go Live: the per-streamer quality tier. Radio PILLS rather than a <select>, because this is a choice you
   want to see the current value of at a glance while checking everything else before going live — and on a
   phone a row of thumb-sized pills beats a dropdown you have to open to read. Wraps on narrow screens. */
.gl-q{margin:8px 0}
.gl-qrow{display:flex;gap:6px;flex-wrap:wrap;margin-top:6px}
.gl-qopt{display:inline-flex;align-items:center;gap:6px;padding:8px 12px;min-height:38px;
  border:1px solid var(--line);border-radius:999px;background:var(--panel2);cursor:pointer;font-size:13px}
.gl-qopt:has(input:checked){border-color:var(--neon);color:var(--cyan);background:rgba(var(--accent-rgb),.16)}
.gl-qopt input{accent-color:var(--neon)}
@media(max-width:820px){
  .gl-qopt{flex:1 1 auto;justify-content:center}   /* fill the row so every target is thumb-sized */
}

/* ---- Voice studio (AI Chat → Clone a voice) ------------------------------------------------
   One row per saved voice: the name is the SELECTOR (not a radio — the whole row target should be
   thumb-sized), a native <audio preload=none> so previewing costs nothing until tapped, and delete.
   Wraps on a phone rather than squeezing the player to a sliver. */
.vs-list{display:flex;flex-direction:column;gap:8px;margin:8px 0}
.vs-row{display:flex;align-items:center;gap:8px;flex-wrap:wrap}
.vs-pick{flex:0 0 auto;min-height:38px;padding:6px 12px;border-radius:9px;cursor:pointer;
  background:var(--panel2);border:1px solid var(--line);color:var(--text);font:inherit}
.vs-pick.on{border-color:var(--neon);box-shadow:0 0 0 1px var(--neon);color:var(--neon)}
.vs-prev{flex:1 1 180px;min-width:0;height:34px}
.vs-del{flex:0 0 auto}
/* .modal textarea forces min-height:220px (right for a post composer, wrong here) — it pushed the
   Speak button off a phone screen for a box that usually holds one sentence. */
#vs-text{min-height:84px}
@media(max-width:480px){
  /* The player takes its own line on a phone — sharing one with the name and the bin left all three
     too narrow to hit. */
  .vs-prev{flex:1 1 100%;order:3}
}
/* Takes accumulate under the composer, oldest first, so you can play two readings back to back
   instead of regenerating the first one (~45s of GPU) just to compare. */
.vs-takes{display:flex;flex-direction:column;gap:10px;margin-top:10px}
.vs-take{padding:10px 10px;border:1px solid var(--line);border-radius:11px;background:var(--panel2)}
.vs-take-hd{display:flex;align-items:center;justify-content:space-between;gap:8px;margin-bottom:4px}
.vs-take-hd b{color:var(--neon);font-size:13px}
.vs-said{font-size:13px;opacity:.85;margin-bottom:6px;overflow-wrap:anywhere}
.vs-take audio{width:100%;height:34px}
/* Keep / download on a take. A generated take is a blob: URL that dies with the modal — "keep" is what
   makes it outlive the session without spending the GPU again. */
/* Both actions are real tap targets, not 20px icons with 2px of padding. They sit at the top-right of
   a take, which is exactly where a thumb arrives holding the phone, so measuring them was worth it:
   they came out 21x19 and 24x24 before this. -6px margin keeps the header the same HEIGHT while the
   hit area grows, so the takes list doesn't get taller on every row. */
.vs-take-acts{display:flex;align-items:center;gap:2px;margin:-6px -4px -6px 0}
.vs-keep,.vs-dl,.vs-drop{display:flex;align-items:center;justify-content:center;min-width:36px;min-height:36px;
  background:none;border:0;padding:0;color:var(--muted);cursor:pointer;border-radius:8px}
.vs-keep:hover,.vs-dl:hover,.vs-drop:hover{color:var(--neon);background:rgba(var(--accent-rgb),.12)}
.vs-keep.on{color:var(--neon);cursor:default}
.vs-keep:disabled{opacity:.5}
/* "Speaking as X" — the studio's answer to "which voice will it use?". A border highlight alone is
   invisible when there is only ONE voice, because it is selected from the moment the sheet opens. */
.vs-active{margin:10px 0 2px;font-size:13px;color:var(--text)}
.vs-active b{color:var(--neon)}
/* The three ways to add a voice. flex:1 so they FILL the row as equal, properly-sized targets rather
   than three shrink-wrapped pills with dead space after them; they wrap to two lines on a narrow
   phone instead of squeezing the labels away. */
.vs-src{display:flex;gap:6px;margin:10px 0;flex-wrap:wrap}
.vs-src .btn{flex:1 1 30%;min-width:104px;min-height:38px;margin:0}

/* Budget: a plan's line items, mirrored into the Expenses list (see planItemRow in budget.js).
   They are DERIVED rows — paid state lives on the plan, so there is no checkbox here, only the
   plan's state echoed in a non-interactive slot that keeps the row aligned with real bills. */
/* Inert ONLY when the whole plan is flagged paid — the item's own state cannot change what is
   shown there, so a live control would appear to do nothing. Everywhere else this slot is a real
   button and inherits .bg-check's 38px touch target. */
.bg-checkless{ display:inline-flex; align-items:center; justify-content:center;
  opacity:.45; cursor:default; pointer-events:none; }
/* The tick on a plan card's own item rows. */
.bg-icheck{ cursor:pointer; background:transparent; border:none; color:var(--muted); font:inherit;
  font-size:15px; padding:2px 4px; min-width:30px; min-height:30px; display:inline-flex;
  align-items:center; justify-content:center; flex:none; }
.bg-item.done .bg-iname{ text-decoration:line-through; opacity:.7; }
.bg-planitem .bg-planflag{ cursor:pointer; text-decoration:underline dotted; }
.bg-planitem .bg-planflag:hover{ opacity:.85; }
/* .bg-name is a nowrap/ellipsis cell, so on a phone the LAST thing in it gets clipped first — which
   would be the plan label, the one part of the row that says where the item came from and the only
   way back to it. Truncate the item's own name instead and let the label keep its width. */
.bg-planitem .bg-name{ display:flex; align-items:center; gap:6px; }
.bg-planitem .bg-itxt{ min-width:0; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
/* flex:none so it is never the thing that gets squeezed, but capped so a long plan name cannot
   crowd the item name off a narrow phone — past the cap the LABEL truncates instead. */
.bg-planitem .bg-flag{ flex:none; margin-inline-start:0; max-width:45%; overflow:hidden;
  text-overflow:ellipsis; white-space:nowrap; }
/* text-decoration does not inherit into a flex container's children the way it does inline text. */
.bg-row.bg-planitem.done .bg-itxt{ text-decoration:line-through; }
/* A phone still needs a real touch target on the label. */
@media (max-width:820px){ .bg-planitem .bg-planflag{ padding:6px 8px; } }

/* ===== offline banner + offline-disabled controls =====================================================
   Anchored to the BOTTOM, not the top: a top bar would either overlap the topbar or force a body padding
   change, and both mean the whole shell reflows every time the radio drops — the exact churn the timeline
   layout work exists to prevent. Bottom-centred it is a pure overlay, so showing/hiding it moves nothing.
   Above .mobilenav (z-index 40) so it is never hidden behind the nav, and lifted clear of it on phones. */
.offline-bar{ position:fixed; inset-inline-start:50%; transform:translateX(-50%);
  bottom:calc(14px + env(safe-area-inset-bottom)); z-index:60;
  display:flex; align-items:center; gap:8px; max-width:min(92vw,460px);
  padding:8px 14px; border-radius:999px; border:1px solid var(--amber);
  background:var(--panel2); color:var(--text); font-size:13px; font-weight:600; line-height:1.3;
  /* Hardcoded dark rgba, not a token: the light themes ship a near-white --panel2 and a token-derived
     shadow disappears on them, leaving the pill floating with no edge over a pale feed. */
  box-shadow:0 6px 24px rgba(0,0,0,.45); }
.offline-bar .ic{ width:16px; height:16px; flex:none; color:var(--amber); }
/* .mobilenav measures 62px (a 48px item + 6px/6px padding + a 1px top border) BEFORE its own
   safe-area padding, which is why the inset is added on both sides of this comparison rather than
   just here. 74px leaves ~12px of real clearance; the first attempt used 64px, which cleared the nav
   by two pixels and would have become an overlap on the first device whose font metrics ran taller. */
@media(max-width:820px){ .offline-bar{ bottom:calc(74px + env(safe-area-inset-bottom)); } }
@media(prefers-reduced-motion:no-preference){
  .offline-bar{ animation:offline-in .18s ease-out; }
  @keyframes offline-in{ from{ opacity:0; transform:translate(-50%,8px); } to{ opacity:1; transform:translate(-50%,0); } }
}
/* Controls that genuinely CANNOT work without the network — a Blossom upload, a server-side AI call, the
   background renderer, the scheduler. Posting is deliberately NOT in this set: an offline post is queued
   in the Outbox, so its button changes label instead of going dead. Dim + block pointer events rather
   than setting `disabled`, so no handler has to be taught about connectivity and nothing gets stuck
   disabled if the state is ever missed. */
body.is-offline .needs-net{ opacity:.45; pointer-events:none; filter:saturate(.4); }
/* A queued post is marked in the timeline itself, next to its timestamp — the same place the pin badge
   lives. The post is real and local; only its delivery is pending, and this is the only thing that says so. */
.note .pending-badge{ display:inline-flex; align-items:center; margin-inline-start:6px; padding:1px 7px;
  border-radius:999px; border:1px solid var(--amber); color:var(--amber);
  font-size:11px; font-weight:700; letter-spacing:.4px; text-transform:uppercase; cursor:pointer; }

/* ===== Notes =========================================================================
   Three panes on a desktop (folders | list | editor) collapsing to ONE on a phone, where
   .nt-open decides whether you are looking at the list or at the note. Two panes side by side
   at 700px is what makes a notes app unusable on a phone: neither half is wide enough to read.

   Heights are dvh, never vh: with the mobile browser's URL bar showing, 100vh is TALLER than the
   visible viewport, which pushes the editor's toolbar under the bottom nav (see the mobile rules
   in check_client_mobile.py). The editor also reserves the fixed .mobilenav's ~62px plus the safe
   area, so the last line of a note is never sitting behind it. */
/* `/ var(--zf)` for the same reason .app does it: the desktop tiers apply body{zoom}, and viewport
   units are NOT scaled by zoom — so a plain calc(100dvh - 60px) rendered the whole pane at 67% of
   the height it needed and left a third of the screen as empty background. */
.nt-wrap,.pv-wrap{display:grid;grid-template-columns:210px 300px 1fr;gap:0;
  height:calc((100dvh - 60px) / var(--zf, 1));
  min-height:0;border-top:1px solid var(--line)}
.nt-side,.pv-side{border-inline-end:1px solid var(--line);padding:12px 10px;overflow-y:auto;min-height:0;
  display:flex;flex-direction:column;gap:9px}
/* The drawer's backdrop exists only below 820px. It must be display:none here and not merely
   invisible: .nt-wrap is a grid, so an unstyled div inside it is a GRID ITEM and would take the
   sidebar's column on every desktop layout. */
.nt-scrim,.pv-scrim{display:none}

/* Search reads as a search field: the icon sits INSIDE the input rather than a bare box that could
   be anything. */
.nt-searchwrap,.pv-searchwrap{position:relative;display:flex;align-items:center}
.nt-searchic,.pv-searchic{position:absolute;inset-inline-start:9px;width:14px;height:14px;opacity:.45;pointer-events:none}
.nt-search,.pv-search{width:100%;padding-inline-start:29px;border-radius:9px}
.nt-search::-webkit-search-cancel-button{filter:grayscale(1);opacity:.5}

.nt-side .nt-new,.pv-side .pv-new{width:100%;justify-content:center;gap:7px}

/* Section headers, so the sidebar has structure instead of being one undifferentiated column of
   controls. Small, quiet, and they carry their own action button — "new folder" belongs beside the
   word Folders, not in a row of equal-weight buttons competing with "New note". */
.nt-sec,.pv-sec{display:flex;align-items:center;justify-content:space-between;gap:6px;
  margin:6px 0 -2px;padding-inline-start:2px;
  font-size:10.5px;font-weight:700;letter-spacing:.09em;text-transform:uppercase;opacity:.5}
.nt-icobtn{flex:none;width:24px;height:24px;display:inline-flex;align-items:center;justify-content:center;
  border-radius:7px;border:none;background:none;color:inherit;cursor:pointer;opacity:.7}
.nt-icobtn svg.ic{width:14px;height:14px}
.nt-icobtn:hover{opacity:1;background:rgba(var(--accent-rgb),.16);color:var(--neon)}

/* Import / Backup are things you do twice a year. They belong at the bottom, quiet, out of the way
   of the two actions used constantly — not as full-size buttons competing for attention. */
.nt-side-foot,.pv-side-foot{margin-top:auto;padding-top:12px;margin-bottom:2px;border-top:1px solid var(--line);
  display:flex;flex-direction:column;gap:7px}
/* TWO across, never three. Each is flex:1 with a nowrap label, so a third squeezes all of them
   below their own text and the last one runs off the edge of the sidebar — which is what "the
   Export button is cut off" was. The row is capped at two and anything else gets its own full-width
   row above it, which is also the honest grouping: Import/Export are a pair, Health is a report. */
.nt-foot-actions,.pv-foot-actions{display:flex;gap:6px}
.nt-foot-actions>*,.pv-foot-actions>*{min-width:0}
/* Visible enough to FIND. These were toned down to keep them out of the way of the two actions
   used constantly, and overshot: a borderless 65%-opacity label at the bottom of a column reads as
   a caption, not a button, and Import is the first thing a new user needs. Bordered, accent-tinted
   icons, and full opacity — quiet next to the primary button, but unmistakably clickable. */
.nt-link,.pv-link{flex:1 1 0;display:inline-flex;align-items:center;justify-content:center;gap:6px;
  padding:9px 8px;border-radius:9px;border:1px solid var(--line);
  background:rgba(var(--accent-rgb),.06);color:inherit;min-height:36px;
  cursor:pointer;font-size:12.5px;font-weight:600;white-space:nowrap;overflow:hidden;transition:.12s}
.nt-link svg.ic,.pv-link svg.ic{width:14px;height:14px;flex:none;color:var(--neon)}
.nt-link:hover,.pv-link:hover{border-color:var(--neon);background:rgba(var(--accent-rgb),.16);color:var(--neon)}
.nt-pending,.pv-pending{color:var(--amber);font-size:11px}
/* The one pointer to the browser add-on inside Passwords. It lives in the SCROLLING list, not in
   .pv-list-head — that header is position:sticky, so a strip there is charged to every visit and to
   every phone viewport forever. Here it scrolls away on its own, and the ✕ retires it for good.
   `flex-wrap` plus a min-width on the text is what keeps it from becoming a three-line block at
   360px: the pitch takes the first row and the two controls sit together on the second. */
.pv-exttip{display:flex;align-items:center;gap:8px;flex-wrap:wrap;
  margin:10px 14px 2px;padding:9px 11px;border:1px solid var(--line);border-radius:10px;
  background:rgba(var(--accent-rgb),.07);font-size:12.5px;color:var(--text)}
.pv-exttip svg.ic{width:15px;height:15px;flex:none;color:var(--neon)}
.pv-exttip-t{flex:1 1 190px;min-width:0;line-height:1.4}
.pv-exttip-get{flex:none;padding:4px 11px;border:1px solid var(--neon);border-radius:999px;
  color:var(--neon);text-decoration:none;font-size:12px;white-space:nowrap}
.pv-exttip-get:hover{background:rgba(var(--accent-rgb),.18)}
.pv-exttip-x{flex:none;width:24px;height:24px;line-height:1;border:0;border-radius:6px;cursor:pointer;
  background:transparent;color:var(--muted);font-size:13px}
.pv-exttip-x:hover{background:rgba(var(--accent-rgb),.14);color:var(--text)}
/* Destructive, and on its OWN row: three across a 190px column truncates every label, which is the
   mistake this sidebar already made once. Muted until hover — it sits one tap from buttons people
   press routinely, so it must not invite the reach. */
.nt-link.nt-danger{width:100%;border-color:transparent;background:none;font-weight:500;
  font-size:12px;opacity:.6}
.nt-link.nt-danger svg.ic{color:#ff6b8a}
.nt-link.nt-danger:hover{opacity:1;color:#ff6b8a;border-color:#ff3366;background:rgba(255,51,102,.12)}

.nt-folders,.pv-folders{display:flex;flex-direction:column;gap:1px}
.nt-folder,.pv-folder{display:flex;align-items:center;justify-content:space-between;gap:8px;width:100%;
  padding:6px 8px;border-radius:8px;border:1px solid transparent;background:none;color:inherit;
  cursor:pointer;text-align:start;font-size:13.5px}
.nt-folder span,.pv-folder span{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.nt-folder i,.pv-folder i{font-style:normal;font-size:10.5px;opacity:.45;flex:none;font-variant-numeric:tabular-nums}
.nt-folder:hover,.pv-folder:hover{background:rgba(var(--accent-rgb),.10)}
/* Selected reads as a filled row, not an outline: a 1px border on a 28px row is a rectangle drawn
   around text, which is what made the old list look like a form. */
.nt-folder.active,.pv-folder.active{background:rgba(var(--accent-rgb),.18);color:var(--neon);font-weight:600}
.nt-folder.active i{opacity:.8}

.nt-tags,.pv-tags{display:flex;flex-wrap:wrap;gap:4px}
.nt-tag,.pv-tag{padding:3px 9px;border-radius:999px;border:1px solid var(--line);background:none;color:inherit;
  cursor:pointer;font-size:11.5px;opacity:.8}
.nt-tag::before,.pv-tag::before{content:"#";opacity:.5;margin-inline-end:1px}
.nt-tag:hover,.pv-tag:hover{opacity:1;border-color:var(--neon)}
.nt-tag.active,.pv-tag.active{border-color:var(--neon);color:var(--neon);background:rgba(var(--accent-rgb),.14);opacity:1}
.nt-tag i,.pv-tag i{font-style:normal;opacity:.5;font-size:10px}

.nt-list,.pv-list{border-inline-end:1px solid var(--line);overflow-y:auto;min-height:0;display:flex;flex-direction:column}
/* The list's own toolbar: what you are looking at, how many, and the search that filters it. Search
   moved here out of the sidebar because it belongs to the pane it acts on — and because on a phone
   the sidebar is a drawer, and a search you have to open a drawer to reach is a search nobody uses. */
.nt-list-head,.pv-list-head{display:flex;flex-direction:column;gap:8px;
  padding:12px 14px;border-bottom:1px solid var(--line);position:sticky;top:0;background:var(--bg);z-index:1}
.nt-list-top,.pv-list-top{display:flex;align-items:center;justify-content:space-between;gap:8px}
.nt-list-head b,.pv-list-head b{font-size:15px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.nt-list-title,.pv-list-title{flex:1 1 auto;min-width:0}
/* The folder handle and the compact New button are the phone layout; above 820px the sidebar is a
   real column and both would be a second way to do what is already on screen. */
.nt-fbtn,.nt-new-m,.pv-fbtn,.pv-new-m{display:none}
.nt-count,.pv-count{flex:none;font-size:11px;opacity:.5;font-variant-numeric:tabular-nums;
  padding:2px 8px;border-radius:999px;background:rgba(var(--accent-rgb),.12)}
.nt-item{display:flex;flex-direction:column;gap:2px;width:100%;padding:11px 14px;border:none;
  border-bottom:1px solid var(--line);background:none;color:inherit;cursor:pointer;text-align:start;
  transition:background .12s}
.nt-item b{font-size:14px;font-weight:600;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.nt-item .nt-snip{opacity:.62;font-size:12.5px}
.nt-item .nt-meta{opacity:.45;font-size:11px}
.nt-snip,.nt-meta{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.nt-item:hover{background:rgba(var(--accent-rgb),.08)}
.nt-item.active{background:rgba(var(--accent-rgb),.15);box-shadow:inset 3px 0 0 var(--neon)}
.nt-item.active b{color:var(--neon)}

.nt-editor,.pv-editor{display:flex;flex-direction:column;min-height:0;overflow:hidden}
.nt-editor:empty::after{content:"Pick a note, or write a new one.";display:block;padding:28px;opacity:.55;font-size:14px}
.nt-ed-head,.pv-ed-head{display:flex;align-items:center;gap:6px;padding:10px 12px;border-bottom:1px solid var(--line)}
/* The title is the heading of the note, so it looks like one — a boxed <input> at the top of a
   document reads as a form field you are obliged to fill in. */
.nt-ed-head .nt-title{flex:1 1 auto;min-width:0;font-size:19px;font-weight:700;letter-spacing:-.01em;
  border:none;background:none;padding:2px 4px;border-radius:7px}
.nt-ed-head .nt-title:focus{background:rgba(var(--accent-rgb),.09);outline:none}
.nt-state{flex:none;min-width:60px;text-align:end}
/* !important, deliberately: the global icon-button utility at the top of this file
   (`button:not(.tl-fab):has(> svg.ic.b-ic:only-child)`) matches this button and, thanks to the
   :has(), carries higher specificity than any sane class selector here. Without the override the
   phone-only back chevron rendered on DESKTOP too, next to the note title, where there is a whole
   list pane beside it and nothing to go back to. */
.nt-ed-head .nt-back{display:none !important;flex:none;background:none;border:none;color:inherit;
  cursor:pointer;padding:4px;border-radius:7px}
.nt-ed-head .nt-back:hover{background:rgba(var(--accent-rgb),.14)}
/* The editor's icon buttons are sized HERE, together, rather than each borrowing whatever .btn
   modifier happened to be typed. Preview was a plain .btn and Delete a .btn.small, so they were
   visibly different heights sitting side by side. Square, identical, and big enough to hit. */
.nt-ed-head .nt-ico{flex:none;width:34px;height:34px;padding:0;display:inline-flex;
  align-items:center;justify-content:center;border-radius:9px;
  background:none;border:1px solid var(--line);color:inherit;opacity:.75}
.nt-ed-head .nt-ico:hover{opacity:1;background:rgba(var(--accent-rgb),.12);border-color:var(--neon)}
/* Delete is destructive but rare: neutral until you reach for it. A permanently red gradient button
   was the single loudest element on the screen, which is backwards for the action you least often
   want. */
.nt-ed-head .nt-del{background:none;border:1px solid var(--line);color:inherit}
.nt-ed-head .nt-del:hover{background:rgba(255,51,102,.16);border-color:#ff3366;color:#ff6b8a;opacity:1}
.nt-ed-head .nt-ico svg.ic{width:17px;height:17px}
.nt-ed-bar{display:flex;gap:6px;padding:7px 12px;border-bottom:1px solid var(--line);flex-wrap:wrap;
  align-items:center}
/* Metadata, not a form: quiet until touched, and the tag field takes what it needs rather than
   stretching the full width of a 1200px pane. */
.nt-ed-bar .input{font-size:12.5px;padding:5px 9px;border-radius:8px;background:none;
  border:1px solid transparent;opacity:.8}
.nt-ed-bar .input:hover{border-color:var(--line)}
.nt-ed-bar .input:focus{opacity:1;border-color:var(--neon);background:rgba(var(--accent-rgb),.07)}
.nt-ed-bar .nt-folder-sel{flex:0 0 auto;width:auto;min-width:110px;max-width:220px}
.nt-ed-bar .nt-tagin{flex:1 1 180px;max-width:340px}
.nt-ed-bar .nt-attach{margin-inline-start:auto;flex:none;background:none;border:1px solid var(--line);
  opacity:.7;padding:5px 10px;border-radius:8px}
.nt-ed-bar .nt-attach:hover{opacity:1;border-color:var(--neon)}

.nt-body{flex:1 1 auto;min-height:0;width:100%;resize:none;border:none;outline:none;background:none;
  color:inherit;padding:16px 18px;font:14px/1.7 var(--mono,ui-monospace,SFMono-Regular,Menlo,monospace)}
/* Reading measure: prose set across a 1200px pane is unreadable, so the rendered view is capped and
   centred like an article. */
.nt-render{flex:1 1 auto;min-height:0;overflow-y:auto;padding:18px 22px;line-height:1.65}
.nt-render>*{max-width:68ch}
.nt-render h1,.nt-render h2,.nt-render h3{margin:1.1em 0 .4em;line-height:1.25}
.nt-render img{max-width:100%;height:auto}
/* The attachment strip is a FOOTER, and has to be bounded like one. It is a wrapping flex row whose
   automatic minimum size is its content, so a note with thirty attachments claimed fifteen rows of
   thumbnails and squeezed the note itself — which has min-height:0 and therefore loses every such
   argument — down to 36px on a phone. The text of the note is the point of the screen; the files
   get a fifth of it and scroll. */
.nt-res{border-top:1px solid var(--line);padding:8px 10px;display:flex;flex-wrap:wrap;gap:6px;align-items:center;
  flex:0 0 auto;min-height:0;max-height:22vh;overflow-y:auto}
.nt-res:empty{display:none}
.nt-res-head{flex:1 0 100%}
.nt-res-item{padding:4px 9px;border-radius:8px;border:1px solid var(--line);background:none;color:inherit;
  cursor:pointer;font-size:13px;max-width:100%;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.nt-res-head i{font-style:normal;opacity:.6;margin-inline-start:4px}
.nt-res-more{align-self:center}   /* the rest is .mini, the app's small-pill button */

.nt-imp-pick{display:flex;gap:8px;flex-wrap:wrap;margin:10px 0}
.nt-imp-pick .btn{cursor:pointer}
.nt-imp-sum{display:flex;flex-direction:column;gap:4px;margin:10px 0}
.nt-warn{color:var(--amber)}
.nt-imp-run{display:flex;flex-direction:column;gap:6px;margin:10px 0}
.nt-imp-bar{height:6px;border-radius:999px;background:rgba(var(--accent-rgb),.18);overflow:hidden;margin:8px 0}
.nt-imp-bar i{display:block;height:100%;background:var(--neon);transition:width .2s}
.nt-imp-done{display:flex;flex-direction:column;gap:5px;margin-top:8px}

@media(max-width:1000px){ .nt-wrap{grid-template-columns:170px 260px 1fr} }
@media(max-width:820px){
  /* One pane at a time. The list IS the page until a note is opened, and then the editor is —
     .nt-open swaps them. Both panes are grid-stacked in the same cell rather than toggled with
     display:none on a parent, so opening a note doesn't reflow the sidebar behind it. */
  .nt-wrap,.pv-wrap{grid-template-columns:1fr;grid-template-rows:1fr;
    height:calc(100dvh - 56px - 62px - env(safe-area-inset-bottom))}
  /* THE FOLDER TREE IS A DRAWER, not a pane. Stacked above the list and capped at 40vh, it took
     half of a phone screen — permanently, whether or not you were doing anything with folders — and
     left the notes themselves a ~300px window. Now it is off-canvas until the folder name in the
     toolbar is tapped, and the list gets the whole screen. */
  .nt-side,.pv-side{position:fixed;z-index:60;inset-inline-start:0;top:0;bottom:0;width:min(84vw,320px);max-height:none;
    padding-top:calc(12px + env(safe-area-inset-top));padding-bottom:calc(12px + env(safe-area-inset-bottom));
    background:var(--bg);border-inline-end:1px solid var(--line);border-bottom:none;
    transform:translateX(-101%);transition:transform .2s ease;box-shadow:2px 0 18px rgba(0,0,0,.35)}
  .nt-wrap.nt-drawer .nt-side,.pv-wrap.pv-drawer .pv-side{transform:none}
  /* Derived from the drawer's own state, so there is no second thing to keep in sync. Same fill as
     the app's other backdrop (.pop-backdrop) — an off-palette black reads wrong on the light themes. */
  .nt-wrap.nt-drawer .nt-scrim,.pv-wrap.pv-drawer .pv-scrim{display:block;position:fixed;inset:0;z-index:59;background:rgba(4,2,12,.5)}
  /* The drawer belongs to the LIST. Opening a note replaces the list with the editor, so anything
     still hanging over it is over the wrong screen. (.nt-side is hidden by the .nt-open rule below.) */
  .nt-wrap.nt-open.nt-drawer .nt-scrim,.pv-wrap.pv-open.pv-drawer .pv-scrim{display:none}
  .nt-fbtn,.pv-fbtn{display:inline-flex;align-items:center;gap:6px;min-width:0;flex:1 1 auto;
    padding:7px 9px;border-radius:9px;border:1px solid var(--line);background:rgba(var(--accent-rgb),.06);
    color:inherit;cursor:pointer;text-align:start}
  .nt-fbtn b,.pv-fbtn b{font-size:15px;min-width:0}
  .nt-fbtn svg.ic,.pv-fbtn svg.ic{width:15px;height:15px;flex:none;opacity:.7}
  .nt-fbtn .nt-fbtn-c,.pv-fbtn .pv-fbtn-c{margin-inline-start:auto;width:13px;height:13px}
  .nt-wrap.nt-drawer .nt-fbtn .nt-fbtn-c,.pv-wrap.pv-drawer .pv-fbtn .pv-fbtn-c{transform:rotate(180deg)}
  .nt-new-m,.pv-new-m{display:inline-flex;align-items:center;justify-content:center;flex:none;
    width:40px;height:40px;padding:0;border-radius:10px}
  .nt-new-m svg.ic,.pv-new-m svg.ic{width:17px;height:17px;margin:0}
  .nt-list-title,.pv-list-title{display:none}         /* the folder button carries the name on a phone */
  .nt-list,.pv-list{border-inline-end:none}
  .nt-editor,.pv-editor{display:none}
  .nt-wrap.nt-open .nt-side,.nt-wrap.nt-open .nt-list,
  .pv-wrap.pv-open .pv-side,.pv-wrap.pv-open .pv-list{display:none}
  .nt-wrap.nt-open,.pv-wrap.pv-open{grid-template-rows:1fr}
  .nt-wrap.nt-open .nt-editor,.pv-wrap.pv-open .pv-editor{display:flex}
  .nt-ed-head .nt-back,.pv-ed-head .pv-back{display:inline-flex !important;align-items:center}
  /* 16px minimum on every text input: iOS Safari ZOOMS the whole page on focus for anything
     smaller, and the page never zooms back out. */
  /* 16px minimum, and specific enough to BEAT `.nt-ed-bar .input` — a two-class desktop rule
     outranks a one-class rule even inside this media query, which is how the toolbar quietly
     reintroduced the iOS zoom trap. */
  .nt-title,.nt-body,.nt-search,.nt-ed-bar .input,.nt-ed-bar .nt-tagin,.nt-ed-bar .nt-folder-sel,
  .pv-title,.pv-search,.pv-ed-body .input,.pv-ed-body textarea{font-size:16px}
  .nt-side-head .nt-new,.pv-side .pv-new{padding:11px 10px}
  .nt-side-actions .btn{padding:10px 12px;font-size:14px}
  /* Touch targets: a 7px-padded row is a ~30px tap target, under the 44px guideline. */
  .nt-folder,.pv-folder{padding:11px 10px}
  .nt-item,.pv-item{padding:12px}
  .nt-ed-head .nt-ico,.pv-ed-head .pv-ico{width:44px;height:44px}
}

/* Notes: folder TREE. Joplin nests notebooks and the import carries the full path across, so a flat
   list reads as unrelated top-level folders with slashes in their names. --d is the depth; the caret
   column is always present (even for leaves) so labels line up down a level instead of stepping in
   and out as branches open. */
.nt-frow{display:flex;align-items:center;gap:2px;padding-inline-start:calc(var(--d, 0) * 13px)}
.nt-frow .nt-folder{flex:1 1 auto;min-width:0}
.nt-fcaret{flex:none;width:20px;height:20px;display:inline-flex;align-items:center;justify-content:center;
  background:none;border:none;color:inherit;cursor:pointer;opacity:.6;padding:0}
.nt-fcaret svg.ic{width:14px;height:14px;transition:transform .15s}
.nt-fcaret.open svg.ic{transform:rotate(90deg)}
.nt-fcaret:hover{opacity:1}

/* Attachment thumbnails. There is no server-side thumbnail for an encrypted blob and there cannot
   be — the server can't read the picture — so these are decrypted in the client and shown at size. */
.nt-res-thumb{display:flex;flex-direction:column;gap:3px;width:96px;padding:4px;border-radius:9px;
  border:1px solid var(--line);background:none;color:inherit;cursor:pointer}
.nt-res-thumb img{width:100%;height:64px;object-fit:cover;border-radius:6px;background:rgba(128,128,128,.15)}
.nt-res-thumb span{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-size:11px}
.nt-res-thumb:hover{border-color:var(--neon)}
/* A picture that hasn't been decrypted YET. It needs a real box for two reasons: an <img> with no
   src is 0px tall, which no IntersectionObserver will ever report as visible (so it would never
   load), and a note whose images pop in at their natural size shoves the paragraph you are reading
   down the page. Same reservation trick as the timeline. */
.nt-render img.nt-img-lazy,.nt-render img.nt-img-fail{display:block;width:100%;max-width:340px;min-height:150px;
  border-radius:8px;background:rgba(128,128,128,.12);border:1px solid var(--line)}
/* The shimmer is the app's own .skel gradient, reused rather than re-invented — which also means it
   is covered by the reduced-motion rule that turns .skel's animation off. A note holds dozens of
   these at once and the ones below the fold shimmer for as long as it is open, so an animation the
   user asked not to see would have run indefinitely on exactly the screen it matters least. */
.nt-render img.nt-img-lazy{cursor:default;background-size:200% 100%;animation:shimmer 1.3s linear infinite;
  background-image:linear-gradient(100deg,var(--panel) 38%,var(--panel2) 50%,var(--panel) 62%)}
@media(prefers-reduced-motion:reduce){ .nt-render img.nt-img-lazy{animation:none} }
/* Failed: still an image, still in place, and asking to be tried again — never a tombstone. */
.nt-render img.nt-img-fail{cursor:pointer;border-style:dashed;border-color:var(--amber);
  font-size:12.5px;padding:8px;object-fit:contain}
.nt-res-thumb img.nt-img-fail{opacity:.5}
/* A pcres: attachment LINK: resolved on click, so it has no href and needs the styling of one. */
.nt-render .nt-res-link{color:var(--neon);cursor:pointer;text-decoration:underline}
/* An image inside a rendered note: never wider than the pane, and clickable to open full size. */
.nt-render img{max-width:100%;height:auto;border-radius:8px;cursor:zoom-in}

@media(max-width:820px){
  .nt-frow{padding-inline-start:calc(var(--d, 0) * 11px)}
  .nt-fcaret{width:32px;height:32px}      /* thumb-sized, or a tree is unusable on a phone */
  .nt-res-thumb{width:calc(50% - 5px)}
  .nt-res-thumb img{height:84px}
}

/* ===== Passwords (vault.js) =====
   The three-pane shell, the phone drawer and the tap targets are the NOTES rules above, with the
   .pv- selectors folded into them — one set of declarations, so the two screens cannot drift apart
   on the layout that was measured and fixed once. Only what is genuinely different lives here. */
.pv-item{display:flex;align-items:center;gap:10px;width:100%;padding:10px 14px;border:none;
  border-bottom:1px solid var(--line);background:none;color:inherit;cursor:pointer;text-align:start}
.pv-item:hover{background:rgba(var(--accent-rgb),.08)}
.pv-item.active{background:rgba(var(--accent-rgb),.15);box-shadow:inset 3px 0 0 var(--neon)}
.pv-item-t{display:flex;flex-direction:column;gap:1px;min-width:0;flex:1 1 auto}
.pv-item-t b{font-size:14px;font-weight:600;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.pv-sub{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-size:12.5px}
/* A letter tile, NOT a favicon: fetching a site's icon is a third-party request that says "this
   person has an account here", which is the one thing this screen exists to keep to itself. The hue
   is derived from the hostname so a site keeps the same colour and stays recognisable at a glance. */
.pv-tile{flex:none;width:30px;height:30px;border-radius:8px;display:inline-flex;align-items:center;
  justify-content:center;font-style:normal;font-weight:700;font-size:13px;
  background:hsl(var(--tile) 60% 42% / .28);color:hsl(var(--tile) 70% 72%)}
.pv-badge{flex:none;font-size:9.5px;font-weight:700;letter-spacing:.05em;padding:2px 5px;border-radius:5px;
  background:rgba(var(--accent-rgb),.18);color:var(--neon)}

.pv-ed-body{flex:1 1 auto;min-height:0;overflow-y:auto;padding:14px 16px;display:flex;
  flex-direction:column;gap:12px}
.pv-fld{display:flex;flex-direction:column;gap:5px;font-size:11.5px;font-weight:700;
  letter-spacing:.07em;text-transform:uppercase;opacity:.62}
.pv-fld .input,.pv-fld textarea{text-transform:none;letter-spacing:normal;font-weight:400;
  opacity:1;font-size:14px;margin:0}
.pv-row{display:flex;gap:6px;align-items:center}
.pv-row .input{flex:1 1 auto;min-width:0}
.pv-row2{display:flex;gap:10px}
.pv-half{flex:1 1 0;min-width:0}
.pv-ed-head{display:flex;align-items:center;gap:6px;padding:10px 12px;border-bottom:1px solid var(--line)}
.pv-ed-head .pv-title{flex:1 1 auto;min-width:0;font-size:18px;font-weight:700;background:none;
  border:1px solid transparent;margin:0}
.pv-ed-head .pv-title:hover{border-color:var(--line)}
.pv-ed-head .pv-ico{width:36px;height:36px;display:inline-flex;align-items:center;justify-content:center;padding:0}
.pv-back{display:none;background:none;border:none;color:inherit;cursor:pointer;padding:6px}
/* The password itself is the one field worth setting in a monospace face: a shoulder-typed `l` vs
   `1` is the difference between logging in and locking yourself out. */
.pv-pass{font-family:var(--mono,ui-monospace,SFMono-Regular,Menlo,monospace);letter-spacing:.02em}
.pv-meter{display:block;height:4px;border-radius:999px;background:rgba(128,128,128,.2);overflow:hidden}
.pv-meter i{display:block;height:100%;width:0;transition:width .18s,background .18s}
.pv-meter i.weak{background:#ff5c7a} .pv-meter i.ok{background:var(--amber)} .pv-meter i.good{background:#3ddc84}
.pv-code{display:flex;align-items:center;gap:8px;min-height:18px}
.pv-otp{font-family:var(--mono,ui-monospace,monospace);font-size:19px;letter-spacing:.14em;color:var(--neon)}
.pv-otp-left{font-variant-numeric:tabular-nums;opacity:.6}
.pv-otp-left.low{color:var(--amber);opacity:1}
.pv-meta{padding-top:4px}
.pv-locked{display:flex;flex-direction:column;align-items:center;gap:10px;padding:40px 20px;text-align:center}
.pv-locked svg.ic{width:26px;height:26px;opacity:.5}

.pv-gen-out{display:flex;align-items:center;gap:8px;margin:10px 0 4px}
.pv-gen-out code{flex:1 1 auto;min-width:0;overflow-wrap:anywhere;padding:10px 12px;border-radius:10px;
  border:1px solid var(--line);background:rgba(var(--accent-rgb),.06);
  font-family:var(--mono,ui-monospace,monospace);font-size:15px;letter-spacing:.02em}
.pv-gen-bits{margin-bottom:6px}
.pv-gen-bits.pv-warn{color:var(--amber)}
.pv-gen-opts{display:flex;flex-wrap:wrap;gap:10px;margin:8px 0}
.pv-gen-opts label{display:inline-flex;align-items:center;gap:5px;font-size:13px}
.pv-h-row{display:flex;align-items:baseline;gap:8px;margin-top:12px}
.pv-h-row b{font-size:18px;color:var(--neon);font-variant-numeric:tabular-nums}
.pv-h-list{display:flex;flex-direction:column;gap:2px;margin:4px 0 0 2px;max-height:26vh;overflow-y:auto}
.pv-h-item{display:flex;gap:8px;align-items:baseline;padding:5px 8px;border-radius:7px;border:none;
  background:none;color:inherit;cursor:pointer;text-align:start;font-size:13px}
.pv-h-item:hover{background:rgba(var(--accent-rgb),.10)}
/* `.modal textarea` (0,1,1) forces min-height:220px — right for a post composer, wrong for a code
   box with rows="4", which it stretched to two thirds of a phone sheet and pushed Copy toward the
   fold. Same trick as .gen-prompt: qualify the element so it wins. */
.modal textarea#pv-code{min-height:0}
.pv-pair-code textarea{font-family:var(--mono,ui-monospace,monospace);font-size:11px;word-break:break-all}
/* The pairing code exists to be carried to another app, so Copy is the primary action of that step —
   a lone hairline .mini under a 4-row textarea read as an afterthought. Full width on a phone (where
   it is the button you actually press), sized to its label on a desktop. */
.pv-pair-copy{display:flex;margin-top:8px}
.pv-pair-copy .btn{flex:1 1 auto;min-height:40px}
@media(min-width:561px){ .pv-pair-copy .btn{flex:0 0 auto} }
.pv-dim{opacity:.55}
.pv-imp-pick{margin:10px 0}
.pv-imp-done{display:flex;flex-direction:column;gap:5px;margin-top:8px}

@media(max-width:820px){
  .pv-row{flex-wrap:wrap}
  .pv-row .mini{flex:0 0 auto;min-height:34px}   /* thumb-sized: these are the buttons you actually press */
  .pv-row2{flex-direction:column;gap:12px}
  /* 16px floor, and it has to live HERE — at the end of the file, after `.pv-fld .input{font-size:14px}`.
     The same declaration in the shared mobile block above has identical specificity and loses on
     source order, so every field in the entry sat at 14px and iOS zoomed the page on focus and never
     zoomed back. check_vault_mobile.py caught it; the eye would not have. */
  .pv-ed-body .input,.pv-ed-body textarea,.pv-title,.pv-search{font-size:16px}
}

/* data saver: an encrypted attachment is offered, not fetched — decrypting it is the tap */
.encatt-ds{display:inline-block;cursor:pointer;border:1px dashed var(--line);border-radius:10px;
  padding:8px 12px;min-height:38px;line-height:22px;color:var(--muted)}
.encatt-ds:hover{border-color:var(--cyan);color:var(--text)}

/* ===== PosterChan OS — the windowed desktop (see static/js/client/os.js) =====================
   Dark, Win11-shaped: a taskbar with a centred start button, rounded windows with a title bar, and
   desktop icons for everything on the left navbar. Built from the theme's own tokens rather than
   hardcoded greys, so it follows whichever of the seven themes is active instead of being a second
   look bolted on. Desktop only — os.js refuses below 1024px. */
body.os-on .app{filter:none}
body.os-on > .app{pointer-events:none}          /* the classic UI is still there, just not clickable */
body.os-on #os-root{pointer-events:auto}
/* A desktop timeline needs a desktop scrollbar.  The phone intentionally spends no width on one,
   but hiding it globally removed both position feedback and the drag fallback from Social when a
   wheel/trackpad driver failed to deliver scrolling.  Scope this to the live shared feed inside an
   OS window so compact/mobile panes and deliberately self-scrolling apps keep their own layout. */
body.os-on .osw-body > .feed{scrollbar-width:thin;scrollbar-color:var(--line) transparent}
body.os-on .osw-body > .feed::-webkit-scrollbar{width:10px;height:10px}
body.os-on .osw-body > .feed::-webkit-scrollbar-track{background:transparent}
body.os-on .osw-body > .feed::-webkit-scrollbar-thumb{background:var(--line);border-radius:8px;border:2px solid transparent;background-clip:padding-box}
body.os-on .osw-body > .feed::-webkit-scrollbar-thumb:hover{background:var(--muted);background-clip:padding-box}
.os-root{position:fixed;inset:0;z-index:300;display:flex;flex-direction:column;
  background:radial-gradient(1200px 800px at 20% 0%, rgba(var(--accent-rgb),.16), transparent 60%),
             linear-gradient(160deg, var(--bg), var(--bg2, var(--bg)));}
.os-desk{position:relative;flex:1;min-height:0;overflow:hidden}
/* Desktop wallpaper — a centred emblem, not a full-bleed photo. Full-bleed fights every window and
   every icon label for contrast; an emblem behind a vignette reads as a desktop and stays out of the
   way. Sized in vmin so it is the same relative size in both orientations, capped so it does not
   dominate a big monitor, and floored so it does not vanish on a small tablet. z-index:0 keeps it
   under the icons and windows (both later in the DOM), and pointer-events:none keeps the desktop's
   own click-to-dismiss working through it. */
.os-desk::before{content:'';position:absolute;inset:0;z-index:0;pointer-events:none;
  background-image:radial-gradient(ellipse at center, rgba(var(--accent-rgb),.13) 0%, transparent 62%),
                   url('/static/os-wallpaper.webp');
  background-repeat:no-repeat,no-repeat;
  background-position:center 46%,center 46%;
  background-size:min(130vmin,1300px) min(130vmin,1300px), clamp(300px, 58vmin, 680px) auto;
  opacity:.34;filter:saturate(1.15) contrast(1.02) drop-shadow(0 0 70px rgba(var(--accent-rgb),.20))}
/* Light themes: the same opacity over a pale ground turns it into grey mush, and the icon labels
   are dark text that needs the contrast back. */
@media (prefers-color-scheme: light){ :root:not([data-theme="dark"]) .os-desk::before{opacity:.12} }
:root[data-theme="light"] .os-desk::before{opacity:.12}
@media (max-width:1100px){ .os-desk::before{opacity:.16} }   /* less room, more windows over it */

/* Desktop icons — a GRID in the top-left, filling across in rows and wrapping.
   One tall column needed 1428px for seventeen features and ran off the bottom; making it scroll
   technically worked and looked terrible. Three across wraps the whole navbar into ~6 rows, every
   icon on screen at once, no scrollbar, and it still reads as a desktop. */
.os-icons{position:absolute;inset-inline-start:14px;top:14px;display:grid;grid-auto-flow:row;
  grid-template-columns:repeat(3, 120px);gap:6px;align-content:start;justify-items:start}
/* The column count is set inline by os.js (iconCols), computed from the space actually available —
   three is only the fallback. Tile geometry is therefore fixed in LAYOUT pixels and must not be
   rescaled per breakpoint, or the script's arithmetic and the stylesheet disagree and the bottom
   row lands under the taskbar. At body{zoom} .67 a 96x80 tile still presents ~64x54 css px, which
   is comfortably over the 44px tap minimum. */
/* COMPACT ROWS, not tiles. Seventeen 84px tiles need 1428px of column and simply run off the
   bottom of any screen — the icons were all there and half of them unreachable. A 40px row fits the
   whole navbar in ~700px, so everything is visible at once without a second column marching across
   the desktop or a scrollbar nobody can see. */
.os-icon{width:120px;height:100px;display:flex;flex-direction:column;align-items:center;
  justify-content:center;gap:5px;background:transparent;border:1px solid transparent;
  border-radius:10px;color:var(--text);cursor:pointer;padding:6px 4px;
  user-select:none;-webkit-user-select:none}
/* Dragging an icon with a FINGER has to beat the browser's own panning gesture, and only on the
   desktop grid — a folder window's list scrolls, and taking touch-action off it would leave a full
   folder unscrollable. (A finger arranges a folder through press-and-hold → Move to desktop.) */
.os-icons .os-icon{touch-action:none}
.os-icon:hover,.os-icon:focus-visible{background:rgba(255,255,255,.09);border-color:var(--line)}
.os-icon .ic{width:40px;height:40px;color:var(--neon);flex:0 0 auto}
.os-icon span{font-size:14px;line-height:1.2;text-align:center;max-width:114px;
  overflow:hidden;display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical}

/* A folder wears its first four apps, so it reads as a container without needing the label to say
   so. The tile's own 26px icon rule would blow these up to full size — they are quarters of it. */
.os-fold{display:grid;grid-template-columns:1fr 1fr;gap:2px;width:36px;height:36px;padding:2px;
  border:1px solid var(--line);border-radius:7px;background:rgba(255,255,255,.06);flex:0 0 auto}
.os-icon .os-fold .ic{width:100%;height:100%;min-width:0}

/* Arranging the desktop. The tile being dragged stays in place at half strength — an icon that
   disappears out of the grid reflows every icon after it, so the row you are aiming at moves while
   you aim at it. What follows the pointer is a copy. */
.os-icon.os-dragging{opacity:.35}
/* Moved with a transform per animation frame (os.js), so promote it to its own compositor layer —
   the same treatment .osw.dragging gets, for the same reason. */
.os-drag{will-change:transform;position:absolute;width:96px;height:80px;display:flex;flex-direction:column;
  align-items:center;justify-content:center;gap:5px;padding:6px 4px;border-radius:10px;
  background:var(--panel);border:1px solid var(--neon);color:var(--text);
  pointer-events:none;opacity:.92;z-index:400;box-shadow:0 10px 26px rgba(0,0,0,.5)}
.os-drag .ic{width:26px;height:26px;color:var(--neon)}
.os-drag .os-fold .ic{width:100%;height:100%}
.os-drag span{font-size:11px;line-height:1.15;text-align:center;max-width:90px;overflow:hidden;
  display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical}
/* The two drops, told apart before the button is released: a ring means "into this", a caret means
   "beside it". A drop that lands somewhere unannounced reads as a bug, which is the same reason the
   window snap zones preview themselves. */
.os-icon.os-into{background:color-mix(in srgb, var(--neon) 22%, transparent);
  border-color:var(--neon);box-shadow:0 0 0 2px var(--neon) inset}
.os-ins{position:absolute;width:2px;background:var(--neon);border-radius:2px;pointer-events:none;
  z-index:400;box-shadow:0 0 8px var(--neon)}

/* The sender card: who actually sent this, from a header that renders as one string. */
.mail-sender-card{display:flex;flex-direction:column;gap:6px;margin:6px 0 14px;text-align:start}
.msc-row{display:flex;flex-direction:column;gap:1px;background:var(--panel2);border:1px solid var(--line);
  border-radius:9px;padding:7px 11px}
.msc-row b{font-size:13.5px;word-break:break-all}
/* The sender's name in a message header is a button; the header itself collapses the message. */
.mm-sender{cursor:pointer;text-decoration:underline;text-decoration-style:dotted;text-underline-offset:3px}
.mm-sender:hover{color:var(--neon)}

/* uiPickOne's list — a question with N answers, in the same overlay as uiConfirm/uiPrompt. */
.uipick-list{display:flex;flex-direction:column;gap:4px;max-height:min(46vh,340px);overflow-y:auto;
  margin-bottom:14px;text-align:start}
.uipick{display:block;width:100%;text-align:start;background:var(--panel2);border:1px solid var(--line);
  color:var(--text);border-radius:9px;padding:9px 12px;font-size:13.5px;cursor:pointer;
  white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.uipick:hover,.uipick:focus-visible{border-color:var(--neon);background:var(--panel)}

/* FREE PLACEMENT. Once an icon has been moved the grid steps aside and every icon carries its own
   left/top (set by os.js in layout px). Kept as one rule so the two layouts cannot both apply. */
.os-icons.os-free{display:block;position:absolute;inset-inline-start:0;top:0;inset-inline-end:0;bottom:0}
.os-icons.os-free .os-icon{position:absolute}

/* A wallpaper from the drive. The emblem behind it goes — two backgrounds fighting is worse than
   either — and the picture covers, because a desktop wallpaper that letterboxes looks broken. */
.os-desk.has-bg{background-size:cover;background-position:center;background-repeat:no-repeat}
.os-desk.has-bg::before{display:none}
/* …and a scrim, so white icon labels stay readable over a bright photo. */
.os-desk.has-bg::after{content:'';position:absolute;inset:0;background:rgba(0,0,0,.28);pointer-events:none}

/* The background picker. */
.os-bgpick{position:fixed;inset-inline-end:8px;bottom:56px;z-index:335;width:min(560px,calc(100vw - 16px));
  max-height:min(560px,calc(100vh - 96px));display:flex;flex-direction:column;overflow:hidden;
  background:var(--panel);border:1px solid var(--line);border-radius:12px;
  box-shadow:0 18px 50px rgba(0,0,0,.55)}
.os-bg-head{display:flex;align-items:center;justify-content:space-between;gap:8px;padding:10px 12px;
  border-bottom:1px solid var(--line);font-size:13px}
.os-bg-x{background:none;border:0;color:var(--muted);font-size:14px;cursor:pointer;padding:2px 6px;border-radius:6px}
.os-bg-x:hover{background:var(--panel2);color:var(--text)}
.os-bg-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(150px,1fr));gap:8px;padding:10px;
  overflow-y:auto}
.os-bg-item{position:relative;display:flex;flex-direction:column;gap:4px;padding:0;border-radius:9px;
  border:1px solid var(--line);background:var(--panel2);color:var(--text);cursor:pointer;overflow:hidden}
.os-bg-item img{width:100%;height:84px;object-fit:cover;display:block;background:var(--bg)}
.os-bg-item span{font-size:11px;padding:4px 6px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.os-bg-item.on{border-color:var(--neon);box-shadow:0 0 0 1px var(--neon) inset}
.os-bg-none{display:flex;align-items:center;justify-content:center;height:84px;font-size:12px;color:var(--muted)}
/* A tile whose picture would not decrypt. It has to be readable at 150px, so it wraps and shrinks
   rather than ellipsing into "⚠ Ope…" — the wording IS the diagnosis. */
.os-bg-err{padding:4px 6px;font-size:10px;line-height:1.25;text-align:center;white-space:normal;
  color:var(--warn,#f3b34a)}
.os-bg-empty{padding:16px;display:flex;flex-direction:column;gap:8px;align-items:flex-start}
.os-bg-empty p{margin:0}

/* ---- the SSH terminal --------------------------------------------------------------------------
   A full-height screen: the emulator sizes itself to its box, so the box has to be a real height and
   not a content-driven one, or xterm's fit computes 1 row. `feed-term` is the full-height feed mode
   the meme builder and Live Translate already use. */
.feed-term{display:flex;flex-direction:column;min-height:0;padding:0;overflow:hidden}
.tty-wrap{display:flex;flex-direction:column;flex:1;min-height:0;background:#07040f}
/* THE `tm-` PREFIX WAS ALREADY TAKEN, and that was the whole bug.
   Further down this file, `.tm-bar` and `.tm-state` belong to a PROGRESS METER: `.tm-bar{height:6px}`
   with a gradient `i` inside it. Those rules come after these, so the terminal's toolbar was handed
   `height:6px` — its controls hung out of a box a tenth their size ("buttons cut off at the top") and
   the meter's 6px gradient rendered as the "weird horizontal line". Both reports, one collision.
   Everything here is `tty-` now; the meter is untouched.
   I spent two attempted fixes on the flexbox (align-content, min-height) before measuring, and both
   were treating a symptom of a rule I had not noticed was there. scripts/check_terminal_mobile.py
   measures every control against its bar at three phone widths, which is what found it. */
.tty-bar{flex:0 0 auto;display:flex;align-items:center;gap:8px;padding:8px 10px;
  flex-wrap:wrap;box-sizing:border-box;
  border-bottom:1px solid var(--line);background:var(--panel2)}
.tty-host{flex:0 1 auto;min-width:0;max-width:60%;font-size:12.5px;padding:6px 8px}
.tty-state{flex:1 1 100%;font-size:11.5px;color:var(--muted);order:9}
.tty-state.ok{color:#5dffb0}
.tty-state.err{color:#ff6b8b}
.tty-find{flex:0 0 auto;display:flex;align-items:center;gap:7px;padding:6px 10px;
  border-bottom:1px solid var(--line);background:var(--panel2)}
.tty-find[hidden]{display:none}
.tty-find .input{flex:1;min-width:80px;padding:6px 8px;font-family:ui-monospace,monospace}
.tty-find>span{min-width:64px;color:var(--muted);font-size:11px;text-align:center;white-space:nowrap}
.tty-find .btn{min-width:32px;padding-inline:8px}
/* "Still running" — sessions this ACCOUNT has open that this device is not attached to, which is
   what makes a shell started on the laptop resumable on the phone. Scrolls sideways rather than
   wrapping: on a phone this sits above the screen and must never eat rows from it. */
.tty-sessions{flex:0 0 auto;display:flex;align-items:center;gap:8px;padding:6px 10px;
  overflow-x:auto;overflow-y:hidden;white-space:nowrap;-webkit-overflow-scrolling:touch;
  border-bottom:1px solid var(--line);background:rgba(0,240,255,.04)}
.tty-sess-lbl{flex:0 0 auto;font-size:10.5px;letter-spacing:.08em;text-transform:uppercase;
  color:var(--muted)}
.tty-sess{flex:0 0 auto;display:inline-flex;align-items:center;gap:6px;padding:3px 4px 3px 9px;
  border:1px solid var(--line);border-radius:999px;background:var(--panel)}
.tty-sess b{font-size:12px;font-weight:600;color:var(--text)}
.tty-sess i{font-size:10.5px;font-style:normal;color:var(--muted)}
.tty-sess button{font-size:11px;padding:3px 9px;border-radius:999px;cursor:pointer;
  border:1px solid var(--line);background:transparent;color:var(--text)}
.tty-sess button:hover{border-color:var(--accent);color:var(--accent)}
.tty-tab{cursor:pointer;border-radius:8px;padding-inline-start:10px}
.tty-tab.active{border-color:var(--accent);background:rgba(0,240,255,.11);
  box-shadow:inset 0 -2px 0 var(--accent)}
.tty-tab-new{flex:0 0 auto;width:30px;height:28px;border-radius:8px;border:1px dashed var(--line);
  background:transparent;color:var(--accent);font-size:19px;line-height:1;cursor:pointer}
.tty-tab-new:hover{border-color:var(--accent);background:rgba(0,240,255,.08)}
.tty-kill,.tty-sess button.tty-kill{color:#ff6b8b;border-color:rgba(255,107,139,.4)}
.tty-kill:hover,.tty-sess button.tty-kill:hover{border-color:#ff6b8b;color:#ff6b8b}
/* The emulator's own element. min-height:0 is load-bearing inside a flex column — without it the
   screen grows to its content and the fit addon never sees a bounded box. */
/* THE TERMINAL IS MEASURED IN ONE SPACE, WHICH IS WHAT MAKES A HIGHLIGHT LAND WHERE YOU DRAGGED.
 *
 * Desktop scales the whole page with body{zoom:.67-.77}. xterm hit-tests a click by taking
 * `clientY - getBoundingClientRect().top` -- both VISUAL pixels under zoom -- and dividing by the
 * cell height it computed from the font, which is a LAYOUT pixel. Measured, at three tiers:
 *
 *     tier      zoom   rendered row   xterm's cell
 *     1366      0.67      10.72px        16px
 *     1600      0.72      11.52px        16px
 *     1920      0.77      12.32px        16px
 *     phone     1.00      16px           16px
 *
 * They agree at zoom 1 and diverge by exactly the zoom factor otherwise, so a click ten rows down
 * is divided by 16 when each row occupies 10.72 and lands on row six. Reported twice -- "terminal
 * does not highlight the line I want, it is a few lines above it", and again after the first
 * attempt at this.
 *
 * THAT FIRST ATTEMPT PUT THE ZOOM ON `.tty-screen` ITSELF, and it shipped without being run. `zoom`
 * scales an element's LAYOUT BOX as well as its rendering, so inside a `flex:1` parent the content
 * was scaled into a box that had not grown: xterm fitted itself to one size while the visible area
 * was another, which is the mismatch that makes a shell wrap in the wrong place and redraw over
 * itself. The next thing reported was the terminal mangling input.
 *
 * So the flex item keeps its own box and an inner host is pinned inside it with `inset:0`, which
 * resolves against that box before the zoom applies -- measured identical to spelling the width and
 * height out with a calc, and one fewer thing to be off by a fraction of a pixel.
 * `fontSize()` in term.js scales by the same factor, or every glyph would render 1/zf bigger and
 * half as many rows would fit.
 *
 * Verified with scripts/check_terminal_selection.py, which measures rendered-row-height against
 * xterm's own cell at every tier -- the two numbers in the table above. */
.tty-screen{flex:1;min-height:0;padding:6px 4px 2px 8px;overflow:hidden;position:relative}
.tty-fit{position:absolute;inset:0;zoom:calc(1 / var(--zf, 1))}
.tty-screen .xterm{height:100%}
/* xterm HARDCODES `background-color:#000` on its viewport and forces `overflow-y:scroll`. Against
   this app's #07040f that black box is a visible seam across the panel — the "weird horizontal
   line" — and the forced scrollbar reserves a gutter that draws a track down the side even with
   nothing to scroll. Both are xterm's defaults for a standalone page, not for a panel inside an app;
   the rest of this client hides its scrollbars the same way (.feed uses scrollbar-width:none). */
.tty-screen .xterm-viewport{background-color:transparent !important;scrollbar-width:none}
.tty-screen .xterm-viewport::-webkit-scrollbar{width:0;height:0}
/* THE KEY BAR — what makes this usable on a phone. A soft keyboard has no Ctrl, Esc, Tab or arrows,
   and a shell without Ctrl-C is not a shell. Scrolls horizontally rather than wrapping, so it stays
   one line tall on the narrowest screen. */
.tty-keys{flex:0 0 auto;display:flex;gap:6px;padding:7px 8px;overflow-x:auto;
  border-top:1px solid var(--line);background:var(--panel2);-webkit-overflow-scrolling:touch}
.tty-keys::-webkit-scrollbar{height:0}
.tty-keys button{flex:none;min-width:42px;height:34px;padding:0 10px;border-radius:8px;
  border:1px solid var(--line);background:var(--panel);color:var(--text);font-size:12px;
  font-family:ui-monospace,monospace;cursor:pointer;-webkit-user-select:none;user-select:none}
.tty-keys button:active{background:var(--cyan,#0ff);color:#04121a}
.tty-keys .tty-ctrl.on{background:var(--cyan,#0ff);color:#04121a;border-color:var(--cyan,#0ff)}
.tty-keys .tty-int{color:#ff6b8b}
.tty-keys .tty-kbd{margin-inline-start:auto}
/* The focus catcher. It must be FOCUSABLE and effectively invisible: a display:none or a
   visibility:hidden field cannot take focus, and a field that cannot take focus cannot raise the
   soft keyboard — which is the only reason it exists. */
.tty-catch{position:absolute;opacity:0;height:1px;width:1px;border:0;padding:0;
  pointer-events:none;inset-inline-start:-9999px}
.os-hibernate{margin-top:24px;padding:16px;display:flex;align-items:center;gap:16px;
  border:1px solid var(--line);border-radius:12px;background:var(--panel2)}
.os-hibernate>div{display:flex;flex:1;min-width:0;flex-direction:column;gap:4px}
.os-hibernate span{color:var(--muted);font-size:12px}.os-hibernate .os-set-ready{color:#5dffb0}
@media(max-width:820px){
  .tty-bar{padding:6px 8px}
  .tty-host{max-width:100%}
  .tty-screen{padding:4px 2px 0 4px}
}

/* ---- desktop widgets -------------------------------------------------------------------------
   Panels that sit ON the desktop. They are ABOVE the wallpaper and BELOW the windows (.osw carries an
   inline z-index in the 10-200 band; see nextZ() in os.js),
   because a widget you can lose behind a window is one you would stop putting things on — and one
   that floats over your work is worse. `contain: layout style` keeps a widget's own repaints from
   invalidating the desktop around it: these things update forever, which is exactly the load this
   file has had to remove three times already. */
/* EVERY SURFACE HERE IS A TOKEN, and that is a rule rather than a preference.
   These panels were written in cyberpunk's own colours — a violet glass gradient, near-white text,
   black input fills, white-on-glass hovers — none of which go through the theme. On the eight other
   palettes that is a dark purple card sitting on a light desktop with dark text on it, which is
   exactly how it was reported: "it looks terrible and dark if you change themes". It is wrong on the
   other DARK themes too (violet glass in the middle of Monero's browns), so this is not a light-theme
   patch: the base is themed, and cyberpunk's flourishes are restored explicitly at the end of this
   section under :root:not([data-theme]). A NEW widget that uses only tokens is correct in all nine
   themes for free; one that hardcodes a colour is broken in eight of them and looks fine to whoever
   wrote it. */
/* touch-action:none is what makes a widget draggable with a FINGER, and its absence is the whole of
   "on a tablet the widget starts to move then stops". The windows (.osw-bar) and the desktop icons
   (.os-icons .os-icon) both carry it; the widgets were the one draggable thing on this desktop that
   did not, so the panel followed the finger for a few pixels and then the browser claimed the
   gesture as a page scroll — which arrives as `pointercancel`, and startWgtDrag (correctly) treats a
   cancel as a release. Measured with real touch input rather than reasoned about: default
   touch-action gives 3 pointermoves → pointercancel → no pointerup and 61px of page scroll; with
   this rule, 12 moves and a clean pointerup with nothing scrolled.
   It does NOT cost the scrolling bodies (news, crypto, the agenda) their finger-scroll: the pan is
   handled by the .os-wgt-body scroller itself, which is BELOW this element, and the same measurement
   shows an overflow:auto box inside a touch-action:none panel still scrolling. A pan that lands on a
   list is a scroll and a pan anywhere else on the panel is a drag, which is the behaviour you would
   want anyway. */
.os-wgt{position:absolute;z-index:6;display:flex;flex-direction:column;border-radius:14px;
  background:linear-gradient(160deg,rgba(var(--accent-rgb),.07),transparent 62%),var(--panel2);
  border:1px solid var(--line);box-shadow:0 10px 30px rgba(0,0,0,.45);overflow:hidden;
  contain:layout style;-webkit-user-select:none;user-select:none;cursor:grab;touch-action:none}
.os-wgt.dragging{cursor:grabbing;will-change:transform;box-shadow:0 18px 44px rgba(0,0,0,.6);
  border-color:var(--neon)}
/* The remove button floats over the corner and only appears on hover or keyboard focus — the widget
   has no title bar to hang it from, and a ✕ sitting on a panel permanently is the chrome this design
   is getting rid of. It stays reachable without a pointer (focus-visible) and on touch, where nothing
   hovers, the right-click/long-press menu carries the same action. */
/* `pointer-events:none` while it is invisible, and that is a correctness rule rather than a tidy-up:
   opacity:0 hides a button from the EYE and from nothing else. It sat armed over the top-right corner
   of every widget, and the moment the search bar came down to 54px that corner became its own submit
   button — a click on ➜ deleted the widget (and, since the layout is published, deleted it on every
   device) instead of searching. It takes clicks exactly when it can be seen. */
.os-wgt-x{position:absolute;top:6px;inset-inline-end:6px;z-index:2;width:20px;height:20px;border:0;
  background:var(--bg2);color:var(--muted);cursor:pointer;border-radius:50%;font-size:10px;
  line-height:1;opacity:0;pointer-events:none;transition:opacity .12s,background .12s,color .12s;
  display:flex;align-items:center;justify-content:center}
.os-wgt:hover .os-wgt-x,.os-wgt-x:focus-visible{opacity:1;pointer-events:auto}
.os-wgt-x:hover{background:#e81123;color:#fff}
/* THE PANEL'S OWN touch-action IS NOT ENOUGH, because the finger lands HERE. This is a scroller
   (overflow:auto), and a touch starting on a scrollable descendant is handed to that scroller —
   the browser fires pointercancel at the drag on the very first move, which startWgtDrag rightly
   reads as a release, so the widget never moves a pixel. Icons drag on the same screen because an
   icon has no scrollable child. Measured as an A/B with real touch input: identical panels, the one
   whose body carried this line completed the drag and the one without it cancelled. */
.os-wgt-body{flex:1;min-height:0;overflow:auto;padding:10px 12px;font-size:12.5px;color:var(--text);
  touch-action:none}
.wgt-dim{color:var(--muted);font-size:11.5px}

/* CPU, memory and network. A number says where the machine is now; the trace says whether that is
   a spike or a trend. Sixty samples at two seconds is a compact two-minute history. */
.wgt-perf{height:100%;min-height:0;display:flex;flex-direction:column;gap:7px;font-variant-numeric:tabular-nums}
.wgt-perf header{display:flex;align-items:flex-start;justify-content:space-between;gap:10px;flex:none}
.wgt-perf header>div{display:flex;flex-direction:column;min-width:0}
.wgt-perf header b{font-size:13px;line-height:1.2;color:var(--text)}
.wgt-perf-sub{font-size:9.5px;color:var(--muted);letter-spacing:.04em;text-transform:uppercase}
.wgt-perf-num{font-size:22px;line-height:1;font-weight:750;color:var(--neon);white-space:nowrap}
.wgt-perf-graph{display:block;width:100%;flex:1;min-height:62px;border:1px solid var(--line);
  border-radius:7px;background:rgba(var(--accent-rgb),.035);overflow:visible}
.wgt-perf-grid path{fill:none;stroke:var(--line);stroke-width:.65;vector-effect:non-scaling-stroke;opacity:.65}
.wgt-perf-line{fill:none;stroke:var(--neon);stroke-width:2;stroke-linecap:round;stroke-linejoin:round}
.wgt-perf-line.secondary{stroke:var(--violet)}
.wgt-perf-stats{display:flex;justify-content:space-between;align-items:center;gap:8px;flex:none;
  color:var(--muted);font-size:10.5px;white-space:nowrap}
.wgt-perf-stats span{overflow:hidden;text-overflow:ellipsis}
.wgt-perf-stats .primary::before,.wgt-perf-stats .secondary:not(:empty)::before{content:'';display:inline-block;
  width:7px;height:2px;margin:0 4px 3px 0;border-radius:2px;background:var(--neon)}
.wgt-perf-stats .secondary:not(:empty)::before{background:var(--violet)}

/* Bitcoin blocks. Pending stacked on the left, confirmed on the right, meeting at the chain tip —
   the arrangement mempool.space uses, and not decoration: it is what makes "my transaction is two
   blocks away" readable at a glance.

   Each tile is tinted by its fee through --mp-h (a hue triplet set inline), so the whole row reads as
   a temperature before a single number is read. The faces are two gradients rather than a border,
   which is what gives the blocks their weight without a single image. */
.wgt-mp{display:flex;flex-direction:column;height:100%;min-height:0;gap:8px;cursor:pointer}
.mp-row{flex:1;min-height:0;display:flex;align-items:center;gap:0;overflow-x:auto;overflow-y:hidden;
  padding:2px 0;-webkit-overflow-scrolling:touch;scrollbar-width:none}
.mp-row::-webkit-scrollbar{display:none}
.mp-side{display:flex;align-items:center;gap:5px;flex:none}
/* The chain tip: a thin seam the two sides meet at. */
.mp-tip{flex:none;width:3px;align-self:stretch;margin:7px 6px;border-radius:3px;
  background:linear-gradient(180deg,transparent,var(--neon) 20%,var(--neon) 80%,transparent);
  opacity:.55;box-shadow:0 0 8px rgba(var(--accent-rgb),.35)}
/* 66px is measured, not chosen: a six-digit block height at 11.5px bold is ~44px, and four tiles
   plus the seam have to fit the default widget width. Wider clipped the heights; narrower and the
   fee range wraps. */
.mp-b{flex:none;width:66px;min-height:58px;border-radius:8px;padding:6px 7px;box-sizing:border-box;
  display:flex;flex-direction:column;justify-content:center;gap:2px;
  background:linear-gradient(150deg,hsl(var(--mp-h)/.30),hsl(var(--mp-h)/.10) 62%,transparent),
             var(--panel);
  border:1px solid hsl(var(--mp-h)/.42);
  box-shadow:inset 0 1px 0 hsl(var(--mp-h)/.35), 0 2px 8px rgba(0,0,0,.28);
  transition:transform .12s ease}
.mp-b:hover{transform:translateY(-2px)}
/* A projected block is not real yet, and should not look like one. */
.mp-b.p{border-style:dashed;opacity:.86}
/* The next block out is the one anybody waiting on a confirmation is looking at. */
.mp-side.pending .mp-b:last-child{opacity:1;box-shadow:inset 0 1px 0 hsl(var(--mp-h)/.45),
  0 0 0 1px hsl(var(--mp-h)/.30), 0 3px 12px rgba(0,0,0,.34)}
.mp-b-top{font-size:11.5px;font-weight:700;line-height:1.2;color:hsl(var(--mp-h)/.98);
  font-variant-numeric:tabular-nums;
  overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.mp-b-mid{font-size:9.5px;color:var(--text);opacity:.85;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.mp-b-bot{font-size:9.5px;color:var(--muted);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.mp-foot{flex:none;font-size:11px;color:var(--muted);white-space:nowrap;overflow:hidden;
  text-overflow:ellipsis}

/* Today. The date is the anchor and the list is what you actually read, so the numeral is large and
   everything else is quiet. Times are tabular for the same reason the ticker's are: a column of
   09:00 / 11:30 / 14:05 that does not line up is harder to scan than one that does. */
.wgt-cal{display:flex;flex-direction:column;gap:7px;height:100%;min-height:0;cursor:pointer}
.wgt-calhead{flex:none;display:flex;align-items:baseline;gap:8px}
.wgt-calhead b{font-size:26px;line-height:1;font-weight:700;color:var(--neon)}
.wgt-calhead span{font-size:11.5px;color:var(--muted);text-transform:uppercase;letter-spacing:.06em;
  overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
/* "Coming up" — a divider, not a heading: it separates today from what follows without competing
   with the date, which is the only thing in this panel meant to be read at a glance. */
.wgt-calnext{margin:6px 0 3px;font-size:9.5px;letter-spacing:.1em;text-transform:uppercase;
  color:var(--muted);border-top:1px solid var(--line);padding-top:6px}
.wgt-calrows{flex:1;min-height:0;overflow-y:auto;display:flex;flex-direction:column;gap:4px;
  touch-action:none}   /* a scroller inside a draggable widget — see .os-wgt-body */
.wgt-calrow{display:flex;align-items:baseline;gap:7px;font-variant-numeric:tabular-nums}
/* A finished appointment is DIMMED, never removed — a day whose entries disappear as it goes on
   reads as a calendar that is losing things. */
.wgt-calrow.gone{opacity:.42}
/* THE FIRST COLUMN HOLDS TWO DIFFERENT KINDS OF THING, and a fixed width only ever fitted one of
   them. Today's rows put a time there ("09:00", "all day"); the "Coming up" rows put a DAY LABEL
   there — "Tomorrow", a weekday, or a date. At 3.6em "Tomorrow" does not fit, and a too-narrow
   flex:none box does not clip, it OVERFLOWS: the word painted straight over the calendar's colour
   swatch and the title beside it. `min-width` keeps the times aligned in a column exactly as before
   (every time is narrower than 3.6em, so they all sit at 3.6em) while letting a long label take the
   room it needs, and the ellipsis bounds a locale whose weekday or date is longer still — several
   spell it out. */
.wgt-calt{flex:none;min-width:3.6em;max-width:7em;font-size:11px;color:var(--muted);
  white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.wgt-calx{flex:none;width:3px;align-self:stretch;border-radius:2px;min-height:12px}
.wgt-caln{flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}

/* Ticker. A monospaced numeral column so the prices line up as they change — proportional digits
   make a live list twitch sideways on every update. */
.wgt-rows{display:flex;flex-direction:column;gap:3px}
.wgt-row{display:flex;align-items:baseline;gap:8px;font-variant-numeric:tabular-nums}
.wgt-sym{flex:none;width:3.4em;font-weight:700;color:var(--neon);font-size:11.5px;letter-spacing:.04em}
.wgt-val{flex:1;min-width:0;text-align:end;overflow:hidden;text-overflow:ellipsis}
.wgt-chg{flex:none;width:5.2em;text-align:end;font-size:11px}
/* --green/--danger, not a fixed pair: on the light themes a neon mint reads as decoration rather than
   as "up", and win98's palette has its own green. --danger is not declared on bare :root, so the
   fallback IS cyberpunk's original pink. */
.wgt-chg.up{color:var(--green)}
.wgt-chg.down{color:var(--danger,#ff6b8b)}

/* Weather. The temperature is the thing you actually look at, so it gets the size and everything
   else is subordinate to it. */
.wgt-wx{display:flex;flex-direction:column;gap:2px;height:100%}
.wgt-wxnow{display:flex;align-items:center;gap:9px}
.wgt-wxglyph{font-size:26px;line-height:1}
.wgt-wxt{font-size:27px;font-weight:700;line-height:1;color:var(--text)}
.wgt-wxt sup{font-size:12px;font-weight:600;color:var(--muted);vertical-align:super}
.wgt-wxwhere{font-size:12px;color:var(--text);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
/* The unit toggle. Small, quiet and up beside the number it changes — it is a preference you set once,
   so it must be findable without competing with the reading itself. */
.wgt-wxu{margin-inline-start:auto;align-self:flex-start;flex:none;background:none;cursor:pointer;
  border:1px solid var(--line);border-radius:7px;color:var(--muted);font-size:10.5px;
  padding:2px 6px;line-height:1.3}
.wgt-wxu:hover{color:var(--cyan,#0ff);border-color:var(--cyan,#0ff)}
.wgt-wxdays{margin-top:auto;display:flex;gap:10px;flex-wrap:wrap;padding-top:6px;
  font-size:11.5px;color:var(--muted)}
.wgt-wxpick{display:flex;flex-direction:column;gap:6px}
.wgt-wxq,.wgt-note,.wgt-sinput{width:100%;box-sizing:border-box;background:var(--bg2);
  border:1px solid var(--line);border-radius:9px;color:var(--text);padding:7px 9px;font:inherit;
  font-size:12.5px;outline:none}
.wgt-wxq:focus,.wgt-note:focus,.wgt-sinput:focus{border-color:var(--cyan,#0ff)}
/* Another scroller inside a widget, and it takes the gesture for the same reason .os-wgt-body did.
   One rule per scroller rather than `.os-wgt *`, so a scrollable list somebody adds later fails
   visibly here rather than being silently covered by a wildcard nobody remembers. */
.wgt-wxres{display:flex;flex-direction:column;gap:2px;max-height:118px;overflow:auto;
  touch-action:none}
.wgt-wxhit{display:flex;flex-direction:column;align-items:flex-start;gap:1px;text-align:start;
  background:none;border:0;color:var(--text);padding:5px 7px;border-radius:7px;cursor:pointer;
  font-size:12px}
.wgt-wxhit:hover{background:rgba(var(--accent-rgb),.14)}

/* Now playing. The panel used to be a title, a row of buttons, and a hole between them; the art and
   what-is-coming fill it with the two things a now-playing panel is for. */
/* The panel DISTRIBUTES rather than pinning the transport to the bottom and leaving a hole. The seek
   row takes the middle, which is the piece a player was missing, and the art grows into whatever is
   left on a large widget instead of floating in space. */
.wgt-music{display:flex;flex-direction:column;gap:8px;height:100%;justify-content:space-between}
.wgt-mtop{display:flex;align-items:center;gap:11px;min-width:0}
/* A record, not a placeholder square: the disc turns while it is playing and stops when it stops,
   which is the state the transport button also carries and the one you read from across a room. */
.wgt-mart{flex:none;width:clamp(42px,22%,64px);aspect-ratio:1;height:auto;border-radius:50%;display:flex;align-items:center;
  justify-content:center;background:radial-gradient(circle at 50% 50%,rgba(var(--accent-rgb),.35) 0 22%,
  var(--bg2) 23% 100%);border:1px solid var(--line);box-shadow:inset 0 0 12px rgba(0,0,0,.55)}
.wgt-mart .ic{width:17px;height:17px;color:var(--neon)}
.wgt-mart.spin{animation:wgtspin 4s linear infinite}
@keyframes wgtspin{to{transform:rotate(360deg)}}
@media(prefers-reduced-motion:reduce){ .wgt-mart.spin{animation:none} }
.wgt-mmeta{min-width:0;flex:1}
.wgt-mnext{font-size:11px;margin-top:2px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.wgt-mtitle{font-size:12.5px;color:var(--text);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.wgt-mctl{display:flex;align-items:center;justify-content:center;gap:6px}
/* Seek. A real control, so it is tall enough to hit with a thumb (the hit area is padded well past
   the 4px groove) and it takes focus for keyboard seeking. */
.wgt-mseek{position:relative;height:14px;display:flex;align-items:center;cursor:pointer;flex:none}
.wgt-mseek::before{content:'';position:absolute;inset-inline-start:0;inset-inline-end:0;height:4px;border-radius:3px;
  background:var(--line)}
.wgt-mseekfill{position:relative;height:4px;border-radius:3px;width:0;
  background:linear-gradient(90deg,var(--cyan,#0bb6cf),var(--neon));box-shadow:0 0 8px rgba(var(--accent-rgb),.5)}
.wgt-mseek:focus-visible{outline:1px solid var(--neon);outline-offset:3px;border-radius:3px}
.wgt-mtimes{display:flex;justify-content:space-between;font-size:10px;color:var(--muted);
  font-variant-numeric:tabular-nums;margin-top:-4px;flex:none}
.wgt-b{background:none;border:1px solid var(--line);border-radius:50%;width:30px;height:30px;
  color:var(--muted);cursor:pointer;font-size:12px;line-height:1;display:inline-flex;
  align-items:center;justify-content:center;transition:transform .12s ease,color .12s,border-color .12s,
  box-shadow .12s}
.wgt-b:hover{color:var(--cyan,#0ff);border-color:var(--cyan,#0ff)}
.wgt-b:active{transform:scale(.9)}
.wgt-bmain{width:36px;height:36px;font-size:14px;color:var(--neon);border-color:var(--neon)}
/* SHUFFLE IS A MODE, so it looks like a switch rather than a press: off it is one more quiet glyph,
   on it is filled and lit, which is the only way "it is already shuffling" is readable at a glance.
   The 12px icon keeps it visually lighter than the transport it sits beside — shuffle is a setting
   you touch once, not a control you aim for while a track is playing. */
.wgt-bsh .ic{width:13px;height:13px;display:block}
/* var(--bg), not a fixed near-black: --neon is a DARK navy on win98 and a dark blue on professional,
   where dark-on-dark makes the "shuffle is on" state unreadable — the same trap .btn-cyan and the
   active settings tab were already fixed for. The theme's own background reads on its own accent. */
.wgt-bsh.on{color:var(--bg);background:var(--neon);border-color:var(--neon);
  box-shadow:0 0 12px rgba(var(--accent-rgb),.55)}
.wgt-bsh.on:hover{color:var(--bg)}
@media(prefers-reduced-motion:reduce){ .wgt-b{transition:none} .wgt-b:active{transform:none} }

/* Sticky note — an actual yellow sticky, not a dark panel like the rest.
   It is the one widget that is a PHYSICAL object rather than a readout, and the whole reason a sticky
   note works is that it does not look like the thing it is stuck to. So this overrides the widget
   chrome: paper, a warm shadow, dark ink, and a title bar that reads as the gummed strip along the
   top. The text colour is hardcoded rather than themed — the paper is yellow in every theme, so a
   token that goes pale in a light theme would put white ink on it. */
.os-wgt[data-type="note"]{background:linear-gradient(170deg,#ffe98a,#ffd94f);
  border-color:rgba(120,88,0,.4);box-shadow:0 12px 26px rgba(0,0,0,.42),0 2px 0 rgba(255,255,255,.35) inset;
  border-radius:3px 3px 8px 3px}
/* The note has no bar to tint any more; the paper IS the widget, which is what a sticky note should
   have looked like from the start. The gummed strip is a band across the top of the paper itself. */
.os-wgt[data-type="note"]::before{content:'';position:absolute;inset-inline-start:0;inset-inline-end:0;top:0;height:9px;
  background:rgba(255,255,255,.30);border-bottom:1px solid rgba(120,88,0,.18);pointer-events:none}
.os-wgt[data-type="note"] .os-wgt-x{background:rgba(255,255,255,.5);color:#7a5c00}
.os-wgt[data-type="note"] .os-wgt-x:hover{background:rgba(160,40,20,.9);color:#fff}
.os-wgt[data-type="note"] .os-wgt-body{padding:14px 11px 8px}
/* The textarea IS the widget — transparent, no frame, so the writing area is the paper itself. */
.wgt-note{flex:1;min-height:60px;height:calc(100% - 16px);resize:none;line-height:1.5;
  background:transparent;border:0;border-radius:0;padding:2px 0;color:#2c2200;
  font-family:'Segoe Print','Bradley Hand','Comic Sans MS',ui-rounded,system-ui,sans-serif;font-size:13px}
.wgt-note:focus{border:0;outline:none}
.wgt-note::placeholder{color:rgba(90,70,0,.45)}
/* THE PAPER IS THE TEXTAREA, and on every theme but cyberpunk it stopped being: the readability
   layer's `:root[data-theme] textarea{background:var(--panel2);color:var(--text)}` is (0,2,1) and
   beats `.wgt-note` (0,1,0), so a white (or silver, or pink) box was painted across the middle of the
   yellow note with the theme's dark text in it — reported as "the text box on the sticky notes widget
   is really bad on the different themes". Same collision, and the same fix, as `.tl-cmp-ta` up in the
   layer itself. The ink stays hardcoded for the reason the paper does: this note is yellow in all
   nine themes, so a token that goes pale in a dark theme would write in white on it. */
:root[data-theme] .wgt-note{ background:transparent; color:#2c2200; }
:root[data-theme] .wgt-note::placeholder{ color:rgba(90,70,0,.45); }
.wgt-notest{height:14px;padding-top:2px;color:rgba(90,70,0,.6)}
.os-wgt[data-type="note"] .wgt-dim{color:rgba(90,70,0,.6)}

/* Search — a BAR: wide, one line tall, the input filling it. The hint sits on the same row rather
   than under it, because a second line is most of the height of a widget this shape. */
/* LEAN. The frame around the input is not decoration, it is dead space: a one-line control had a
   96px panel around it and 8px of body padding inside that, so the widget read as a box that happens
   to contain a search box rather than as a search box. The height comes down in wgtBox (see `bar`)
   and the padding here, and the input keeps a 1px border — asked for as "reduce the border width
   around the text input so it's thinner and leaner". */
/* The right padding is the ✕'s column, not decoration. At 54px tall the remove button and the ➜
   button want the same corner, and hovering the widget arms the ✕ — so the form ends before it. */
.os-wgt[data-type="search"] .os-wgt-body{display:flex;align-items:center;padding:5px 28px 5px 7px}
.wgt-search{display:flex;gap:6px;align-items:center;width:100%}
.wgt-sinput{flex:1;min-width:0}
/* NO OUTLINE, A FILL. A 1px box around a field inside a panel that is itself a box was two frames
   deep for one input — "looks ugly, remove the textbox outline and fill it". So the field IS its
   fill: a tinted pill in the theme's own accent, which reads as a search box on every palette
   without drawing a single line. Focus deepens the fill instead of adding the border back. */
.os-wgt[data-type="search"] .wgt-sinput{padding:6px 11px;border:0;border-radius:999px;
  background:rgba(var(--accent-rgb),.11)}
.os-wgt[data-type="search"] .wgt-sinput:focus{background:rgba(var(--accent-rgb),.18)}
.os-wgt[data-type="search"] .wgt-sinput::placeholder{color:var(--muted)}
/* The button matches it: filled, round, no border — an outlined button beside a borderless field is
   the same mismatch one level down. */
.wgt-sgo{flex:none;width:30px;height:30px;border-radius:50%;border:0;
  background:rgba(var(--accent-rgb),.18);color:var(--neon);cursor:pointer;font-size:13px;
  display:flex;align-items:center;justify-content:center;transition:background .12s,color .12s}
.wgt-sgo:hover{background:var(--neon);color:var(--bg)}
/* No room for it in a bar, and it says nothing the placeholder does not. */
.os-wgt[data-type="search"] .wgt-shint{display:none}
.wgt-shint{padding-top:6px}

/* Clock. The NUMERAL is the widget: the taskbar already carries HH:MM in 11px, so a second copy of
   that would be pointless — this one exists to be read from across the room, with the date under it
   and the cities you follow under that. Tabular figures, or the panel twitches sideways every time a
   1 becomes a 7. The size follows the widget's own size name rather than a container query: the four
   sizes are the vocabulary the rest of this file already speaks. */
/* CENTRED, because a clock is one short line and a date under it: pinned to the top-left of a panel
   that is mostly empty, it read as a corner of a box rather than as the thing the panel is for
   ("could be centered, it's wasting too much space"). The city rows keep their own left/right
   columns — a world clock is a table and centring those would make the times stop lining up. */
.wgt-clk{display:flex;flex-direction:column;gap:2px;height:100%;min-height:0;
  justify-content:center;align-items:center;text-align:center;position:relative}
.wgt-clkt{flex:none;display:flex;align-items:baseline;justify-content:center;gap:6px;
  font-variant-numeric:tabular-nums}
.wgt-clkh{font-size:34px;font-weight:700;line-height:1.05;color:var(--text)}
.os-wgt[data-size="s"] .wgt-clkh{font-size:27px}
.os-wgt[data-size="l"] .wgt-clkh{font-size:42px}
.wgt-clkap{font-style:normal;font-size:12px;font-weight:600;color:var(--muted);text-transform:uppercase}
/* Out of the flow entirely, at the BOTTOM-LEFT: it used to sit at the end of the time row on a
   `margin-inline-start:auto`, which is exactly the thing that stopped the numeral being centred. Bottom-left
   because the widget's own ✕ owns the top-right and fades in on the same hover — two round buttons in
   one corner is a target you hit by accident. */
.wgt-clkadd{position:absolute;inset-inline-start:0;bottom:0;flex:none;width:20px;height:20px;border-radius:50%;
  border:1px solid var(--line);background:transparent;color:var(--muted);cursor:pointer;font-size:11px;
  line-height:1;display:flex;align-items:center;justify-content:center;opacity:.5;
  transition:opacity .12s,color .12s,border-color .12s}
.os-wgt:hover .wgt-clkadd,.wgt-clkadd:focus-visible{opacity:1}
.wgt-clkadd:hover{color:var(--neon);border-color:var(--neon)}
.wgt-clkd{flex:none;font-size:11.5px;color:var(--muted);text-transform:uppercase;letter-spacing:.06em;
  overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
/* `flex:0 1 auto`, not `flex:1`: growing to fill the panel is what pushed the clock to the top. It
   takes the height it needs, scrolls when four cities do not fit, and the whole group stays centred.
   Width is pinned so the city rows are a column rather than each row being its own width. */
.wgt-clkz{flex:0 1 auto;min-height:0;width:100%;overflow-y:auto;display:flex;flex-direction:column;
  gap:3px;margin-top:5px;text-align:start;touch-action:none}   /* see .os-wgt-body */
.wgt-clkrow{display:flex;align-items:baseline;gap:8px;font-size:12px;cursor:pointer;
  font-variant-numeric:tabular-nums;border-radius:5px}
.wgt-clkrow:hover{background:rgba(var(--accent-rgb),.10)}
.wgt-clkzn{flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;color:var(--text)}
.wgt-clkzt{flex:none;color:var(--muted)}
/* A zone this browser cannot resolve says so rather than showing the local time under another city's
   name — which is a clock that is confidently, silently wrong. */
.wgt-clkrow.bad .wgt-clkzt{color:var(--danger,#ff6b8b)}
.wgt-clkpick{display:flex;flex-direction:column;gap:6px}
/* The ✕ is the touch answer to "I changed my mind": clicking outside dismisses the picker too, but
   that is not a gesture anybody tries on a phone. */
.wgt-clkprow{display:flex;align-items:center;gap:6px}
.wgt-clkprow .wgt-clkq{flex:1;min-width:0}
.wgt-clkx{flex:none;width:24px;height:24px;border-radius:7px;border:1px solid var(--line);
  background:transparent;color:var(--muted);cursor:pointer;font-size:11px;line-height:1;
  display:flex;align-items:center;justify-content:center}
.wgt-clkx:hover{color:var(--neon);border-color:var(--neon)}

/* Headlines. One row is a title and its source; the panel holds as many as fit and rotates by one.
   Each repaint replaces the rows, so the animation runs on arrival for free — and is dropped whole
   for anyone who has asked for less motion, where a ticker is exactly the thing they meant. */
.wgt-news{display:flex;flex-direction:column;gap:5px;height:100%;min-height:0;overflow:hidden}
.wgt-news-tools{display:flex;align-items:center;gap:8px;flex:none;min-height:20px;border-bottom:1px solid var(--line);padding-bottom:4px}
.wgt-news-tools b{font-size:10px;letter-spacing:.08em;text-transform:uppercase;color:var(--muted)}
.wgt-news-tools button{margin-inline-start:auto;border:0;background:transparent;color:var(--neon);font:inherit;font-size:10.5px;cursor:pointer;padding:2px 4px;border-radius:5px}
.wgt-news-tools button:hover{background:rgba(var(--accent-rgb),.12)}
.wgt-nrow{display:block;flex:none;text-decoration:none;color:inherit;border-radius:6px;
  animation:wgtnews .42s ease}
.wgt-nt{display:block;font-size:12.5px;line-height:1.25;color:var(--text);
  overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.wgt-nm{display:block;font-size:10.5px;color:var(--muted);
  overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.wgt-nrow:hover .wgt-nt{color:var(--neon)}
/* An item whose feed gave no usable link is still worth reading — it just is not a link, and must
   not pretend to be one by lighting up under the pointer. */
.wgt-nrow.nolink{cursor:default}
.wgt-nrow.nolink:hover .wgt-nt{color:var(--text)}
/* "No more News" is the panel's REST STATE, not an error — you have read everything, which is a good
   outcome and should look like one. Centred in the panel rather than a dim line in the top corner. */
.wgt-news-done{flex:1;display:flex;align-items:center;justify-content:center;text-align:center;
  color:var(--muted);font-size:12.5px}
@keyframes wgtnews{from{opacity:0;transform:translateY(7px)}to{opacity:1;transform:none}}
@media(prefers-reduced-motion:reduce){ .wgt-nrow{animation:none} }

/* Community. The same five counters the tray panel shows, at a size you can read without opening
   anything — and all five ALWAYS, including the zeroes (see _statCells: a cell that vanishes at zero
   reads as a missing feature). auto-fit rather than a fixed column count, because this panel is
   resizable and five cells across a small widget is five unreadable cells. */
/* CENTRED in both axes. Five cells over three columns leaves two on the last row, and pinned to the
   top-left that is a panel with a hole in the bottom-right of it — the same "wasting too much space"
   the clock had. align-content centres the rows in whatever height the widget has, justify-items
   centres each tile in its column, and the tile centres its own icon+number pair. */
.wgt-st{display:grid;grid-template-columns:repeat(auto-fit,minmax(116px,1fr));gap:16px 18px;
  height:100%;align-content:center;justify-items:center;cursor:pointer}
.wgt-stc{display:grid;grid-template-columns:24px auto;grid-template-rows:auto auto;gap:2px 9px;
  align-items:center;justify-content:center}
.wgt-stc .ic{width:24px;height:24px;color:var(--muted);grid-row:span 2}
.wgt-stc b{font-size:24px;font-weight:750;color:var(--text);line-height:1.05;
  font-variant-numeric:tabular-nums}
.wgt-stc i{font-style:normal;font-size:13px;color:var(--muted);line-height:1.2;
  overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
/* Small is 300×150 (276×130 after body padding). The generic 116px minimum fits only two columns,
   which turns five facts into THREE rows: their line boxes plus two 16px gaps consume the entire
   height and clip the labels against both edges. Small mode is deliberately 3+2, with dimensions
   that fit rather than relying on overflow to shave the text. */
.os-wgt[data-size="s"] .wgt-st{grid-template-columns:repeat(3,minmax(0,1fr));gap:9px 5px;
  align-content:center;padding-block:2px;box-sizing:border-box}
.os-wgt[data-size="s"] .wgt-stc{grid-template-columns:18px minmax(0,auto);gap:1px 5px;min-width:0}
.os-wgt[data-size="s"] .wgt-stc .ic{width:18px;height:18px}
.os-wgt[data-size="s"] .wgt-stc b{font-size:17px;line-height:1.1}
.os-wgt[data-size="s"] .wgt-stc i{font-size:10.5px;line-height:1.25;max-width:68px}
/* Something is actually live: the icon lights and breathes. Only ever for a NON-ZERO counter — this
   is the one cell whose value changes what the panel is for. */
.wgt-stc.live .ic{color:var(--neon);animation:wgtpulse 2.2s ease-in-out infinite}
@keyframes wgtpulse{0%,100%{opacity:1}50%{opacity:.45}}
@media(prefers-reduced-motion:reduce){ .wgt-stc.live .ic{animation:none} }

/* ---- cyberpunk's own flourishes ---------------------------------------------------------------
   Everything above is themed now, which costs the flagship theme the two surfaces that were genuinely
   ITS look rather than an accident: the violet glass panel and the dark record label. They are given
   back here for the bare :root only — cyberpunk is the one theme that sets no data-theme attribute —
   which is the same shape the auth gate's neon decor already uses. Nothing else needs restoring: the
   tokens the rules now read ARE cyberpunk's old values (--text is #edeefa where the panels hardcoded
   #eaf0ff, --line is the same groove the seek bar drew by hand). The sticky note is excluded from
   both: its paper is yellow in every theme, including this one. */
/* `background-image`, not the `background` shorthand: the shorthand resets background-COLOR to
   transparent, which leaves the panel with nothing but a translucent gradient over the wallpaper —
   it looks nearly right and there is no solid surface under the text. (It is also unmeasurable, which
   is how check_os_desktop's theme check found it: a computed backgroundColor of rgba(0,0,0,0) tells
   you nothing about what the reader sees.) Keeping the layer separate puts cyberpunk's violet over
   the same --panel2 every other theme gets. */
:root:not([data-theme]) .os-wgt:not([data-type="note"]){
  background-image:linear-gradient(160deg,rgba(20,12,44,.86),rgba(10,4,24,.9))}
:root:not([data-theme]) .os-wgt:not([data-type="note"]) .os-wgt-x{background:rgba(8,4,20,.72)}
:root:not([data-theme]) .wgt-mart{background:radial-gradient(circle at 50% 50%,
  rgba(var(--accent-rgb),.35) 0 22%,#150a2c 23% 100%)}

/* A block tile carries its fee as a HUE, and that hue is a mid-lightness colour picked against a dark
   tile — 55-62% lightness, which reads as a bright green or amber on violet glass and as a pale smear
   on white. The TINT stays on the light themes (it is what makes the row readable as a temperature at
   a glance); the number on top goes back to the theme's text colour. Same five slugs as the
   color-scheme:light group near the top of this file — they are the light palettes. */
:root[data-theme="cherryblossom"] .mp-b-top,:root[data-theme="professional"] .mp-b-top,
:root[data-theme="win98"] .mp-b-top,:root[data-theme="winxp"] .mp-b-top,
:root[data-theme="animegirl"] .mp-b-top{color:var(--text)}

/* The picker — the wallpaper panel's frame, a list rather than a grid of thumbnails. */
.os-wgt-grid{display:flex;flex-direction:column;gap:6px;padding:10px;max-height:56vh;overflow:auto}
.os-wgt-pick{display:grid;grid-template-columns:22px 1fr;gap:3px 10px;align-items:center;
  text-align:start;padding:9px 11px;border-radius:10px;border:1px solid var(--line);
  background:var(--panel2);color:var(--text);cursor:pointer}
.os-wgt-pick:hover{border-color:var(--neon)}
.os-wgt-pick .ic{width:18px;height:18px;color:var(--neon);grid-row:span 2}
.os-wgt-pl{font-weight:600;font-size:13px}
.os-wgt-pl i{font-style:normal;color:var(--muted);font-weight:400;font-size:11.5px}
.os-wgt-pb{font-size:11.5px;color:var(--muted)}

/* The right-click menu — on an icon, and on the desktop itself. */
.os-ctx{position:absolute;z-index:420;min-width:184px;max-width:280px;max-height:60vh;overflow-y:auto;
  background:var(--panel);border:1px solid var(--line);border-radius:10px;padding:5px;
  box-shadow:0 16px 40px rgba(0,0,0,.55)}
.os-ctx hr{border:0;border-top:1px solid var(--line);margin:4px 2px}
.os-ctx-b{display:block;width:100%;text-align:start;background:transparent;border:0;color:var(--text);
  font-size:12.5px;padding:7px 9px;border-radius:7px;cursor:pointer;white-space:nowrap;
  overflow:hidden;text-overflow:ellipsis}
.os-ctx-b:hover,.os-ctx-b:focus-visible{background:rgba(255,255,255,.09)}

/* Windows */
/* Their z-index is INLINE and assigned by os.js, which is why nothing here sets one. It is bounded
   to 10-200 (nextZ) so a window can never climb over the start menu (320), the flyouts (330/335),
   the desktop toasts (345) or the right-click menu (420) — an unbounded focus counter did exactly
   that after a few hundred view switches, and every one of those panels then opened invisibly
   behind a window. Anything new that must sit over the desktop goes ABOVE 200, never in the band. */
.osw{position:absolute;display:flex;flex-direction:column;min-width:420px;min-height:260px;
  background:var(--panel);border:1px solid var(--line);border-radius:12px;overflow:hidden;
  box-shadow:0 18px 50px rgba(0,0,0,.55)}
.os-root.os-fx .osw:not(.osw-document){
  background:color-mix(in srgb,var(--panel) 94%,transparent);
  -webkit-backdrop-filter:blur(10px);backdrop-filter:blur(10px);
  transition:border-color .14s ease,box-shadow .14s ease}
.os-root.os-fx .osw.focused:not(.osw-document){box-shadow:0 24px 68px rgba(0,0,0,.58),0 0 0 1px rgba(var(--accent-rgb),.12)}
.os-root.os-fx .osw-bar{background:color-mix(in srgb,var(--panel2) 91%,transparent)}
.os-root.os-fx .os-bar{box-shadow:0 -10px 32px rgba(0,0,0,.18)}
/* Explicit low-power/OFF means off: it also removes the desktop's pre-existing frost and depth.
   Geometry, stacking and hit testing are deliberately absent from both effect modes. */
.os-root.os-fx-off .osw,.os-root.os-fx-off .osw.focused,
.os-root.os-fx-off .osw.osw-document,.os-root.os-fx-off .os-bar,
.os-root.os-fx-off .os-startmenu,.os-root.os-fx-off .os-layouts,.os-root.os-fx-off .os-ghost{
  box-shadow:none!important;-webkit-backdrop-filter:none!important;backdrop-filter:none!important;
  transition:none!important}
.os-root.os-fx-off .osw:not(.osw-document),.os-root.os-fx-off .osw-bar{background:var(--panel)}
@media(prefers-reduced-motion:reduce){.os-root.os-fx .osw{transition:none}}
/* Optional macOS desktop experience. It remains presentation-only (changing it never recreates an
   app), but it is a DESKTOP layout, not three coloured buttons: a top menu material, floating Dock,
   top-right status area, Launchpad-like Start panel, Finder-spaced icons and layered window chrome. */
.os-root.os-style-mac{--mac-menu-h:30px;--mac-dock-h:66px;--mac-glass:rgba(31,34,42,.72);
  --mac-line:rgba(255,255,255,.18);font-family:-apple-system,BlinkMacSystemFont,"SF Pro Text",
  "Helvetica Neue",sans-serif;background:#17202d}
.os-root.os-style-mac::before{content:'';position:absolute;z-index:305;inset:0 0 auto;height:var(--mac-menu-h);
  border-bottom:1px solid rgba(255,255,255,.14);background:rgba(22,25,31,.72);
  -webkit-backdrop-filter:blur(24px) saturate(145%);backdrop-filter:blur(24px) saturate(145%);
  box-shadow:0 1px 12px rgba(0,0,0,.22);pointer-events:none}
.os-mac-menu{display:none}
.os-root.os-style-mac .os-mac-menu{position:absolute;z-index:308;inset:0 min(430px,42vw) auto 7px;
  height:var(--mac-menu-h);display:flex;align-items:center;gap:1px;overflow:visible;white-space:nowrap}
.os-root.os-style-mac .os-mac-menu button{flex:0 0 auto;height:26px;padding:0 8px;border:0;border-radius:6px;
  background:transparent;color:rgba(255,255,255,.94);font:500 12px/1 -apple-system,BlinkMacSystemFont,
  "SF Pro Text","Helvetica Neue",sans-serif;cursor:pointer;text-shadow:0 1px 2px #0008}
.os-root.os-style-mac .os-mac-menu button:hover,.os-root.os-style-mac .os-mac-menu button:focus-visible{
  background:rgba(255,255,255,.16);outline:0}.os-root.os-style-mac .os-mac-menu button:first-child{font-weight:600}
.os-root.os-style-mac .os-mac-appmenu{position:relative;height:30px;color:#fff}
.os-root.os-style-mac .os-mac-appmenu summary{height:26px;padding:0 8px;display:flex;align-items:center;gap:5px;
  border-radius:6px;cursor:pointer;list-style:none;font-size:12px;text-shadow:0 1px 2px #0008}
.os-root.os-style-mac .os-mac-appmenu summary::-webkit-details-marker{display:none}
.os-root.os-style-mac .os-mac-appmenu[open] summary,.os-root.os-style-mac .os-mac-appmenu summary:hover{background:rgba(255,255,255,.16)}
.os-root.os-style-mac .os-mac-dropdown{position:absolute;z-index:360;top:28px;left:0;width:190px;padding:5px;
  border:1px solid rgba(255,255,255,.2);border-radius:8px;background:rgba(38,41,49,.96);box-shadow:0 15px 36px #0008}
.os-root.os-style-mac .os-mac-dropdown button{display:block;width:100%;height:30px;text-align:left;padding:0 10px}
/* Make the desk itself the usable rectangle. Widgets, free icons, window clamps and pointer hit tests
   all measure this element, so margins reserve the menu/Dock in one place instead of merely painting
   padding over a full-height coordinate system and clipping anything placed at its bottom edge. */
.os-root.os-style-mac .os-desk{margin-top:var(--mac-menu-h);margin-bottom:calc(var(--mac-dock-h) + 18px)}
.os-root.os-style-mac .os-icons{padding-top:12px;padding-bottom:6px}
.os-root.os-style-mac .os-icon{border-radius:10px;text-shadow:0 1px 3px #000,0 2px 8px #000b}
.os-root.os-style-mac .os-icon:hover,.os-root.os-style-mac .os-icon.selected{
  background:rgba(255,255,255,.15);box-shadow:inset 0 0 0 1px rgba(255,255,255,.13)}
.os-root.os-style-mac .osw{border:1px solid var(--mac-line);border-radius:13px;
  background:rgba(34,37,45,.88);box-shadow:0 26px 72px rgba(0,0,0,.48),0 2px 9px rgba(0,0,0,.34);
  transition:box-shadow .15s ease,border-color .15s ease,filter .15s ease}
.os-root.os-style-mac .osw:not(.focused){border-color:rgba(255,255,255,.09);filter:saturate(.84) brightness(.91);
  box-shadow:0 13px 38px rgba(0,0,0,.34)}
.os-root.os-style-mac .osw.focused{border-color:rgba(255,255,255,.22);
  box-shadow:0 30px 86px rgba(0,0,0,.55),0 1px 0 rgba(255,255,255,.12) inset}
.os-root.os-style-mac .osw-bar{height:37px;padding:0 10px;gap:7px;
  background:linear-gradient(180deg,rgba(74,77,86,.92),rgba(48,51,59,.9));
  border-bottom:1px solid rgba(0,0,0,.36)}
.os-root.os-style-mac .osw-btns{order:-1;gap:7px;margin-inline-end:2px}
.os-root.os-style-mac .osw-b[data-w="min"],
.os-root.os-style-mac .osw-b[data-w="max"],
.os-root.os-style-mac .osw-b[data-w="close"]{width:13px;height:13px;border-radius:50%;font-size:0}
.os-root.os-style-mac .osw-b[data-w="close"]{order:-3;background:#ff5f57}
.os-root.os-style-mac .osw-b[data-w="min"]{order:-2;background:#febc2e}
.os-root.os-style-mac .osw-b[data-w="max"]{order:-1;background:#28c840}
.os-root.os-style-mac .osw-b[data-w="close"]:hover{background:#ff766f}
.os-root.os-style-mac .osw-b[data-w="min"]:hover{background:#ffd05c}
.os-root.os-style-mac .osw-b[data-w="max"]:hover{background:#50da65}
.os-root.os-style-mac .osw-btns:hover .osw-b[data-w="close"]::after{content:'×';color:#68110d;font-size:10px}
.os-root.os-style-mac .osw-btns:hover .osw-b[data-w="min"]::after{content:'−';color:#765200;font-size:10px}
.os-root.os-style-mac .osw-btns:hover .osw-b[data-w="max"]::after{content:'+';color:#075d1b;font-size:10px}
.os-root.os-style-mac .osw-ai{width:27px;height:28px}
.os-root.os-style-mac .osw-ico{display:none}
.os-root.os-style-mac .osw-title{text-align:center;padding-inline-end:60px;color:rgba(255,255,255,.9);
  font-size:12px;font-weight:600}
.os-root.os-style-mac .osw-body{background:rgba(20,23,29,.94)}
.os-root.os-style-mac .osw.osw-document,.os-root.os-style-mac .osw.osw-document.focused{
  border-radius:13px;border-color:rgba(255,255,255,.2);box-shadow:0 28px 78px rgba(0,0,0,.48)}
.os-root.os-style-mac .osw.osw-document .osw-bar{background:linear-gradient(#ececec,#d5d5d5);
  border-bottom-color:#aaa}.os-root.os-style-mac .osw.osw-document .osw-title{color:#272727}
/* The ordinary full-width bottom taskbar becomes a real floating Dock. The status controls remain
   usable in the menu bar instead of wasting a second strip at the bottom. */
.os-root.os-style-mac .os-bar{position:absolute;z-index:309;left:0;right:0;bottom:10px;margin-inline:auto;
  width:max-content;max-width:calc(100% - 28px);height:58px;min-height:58px;padding:5px 9px;
  border:1px solid rgba(255,255,255,.22);border-radius:17px;background:rgba(51,55,65,.92);
  box-shadow:0 14px 38px rgba(0,0,0,.46),inset 0 1px rgba(255,255,255,.12);
  /* A filtered ancestor becomes the containing block for position:fixed descendants. The tray is
     intentionally fixed in the top menu, but these filters trapped it inside the 58px Dock and
     compressed Start, search, tasks, volume, network and clock into one overlapping strip. Keep
     the Dock opaque enough for readability and leave fixed positioning rooted at the desktop. */
  -webkit-backdrop-filter:none;backdrop-filter:none;
  transform:none;overflow:visible}
.os-root.os-style-mac .os-qbox{display:none}
.os-root.os-style-mac .os-start{width:48px;height:48px;border-radius:12px;border:0;background:transparent}
.os-root.os-style-mac .os-start img{width:38px;height:38px;border-radius:10px}
.os-root.os-style-mac .os-tasks{flex:0 1 auto;min-width:0;max-width:min(68vw,900px);gap:5px;padding-inline-start:7px;
  border-inline-start:1px solid rgba(255,255,255,.22)}
.os-root.os-style-mac .os-task{position:relative;flex-basis:48px;width:48px;height:48px;border-radius:12px;
  border:0;background:transparent;transition:transform .13s ease,background .13s ease}
.os-root.os-style-mac .os-task:hover{background:rgba(255,255,255,.13);transform:translateY(-7px) scale(1.12)}
.os-root.os-style-mac .os-task.on{background:rgba(255,255,255,.1);box-shadow:none}
.os-root.os-style-mac .os-task.on::after{content:'';position:absolute;bottom:-4px;width:4px;height:4px;
  border-radius:50%;background:#fff;box-shadow:0 0 5px #fff8}
/* macOS mode gets an icon language of its own. Native applications keep their real desktop-file
   artwork, clipped to the rounded-square mask used by the Dock. PosterChan's monochrome sprite
   fallbacks become coloured squircle tiles rather than bare Windows-style line glyphs. */
.os-root.os-style-mac .os-task>.os-app-ic{width:39px;height:39px;display:block;object-fit:cover;
  border-radius:10px;box-shadow:0 2px 7px #0007,0 1px 0 rgba(255,255,255,.25) inset}
.os-root.os-style-mac .os-task>.ic{box-sizing:border-box;width:39px;height:39px;padding:8px;border-radius:10px;
  color:#fff;background:linear-gradient(145deg,#58a9ff,#0969d7);box-shadow:0 2px 7px #0007,
  0 1px 0 rgba(255,255,255,.28) inset}
.os-root.os-style-mac .os-task:nth-child(4n+2)>.ic{background:linear-gradient(145deg,#b37cff,#7042d7)}
.os-root.os-style-mac .os-task:nth-child(4n+3)>.ic{background:linear-gradient(145deg,#ff9768,#e24c39)}
.os-root.os-style-mac .os-task:nth-child(4n)>.ic{background:linear-gradient(145deg,#58d39a,#16815c)}
/* The top-right status area is menu-bar furniture: compact monochrome symbols, never Dock tiles. */
.os-root.os-style-mac .os-tray .ic,.os-root.os-style-mac .os-tray-group svg{width:16px;height:16px;color:#fff}
.os-root.os-style-mac .os-tray-group{height:26px;padding:0 5px;border:0;background:transparent;color:#fff}
.os-root.os-style-mac .os-tray{position:fixed;z-index:307;inset:0 9px auto auto;height:var(--mac-menu-h);
  gap:7px;color:#fff}.os-root.os-style-mac .os-tray button{height:26px;border-radius:6px;background:transparent}
.os-root.os-style-mac .os-tray button:hover{background:rgba(255,255,255,.14)}
/* THE MACHINE'S OWN TRAY IN THE MENU BAR. `#os-shell` is filled by osshell.js — wifi, battery,
   volume, power — and it had NO macOS rules at all, so its chips kept taskbar sizing (a 48px bar)
   inside a 30px menu bar and kept `color:var(--text)`, which on a light theme is dark text on the
   dark glass of the menu bar. Reported as the wifi/power widget displaying weird and cut off.

   Sized to the same 26px as the relay light and the bell beside it, so the row reads as one row —
   which is the rule the chip styling already states for the PosterChan taskbar, applied to the bar
   this style actually has. */
.os-root.os-style-mac .os-sys{gap:2px;max-width:46vw;overflow:hidden}
.os-root.os-style-mac .os-sys .os-chip{height:26px;display:inline-flex;align-items:center;
  padding:0 6px;font-size:12px;line-height:1;color:#fff;border-color:transparent;border-radius:6px;
  background:transparent}
.os-root.os-style-mac .os-sys .os-chip:hover{background:rgba(255,255,255,.14);border-color:transparent}
/* A reading that could not be taken stays marked as unknown — dimmed against the menu bar rather
   than against the taskbar's palette, or it reads as an ordinary value. */
.os-root.os-style-mac .os-sys .os-chip.os-unknown{color:rgba(255,255,255,.62)}
.os-root.os-style-mac .os-sys .ic,.os-root.os-style-mac .os-sys svg{width:15px;height:15px;color:#fff}
.os-root.os-style-mac .os-clock{align-items:flex-end;padding:0 6px}.os-root.os-style-mac .os-clock span{display:none}
.os-root.os-style-mac .os-clock b{font-size:11.5px;color:#fff}
.os-root.os-style-mac .os-startmenu{left:50%;bottom:82px;width:min(780px,calc(100vw - 30px));
  height:min(920px,calc(100vh - 78px));max-height:calc(100vh - 78px);border:1px solid rgba(255,255,255,.22);border-radius:22px;
  background:rgba(35,39,48,.82);box-shadow:0 28px 85px rgba(0,0,0,.55);
  -webkit-backdrop-filter:blur(30px) saturate(150%);backdrop-filter:blur(30px) saturate(150%);
  transform:translate(-50%,0)}
.os-root.os-style-mac .os-startmenu .os-app,.os-root.os-style-mac .os-startmenu button{border-radius:13px}
.os-root.os-style-mac .os-context,.os-root.os-style-mac .os-notifications,.os-root.os-style-mac .os-netfly{
  border-color:rgba(255,255,255,.2);background:rgba(35,39,48,.84);
  -webkit-backdrop-filter:blur(26px);backdrop-filter:blur(26px);box-shadow:0 22px 65px #0008}
/* Settings belongs to the selected desktop experience too. Keep its content coherent with the
   macOS window chrome instead of dropping a cyberpunk-black application inside a glass window. */
.os-root.os-style-mac .os-settings{background:#e9e9eb;color:#202124}
.os-root.os-style-mac .os-set-nav{background:rgba(238,238,241,.9);border-right-color:#c7c7ca;
  box-shadow:none}.os-root.os-style-mac .os-set-nav button{color:#4b4c50}
.os-root.os-style-mac .os-set-nav button.on,.os-root.os-style-mac .os-set-nav button:hover{
  background:#0a84ff;color:#fff}
.os-root.os-style-mac .os-set-main{background:linear-gradient(180deg,#f7f7f8,#e9e9eb)}
.os-root.os-style-mac .os-set-pagehead>div{background:#0a84ff;color:#fff;box-shadow:0 3px 9px #0067c755}
.os-root.os-style-mac .os-set-pagehead p,.os-root.os-style-mac .os-set-cardhead span,
.os-root.os-style-mac .os-set-control>div>span,.os-root.os-style-mac .os-set-control output{color:#68696e}
.os-root.os-style-mac .os-set-card,.os-root.os-style-mac .os-setting-row,
.os-root.os-style-mac .os-liveusb{background:rgba(255,255,255,.82)!important;border-color:#c9c9cc;
  box-shadow:0 1px 3px #0002}
.os-root.os-style-mac .os-display-map{background:#d7d7da;border-color:#b6b6ba}
.os-root.os-style-mac .os-display-controls,.os-root.os-style-mac .os-liveusb-grid>div{
  background:#f7f7f8;border-color:#cdcdcf}
.os-root.os-style-mac .os-set-control select,.os-root.os-style-mac .os-display-controls select,
.os-root.os-style-mac .os-liveusb-grid select,.os-root.os-style-mac .os-path-pick input{
  background:#fff;color:#202124;border-color:#b9b9bd}
.os-root.os-style-mac .os-liveusb-status{background:#f4f4f5;color:#3a3b3e;border-color:#cacacd}
.os-root.os-style-mac .os-about-grid{background:#ceced1}.os-root.os-style-mac .os-about-grid>div{background:#fff}
.os-root.os-style-mac .empty{color:#68696e}
@media(max-width:1180px){.os-root.os-style-mac .os-mac-menu{inset-inline-end:min(390px,46vw)}
  .os-root.os-style-mac .os-mac-menu button{padding-inline:6px}.os-root.os-style-mac .os-tray{gap:3px}
  .os-root.os-style-mac .os-task{flex-basis:43px;width:43px}}
.osw.focused{border-color:color-mix(in srgb, var(--neon) 55%, var(--line));
  box-shadow:0 22px 64px rgba(0,0,0,.62)}
.osw.osw-document,.osw.osw-document.focused{border-color:#4b5563;border-radius:4px;
  box-shadow:0 8px 24px rgba(0,0,0,.38)}
.osw.osw-document .osw-bar{background:#20242b;border-bottom-color:#3d444d}
.osw.osw-document .osw-ico .ic{color:#cbd5e1;filter:none}
/* Cyberpunk's fixed scanline layer otherwise crosses the white PDF/Office/email page. */
.pc-document-focus .scanlines{display:none!important}

/* Alt+Tab is a visible choice, not a focus shortcut. It is centered on this renderer/output and
   every card retains an icon/title even when a native thumbnail is temporarily unavailable. */
.os-alt-switch{position:fixed;z-index:2147483500;left:50%;top:50%;transform:translate(-50%,-50%);
  display:flex;gap:14px;max-width:min(92vw,1120px);padding:18px;border:1px solid var(--line);
  border-radius:18px;background:rgba(15,18,24,.94);box-shadow:0 28px 90px rgba(0,0,0,.65);
  overflow-x:auto;overflow-y:hidden;backdrop-filter:blur(18px);scrollbar-width:none}
.os-alt-card{flex:0 0 210px;min-width:0;padding:8px;border:2px solid transparent;border-radius:12px;
  background:rgba(255,255,255,.045)}
.os-alt-card.selected{border-color:var(--accent);background:rgba(var(--accent-rgb),.16);
  box-shadow:0 0 0 2px rgba(var(--accent-rgb),.16)}
.os-alt-preview{height:126px;overflow:hidden;border-radius:8px;background-size:cover;
  background-position:center;background-color:#11151b;pointer-events:none}
.os-alt-preview.empty{background:linear-gradient(145deg,#202734,#11151b)}
.os-alt-preview>*{width:480px!important;height:288px!important;transform:scale(.42);transform-origin:top left;
  pointer-events:none!important}
.os-alt-title{display:flex;align-items:center;gap:8px;margin-top:9px;min-width:0;color:#f4f7fb}
.os-alt-title .ic{width:22px;height:22px;flex:none}.os-alt-title span{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.osw.minimised{display:none}
/* The start button's LAST resort. When the instance logo cannot be fetched and the bundled PNG is
   missing too, the button draws the sprite flower — sized to match the image it replaces, so the
   taskbar does not reflow at the moment the logo fails. */
.os-start-ic{display:inline-flex;align-items:center;justify-content:center;width:22px;height:22px}
.os-start-ic .ic{width:20px;height:20px}

/* WHERE 100% IS, on a slider that goes to 150. Everything past the mark is PipeWire scaling the
   samples in software rather than the hardware being asked for more, and a slider with no mark
   makes "loud" and "boosted" the same gesture — you cannot tell you have left the safe range until
   something clips. The mark is drawn at exactly 100/150 of the track and is decoration only
   (pointer-events:none), so it can never swallow a drag. */
.os-boostable{background-image:linear-gradient(to right,transparent calc(66.667% - 1px),
  color-mix(in srgb, var(--neon) 70%, transparent) calc(66.667% - 1px),
  color-mix(in srgb, var(--neon) 70%, transparent) calc(66.667% + 1px),transparent calc(66.667% + 1px));
  background-repeat:no-repeat;background-size:100% 100%}
/* SPLIT ON PURPOSE. One selector list holding a -webkit- and a -moz- pseudo-element is invalid in
   BOTH engines — an unknown pseudo poisons the whole list, so the combined rule applied nowhere and
   the native track painted over the mark. Two rules, each understood by one engine. */
.os-boostable::-webkit-slider-runnable-track{background:transparent}
.os-boostable::-moz-range-track{background:transparent}

/* While a window is being dragged it is moved with a transform on the compositor, so promote it to
   its own layer and stop hit-testing its contents — on a tablet the pointer-events work alone was
   showing up in the gesture. Both are removed on release. */
.osw.dragging{will-change:transform;transition:none!important;
  /* Sampling a full-screen blurred backdrop again for every transformed Communities frame is a
     multi-megapixel repaint on each pointer event. Preserve the glass effect at rest; while the
     pointer is down use the same opaque panel so movement follows the cursor without flashing. */
  -webkit-backdrop-filter:none!important;backdrop-filter:none!important;background:var(--panel)!important}
/* No text/icon selection on the title bar: a native drag of the title steals the gesture, the
   browser stops sending pointer events, and the window ends up glued to the cursor. */
.osw-bar{user-select:none;-webkit-user-select:none}
.osw-native.native-handoff-prepared{visibility:hidden;pointer-events:none}
.osw-bar *{-webkit-user-drag:none}
.osw.dragging .osw-body{pointer-events:none}
/* touch-action:none is what makes dragging work with a FINGER. Pointer events fire on touch either
   way, but without this the browser claims the gesture as a scroll and the window never moves —
   which is the whole feature on an Android tablet. Same for the resize grip. */
.osw-bar{flex:0 0 auto;display:flex;align-items:center;gap:8px;padding:0 4px 0 10px;height:34px;
  background:var(--panel2);border-bottom:1px solid var(--line);cursor:grab;user-select:none;
  touch-action:none}
.osw-bar:active{cursor:grabbing}
.osw-ico .ic{width:15px;height:15px;color:var(--neon);display:block}
.osw-title{flex:1 1 auto;min-width:0;font-size:12.5px;font-weight:600;color:var(--text);
  overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.osw-btns{flex:0 0 auto;display:flex}
.osw-b{width:38px;height:34px;border:0;background:transparent;color:var(--muted);cursor:pointer;
  font-size:13px;line-height:1}
.osw-b:hover{background:rgba(255,255,255,.10);color:var(--text)}
.osw-x:hover{background:#e81123;color:#fff}
.osw-ai{position:relative;color:var(--accent);font-size:15px}.osw-ai:hover{background:rgba(var(--accent-rgb),.17);color:var(--text);text-shadow:0 0 10px var(--accent)}
.osw.ai-context{box-shadow:0 0 0 2px rgba(var(--accent-rgb),.58),0 18px 50px rgba(0,0,0,.55)}.osw.ai-watching .osw-ai::after{content:'';position:absolute;width:5px;height:5px;margin:8px 0 0 -3px;border-radius:50%;background:var(--accent)}.osw.ai-alert .osw-ai{animation:os-ai-pulse 1.15s ease-in-out infinite}@keyframes os-ai-pulse{50%{background:rgba(var(--accent-rgb),.28);text-shadow:0 0 16px var(--accent);transform:scale(1.12)}}
.osw-ai-panel{position:absolute;z-index:12;inset-inline-end:8px;top:40px;width:min(390px,calc(100% - 16px));padding:13px;border:1px solid rgba(var(--accent-rgb),.42);border-radius:14px;background:color-mix(in srgb,var(--panel2) 94%,transparent);box-shadow:0 20px 55px #000b,0 0 26px rgba(var(--accent-rgb),.12);backdrop-filter:blur(18px);color:var(--text)}
.osw-ai-panel header{display:flex;align-items:center;gap:10px}.osw-ai-panel header>span{display:grid;place-items:center;width:34px;height:34px;border-radius:10px;background:rgba(var(--accent-rgb),.16);font-size:18px}.osw-ai-panel header>div{display:flex;flex:1;min-width:0;flex-direction:column}.osw-ai-panel header b{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.osw-ai-panel header small,.osw-ai-panel footer>span{color:var(--muted);font-size:11px}.osw-ai-panel header>button{border:0;background:none;color:var(--muted);cursor:pointer;font-size:15px}
.osw-ai-actions{display:grid;gap:6px;margin:12px 0}.osw-ai-actions>button{display:flex;flex-direction:column;gap:2px;padding:9px 11px;border:1px solid var(--line);border-radius:9px;background:rgba(255,255,255,.035);color:var(--text);text-align:left;cursor:pointer}.osw-ai-actions>button:hover{border-color:rgba(var(--accent-rgb),.55);background:rgba(var(--accent-rgb),.10)}.osw-ai-actions span{color:var(--muted);font-size:11px}
.osw-ai-tip{margin-top:10px;color:var(--muted);font-size:11px}.osw-ai-context{display:grid;grid-template-columns:1fr auto;gap:2px 8px;margin-top:10px;padding:8px 10px;border-radius:8px;background:rgba(var(--accent-rgb),.1)}.osw-ai-context>span{grid-column:1/2;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;color:var(--muted);font-size:11px}.osw-ai-context>button{grid-column:2;grid-row:1/3;border:0;background:none;color:var(--accent);cursor:pointer}
.osw-ai-panel>label{display:flex;flex-direction:column;gap:5px;font-size:12px;font-weight:600}.osw-ai-panel textarea{box-sizing:border-box;width:100%;resize:vertical;padding:9px;border:1px solid var(--line);border-radius:8px;background:var(--bg);color:var(--text);font:inherit;font-weight:400}.osw-ai-panel footer{display:flex;align-items:center;justify-content:space-between;gap:10px;margin-top:10px}
.osw-ai-panel .osw-ai-agent{flex-direction:row;align-items:center;margin-top:9px;padding:8px 9px;border-radius:8px;background:rgba(var(--accent-rgb),.08);font-weight:500}
/* The window body IS the feature's container: while focused it carries id="feed", so it must scroll
   and size exactly like the real one. */
/* The window body HOLDS the real feed element while focused (moved, not copied — see os.js), so it
   is a plain flex container; the feed brings its own scrolling. The slot beside it shows a snapshot
   of what this window was displaying while another one has the live element. */
.osw-body{flex:1;min-height:0;display:flex;flex-direction:column;overflow:hidden;background:var(--bg)}
.osw-body > #feed{flex:1;min-height:0}
.osw-slot{flex:1;min-height:0;overflow:auto;padding:8px 10px 14px;opacity:.72}
.osw-slot:empty{display:none}
/* A parked Terminal/Code window is still a live application, not a dimmed snapshot. os.js mirrors
   the feed layout class while its nodes are parked; keep the full-height flex contract intact. */
.osw-slot.feed-term,.osw-slot.feed-code{display:flex;flex-direction:column;padding:0;overflow:hidden;opacity:1}
.osw-grip{position:absolute;inset-inline-end:0;bottom:0;width:16px;height:16px;cursor:nwse-resize;
  touch-action:none;background:linear-gradient(135deg,transparent 50%,var(--line) 50%)}

/* Taskbar */
/* Windows-style persistent flow: Start/search first, then open apps in stable left-to-right order,
   with the tray pinned right. Recentring the entire group whenever an app opens makes every target
   move under the pointer and does not match the requested desktop behaviour. */
.os-bar{position:relative;flex:0 0 48px;display:flex;align-items:center;justify-content:flex-start;gap:8px;
  padding:0 min(390px,32vw) 0 10px;
  background:color-mix(in srgb, var(--panel2) 88%, transparent);
  border-top:1px solid var(--line);backdrop-filter:blur(10px)}
.os-start{flex:0 0 auto;width:40px;height:40px;border-radius:10px;border:1px solid transparent;
  background:transparent;cursor:pointer;display:grid;place-items:center;padding:0}
/* Round, and big enough to read as a face — it is a character head, not a glyph. */
.os-start img{width:30px;height:30px;border-radius:50%;display:block;object-fit:cover}
.os-start:hover,.os-start.on{background:rgba(255,255,255,.12);border-color:var(--line)}
/* "New post" beside Start. The classic ＋ lives inside the timeline; in a windowed desktop that is
   the corner of one window, so the action gets a taskbar button of its own. */
.os-new{flex:0 0 auto;display:flex;align-items:center;gap:6px;height:38px;padding:0 14px;
  border-radius:9px;border:0;cursor:pointer;font-size:12.5px;font-weight:600;
  background:linear-gradient(135deg,var(--neon),var(--neon2));color:#fff}
.os-new:hover{filter:brightness(1.08)}
.os-new .ic{width:15px;height:15px}
@media (pointer:coarse){ .os-new{height:calc(42px / var(--zf, 1))} }
.os-tasks{position:static;flex:1 1 auto;width:auto;max-width:none;min-width:0;display:flex;justify-content:flex-start;
  gap:6px;overflow-x:auto;scrollbar-width:none}
.os-tasks::-webkit-scrollbar{display:none}
.os-task{flex:0 0 40px;width:40px;display:grid;place-items:center;height:38px;
  padding:0;border-radius:9px;border:1px solid transparent;background:rgba(255,255,255,.05);
  color:var(--text);cursor:pointer;font-size:12.5px}
.os-task:hover{background:rgba(255,255,255,.11)}
.os-task.on{background:color-mix(in srgb, var(--neon) 18%, transparent);
  border-color:color-mix(in srgb, var(--neon) 45%, var(--line))}
.os-task .ic{width:20px;height:20px;color:var(--neon);flex:0 0 auto}
.os-task span{display:none}
.os-tray{position:absolute;inset-inline-end:10px;top:0;height:100%;display:flex;align-items:center;gap:10px}
.os-exit{height:34px;padding:0 12px;border-radius:9px;border:1px solid var(--line);
  background:transparent;color:var(--muted);cursor:pointer;font-size:12px}
.os-exit:hover{color:var(--text);border-color:var(--neon)}
.os-clock b{font-size:13px;color:var(--text);font-weight:600}

/* Start menu */
/* The desktop is a z-index:300 overlay, and the app's page-level layers were all authored below it
   (.modal-bg 100, the music player 120, the upload badge 121, the offline bar 60). In OS mode they
   rendered BEHIND the desktop: present, invisible and unclickable. That is one bug wearing many
   hats — "can't post", "can't reply", "the AI splash buttons do nothing", "settings won't open" —
   because reply/quote/confirm/settings all go through modal(). Raised above .os-root (300) and
   .os-startmenu (320), and kept below the lightbox (400) and toasts (500), which already worked.
   The app's own full-screen panes (mail reading pane, DM thread, notification drawer) need nothing:
   they are position:fixed only inside @media(max-width:820px), and the desktop refuses to open
   below 1024px. Guarded by check_os_desktop.py's modal-buried assertion. */
/* Snap preview + Snap Layouts (Windows 11 style). The ghost is the whole point of edge snapping:
   it says where the window is ABOUT to go, so releasing the pointer is never a surprise. It sits
   under the windows (they are z-index'd from zTop upward) but above the wallpaper and the icons. */
.os-ghost{position:absolute;display:none;z-index:1;border-radius:12px;pointer-events:none;
  border:2px solid var(--neon);background:rgba(var(--accent-rgb),.14);
  box-shadow:0 0 30px rgba(var(--accent-rgb),.28) inset;transition:inset-inline-start .09s,top .09s,width .09s,height .09s}
.os-layouts{position:absolute;z-index:99999;display:grid;grid-template-columns:repeat(4,38px);
  gap:6px;padding:9px;border-radius:12px;background:var(--panel2);border:1px solid var(--line);
  box-shadow:0 14px 34px rgba(0,0,0,.5)}
.os-lay{width:38px;height:30px;border-radius:6px;border:1px solid var(--line);background:var(--panel);
  cursor:pointer;padding:3px;display:block;position:relative}
.os-lay:hover{border-color:var(--neon);background:rgba(var(--accent-rgb),.12)}
/* Each button is a tiny picture of the zone: the filled block IS the half/quarter it snaps to. */
.os-lay i{position:absolute;display:block;background:var(--neon);border-radius:2px;opacity:.85}
.os-lay-left i {inset-inline-start:3px;top:3px;bottom:3px;width:14px}
.os-lay-right i{inset-inline-end:3px;top:3px;bottom:3px;width:14px}
.os-lay-max i  {inset:3px}
.os-lay-tl i{inset-inline-start:3px;top:3px;width:14px;height:10px}
.os-lay-tr i{inset-inline-end:3px;top:3px;width:14px;height:10px}
.os-lay-bl i{inset-inline-start:3px;bottom:3px;width:14px;height:10px}
.os-lay-br i{inset-inline-end:3px;bottom:3px;width:14px;height:10px}

/* Taskbar search — Win11 puts one next to Start. This one searches Nostr (the start menu already
   filters apps), so it is deliberately wide enough to type a name or an npub into. */
.os-qbox{display:flex;align-items:center;gap:7px;height:34px;padding:0 12px;border-radius:17px;
  background:var(--panel2);border:1px solid var(--line);min-width:150px;max-width:290px;flex:0 1 260px}
.os-qbox .ic{width:15px;height:15px;color:var(--muted);flex:0 0 auto}
.os-qin{flex:1;min-width:0;background:none;border:0;outline:none;color:var(--text);font-size:13px;
  font-family:inherit;-webkit-appearance:none;appearance:none;box-shadow:none}
.os-qin:focus,.os-qin:focus-visible{outline:none;box-shadow:none;border:0}
/* type=search draws its own furniture in WebKit — a clear button and, in some builds, an inner
   border that shows through the pill as a second rectangle. */
.os-qin::-webkit-search-decoration,.os-qin::-webkit-search-cancel-button,
.os-qin::-webkit-search-results-button,.os-qin::-webkit-search-results-decoration{-webkit-appearance:none;display:none}
.os-qin::placeholder{color:var(--muted)}
/* Focus tints the pill's own border and nothing else — no ring, no second rectangle inside it. */
.os-qbox:focus-within{border-color:color-mix(in srgb, var(--neon) 55%, var(--line));box-shadow:none}
/* AFTER the base rule, or same-specificity cascade means these never apply. On a narrow desktop the
   task buttons matter more than the search box, so it shrinks and then goes. */
@media (max-width:1200px){ .os-qbox{flex:0 1 170px;min-width:120px} }
@media (max-width:1080px){ .os-qbox{display:none}.os-bar{padding-inline-end:300px} }

/* The community counters, moved into the start menu because the desktop hides the sidebar. */
/* Notification centre — the flyout Windows opens from the clock. Above .os-root (300) and the start
   menu (320) for the same reason modals are, and below the lightbox (400). */
.os-clock{position:relative;display:flex;flex-direction:column;align-items:flex-end;justify-content:center;
  gap:1px;padding:0 12px;height:40px;border:0;border-radius:8px;background:none;color:var(--text);
  cursor:pointer;line-height:1.15;font-family:inherit}
.os-clock:hover,.os-clock.on{background:var(--panel2)}
.os-clock b{font-size:12.5px;font-weight:600}
.os-clock span{font-size:10.5px;color:var(--muted)}
.os-dot{position:absolute;top:1px;inset-inline-end:2px;min-width:15px;height:15px;padding:0 4px;border-radius:8px;
  background:var(--neon);color:#04222a;font-size:9.5px;font-weight:700;font-style:normal;
  display:flex;align-items:center;justify-content:center}
/* Tall on purpose: this is a desktop, and the point of the centre over a phone's is that you can
   see more than three items without scrolling. It stops 84px short of the viewport so it never
   collides with the taskbar or runs off the top. */
.os-noti{position:fixed;inset-inline-end:8px;bottom:56px;z-index:330;width:min(430px,calc(100vw - 16px));
  max-height:calc(100vh - 84px);display:flex;flex-direction:column;border-radius:14px;
  background:linear-gradient(var(--panel2),var(--panel2)), var(--bg);
  backdrop-filter:blur(14px);-webkit-backdrop-filter:blur(14px);
  border:1px solid var(--line);box-shadow:0 18px 50px rgba(0,0,0,.55);
  overflow:hidden}
.os-noti-head{display:flex;align-items:center;justify-content:space-between;gap:10px;
  padding:12px 14px;border-bottom:1px solid var(--line)}
.os-noti-head b{font-size:14px;letter-spacing:.4px}
.os-noti-hb{display:flex;align-items:center;gap:2px}
.os-noti-x{background:none;border:0;color:var(--neon);cursor:pointer;font-size:12.5px;
  font-family:inherit;padding:4px 6px;border-radius:6px}
.os-noti-x:hover{background:rgba(var(--accent-rgb),.12)}
.os-noti-mail{display:flex;align-items:center;gap:9px;width:100%;padding:11px 14px;background:none;
  border:0;border-bottom:1px solid var(--line);color:var(--text);cursor:pointer;font-family:inherit;
  font-size:13px;text-align:start}
.os-noti-mail:hover{background:rgba(var(--accent-rgb),.09)}
.os-noti-mail .ic{width:17px;height:17px;color:var(--neon);flex:0 0 auto}
.os-noti-mail span{color:var(--muted)}
.os-noti-perm{display:flex;align-items:flex-start;gap:9px;width:100%;padding:11px 14px;
  border-bottom:1px solid var(--line);text-align:start;font-size:12.5px;color:var(--text)}
.os-noti-perm.as-btn{background:none;border:0;border-bottom:1px solid var(--line);cursor:pointer;
  font-family:inherit}
.os-noti-perm.as-btn:hover{background:rgba(var(--accent-rgb),.09)}
.os-noti-perm .ic{width:17px;height:17px;color:var(--neon);flex:0 0 auto;margin-top:1px}
.os-noti-perm span{min-width:0}
.os-noti-perm i{display:block;font-style:normal;color:var(--muted);font-size:11.5px;margin-top:2px}
.os-noti-list{overflow-y:auto;overscroll-behavior:contain;padding:4px 0}
/* The rows are the client's own .notif markup, so only the extra action strip is styled here. */
.os-noti-row .notif{position:relative;padding-inline-end:74px}
.os-noti-act{position:absolute;inset-inline-end:8px;top:50%;transform:translateY(-50%);display:flex;gap:4px}
.os-na{width:29px;height:29px;border-radius:8px;border:1px solid var(--line);background:var(--panel);
  color:var(--muted);cursor:pointer;display:flex;align-items:center;justify-content:center;padding:0}
.os-na:hover{border-color:var(--neon);color:var(--neon)}
.os-na .ic{width:14px;height:14px}

/* Networking widget — the tray icon and its flyout. Colour is the whole signal here, so the four
   states are deliberately far apart rather than four shades of the accent: a half-connected pool has
   to be distinguishable from a healthy one at a glance and at tray size. --neon is the theme's own
   accent (it means "working" everywhere else in this client), and the amber/red are hardcoded
   because they must not become unreadable under a light theme's palette swap. */
/* The notification BELL — the tray's own button, beside the relay icon and the clock. It carries
   the count (.os-dot), which used to sit on the clock: a number on a clock reads as part of the
   time and nothing about it says it can be pressed. Same geometry as .os-net so the tray stays one
   row of equal targets. */
.os-bell{position:relative;display:flex;align-items:center;justify-content:center;width:40px;height:40px;
  border:0;border-radius:8px;background:none;color:var(--muted);cursor:pointer;padding:0}
.os-bell:hover,.os-bell.on{background:var(--panel2)}
.os-bell .ic{width:23px;height:23px}
/* Something is waiting: the bell itself takes the accent, so it reads at a glance and not only by
   the badge — which is 15px and easy to miss on a wide screen. */
.os-bell:has(.os-dot){color:var(--neon)}
.os-net{position:relative;display:flex;align-items:center;justify-content:center;width:40px;height:40px;
  border:0;border-radius:8px;background:none;color:var(--muted);cursor:pointer;padding:0}
.os-net:hover,.os-net.on{background:var(--panel2)}
.os-net .ic{width:23px;height:23px}
.os-net.net-ok{color:var(--neon)}
.os-net.net-partial{color:#ffcf2b}
.os-net.net-connecting{color:var(--muted)}
.os-net.net-off{color:#ff6b6b}
/* The connecting state pulses. A static grey icon is indistinguishable from a disabled one, and
   "connecting" is precisely the state where somebody is waiting to see whether anything is happening. */
.os-net.net-connecting .ic{animation:os-net-pulse 1.4s ease-in-out infinite}
@keyframes os-net-pulse{0%,100%{opacity:.35}50%{opacity:1}}
@media (prefers-reduced-motion:reduce){ .os-net.net-connecting .ic{animation:none;opacity:.7} }

/* Narrower than the notification centre: this is a list of hostnames, not of posts. */
.os-net-panel{width:min(520px,calc(100vw - 16px));font-size:16px}
.os-net-sum{display:flex;align-items:flex-start;gap:14px;padding:18px 20px;border-bottom:1px solid var(--line)}
.os-net-sum .ic{width:30px;height:30px;flex:0 0 auto;margin-top:1px;color:var(--muted)}
.os-net-sum.net-ok .ic{color:var(--neon)}
.os-net-sum.net-partial .ic{color:#ffcf2b}
.os-net-sum.net-off .ic{color:#ff6b6b}
.os-net-sum span{min-width:0}
.os-net-sum b{display:block;font-size:18px}
.os-net-sum i{display:block;font-style:normal;font-size:14px;color:var(--muted);margin-top:4px}
.os-net-list{overflow-y:auto;overscroll-behavior:contain;padding:4px 0;max-height:46vh}
.os-net-row{display:flex;align-items:center;gap:12px;padding:14px 20px;font-size:16px}
/* min-width:0 on the NAME, so a long hostname ellipsises instead of squeezing the state text — which
   is the half of the row that answers the question the panel was opened to answer. */
.os-net-n{flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.os-net-s{flex:0 0 auto;color:var(--muted);font-size:14px}
.os-net-tag{font-style:normal;font-size:10px;color:var(--muted);border:1px solid var(--line);
  border-radius:5px;padding:1px 4px;margin-inline-start:6px}
.os-net-led{flex:0 0 auto;width:8px;height:8px;border-radius:50%;background:var(--muted)}
.os-net-led.ok{background:var(--neon)}
.os-net-led.quiet{background:#ffcf2b}
.os-net-led.connecting{background:var(--muted)}
.os-net-led.off{background:#ff6b6b}
.os-net-empty{padding:14px}
/* The community counters, moved here from the start menu. A grid rather than the start menu's wrap:
   this panel is 340px, and five free-flowing cells there left an orphan on its own line depending on
   the digit count — the numbers change, so a layout that only looks right at 3 digits is a layout
   that comes apart on its own. Three columns, so it is always two tidy rows. */
/* Scoped to the PANEL, not bare `.os-net-stats`: `.os-stats` (the start menu's flex row) is defined
   further DOWN this file, so at equal specificity it won and the grid never applied — five cells laid
   out 4+1 instead of 3+2. Measured, not assumed; see the same trap in the icon/label button rules. */
.os-net-panel .os-net-stats{display:grid;grid-template-columns:repeat(3,1fr);gap:12px 8px;
  padding:15px 18px 17px;margin-top:0;border-top:1px solid var(--line)}
.os-net-panel .os-net-stats .os-stat{min-width:0;font-size:14px;gap:6px}
.os-net-panel .os-net-stats .os-stat .ic{width:22px;height:22px}
.os-net-panel .os-net-stats .os-stat b{font-size:18px}
.os-net-panel .os-net-stats .os-stat i{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}

/* Arrival toasts, bottom-right, the way Windows does them. Above the notification centre so one
   landing while the panel is open is still readable. */
.os-toasts{position:fixed;inset-inline-end:10px;bottom:58px;z-index:345;display:flex;flex-direction:column-reverse;
  gap:8px;pointer-events:none;max-width:min(370px,calc(100vw - 20px))}
.os-toast{pointer-events:auto;display:flex;align-items:flex-start;gap:10px;padding:11px 13px;
  border-radius:12px;background:var(--panel2);border:1px solid var(--line);
  box-shadow:0 14px 36px rgba(0,0,0,.5);cursor:pointer;animation:os-slide .18s ease-out}
.os-toast:hover{border-color:var(--neon)}
.os-toast .ic{width:17px;height:17px;color:var(--neon);flex:0 0 auto;margin-top:1px}
.os-toast div{min-width:0}
.os-toast b{display:block;font-size:12.5px;margin-bottom:2px}
.os-toast span{display:block;font-size:12px;color:var(--muted);overflow:hidden;
  display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical}
@keyframes os-slide{from{opacity:0;transform:translateX(14px)}to{opacity:1;transform:none}}
@media (prefers-reduced-motion:reduce){ .os-toast{animation:none} }

/* "Click me for Desktop Mode" — a speech bubble off the instance logo, desktop widths only (the
   script gates on that too; this is belt and braces because the sidebar is hidden on mobile anyway). */
/* Everything below the font-size is in em, so --osh scales the whole bubble — arrow, padding and all.
   It hangs off <html> (see _showOsHint) and so never sees the desktop body{zoom:.67-.77} the logo it
   points at is drawn under; JS sets --osh to that zoom. Unset = 1, which is right at >=1921px and on
   the phone widths this is hidden at anyway. */
.os-hint{position:fixed;transform:translateY(-50%);z-index:360;
  display:flex;align-items:center;gap:.5em;white-space:nowrap;padding:.58em .75em .58em .92em;border-radius:.83em;
  background:var(--panel2);border:1px solid var(--neon);color:var(--text);font-size:calc(12px * var(--osh,1));
  box-shadow:0 6px 22px rgba(0,0,0,.45);animation:os-hint-in .25s ease-out}
.os-hint span{cursor:pointer}
.os-hint::before{content:'';position:absolute;inset-inline-end:100%;top:50%;transform:translateY(-50%);
  border:.5em solid transparent;border-inline-end-color:var(--neon)}
.os-hint-x{background:none;border:0;color:var(--muted);cursor:pointer;font-size:.92em;line-height:1;
  padding:.17em .25em;border-radius:.42em;font-family:inherit}
.os-hint-x:hover{color:var(--text);background:var(--panel)}
@keyframes os-hint-in{from{opacity:0;transform:translateY(-50%) translateX(-6px)}to{opacity:1;transform:translateY(-50%)}}
@media (max-width:1023px){ .os-hint{display:none} }
@media (prefers-reduced-motion:reduce){ .os-hint{animation:none} }

/* Account switcher — the anchored flyout (one click to switch) and the manage modal's rows. */
.acct-pop{min-width:min(268px,86vw)}
.acct-p{display:flex;align-items:center;gap:9px;width:100%;text-align:start;padding:7px 9px;
  border-radius:8px;border:0;background:none;color:var(--text);cursor:pointer;font-family:inherit;
  font-size:13px}
.acct-p:hover{background:rgba(var(--accent-rgb),.12)}
.acct-p img{width:26px;height:26px;border-radius:50%;object-fit:cover;flex:0 0 auto}
.acct-p > span{flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.acct-p.on{color:var(--neon)}
.acct-p .acct-cur{color:var(--neon);font-size:10px;flex:0 0 auto}
.acct-plain{padding-inline-start:11px;color:var(--muted)}
.acct-plain:hover{color:var(--text)}
.acct-sep{height:1px;background:var(--line);margin:5px 4px}

/* Account switcher rows */
.acct-list{display:flex;flex-direction:column;gap:6px;max-height:52vh;overflow-y:auto}
.acct-row{display:flex;align-items:center;gap:10px;text-align:start;padding:8px 10px}
.acct-row img{width:30px;height:30px;border-radius:50%;object-fit:cover;flex:0 0 auto}
.acct-n{flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.acct-m{font-style:normal;font-size:10.5px;color:var(--muted);margin-inline-start:6px;border:1px solid var(--line);
  border-radius:5px;padding:1px 4px}
.acct-cur{font-size:11px;color:var(--neon);font-weight:600;flex:0 0 auto}
.acct-x{font-style:normal;color:var(--muted);font-size:12px;padding:2px 5px;border-radius:6px;flex:0 0 auto}
.acct-x:hover{color:var(--bad,#f66);background:var(--panel)}
.acct-row.on{border-color:var(--neon)}

/* A folder window's contents: the same icon tiles the desktop uses, wrapped in the window. */
.osw-slot.os-folder{display:grid;grid-template-columns:repeat(auto-fill,96px);gap:6px;
  align-content:start;justify-content:start;padding:14px;overflow-y:auto;flex:1;min-height:0}
.osw-slot.os-folder .os-icon{position:static}

/* The Music view's header — one obvious action, so a library reads as a player and not a folder. */
.music-head{display:flex;align-items:center;gap:8px 12px;padding:2px 2px 12px;flex-wrap:wrap}
.music-head-primary,.music-head-secondary{display:flex;align-items:center;gap:8px;flex-wrap:wrap}
.music-count{flex:1 1 180px;min-width:0}
.music-head-secondary{margin-inline-start:auto}
.music-head .btn[disabled]{opacity:.45;cursor:default}
/* Android phones remain phones in landscape. The shell's own mobile boundary is 820px; stopping
   this compact row at 560px let a rotated phone fall back to the wrapping desktop toolbar. */
@media(max-width:820px){
  .music-head{align-items:stretch;gap:6px}
  .music-head-primary{order:1;display:grid;grid-template-columns:repeat(3,minmax(0,1fr));
    width:100%;gap:5px}
  .music-head-primary .btn{min-width:0;width:100%;padding:6px 4px;white-space:nowrap}
  .music-count{order:2;flex:1 1 100%;width:100%;padding-inline:2px}
  .music-head-secondary{order:3;margin-inline-start:0;width:100%}
}

/* A track the index knows about whose bytes are no longer on the server. Shown, not hidden: a
   library that quietly drops half its rows looks broken, and one that lies about them fails on play. */
.track.gone{opacity:.55}
.track.gone .track-name{text-decoration:line-through}
.track.gone .track-play{color:var(--muted);display:flex;align-items:center;justify-content:center}

/* Markdown tables (GFM) — READMEs use them for feature matrices. Their own scroll container, so a
   wide table scrolls inside itself instead of making the page scroll sideways. */
.md-table{overflow-x:auto;margin:12px 0;-webkit-overflow-scrolling:touch}
.md-table table{border-collapse:collapse;width:auto;min-width:100%;font-size:13.5px}
.md-table th,.md-table td{border:1px solid var(--line);padding:7px 11px;text-align:start;vertical-align:top}
.md-table thead th{background:var(--panel2);font-weight:600;white-space:nowrap}
.md-table tbody tr:nth-child(even){background:rgba(255,255,255,.02)}
.md-table code{white-space:nowrap}

/* An icon-only button in a row of labelled ones: square, so it does not inherit the label padding
   and end up a lopsided rectangle. (The refresh control was a ↻ TEXT glyph, which this app's font
   does not carry — it rendered as a tofu box.) */
/* Keep the button's OWN padding — that is what gives it height. Zeroing it to force a square
   collapsed the control to 11px tall, which is worse than the glyph it replaced. */
.btn.icon-only{display:inline-flex;align-items:center;justify-content:center;flex:0 0 auto}
.btn.icon-only .ic{margin:0}

/* Torrents: the tab strip carries the tab's action on the right. The strip is a sticky flex row, so
   the button rides in it rather than needing a row of its own underneath. */
.tor-tabs{align-items:center}
.tor-tabs .tor-sp{flex:1;min-width:8px}
.tor-tabs .tor-act{flex:0 0 auto;align-self:center;margin:0 0 8px}
.tor-tabs .tor-act:last-child{margin-inline-end:2px}
/* Below 560px the strip would have to scroll to reach the buttons, which defeats the point of
   putting them there. The list already refreshes every two seconds, so manual refresh is the one to
   drop; Add torrent is not. */
@media (max-width:560px){ .tor-tabs #tm-refresh{display:none} .tor-tabs{padding-inline-end:8px} }

/* Torrent manager (Discover → Torrents → Downloads). Card-per-torrent rather than a table: a table
   needs horizontal room this view does not have on a phone, and every column here is short enough to
   wrap into a meta row. */
.tm-list{display:flex;flex-direction:column;gap:9px;padding:2px 0 14px}
.tm-item{padding:11px 12px;border-radius:12px;background:var(--panel2);border:1px solid var(--line)}
.tm-item.done{border-color:color-mix(in srgb, var(--neon) 40%, var(--line))}
.tm-item.paused{opacity:.72}
.tm-head{display:flex;align-items:baseline;gap:10px}
.tm-name{flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-size:14px}
.tm-state{flex:0 0 auto;font-size:11px;color:var(--muted);text-transform:uppercase;letter-spacing:.5px}
.tm-item.done .tm-state{color:var(--neon)}
.tm-bar{height:6px;border-radius:4px;background:var(--panel);overflow:hidden;margin:9px 0 7px;
  border:1px solid var(--line)}
.tm-bar i{display:block;height:100%;background:linear-gradient(90deg,var(--cyan,#0bb6cf),var(--neon));
  transition:width .4s ease}
.tm-item.done .tm-bar i{background:var(--neon)}
/* Wraps rather than scrolls: five short facts fit two lines at 360px and none of them is worth a
   sideways scroll. */
.tm-meta{display:flex;flex-wrap:wrap;gap:4px 14px;font-size:12px;color:var(--muted)}
.tm-acts{display:flex;flex-wrap:wrap;gap:7px;margin-top:9px}
.tm-acts .btn{flex:0 1 auto}
@media (max-width:420px){
  .tm-meta{gap:3px 11px;font-size:11.5px}
  .tm-acts .btn{flex:1 1 auto}          /* thumb-sized targets when there is room for exactly two */
}

/* The Music APP — a player, not the file manager's Music folder. */
.music-app{display:flex;flex-direction:column;gap:9px;padding:10px 12px;min-height:0;height:100%}
.ma-now{position:relative;isolation:isolate;overflow:hidden;display:flex;align-items:center;gap:9px 11px;
  flex-wrap:wrap;padding:10px 12px;border-radius:14px;background:var(--panel2);border:1px solid var(--line)}
.ma-now>*:not(.ma-viz){position:relative;z-index:1}
.ma-art{width:44px;height:44px;border-radius:10px;flex:0 0 auto;display:flex;align-items:center;
  justify-content:center;background:linear-gradient(135deg,rgba(var(--accent-rgb),.28),transparent);
  border:1px solid var(--line)}
.ma-art .ic{width:26px;height:26px;color:var(--neon)}
.ma-meta{flex:1 1 170px;min-width:0}
.ma-meta b{display:block;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-size:15px}
.ma-ctl{display:flex;align-items:center;justify-content:center;gap:9px;flex-wrap:nowrap}
.ma-ctl .btn{width:36px;height:36px;padding:0;justify-content:center;border-radius:50%}
.ma-ctl #ma-play{width:44px;height:44px;box-shadow:0 0 18px rgba(var(--accent-rgb),.35)}
.ma-viz{display:block;position:absolute;z-index:0;inset:0;width:100%;height:100%;opacity:.34;
  pointer-events:none;background:radial-gradient(ellipse at bottom,rgba(0,240,255,.13),transparent 72%)}
.ma-tools{flex:1 1 100%;display:flex;justify-content:flex-end;gap:6px;flex-wrap:wrap}
/* The Music app's seek row. Full width of the panel (flex-basis 100% in a wrapping .ma-now), so the
 * bar is never squeezed to a stub by the buttons beside it. `touch-action:none` is load-bearing on a
 * phone: without it the browser claims the gesture as a page scroll and the pointermove events that
 * drag the handle simply stop arriving mid-drag. */
.ma-seek-row{flex:1 1 100%;display:flex;align-items:center;gap:10px;margin-top:0}
/* APK-only Bluetooth autoplay: one compact setting, not a debug panel. */
.ma-car{display:flex;align-items:center;margin:6px 0 0}
.ma-car-sw{display:flex;align-items:center;gap:8px;font-size:13px;color:var(--muted);cursor:pointer}
.ma-car-sw input{accent-color:var(--neon);width:16px;height:16px;flex:none}
/* The playlist bar. A horizontally SCROLLING strip, never a wrapping block: a library with a dozen
 * playlists would otherwise push the track list off a phone screen entirely, and the list is what
 * the screen is for. Same shape the folder chips use in Files for the same reason. */
.ma-pls{display:flex;gap:8px;align-items:center;overflow-x:auto;padding:10px 0 2px;
  scrollbar-width:none;-ms-overflow-style:none}
.ma-pls::-webkit-scrollbar{display:none}
.ma-pl{flex:0 0 auto;background:var(--panel2);border:1px solid var(--line);color:var(--muted);
  font-size:13px;font-weight:600;padding:7px 13px;border-radius:999px;cursor:pointer;
  white-space:nowrap;min-height:36px;transition:.14s}
.ma-pl:hover{border-color:var(--neon);color:var(--text)}
.ma-pl.on{background:linear-gradient(135deg,var(--neon),var(--neon2));color:#06010f;
  border-color:transparent;box-shadow:0 0 12px rgba(var(--accent-rgb),.35)}
.ma-pln{opacity:.7;font-weight:500;margin-inline-start:2px}
.ma-plnew{border-style:dashed;color:var(--cyan);border-color:var(--cyan)}
.ma-plsp{flex:0 0 auto;width:1px;height:22px;background:var(--line);margin:0 2px}
.ma-plpick{display:flex;flex-direction:column;gap:8px;max-height:46vh;overflow-y:auto;margin-top:8px}
.ma-plpickb{justify-content:flex-start;text-align:start}
/* A finger needs a bigger chip than a mouse, and this strip is scrolled as well as tapped. */
@media (max-width:820px){ .ma-pl{padding:9px 14px;min-height:40px} }
.ma-seek-row .ma-t{font-size:12px;color:var(--muted);font-variant-numeric:tabular-nums;min-width:38px}
.ma-seek-row .ma-t:last-child{text-align:end}
/* 14px, not the widget's 7 — and that is not a taste call. The client scales the whole app down on
 * desktop (body{zoom} ~.72 between 821 and 1920px), so a bar authored at 10px is drawn at seven
 * PHYSICAL pixels: measured, not assumed. This is the one control here you have to aim ALONG, and a
 * 7px-tall target is a miss waiting to happen with a mouse and hopeless with a thumb. */
.ma-seek{flex:1 1 auto;margin:0;height:14px;touch-action:none}
.ma-seek:focus-visible{outline:2px solid var(--neon);outline-offset:3px}
/* No zoom applies on a phone, so the number here is the number the thumb gets. */
@media (max-width:820px){ .ma-seek{height:16px} .ma-seek-row{gap:8px} }
.mp-seek{touch-action:none}
/* The mini player's playlist picker. Shares the search row, so it takes only what it needs and the
 * search box keeps the rest — a full-width select would push the search onto a second line in a
 * panel that is already short. */
.mp-search-row{display:flex;gap:6px;align-items:center}
.mp-pl{flex:0 1 42%;min-width:96px;max-width:150px;background:var(--panel2);color:var(--text);
  border:1px solid var(--line);border-radius:9px;padding:6px 8px;font-size:12px;font-family:inherit;
  min-height:32px}
.mp-pl:focus-visible{outline:2px solid var(--neon);outline-offset:2px}
.ma-ctl .btn.on{border-color:var(--neon);color:var(--neon)}
.ma-eq{flex:1 1 100%;padding:13px;border:1px solid var(--line);border-radius:12px;
  background:linear-gradient(145deg,rgba(var(--accent-rgb),.10),var(--panel));min-width:0}
.ma-eq-head{display:flex;align-items:baseline;justify-content:space-between;gap:12px;margin-bottom:10px}
.ma-eq-presets{display:flex;gap:7px;overflow-x:auto;padding-bottom:10px;scrollbar-width:none}
.ma-eq-presets::-webkit-scrollbar{display:none}
.ma-eq-preset{flex:0 0 auto;min-height:34px;padding:6px 12px;border-radius:999px;border:1px solid var(--line);
  background:var(--panel2);color:var(--muted);font:600 12px inherit;cursor:pointer}
.ma-eq-preset.on{color:#06010f;border-color:transparent;background:linear-gradient(135deg,var(--neon),var(--neon2))}
.ma-eq-bands{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:12px}
.ma-eq-bands label{display:flex;flex-direction:column;gap:7px;color:var(--muted);font-size:12px;min-width:0}
.ma-eq-bands label>span{display:flex;justify-content:space-between;gap:6px}
.ma-eq-bands output{color:var(--text);font-variant-numeric:tabular-nums}
.ma-eq-bands input{width:100%;accent-color:var(--neon);min-height:24px}
.ma-q{width:100%}
.music-app .music-list{flex:1;min-height:0;overflow-y:auto}
@media(max-width:560px){
  .music-app{padding:7px 8px;gap:7px}
  .ma-now{justify-content:center;padding:7px 8px;gap:6px 7px}
  .ma-art{display:none}
  .ma-meta{flex:1 1 76px}.ma-meta b{font-size:13px}.ma-meta .small{font-size:10px}
  .ma-ctl{flex:0 0 auto;order:2;gap:4px}
  .ma-ctl .btn{width:32px;height:32px}.ma-ctl #ma-play{width:38px;height:38px}
  .ma-viz{order:0}.ma-seek-row{order:5}
  .ma-tools{order:6;justify-content:center}.ma-tools .btn{min-height:32px;padding:5px 9px}
  .ma-eq{order:7}
  .ma-eq-bands{grid-template-columns:1fr;gap:8px}
}

/* The account chip at the foot of the start menu — where Windows 11 keeps it. */
.os-foot{display:flex;align-items:center;gap:8px;padding:9px 4px 2px;margin-top:8px;
  border-top:1px solid var(--line)}
.os-foot .spacer{flex:1}
.os-foot .os-exit{flex:0 0 auto}
.os-acct{display:flex;align-items:center;gap:9px;padding:6px 9px;border-radius:9px;border:0;
  background:none;color:var(--text);cursor:pointer;font-family:inherit;font-size:15px;min-height:48px;
  max-width:100%;min-width:0}
.os-acct:hover{background:rgba(var(--accent-rgb),.12)}
.os-acct img{width:36px;height:36px;border-radius:50%;object-fit:cover;flex:0 0 auto}
.os-acct > .ic{width:18px;height:18px;color:var(--muted);flex:0 0 auto}
.os-acct span{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.os-acct i{font-style:normal;color:var(--muted);font-size:10px;flex:0 0 auto}

.os-app-find{color:var(--neon)}
.os-app-find .ic{color:var(--neon)}

.os-stats{display:flex;flex-wrap:wrap;gap:9px 18px;padding:14px 4px 4px;margin-top:11px;
  border-top:1px solid var(--line)}
.os-stat{display:inline-flex;align-items:center;gap:7px;font-size:14px;color:var(--muted)}
.os-stat .ic{width:18px;height:18px;color:var(--muted)}
.os-stat b{color:var(--text);font-weight:600}
.os-stat i{font-style:normal}
/* A non-zero counter lights its indicator; a zero one stays grey, so the row reads at a glance. */
.os-stat.on .ic{color:var(--neon);filter:drop-shadow(0 0 4px rgba(var(--accent-rgb),.55))}

/* The sign-in screen must never be BEHIND the desktop. showAuth() suspends PosterChan OS before
   opening the gate, so in practice `.os-on` is already gone by the time this could apply — this is
   the belt to that braces, because the failure it prevents is silent and total: the gate is
   position:fixed at z-index 50 and #os-root is 300, so the login form renders complete and correct
   underneath the desktop icons, and every button on it belongs to whatever is drawn above. Someone
   who hits that concludes the login is broken and cannot detect their extension. Above the taskbar,
   below the lightbox, matching the other os-on overrides here. */
body.os-on .auth-gate{z-index:360}
body.os-on .modal-bg{z-index:340}
body.os-on .modal-bg.modal-sub{z-index:342}   /* …and its sub-modals stay above it */
body.os-on .offline-bar{z-index:330}
/* These two are appended to documentElement, so they are SIBLINGS of <body> and a `body.os-on …`
   rule cannot reach them — the music player kept z-index:120 and played on, invisible, under the
   desktop. os.js puts the class on <html> as well; keep both selectors so either spelling works. */
/* NO floating music player on the desktop. The Music WINDOW is the player, and a second smaller one
   appearing beside it — or in its place when you close it — is clutter with two sets of controls for
   one thing. Safe because there is no way to be left with unreachable playback: closing the Music
   window stops it, and merely switching windows leaves that window sitting in the taskbar. The
   z-index rule stays for the case where it IS shown (it is not, here) so it never renders under the
   desktop again. */
html.os-on #music-player{display:none}
html.os-on #music-player.mp,html.os-on #music-player.mp-mini{z-index:332}
html.os-on #upload-badge.upbadge{z-index:333}

/* WINDOWS 11 SIZES THIS MENU FOR A LIST, and so does this now. 520x60vh held about ten rows before
   it ran out of room, and the row that ran out was inside a scroller nobody could tell was one —
   reported as "start menu not scrollable". Wider and taller so the whole of a normal machine fits,
   and `overscroll-behavior:contain` so a flick at the end of the list stops there instead of
   scrolling the desktop behind it, which is what "not scrollable" feels like from the other side. */
.os-startmenu{position:absolute;inset-inline-start:10px;bottom:56px;
  width:min(780px,calc(100vw - 20px));height:min(920px,calc(100vh - 78px));
  max-height:calc(100vh - 78px);
  display:flex;flex-direction:column;gap:10px;padding:16px;border-radius:16px;
  /* --panel is translucent by design (rgba .72), which over a wallpaper leaves the app list reading
     through it. Paint the panel tint over an OPAQUE base so the menu keeps its colour and stops
     being see-through, with a blur behind for the frosted edge. */
  background:linear-gradient(var(--panel),var(--panel)), var(--bg);
  backdrop-filter:blur(14px);-webkit-backdrop-filter:blur(14px);
  border:1px solid var(--line);box-shadow:0 20px 60px rgba(0,0,0,.6);z-index:320}
/* A NATIVE APP THAT IS BEHIND IS STILL A WINDOW ON THE DESKTOP.
   This was `visibility:hidden;pointer-events:none`, and it is the whole of "Telegram disappeared":
   when its Wayland surface was put away the FRAME went with it — title bar, border, the lot — so
   an app that had merely gone behind another window left the screen entirely, with only a taskbar
   button to prove it had ever been there. (The rule it replaced was written to stop the frame
   showing as a black rectangle, which is a real thing to fix and not this way: the body is given
   something to say instead.)
   Now it behaves like any background window — visible, occluded by whatever is in front of it, and
   clickable, which is what brings the app back. See stashPlan() in osnative.js; the two halves are
   one fix. */
.osw.native-stashed .osw-body{position:relative}
.osw.native-stashed .osw-body::after{
  content:'App is behind another window · click to bring it forward';
  position:absolute;inset:0;display:flex;align-items:center;justify-content:center;
  padding:12px;text-align:center;font-size:13px;font-weight:700;letter-spacing:.025em;
  color:#fff;text-shadow:0 1px 3px #000;
  /* Capture can be unavailable (another native surface overlaps, grim is absent, or the 16 MiB
     memory cap rejects it). Never fall back to a near-black empty body: that is indistinguishable
     from Firefox failing to render. This unmistakable system card remains useful and clickable. */
  background:
    radial-gradient(circle at 22% 28%,rgba(0,240,255,.72),transparent 31%),
    radial-gradient(circle at 78% 74%,rgba(203,76,255,.68),transparent 34%),
    linear-gradient(135deg,#174b67 0%,#3b326a 52%,#175466 100%);
  border-top:1px solid var(--line)}
.osw.native-stashed.native-stash-preview .osw-body::after{
  content:'click to bring this window forward';align-items:flex-end;justify-content:flex-start;
  color:#fff;text-shadow:0 1px 3px #000;background-color:var(--panel,#0b0a14);
  background-image:linear-gradient(transparent 70%,rgba(0,0,0,.7)),var(--native-stash-preview);
  background-position:center;background-repeat:no-repeat;background-size:cover}
/* A window BEING DRAGGED already suppresses pointer events in its body; a stashed one must not, or
   the click that is supposed to raise it lands on nothing. */
.osw.native-stashed{pointer-events:auto}
.osw.native-fullscreen-frame{visibility:hidden;pointer-events:none}
.osw.osw-handoff-preview{pointer-events:none;opacity:.82;border-style:dashed;
  box-shadow:0 0 0 2px color-mix(in srgb,var(--neon) 60%,transparent),0 24px 70px rgba(0,0,0,.7)}
.osw-transfer-mark{height:100%;display:grid;place-items:center;color:var(--neon);font-weight:700;
  background:linear-gradient(135deg,rgba(0,240,255,.08),rgba(145,70,255,.12))}
.os-search{font-size:16px;min-height:46px;padding-inline:14px}
.os-applist{flex:1 1 auto;min-height:0;overflow-y:auto;overscroll-behavior:contain;display:grid;
  grid-template-columns:repeat(auto-fill,minmax(160px,1fr));gap:4px;align-content:start}
/* A HEADING SPANS THE WHOLE ROW. Without this it is a grid ITEM like any other and lands in one
   150px cell with apps beside it — "This computer" reading as a column header for one app. */
.os-applist-h{grid-column:1/-1}
/* The scroller says it is one. A list that is clearly cut off mid-row invites a scroll; a list that
   ends flush with a panel edge looks finished. */
.os-applist::-webkit-scrollbar{width:10px}
.os-applist::-webkit-scrollbar-thumb{background:var(--line);border-radius:6px}
.os-applist::-webkit-scrollbar-thumb:hover{background:var(--muted)}
.os-app{display:flex;align-items:center;gap:13px;padding:13px 14px;border-radius:10px;border:0;
  background:transparent;color:var(--text);cursor:pointer;text-align:start;font-size:17px;min-height:54px}
.os-app:hover{background:rgba(255,255,255,.09)}
.os-app .ic{width:26px;height:26px;color:var(--neon);flex:0 0 auto}
/* A program's REAL icon, at the same box as the sprite glyph beside it. `object-fit:contain` keeps
   a non-square icon from being stretched into a different logo. */
.os-app-ic{width:26px;height:26px;flex:0 0 auto;object-fit:contain;image-rendering:auto}
.os-app span{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}

/* The logo is the way in, so it has to look like a control rather than decoration. */
.brand-logo{cursor:pointer;transition:transform .15s, filter .15s}
.brand-logo:hover{transform:scale(1.06);filter:drop-shadow(0 0 8px rgba(var(--accent-rgb),.55))}

/* TOUCH (Android tablets, the Windows app on a 2-in-1). A 34px title-bar button is a mouse target,
   not a thumb one, and a 16px corner grip is unhittable with a finger.
   EVERY SIZE HERE IS DIVIDED BY --zf, and that is the point: a tablet at 1280px gets the desktop
   scaling tier (body{zoom:.67}), so a plain 44px button PAINTS at 29px — measured — and is no
   longer a thumb target at all. Dividing by the zoom factor makes these real, on-screen pixels. */
@media (pointer:coarse){
  .osw-bar{height:calc(44px / var(--zf, 1))}
  .osw-b{width:calc(46px / var(--zf, 1));height:calc(44px / var(--zf, 1));
         font-size:calc(15px / var(--zf, 1))}
  .osw-grip{width:calc(30px / var(--zf, 1));height:calc(30px / var(--zf, 1))}
  .os-task{height:calc(42px / var(--zf, 1));padding:0 calc(13px / var(--zf, 1))}
  .os-start{width:calc(42px / var(--zf, 1));height:calc(42px / var(--zf, 1))}
  .os-app{min-height:calc(46px / var(--zf, 1))}
  .os-exit{height:calc(40px / var(--zf, 1))}
}

/* ---- Folder sync -------------------------------------------------------------------------------
   A folder is a CARD, not a row: it carries a path, a status sentence, an exclusion box and four
   actions, and squeezing that into a list line is how the music library ended up with 18px of room
   for a song title. */
.sync-view{padding:16px;max-width:760px}
.sync-view h2{display:flex;align-items:center;gap:8px}
.sync-card{margin:12px 0;padding:14px;border-radius:14px;border:1px solid var(--line);
  background:linear-gradient(180deg,var(--panel2),var(--panel));display:flex;flex-direction:column;gap:10px}
.sync-head{display:flex;flex-wrap:wrap;align-items:baseline;gap:8px;min-width:0}
.sync-head b{font-size:17px}
.sync-head .muted{overflow-wrap:anywhere;word-break:break-all}   /* a path is an unbounded string */
.sync-status{color:var(--cyan)}
.sync-ex{display:flex;flex-direction:column;gap:4px}
.sync-ex .input{margin:0;font-size:15px;resize:vertical;min-height:52px;font-family:ui-monospace,monospace}
.sync-opts{display:flex;flex-wrap:wrap;gap:14px}
.sync-opts label{display:flex;align-items:center;gap:6px;font-size:15px}
/* ONE SIZE, BECAUSE THE ROW IS A SET OF PEERS. A wrapping flex row sizes every button to its own
   label, so nine actions whose names run from "Pause" to "Put 172 files back everywhere" came out
   nine different widths on three ragged lines — "make all buttons the same size. Very
   inconsistent". A grid of equal tracks is the fix rather than a min-width, because a min-width
   still lets the long ones grow: `auto-fit` + `1fr` makes every cell identical and reflows the
   count per row with the card's width, so it stays right on a phone and on a wide desktop without
   a second set of rules. `min-height` keeps them level when one label wraps to two lines. */
.sync-actions{display:grid;grid-template-columns:repeat(auto-fit,minmax(11.5rem,1fr));gap:8px}
.sync-actions .btn{min-height:40px;display:inline-flex;align-items:center;justify-content:center;
  gap:6px;text-align:center;padding-left:10px;padding-right:10px}
/* The sprite sits with the label, never stretched by the flex row it is in. */
.sync-actions .btn .ic{flex:0 0 auto}
.sync-actions .danger{color:var(--danger);border-color:rgba(255,107,139,.5)}
@media(max-width:820px){
  .sync-view{padding:12px}
  /* Narrower tracks so two still fit side by side on a phone, and a thumb-sized target. */
  .sync-actions{grid-template-columns:repeat(auto-fit,minmax(9rem,1fr))}
  .sync-actions .btn{min-height:44px}
  .sync-ex .input{font-size:16px}   /* 16px minimum or iOS zooms on focus and never zooms back */
}
/* What a sweep actually did, per file — the part that lets someone believe the headline, and the
   part that makes a failure actionable instead of one word. */
.sync-details{display:flex;flex-direction:column;gap:8px;max-height:280px;overflow-y:auto;
  padding:8px 10px;border-radius:10px;background:var(--panel2);border:1px solid var(--line)}
.sync-grp b{font-size:13px;color:var(--cyan)}
.sync-grp ul{margin:4px 0 0;padding-inline-start:18px}
.sync-grp li{font-size:13px;overflow-wrap:anywhere;word-break:break-word;line-height:1.5}
.sync-bg{display:flex;gap:10px;align-items:flex-start;margin-top:14px;padding:12px;border-radius:12px;
  border:1px solid var(--line);background:var(--panel2)}
.sync-bg input{margin-top:3px;flex:0 0 auto}
.sync-orphans{margin-top:16px;padding:12px;border-radius:12px;border:1px dashed var(--line);background:var(--panel2)}
.sync-orphan{display:flex;align-items:center;justify-content:space-between;gap:10px;padding:6px 0}
.sync-orphan span{overflow-wrap:anywhere;word-break:break-all;min-width:0}
.sync-pair{flex:1 1 100%;opacity:.75}
/* A folder that has not been started. Deliberately loud: it is the one moment where doing nothing is
   correct, and the screen has to say that rather than look like a sync that failed. */
.sync-new{display:flex;flex-direction:column;gap:4px;padding:10px 12px;border-radius:10px;
  border:1px solid var(--neon);background:rgba(var(--accent-rgb),.10)}
.sync-new b{color:var(--text)}

/* ===== Files → Blossom: an Explorer layout =======================================================
   The folder list is a PANE, not a row of chips. A chip row wraps to three lines the moment there
   are eight folders and pushes the files themselves below the fold; a sidebar is the shape every
   file manager converges on for the same reason. The markup is unchanged — these are the same
   .folder-chip buttons with the same handlers — so uploading, selecting and deleting are untouched.
   Every colour is a theme token, so all seven themes get it for free. */
.fx-explorer{display:grid;grid-template-columns:220px minmax(0,1fr);gap:0;
  border:1px solid var(--line);border-radius:14px;overflow:hidden;background:var(--panel)}
/* BACK AND UP. Square, quiet, and always PRESENT — disabled rather than removed, because a toolbar
   whose buttons appear and disappear as you navigate reflows the breadcrumbs under the cursor. Same
   size as the view toggles beside them so the bar reads as one row of controls. */
.fx-nav{display:flex;gap:4px;flex:0 0 auto}
.fx-nb{width:30px;height:30px;padding:0;border:1px solid var(--line);border-radius:8px;
  background:var(--panel);color:var(--text);font-size:16px;line-height:1;cursor:pointer;
  display:inline-flex;align-items:center;justify-content:center}
.fx-nb:hover:not(:disabled){border-color:var(--neon);color:var(--neon)}
.fx-nb:disabled{opacity:.35;cursor:default}
.fx-side{border-inline-end:1px solid var(--line);background:var(--panel2);padding:10px;min-width:0;
  max-height:70vh;overflow-y:auto}
.fx-side-mobile-head,.fx-locations-open{display:none}
.fx-tree{display:flex;flex-direction:column;gap:4px}
.fx-tree-node{display:flex;flex-direction:column;gap:2px}
.fx-tree-head{display:flex;align-items:center;gap:7px;width:100%;padding:8px;border:0;border-radius:8px;
  background:none;color:var(--text);text-align:start;cursor:pointer;font:inherit}
.fx-tree-head:hover,.fx-tree-head.active{background:rgba(var(--accent-rgb),.12)}
.fx-tree-head .chev{width:12px;color:var(--muted);font-size:12px;text-align:center}
.fx-tree-head b{font-size:13px}
.fx-tree-children{padding-inline-start:20px;border-inline-start:1px solid var(--line);margin-inline-start:13px}
.fx-tree-children.hidden{display:none}
.fx-main{min-width:0;padding:12px;display:flex;flex-direction:column;gap:10px}
.fx-search-results{display:flex;flex-direction:column;min-width:0;overflow:auto}
.fx-search-hit{display:grid;grid-template-columns:110px minmax(160px,1.5fr) minmax(120px,1fr) 90px 150px;
  gap:10px;align-items:center;width:100%;min-width:0;padding:9px 10px;border:0;border-bottom:1px solid var(--line);
  background:transparent;color:var(--text);text-align:start;cursor:pointer}
.fx-search-hit:hover,.fx-search-hit:focus-visible{background:rgba(var(--accent-rgb),.12);outline:none}
.fx-search-source{color:var(--cyan);font-size:12px;font-weight:700}
.fx-search-name{display:flex;align-items:center;gap:8px;min-width:0;font-weight:650;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.fx-search-name .fx-file-ic{flex:0 0 auto}
.fx-search-path{min-width:0;color:var(--muted);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.fx-search-size,.fx-search-date{color:var(--muted);font-size:12px;white-space:nowrap}
/* The chips become tree rows: full width, left-aligned, one per line. */
.fx-side .folder-bar{display:flex;flex-direction:column;gap:2px;padding:0;margin:0;overflow:visible}
.fx-side .folder-chip{width:100%;justify-content:flex-start;text-align:start;border-radius:8px;
  border:1px solid transparent;background:none;padding:8px 10px;font-size:14px;
  overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.fx-side .folder-chip:hover{background:rgba(var(--accent-rgb),.10)}
.fx-side .folder-chip.active{background:rgba(var(--accent-rgb),.16);border-color:var(--neon);color:var(--text)}
.fx-side .folder-chip.newfolder{margin-top:8px;border-top:1px solid var(--line);border-radius:0 0 8px 8px;
  padding-top:12px;color:var(--muted)}
.fx-side .folder-chip.delfolder{color:var(--danger)}
@media(max-width:820px){
  /* Mobile uses a real LOCATIONS DRAWER. A desktop tree flattened into two horizontal chip rails
     consumed the top third of the phone and duplicated Synced Folders. Keep the tree hierarchical,
     reveal it only when asked, and give the files the screen the rest of the time. */
  .fx-explorer{display:block;border:0;border-radius:0;background:transparent;overflow:visible}
  .fx-side{display:none;position:fixed;z-index:320;inset:0 auto 0 0;width:min(94vw,390px);
    max-height:none;padding:12px;background:var(--bg,#10131a);color:var(--text);border:0;
    border-inline-end:1px solid var(--line);box-shadow:18px 0 50px rgba(0,0,0,.75),
    100vw 0 0 100vw rgba(0,0,0,.58);overflow-y:auto}
  .fx-explorer.fx-locations-on>.fx-side{display:block}
  .fx-side-mobile-head{display:flex;align-items:center;justify-content:space-between;position:sticky;
    top:-12px;z-index:2;margin:-12px -12px 10px;padding:14px;background:var(--bg,#10131a);
    border-bottom:1px solid var(--line);font-size:18px}
  .fx-locations-open{display:inline-flex}
  .fx-tree{display:flex;flex-direction:column;gap:5px}
  .fx-tree-node{display:flex}
  .fx-tree-node>.fx-tree-head{min-height:48px;padding:10px;background:var(--panel2);border:1px solid var(--line)}
  .fx-tree-node>.fx-tree-head.active,.fx-tree-node>.fx-tree-head.mobile-on{border-color:var(--neon);color:var(--text)}
  .fx-tree-node>.fx-tree-head b{font-size:15px}
  .fx-tree-node>.fx-tree-children{padding-inline-start:18px;margin-inline-start:15px}
  .fx-side .folder-bar{flex-direction:column;overflow:visible;gap:3px}
  .fx-side .folder-chip{width:100%;min-height:46px;flex:none;border:1px solid var(--line);
    border-radius:9px;padding:10px 12px;background:var(--panel);color:var(--text)}
  .fx-main{padding:6px 0}
  .fx-bar{gap:7px;padding:0 2px 10px}
  .fx-nav{order:1}
  .fx-crumbs{order:2;flex:1;flex-wrap:nowrap;overflow:hidden}
  .fx-crumbs .fx-crumb:not(.on),.fx-crumbs .fx-sep{display:none}
  .fx-crumb.on{max-width:100%;font-size:17px}
  .fx-views{order:3}
  .fx-find{order:4!important;flex:1 1 100%;max-width:none;margin-top:2px!important;min-height:44px}
  .fx-newfolder span{display:none}
  .fx-newfolder{min-width:42px;justify-content:center}
  .drop-zone{margin:4px 2px;padding:8px;border-width:1px;border-radius:10px}
  .dz-inner{flex-direction:row;justify-content:center;flex-wrap:wrap;gap:6px;font-size:13px}
  .dz-inner>.dz-ic,.dz-inner>.muted{display:none}
}

/* ---- Synced folders, as roots beside the drive's own folders --------------------------------- */
.fx-sec{margin-top:12px;padding-top:10px;border-top:1px solid var(--line);display:flex;flex-direction:column;gap:2px}
.fx-sec > b{font-size:11px;letter-spacing:1px;text-transform:uppercase;color:var(--muted);padding:0 10px 6px}
.fx-secnote{padding:0 10px 4px}
.folder-chip .fx-n{margin-inline-start:auto;font-size:11px;font-weight:600;color:var(--muted);
  background:rgba(var(--accent-rgb),.12);border-radius:999px;padding:1px 7px}
.folder-chip.active .fx-n{background:rgba(0,0,0,.18);color:inherit}

/* ---- The toolbar: breadcrumbs on the left, the view switch on the right ----------------------- */
.fx-bar{display:flex;align-items:center;justify-content:space-between;gap:10px;flex-wrap:wrap;
  padding-bottom:8px;border-bottom:1px solid var(--line)}
.fx-crumbs{display:flex;align-items:center;gap:2px;min-width:0;flex-wrap:wrap}
.fx-crumb{background:none;border:0;font:inherit;font-size:14px;font-weight:600;color:var(--muted);
  cursor:pointer;padding:4px 7px;border-radius:8px;max-width:240px;
  overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.fx-crumb:hover:not(:disabled){background:rgba(var(--accent-rgb),.12);color:var(--text)}
.fx-crumb.on{color:var(--text);cursor:default}
.fx-sep{color:var(--muted);opacity:.55}
/* The drive's search box. Sits between the crumbs and the view toggle and takes the slack, so it is
 * a real box on a desktop and still shares one line with them on a phone. */
.fx-find{flex:1 1 180px;min-width:110px;max-width:340px;height:34px;padding:6px 10px;font-size:13px}
@media (max-width:520px){ .fx-find{flex:1 1 100%;max-width:none;order:3;margin-top:6px} }
.fx-views{display:flex;gap:4px;flex:0 0 auto}
.fx-sort-wrap{display:flex;align-items:center;gap:5px;color:var(--muted);font-size:12px;white-space:nowrap}
.fx-sort{height:32px;max-width:132px;border:1px solid var(--line);border-radius:8px;background:var(--panel2);
  color:var(--text);padding:4px 24px 4px 8px;font:inherit;font-size:12px}
.fx-sort-dir{width:32px;height:32px;border:1px solid var(--line);border-radius:8px;background:none;
  color:var(--muted);cursor:pointer}
.fx-sort-dir:hover{color:var(--text);border-color:var(--cyan)}
.fx-newfolder{display:inline-flex;align-items:center;gap:6px;background:rgba(var(--accent-rgb),.12);
  border:1px solid var(--cyan);border-radius:8px;color:var(--text);cursor:pointer;font:inherit;
  font-size:13px;font-weight:650;line-height:1;padding:6px 10px;min-height:32px;white-space:nowrap}
.fx-newfolder:hover{background:rgba(var(--accent-rgb),.22);border-color:var(--neon)}
.fx-newfolder .ic{width:14px;height:14px}
.fx-vw{background:none;border:1px solid var(--line);border-radius:8px;color:var(--muted);cursor:pointer;
  font-size:15px;line-height:1;padding:6px 10px;min-height:32px}
.fx-vw:hover{color:var(--text);border-color:var(--cyan)}
.fx-vw.on{color:var(--text);border-color:var(--neon);background:rgba(var(--accent-rgb),.16)}

/* ---- Details view ----------------------------------------------------------------------------
   The SAME cards as the tiles view — same classes, same handlers — laid out as rows. Both the header
   and the rows are grid children of the same container and share one template, so the headings stay
   over their columns however wide the pane is. */
.files-grid.details{display:block;padding:0;gap:0}
/* THE ACTIONS COLUMN IS A FIXED WIDTH, NOT `auto`, AND THAT IS LOAD-BEARING. Every row is its own
   grid container, so an `auto` last column resolves against ITS OWN buttons: a row with four of them
   takes 52px out of the name column and a header with none takes zero, so the headings sat 52px off
   their columns and the sizes appeared under "Type". An encrypted row (no Copy URL) and a synced
   folder row (no buttons at all) would each land somewhere different again.
   scripts/check_files_explorer.py measures this per column, per row. */
/* One width for both, because a synced folder's rows now carry four buttons (download, keep a copy,
   rename, delete) — the same count as the drive's. `.nosel` used to narrow this to 40px, from when
   the only action there was Download; a row that outgrows the column steals the width back from the
   NAME, which is the drift a fixed width exists to prevent. */
.files-grid.details{--fx-acts:124px}
.fx-cols,.files-grid.details .file-card.row{
  display:grid;grid-template-columns:24px minmax(0,1fr) 88px 140px 172px var(--fx-acts);align-items:center;gap:8px}
.files-grid.details.nosel .fx-cols,
.files-grid.details.nosel .file-card.row{grid-template-columns:minmax(0,1fr) 88px 140px 172px var(--fx-acts)}
/* “This Computer” lives beside the explorer sidebar, so viewport media queries do not describe the
   width its details table actually has. Keep Type out so Name remains readable, but Date created is
   essential list metadata and must not disappear on desktop. The phone rule below still removes it
   when four useful columns genuinely cannot fit. */
#host-pane .fx-cols,
#host-pane .files-grid.details .file-card.row{
  grid-template-columns:24px minmax(140px,1fr) 88px 172px var(--fx-acts)
}
#host-pane .fx-c-type,#host-pane .fx-type{display:none}
.fx-cols{position:sticky;top:0;z-index:2;padding:4px 8px;background:var(--panel);
  border-bottom:1px solid var(--line)}
.fx-col{background:none;border:0;font:inherit;font-size:12px;font-weight:700;letter-spacing:.5px;
  text-transform:uppercase;color:var(--muted);cursor:pointer;text-align:start;padding:6px 4px;
  border-radius:6px;display:flex;align-items:center;gap:4px;min-width:0}
.fx-col:hover{color:var(--text);background:rgba(var(--accent-rgb),.10)}
.fx-col.on{color:var(--text)}
.fx-arrow{font-size:9px;opacity:.9}
.fx-c-size,.fx-c-type,.fx-c-modified{justify-content:flex-start}
/* Rows override the tile chrome: no card border, no rounded corners, no thumbnail box. */
.files-grid.details .file-card.row{border:0;border-bottom:1px solid var(--line);border-radius:0;
  background:none;padding:6px 8px;overflow:visible}
.files-grid.details .file-card.row:hover{background:rgba(var(--accent-rgb),.07)}
.files-grid.details .file-card.row.selected{background:rgba(var(--accent-rgb),.16);box-shadow:none}
.files-grid.details .file-card.row.isdir{cursor:pointer}
/* The checkbox is a normal column here, not an overlay on a photo — so it loses the contrast ring it
   needs when it sits ON an image. */
.files-grid.details .file-card.row .selbox{position:static;width:18px;height:18px;box-shadow:none}
.fx-name{display:flex;align-items:center;gap:8px;min-width:0;color:var(--text);text-decoration:none}
.fx-name .fname{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-size:14px}
.fx-name:hover .fname{color:var(--cyan)}
.fx-ic{flex:0 0 auto;font-size:17px;line-height:1}
.fx-size,.fx-type,.fx-mod{font-size:12px;color:var(--muted);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.files-grid.details .file-card.row .fc-acts{display:flex;align-items:center;gap:2px;justify-self:end}
.fx-mobile-actions{display:none}
/* Every action is a plain icon button on a row — the tile view positions .del/.copy absolutely over
   the thumbnail, which would drop them out of the row entirely. */
.files-grid.details .file-card.row .del,
.files-grid.details .file-card.row .copy{position:static;background:none;border:0;padding:4px 6px;line-height:1}
.files-grid.details .file-card.row .dlbtn,
.files-grid.details .file-card.row .movebtn,
.files-grid.details .file-card.row .dlsync{background:none;border:0;color:var(--muted);cursor:pointer;
  padding:4px 6px;line-height:1}
.files-grid.details .file-card.row .dlbtn:hover,
.files-grid.details .file-card.row .movebtn:hover,
.files-grid.details .file-card.row .dlsync:hover{color:var(--cyan)}
.files-grid.details .file-card.row.enc{border-bottom-color:rgba(255,210,90,.35)}
/* A synced folder's TILES: no thumbnails are possible (the bytes are ciphertext), so the icon block
   carries the whole card and must not keep the 140px photo well. */
.files-grid .file-card.isdir{cursor:pointer}
.files-grid .file-card.isdir .file-icon{height:96px;font-size:34px}
/* The synced-folder row's four actions are one family — a button that keeps the browser's default
   chrome beside three that don't reads as a different KIND of control, which is how "Save a copy"
   looked next to Download. Both views put these inside .fc-acts, so one selector covers tiles and
   rows alike. */
/* THE CHOOSER. Big targets with a name and a line of what each one is — the point of asking is that
   the answer is obvious, so it is not a list of two words. */
.openwith{display:flex;flex-direction:column;gap:8px;margin-top:4px}
.ow-opt{display:flex;align-items:center;gap:12px;width:100%;padding:12px 14px;cursor:pointer;
  border:1px solid var(--line);border-radius:12px;background:var(--panel2);color:var(--text);
  font-family:inherit;text-align:start}
.ow-opt:hover{border-color:var(--neon);background:rgba(var(--accent-rgb),.10)}
.ow-ic{font-size:26px;line-height:1;flex:0 0 auto;width:34px;text-align:center}
.ow-t{display:flex;flex-direction:column;gap:2px;min-width:0}
.ow-t b{font-size:15px}
.ow-t i{font-style:normal;font-size:12px;color:var(--muted)}
.fc-acts .dlsync,.fc-acts .keepsync,.fc-acts .rnsync,.fc-acts .rmsync{background:none;border:none;
  color:var(--muted);cursor:pointer;font-size:15px;padding:4px 6px;line-height:1}
.fc-acts .dlsync:hover,.fc-acts .keepsync:hover,.fc-acts .rnsync:hover{color:var(--cyan)}
/* Deletion is the one action here that reaches every other device and takes something away. */
.fc-acts .rmsync:hover{color:var(--danger,#ff6b6b)}
.fc-acts button[disabled]{opacity:.5;cursor:default}

@media(max-width:820px){
  /* The sidebar is a horizontal strip on a phone, so the synced section joins the same strip rather
     than stacking a second scroller under it. */
  .fx-sec{margin-top:8px;padding-top:8px;flex-direction:row;align-items:center;gap:6px;
    overflow-x:auto;scrollbar-width:none}
  .fx-sec::-webkit-scrollbar{display:none}
  .fx-sec > b{padding:0 4px 0 0;flex:0 0 auto}
  .fx-secnote{padding:0;flex:0 0 auto}
  .folder-chip .fx-n{margin-inline-start:6px}
  /* Type and Date created are the first columns to go: a name and a size are what a phone screen has
     room for, and a truncated date is worse than no date. */
  .fx-cols,.files-grid.details .file-card.row{grid-template-columns:44px minmax(0,1fr) 66px 44px;gap:4px}
  .fx-cols{padding-inline:4px}
  .files-grid.details.nosel .fx-cols,
  .files-grid.details.nosel .file-card.row{grid-template-columns:minmax(0,1fr) 66px 44px}
  .fx-c-type,.fx-c-modified,.fx-type,.fx-mod{display:none}
  .files-grid.details .file-card.row>.fc-acts{display:none}
  .fx-mobile-actions{display:block;position:relative;justify-self:end;z-index:4}
  .fx-mobile-actions>summary{list-style:none;width:42px;height:42px;border-radius:50%;cursor:pointer;
    display:grid;place-items:center;color:var(--muted);font-size:24px;line-height:1}
  .fx-mobile-actions>summary::-webkit-details-marker{display:none}
  .fx-more-dots{display:block;width:4px;height:4px;border-radius:50%;background:currentColor;
    box-shadow:0 -7px currentColor,0 7px currentColor}
  .fx-mobile-actions[open]>summary{background:rgba(var(--accent-rgb),.16);color:var(--text)}
  .fx-mobile-actions>.fc-acts{display:flex!important;position:absolute;inset-inline-end:0;top:44px;
    z-index:20;padding:7px;background:var(--panel);border:1px solid var(--line);border-radius:10px;
    box-shadow:0 12px 30px rgba(0,0,0,.65);gap:4px;min-width:max-content}
  .fx-mobile-actions>.fc-acts button{position:static!important;display:grid;place-items:center;
    min-width:42px;min-height:42px;padding:8px!important;background:var(--panel2)!important;
    border:1px solid var(--line)!important;border-radius:8px!important}
  .fx-cols .fx-c-acts{display:block}
  /* Everything you tap gets a thumb-sized box. A 27px sort heading and a 27px crumb are the two
     controls this screen is navigated with. */
  .fx-crumb{font-size:15px;max-width:150px;display:inline-flex;align-items:center;min-height:36px}
  .fx-col{min-height:36px}
  .files-grid.details .file-card.row{min-height:56px;padding:6px 4px}
  .fx-vw{min-height:38px;min-width:42px}
}

/* ---- Files → Blossom: the drive's home ----------------------------------------------------
 * What you land on: folders, not every blob on the account. Tiles rather than the details rows
 * the file list uses, because there are a handful of these and each is a destination — the row
 * layout earns its keep when you are comparing sizes and dates, which folders do not have. */
.fx-home{ display:grid; gap:10px; padding:12px 0;
  grid-template-columns:repeat(auto-fill, minmax(160px, 1fr)); }
/* The home is written INTO #bl-grid, which is `.files-grid` — itself a grid of 150px file-card
 * tracks. A single child of that lands in ONE track, so the tile grid inside it had ~150px to lay
 * out in, fell to one column, and stacked every folder into a strip that ran off the bottom of the
 * page. MEASURED, not reasoned: .fx-home came out 150px wide and 801px tall in a 673px pane. It has
 * to span the host's tracks. Written as a child selector so it applies only while the home is the
 * thing in there — the file grid needs no cleanup when it takes the element back. */
.files-grid > .fx-home{ grid-column:1/-1; }
.fx-used{ grid-column:1/-1; padding:2px 2px 6px; font-size:.9rem; color:var(--muted); }
.fx-used b{ color:var(--text); font-size:1.05rem; }
.fx-home-sec{ grid-column:1/-1; margin:6px 0 0; font-weight:600; opacity:.7; font-size:.85rem; }
.fx-home-synced{display:contents}
.fx-home-tile{ display:flex; flex-direction:column; align-items:flex-start; gap:2px;
  padding:14px; border-radius:12px; text-align:start; cursor:pointer;
  background:var(--panel, rgba(255,255,255,.04)); border:1px solid var(--line, rgba(255,255,255,.10));
  color:inherit; font:inherit; transition:border-color .12s, transform .12s; }
.fx-home-tile:hover{ border-color:var(--accent, #4dd0e1); transform:translateY(-1px); }
.fx-home-ic{ font-size:1.6rem; line-height:1.1; }
/* The name must not push the tile wider than its column — a long folder name would otherwise
 * stretch the grid track and misalign every other tile in the row. */
.fx-home-name{ font-weight:600; max-width:100%; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
@media (max-width:820px){
  /* The horizontal source strip already contains synced folders. Repeating each one as a large
     landing-page tile made mobile Blossom look like two unrelated file managers stacked together.
     Mobile uses one compact, conventional folder list; desktop retains its roomy tile dashboard. */
  .fx-home{grid-template-columns:1fr;gap:5px;padding:8px 0}
  .fx-home-synced{display:none}
  .fx-home-tile{min-height:48px;padding:8px 10px;display:grid;
    grid-template-columns:28px minmax(0,1fr) auto;align-items:center;column-gap:8px;border-radius:9px}
  .fx-home-ic{font-size:1.25rem;grid-row:1;grid-column:1}
  .fx-home-name{grid-row:1;grid-column:2}
  .fx-home-sub{grid-row:1;grid-column:3;text-align:end;white-space:nowrap}
  .fx-used{padding:2px 2px 8px}
  .fx-home-sec{margin:8px 0 2px}
}

/* ---- A stream in its own window (?popout=1) ------------------------------------------------
 * Everything that navigates is gone: this window has one job, and a sidebar in it is a way to
 * lose the stream you opened it for. The view fills the window so the player and the chat get
 * the whole of a monitor when it is dragged to one. */
body.popout .sidebar,
body.popout .rightbar,
body.popout .mobilenav,
body.popout .topbar,
body.popout #st-back{ display:none !important; }
body.popout #feed{ max-width:none; width:100%; margin:0; padding:0; }
/* .stream-view / .stream-layout / .stream-main / .stream-player / .stream-chat are the real class
 * names in this view — checked against the markup rather than guessed, because a popout styled
 * against invented selectors looks exactly like one that was never styled. */
body.popout .stream-view{ height:100dvh; display:flex; flex-direction:column; padding:0; }
body.popout .stream-layout{ flex:1 1 auto; min-height:0; }
body.popout .stream-main{ min-height:0; }

/* ---- Encrypted thumbnails in a synced folder -----------------------------------------------
 * The icon box doubles as the preview surface: when the blob has been fetched and decrypted its
 * bitmap becomes the background and the glyph/extension label is hidden. Doing it this way means
 * a card is the same size before and after the picture arrives, so a folder does not reflow line
 * by line as thumbnails land. */
.file-icon[data-thumb]{ background-size:cover; background-position:center; }
.file-icon.has-thumb > *{ visibility:hidden; }
.file-icon.has-thumb{ color:transparent; }
.fx-ic.has-thumb{width:34px;height:34px;border-radius:6px;background-size:cover;
  background-position:center;color:transparent}

/* The popout is ONE VIEW, so it must not take the app's desktop shrink.
 *
 * The client scales the whole app down (body{zoom:.67-.77} between 821 and 1920px) because the full
 * layout — sidebar, feed, rightbar — has three columns to fit. A stream window has one, and it
 * opens at whatever fraction of a screen fits, so it lands in the .67 tier and draws everything at
 * two-thirds size: reported as "all the elements are super tiny". At zoom 1 the content is at its
 * natural size and the layout flexes to the window, which is the auto-adjustment wanted.
 *
 * Placed after the tiers and matched on `body.popout` so it wins on both order and specificity, and
 * .app's height has to come with it: those rules pair height:calc(100dvh / zoom) with the zoom, so
 * leaving the old height behind makes the window scroll by exactly the difference. */
body.popout{ zoom:1 !important; --zf:1 !important; }
body.popout .app{ height:100dvh !important; }
body.popout .main{ height:100% !important; }

/* The popout window's own layout.
 *
 * The stream view is built for a page with a sidebar beside it: a header row, the poster's about
 * block, breadcrumbs, and a player sized to a column. In a window whose entire job is "the stream
 * and its chat" all of that is competing for the space the two things actually need, and the
 * chat — the reason for the second monitor — ends up as a strip at the bottom.
 *
 * So the window is a two-pane split: the player takes the room it can, the chat takes a real column
 * beside it and scrolls on its own. Below 900px there is no room for a column, so it stacks. */
body.popout .stream-view > .row:first-child,
body.popout .stream-view .about,
body.popout .fx-bar{ display:none !important; }
/* The cap is on .stream-view itself (max-width:900px; margin:0 auto), NOT on #feed — which is why
 * overriding #feed did nothing and the window kept drawing a 900px column down the middle of a
 * 1800px screen. The player and the chat carry fixed viewport heights (62vh / 44vh) on the elements
 * too, sized for a page you scroll; in a window that fits exactly one thing, both have to become
 * "take what is left". */
/* .app is `grid-template-columns:300px 1fr` — the sidebar column. display:none on the sidebar hides
 * the ELEMENT and leaves the TRACK, so the window kept a 300px empty column and gave the stream what
 * was left. Measured, not guessed: in a 1600px window .stream-view came out 300px wide. The grid has
 * to become one column, and .app/.main must stop being 100vh-with-padding. */
body.popout .app{ grid-template-columns:1fr !important; padding:0 !important; column-gap:0 !important; }
body.popout .main{ height:100dvh !important; }
body.popout .stream-view{ max-width:none !important; padding:0 !important; margin:0 !important;
  width:100% !important; }
body.popout .stream-layout{
  display:flex !important; flex-direction:row; align-items:stretch; gap:0;
  height:100%; min-height:0; }
body.popout .stream-main{ flex:1 1 auto; min-width:0; display:flex; flex-direction:column;
  justify-content:center; align-items:center; }
/* `.stream-player` IS the <video> — the class is on the element itself (see the .stream-player rule
 * in the Streams block), NOT on a wrapper around one. Two earlier passes styled `.stream-player
 * video`, a selector that matches nothing in this markup, which is why the window kept rendering
 * exactly as if none of it had been written.
 *
 * The bands: stretching the element (flex:1 1 auto / height:100%) makes the BOX the size of the
 * pane, and a 16:9 picture in a 1600x768 box letterboxes itself inside — the black above and below
 * in the render. So the element is sized by its own aspect ratio instead: width/height auto with
 * max-width/height 100% scales a replaced element down to fit while keeping its ratio, so the box
 * IS the picture and there is nothing left over to paint black. The pane centres what remains. */
body.popout .stream-player{
  flex:0 1 auto; min-height:0;
  width:100% !important; height:auto !important;
  max-width:100% !important; max-height:100% !important;
  /* `auto 16/9`, not a plain ratio: the keyword means "use the intrinsic ratio when there is one,
   * and this only when there is not". Before metadata arrives a <video> with no poster has NO
   * natural size, and CSS gives it the default 300x150 — so sizing purely from content drew a
   * postage stamp in the middle of a black window for the seconds hls.js takes to attach, and
   * permanently when the stream URL is dead (the popout hides the button row and an empty #st-note,
   * so there is nothing else on screen to explain it). Once the first frame lands the real ratio
   * takes over and the box is the picture again. */
  aspect-ratio:auto 16/9;
  /* Only reachable for a stream whose shape the pane cannot match — a portrait phone broadcast in a
   * landscape window, where some empty space is unavoidable. Transparent, so it is the window's own
   * background rather than a black block sitting inside a dark one. */
  object-fit:contain;
  margin:0 !important; background:transparent !important;
  border:0 !important; border-radius:0 !important; box-shadow:none !important; }
/* "Pop out player" and "Open stream URL" inside a window that IS the popped-out player: the first
 * does nothing you have not already done, and the second sends you out to a raw HLS URL.
 * NOT `:last-child` — a stream with a `summary` renders `.about` after this row, and every live
 * stream measured on the relay has one, so the qualifier meant the row was never matched. There is
 * only one `.row` in `.stream-main`, so it needs no qualifier at all. */
body.popout .stream-main > .row{ display:none !important; }
body.popout #st-note:empty{ display:none; }
body.popout .stream-chat{
  flex:0 0 340px !important; width:auto !important; height:auto !important;
  max-width:38vw; min-width:260px; display:flex; flex-direction:column; min-height:0;
  border:0 !important; border-radius:0 !important;
  border-inline-start:1px solid var(--line, rgba(255,255,255,.10)) !important; }
body.popout .stream-chat .scm-list{ flex:1 1 auto; min-height:0; overflow-y:auto; }
body.popout .stream-layout.chat-hidden .stream-chat{ display:none !important; }
@media (max-width:900px){
  body.popout .stream-layout{ flex-direction:column; }
  body.popout .stream-chat{ flex:1 1 auto; max-width:none; border-inline-start:0;
    border-top:1px solid var(--line, rgba(255,255,255,.10)); }
}

/* ── CHAT-ONLY POPOUT (`?popout=1&chat=1`) ──────────────────────────────────────────────────────
 * The chat on its own, for the second monitor. The player is not hidden as a styling preference —
 * it must not be RUNNING: a second decode of the same broadcast is the bandwidth and CPU the full
 * popout's teardown exists to avoid, and here the user is still watching in the original tab.
 *
 * THE CHAT IS FLUID, and that is the whole ask: it fills whatever the window is and grows when the
 * window is dragged bigger. Everything below is undoing a fixed size that is correct in the other
 * popout and wrong in this one — `flex:0 0 340px` with `max-width:38vw` is a COLUMN BESIDE A VIDEO,
 * so at 420px wide it would sit at 340px with dead space, and at 1200px it would still be 340px.
 * These come after the `body.popout` rules and match with equal specificity, so they win on order;
 * the `!important`s are only there to beat the `!important`s above, not to win a fight with a tier.
 *
 * The big title and byline are hidden because they are sized for a page and would eat the top third
 * of a narrow window; the stream is identified in the WINDOW TITLE instead (see _chatPopout's
 * document.title), which is what a user reads when the window is one of several on a monitor. */
/* `.stream-view` keeps the height `body.popout` already gives it (100dvh, flex column). Two other
 * shapes were tried here and both are wrong, recorded so they are not tried again: `height:100%`
 * needs a definite parent height and #feed has none, so it resolves to CONTENT height — a
 * 200-message backlog put the composer at y=5326; and `position:fixed; inset:0` was chasing a 6px
 * overflow that does not exist. That 6px is `@keyframes fade{from{transform:translateY(6px)}}` on
 * `.stream-view`, frozen at its first keyframe because headless --dump-dom never presents a frame.
 * A layout measured through an in-flight entry animation is measuring the animation. */
body.popout-chat .stream-main{ display:none !important; }
body.popout-chat .stream-view > h1.av-title,
body.popout-chat .stream-view > .av-by{ display:none !important; }
body.popout-chat .stream-layout{ flex-direction:column !important; gap:0 !important; height:100% !important; }
body.popout-chat .stream-chat{
  flex:1 1 auto !important; width:100% !important; height:100% !important;
  max-width:none !important; min-width:0 !important; min-height:0 !important;
  border:0 !important; border-radius:0 !important; }
/* The list takes the slack and the composer stays pinned: without `min-height:0` a flex item will
 * not shrink below its content, so a long backlog pushes the input off the bottom of the window —
 * the one control the chat popout exists to reach. */
body.popout-chat .scm-list{ flex:1 1 auto !important; min-height:0 !important; overflow-y:auto !important; }
body.popout-chat .scm-input{ flex:0 0 auto !important; }

/* Bulk select in a synced folder — the conflict-storm cleanup tool */
.sync-selbar{display:flex;align-items:center;gap:8px;flex-wrap:wrap;padding:6px 2px 10px}
.files-grid.selmode .file-card,.files-grid.selmode .fx-row{cursor:pointer}
.files-grid.selmode .selrow{outline:2px solid var(--cyan,#0bc);outline-offset:-2px;border-radius:8px}

/* The reorder handles in Settings → Sidebar: a fixed-width column on the LEFT, like every list
 * with a grip — mid-row arrows between label and switch read as clutter ("ugly as hell"). */
.nav-ord{display:inline-flex;gap:2px;width:52px;flex:0 0 52px;justify-content:flex-start}
.nav-ord .mini{background:none;border:1px solid var(--line,rgba(255,255,255,.15));border-radius:6px;
  color:var(--muted);cursor:pointer;font-size:10px;line-height:1;padding:4px 6px}
.nav-ord .mini:hover{color:var(--cyan,#0ff);border-color:var(--cyan,#0ff)}
/* The 🔒 sits in the switch's column, so every label starts at the same x whether a row toggles
 * or not — the clustered layout ("enable buttons next to the up-down buttons") only reads clean
 * when the name column is actually a column. */
.nav-lock{display:inline-flex;width:44px;flex:0 0 44px;justify-content:center}


/* rows a permission gate removed — its own class so hidden's and nav-off's owners never fight it */
.gated-off{display:none !important}

/* ---------- Shorts (Discover → Shorts): a vertical snap feed, one video on screen at a time ----- */
.shorts-wrap{height:calc(100dvh - 130px);overflow-y:auto;scroll-snap-type:y mandatory;
  border-radius:14px;background:var(--panel,#101014);-webkit-overflow-scrolling:touch}
.short-card{position:relative;height:100%;scroll-snap-align:start;scroll-snap-stop:always;
  background:#000;overflow:hidden;border-radius:14px;margin-bottom:10px}
.short-media,.short-media video,.short-media img{position:absolute;inset:0;width:100%;height:100%;
  object-fit:cover;background:#000}
.short-top{position:absolute;top:10px;right:10px;z-index:3}
.short-top .short-mute{background:rgba(0,0,0,.55);color:#fff;border:none;border-radius:50%;
  width:38px;height:38px;font-size:16px}
.short-overlay{position:absolute;left:12px;right:64px;bottom:14px;z-index:3;color:#fff;
  text-shadow:0 1px 3px rgba(0,0,0,.8)}
.short-auth{display:flex;align-items:center;gap:8px;cursor:pointer;margin-bottom:6px}
.short-auth img{width:32px;height:32px;border-radius:50%;border:1.5px solid rgba(255,255,255,.85);flex:none}
.short-auth b{color:#fff;font-size:14px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.short-title{font-size:14px;line-height:1.35;max-height:3.9em;overflow:hidden}
.short-rail{position:absolute;right:10px;bottom:18px;z-index:3;display:flex;flex-direction:column;gap:12px}
.short-act{background:rgba(0,0,0,.55);border:none;border-radius:50%;width:44px;height:44px;
  display:flex;align-items:center;justify-content:center;color:#fff;cursor:pointer}
.short-act .ic{width:20px;height:20px;fill:currentColor}
/* Desktop: a centered portrait column, like every phone-shaped surface here */
@media (min-width:1024px){ .shorts-wrap{max-width:440px;margin:0 auto;height:calc(100vh - 150px)} }
/* Phones: full-bleed under the topbar, above the bottom bar */
@media (max-width:700px){ .shorts-wrap{height:calc(100dvh - 118px);border-radius:10px} }

/* Shorts browse grid — the front door; the full-screen player is where a tap lands */
.shorts-grid{display:grid;grid-template-columns:repeat(2,1fr);gap:8px;padding:4px}
@media (min-width:700px){ .shorts-grid{grid-template-columns:repeat(3,1fr)} }
@media (min-width:1100px){ .shorts-grid{grid-template-columns:repeat(4,1fr)} }
.short-tile{position:relative;aspect-ratio:9/16;border-radius:12px;overflow:hidden;cursor:pointer;
  background:#000;border:1px solid var(--line)}
.short-tile img{width:100%;height:100%;object-fit:cover;display:block}
.short-tile-blank{display:flex;align-items:center;justify-content:center;height:100%;font-size:34px}
.short-dur{position:absolute;top:6px;right:6px;background:rgba(0,0,0,.65);color:#fff;
  font-size:11px;padding:1px 6px;border-radius:8px}
.short-tile-t{position:absolute;left:8px;right:8px;bottom:26px;color:#fff;font-size:12px;line-height:1.3;
  max-height:2.6em;overflow:hidden;text-shadow:0 1px 3px rgba(0,0,0,.85)}
.short-tile-a{position:absolute;left:8px;right:8px;bottom:6px;display:flex;align-items:center;gap:5px;
  color:#ddd;font-size:11px;overflow:hidden;white-space:nowrap;text-overflow:ellipsis;
  text-shadow:0 1px 3px rgba(0,0,0,.85)}
.short-tile-a img{width:16px;height:16px;border-radius:50%;flex:none}
.short-top{display:flex;justify-content:space-between;left:10px;right:10px;position:absolute;top:10px;z-index:3}
.short-back{background:rgba(18,10,35,.92);color:#fff;border:1px solid var(--neon);border-radius:10px;
  min-height:42px;padding:8px 14px;font-weight:800;box-shadow:0 0 16px rgba(var(--accent-rgb),.42)}

/* Files → the per-device trash block: subordinate to the folder chips, never dressed like them */
/* The .fx-trash block is gone: the trash is browsed as a folder in the ordinary listing now,
   so it wears the file-card/details-row styles every other folder does. */

/* The synced view's selection bar is injected INSIDE the file grid, and a grid treats any child
 * as a tile — so the bar was squeezed into a column at the same level as the folder cards,
 * bleeding into them. It is a toolbar: it spans the whole row, always. Same for the details
 * header when it rides inside the grid. */
.files-grid .sync-selbar, .files-grid .fx-cols{grid-column:1 / -1}

/* 🔊 READ ALOUD — the stop control, which the feature shipped without.
   Fixed to the viewport rather than to the post, because the post is exactly what scrolls away and
   the whole point is to still be able to stop it. Above the mobile nav bar, clear of the safe area,
   and never wider than the screen. */
.narrate-chip{position:fixed;left:50%;transform:translateX(-50%);
  bottom:calc(76px + env(safe-area-inset-bottom,0px));z-index:70;
  max-width:min(92vw,26rem);display:inline-flex;align-items:center;gap:8px;
  padding:10px 16px;border-radius:999px;cursor:pointer;
  border:1px solid var(--line);background:var(--panel2);color:var(--text);
  font-size:14px;font-weight:600;box-shadow:0 6px 24px rgba(0,0,0,.35);
  overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.narrate-chip .muted{color:var(--muted);font-weight:400}
.narrate-chip:hover{transform:translateX(-50%) translateY(-1px)}
@media(min-width:821px){ .narrate-chip{bottom:calc(28px + env(safe-area-inset-bottom,0px))} }

/* Folder Sync — the two numbers every report in this feature has implicitly been about. Chips
   rather than a sentence: they are read at a glance and compared against each other, which is the
   whole point of showing them together. */
.sync-counts{display:flex;flex-wrap:wrap;gap:6px;margin:2px 0 4px}
.sync-counts span{padding:2px 8px;border-radius:999px;background:var(--panel2);border:1px solid var(--line)}
/* Bytes the folder names and the store does not hold: nothing can fetch these, so the chip is not
   the same colour as the ones that are simply counts. */
.sync-counts span.warn{color:var(--danger);border-color:rgba(255,107,139,.5)}

/* ── PosterChanOS shell ──────────────────────────────────────────────────────────────────────────
   The launcher, the taskbar of REAL windows and the system panel. Present only when PosterChan is
   the desktop; everywhere else the markup is never emitted, so none of this applies.
   Fixed to the screen edges rather than laid out in the page: the desktop underneath scrolls and
   these must not go with it. */
/* THE SHELL ADDS NO BARS OF ITS OWN.
   It used to: a launcher fixed bottom-left, a window list bottom-centre and a system panel
   bottom-right, floating at z-index 310 over a desktop that already has a taskbar with a start
   menu, a task list and a tray. Two of everything, none of it matching, all of it over the content
   — "a bar that gets in the way", "an ugly bar for wifi", "not even looks part of the desktop".
   The apps went into the start menu, the machine's windows into the same taskbar as ours, and the
   readings below into the same tray as the clock. `.os-launcher` and `.os-taskbar` are still
   emitted by `render()` for a standalone host (and by the tests, which click them), so they are
   still laid out — as an ordinary inline row, never as furniture nailed to the screen. */
.os-launcher, .os-taskbar, .os-panel{ display:flex; gap:6px; align-items:center; min-width:0; }
.os-sys{ display:flex; gap:2px; align-items:center; min-width:0; }
/* SCOPED TO THE SHELL'S OWN CONTAINERS, and that scoping is the whole of a bug reported as "the
   start menu is blank". `.os-app` is ALSO the desktop start menu's row (styled far above, at
   .os-startmenu / .os-applist) and `.os-task` is ALSO the desktop taskbar's window button — the
   shell reused two class names that were already taken. Being later in the file with equal
   specificity, this block won every property it shared with them: the row padding, and, until the
   line above was fixed, the COLOUR — a hard-coded #d6e2ff read through `var(--fg, …)`, a custom
   property this stylesheet has never defined. Measured on the five light themes, that put the app
   labels at 1.14-1.40:1 against their own panel (Cherry Blossom 1.22, Professional 1.21, Windows 98
   1.40, Windows XP 1.14, Anime Girl 1.17) — pale blue on near-white, which is a start menu that
   opens, is full of apps, and reads as empty. */
.os-launcher .os-app, .os-taskbar .os-task, .os-chip{
  background:none; border:1px solid transparent; color:var(--text);
  font:inherit; font-size:13px; padding:4px 10px; border-radius:7px; cursor:pointer;
  white-space:nowrap;
}
/* In the tray a chip is a tray button: the same height and weight as the relay light and the bell
   beside it, so the row reads as one row rather than as something bolted onto the end of it. */
.os-sys .os-chip{ font-size:12px; padding:3px 7px; opacity:.92; }
.os-sys .os-chip:hover{ opacity:1; }
.os-launcher .os-app:hover, .os-taskbar .os-task:hover, .os-chip:hover{ border-color:var(--neon); }
.os-taskbar .os-task.on{ background:color-mix(in srgb, var(--neon) 14%, transparent); border-color:var(--neon); }
/* A reading that could not be taken is DIM and marked, never a plausible default — a full-strength
   wifi icon on a machine whose NetworkManager is dead is a lie that costs somebody an hour. */
.os-chip.os-unknown{ color:var(--muted,#7a8699); font-style:italic; }
@media (max-width:900px){ .os-sys .os-chip{ padding:3px 5px; } }

/* The tray popovers: network, volume, brightness, power. Above the desktop and its windows, since
   they are opened FROM the taskbar, which is above both. */
.os-pop{
  position:fixed; z-index:340; min-width:230px; max-width:320px;
  background:var(--panel2,rgba(20,20,34,.97)); border:1px solid var(--line);
  border-radius:12px; box-shadow:0 18px 44px rgba(0,0,0,.55); overflow:hidden;
  backdrop-filter:blur(10px); font-size:13px;
}
.os-pop-h{ padding:9px 12px 7px; font-weight:600; border-bottom:1px solid var(--line); }
.os-pop-b{ padding:8px; display:flex; flex-direction:column; gap:2px; max-height:44vh; overflow-y:auto; }
.os-pop-b.os-pop-profs{ flex-direction:row; flex-wrap:wrap; gap:6px; }
.os-pop-row{
  display:flex; align-items:center; justify-content:space-between; gap:10px; width:100%;
  background:none; border:1px solid transparent; color:var(--text); font:inherit;
  padding:7px 9px; border-radius:8px; cursor:pointer; text-align:left;
}
.os-pop-row:hover{ background:rgba(60,232,255,.10); border-color:var(--neon,#3ce8ff); }
.os-pop-row.on{ background:rgba(60,232,255,.16); }
.os-pop-row.os-pop-danger:hover{ background:rgba(255,80,80,.16); border-color:#ff6b6b; }
.os-pop-nm{ overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
.os-pop-sig{ color:var(--muted,#7a8699); font-size:12px; flex:none; }
.os-pop-none{ padding:10px; color:var(--muted,#7a8699); }
.os-pop-b .os-pop-range{ flex:1; min-width:0; }
/* Only the tiny one-control volume/brightness popup puts its DIRECT slider and value on one row.
   The full Volume Mixer also contains sliders, but they are nested inside Devices and Apps
   sections. Matching every descendant turned those sections into one long horizontal strip and
   clipped the right/bottom of the mixer. Keep its sections vertical and let .os-pop-b scroll. */
.os-pop-b:has(> .os-pop-range){ flex-direction:row; align-items:center; gap:10px; padding:12px; }
.os-pop-val{ flex:none; min-width:44px; text-align:right; color:var(--muted,#7a8699); }
.os-pop-f{ padding:0 8px 8px; }
.os-pop-btn{
  background:none; border:1px solid var(--line); color:var(--text); font:inherit;
  font-size:12px; padding:5px 10px; border-radius:7px; cursor:pointer;
}
.os-pop-btn:hover, .os-pop-btn.on{ border-color:var(--neon,#3ce8ff); background:rgba(60,232,255,.12); }
/* The action row on "This Computer" — Up / New folder / dotfiles, and the selection actions when
   there are any. Its own rule because it is the one control strip the drive has no equivalent of:
   a Blossom blob has no parent directory and no dotfiles. Everything else that tab draws is the
   drive's own markup (`.fx-bar`, `.fx-cols`, `.files-grid`, `.file-card`), which is the point —
   the previous version invented eleven class names and the stylesheet defined none of them. */
.fx-actions{
  display:flex; align-items:center; gap:6px; flex-wrap:wrap;
  padding:6px 2px 8px; border-bottom:1px solid var(--line); margin-bottom:8px;
}
.fx-actions .spacer{ flex:1; }

/* ── The Windows 11 tray group, bottom-right ────────────────────────────────────────────────────
 *
 * Network, sound and battery are ONE button. It was six text chips reading `95% Tribble` `72%`
 * `33%` `off` `100% ⚡` `⏻` — five bare percentages with no picture of what they measured. Windows
 * groups exactly these three and puts everything else inside the flyout, which is what was asked
 * for: "we need the desktop experience to mirror win11 basically — including all the control in the
 * bottom-right". */
.os-tray-group{
  display:flex; align-items:center; gap:9px; height:40px; padding:0 12px;
  background:transparent; border:1px solid transparent; border-radius:8px;
  color:var(--text); cursor:pointer;
}
.os-tray-group:hover, .os-tray-group.on{ background:var(--panel2); border-color:var(--line); }
.os-tray-group .ic{ width:22px; height:22px; flex:0 0 auto; }
/* The battery is drawn at the tray's own colour; the CHARGE inside it takes the accent, so a glance
   separates the shell from the level without reading a number. */
.os-bat-fill{ color:var(--neon,#3ce8ff); }
.os-bat.os-bat-low{ color:#ff8a6b; }
.os-bat-bolt{ color:var(--neon,#3ce8ff); }

/* ── Quick Settings ─────────────────────────────────────────────────────────────────────────────
   A tile grid over sliders, the way Windows lays it out — a tile is a state you toggle and a slider
   is a value you drag, and mixing them into one list makes both harder to hit. */
.os-pop-quick{ width:480px; max-width:calc(100vw - 16px); font-size:16px; }
/* Power actions are a real control surface, not a cramped context menu. A 2x2 grid gives every
   destructive/session action a square, unmistakable target while retaining the same right-edge
   anchor and visual language as Quick Settings. */
.os-pop-power{ width:480px; }
.os-pop-power .os-pop-h{ display:flex; align-items:center; gap:10px; padding:16px 18px 13px; font-size:17px; }
.os-pop-power .os-pop-h .ic{ width:22px; height:22px; flex:none; color:var(--neon,#3ce8ff); }
.os-pop-power .os-pop-profs{ padding:12px 14px 4px; }
.os-pop-power-acts{
  padding:14px; gap:10px; display:grid; grid-template-columns:repeat(2,minmax(0,1fr));
  overflow:visible; max-height:none;
}
.os-pop-power-acts .os-pop-row{
  display:flex; flex-direction:column; align-items:center; justify-content:center; gap:11px;
  aspect-ratio:1.3/1; min-height:112px; padding:14px; border-radius:14px; font-size:15px;
  background:var(--panel2); border-color:var(--line); text-align:center;
}
.os-pop-power-acts .os-pop-row:hover{ transform:translateY(-1px); }
.os-pop-power-acts .os-pop-row .ic{ width:30px; height:30px; flex:none; opacity:.9; color:var(--neon,#3ce8ff); }
.os-pop-power-acts .os-pop-row.os-pop-danger .ic{ color:#ff6b6b; opacity:1; }
@media(max-width:480px){
  .os-pop-power{ width:calc(100vw - 16px); }
  .os-pop-power-acts .os-pop-row{ min-height:96px; }
}

/* Desktop tabs must remain readable after the shell's desktop zoom is applied. Phone sizing is
   already touch-oriented and stays unchanged. */
@media(min-width:900px){
  .tl-tab,.ntab,.prof-tab,.cmp-tab,.rv-tab,.us-tab,.ftab,.st-tab,.np-tab,.bg-tab{
    font-size:16px; min-height:42px;
  }
}
.os-qs{ padding:16px; display:flex; flex-direction:column; gap:16px; }
.os-qs-tiles{ display:grid; grid-template-columns:1fr 1fr; gap:8px; }
.os-qs-tile{
  display:flex; flex-direction:column; align-items:flex-start; gap:2px;
  padding:16px; min-height:96px; border-radius:12px; cursor:pointer;
  border:1px solid var(--line); background:var(--panel2); color:var(--text); text-align:start;
}
.os-qs-tile:hover{ border-color:var(--neon,#3ce8ff); }
.os-qs-tile.on{ background:rgba(60,232,255,.16); border-color:var(--neon,#3ce8ff); }
.os-qs-tile .ic{ width:28px; height:28px; margin-bottom:5px; color:var(--neon,#3ce8ff); }
.os-qs-tile b{ font-size:16px; font-weight:600; }
.os-qs-tile span{
  font-size:14px; color:var(--muted,#7a8699);
  max-width:100%; overflow:hidden; text-overflow:ellipsis; white-space:nowrap;
}
.os-qs-row{ display:flex; align-items:center; gap:9px; }
.os-qs-ic{
  display:flex; align-items:center; justify-content:center; width:30px; height:30px; flex:none;
  border:0; border-radius:8px; background:transparent; color:var(--text); cursor:pointer;
}
.os-qs-ic:hover{ background:var(--panel2); }
.os-qs-ic.os-qs-static{ cursor:default; }
.os-qs-ic .ic{ width:17px; height:17px; }
.os-qs-range{ flex:1; min-width:0; accent-color:var(--neon,#3ce8ff); }
.os-qs-val{ flex:none; min-width:40px; text-align:end; font-size:12px; color:var(--muted,#7a8699); }
.os-qs-more{
  display:flex; align-items:center; justify-content:center; width:26px; height:26px; flex:none;
  border:0; border-radius:7px; background:transparent; color:var(--muted,#7a8699); cursor:pointer;
}
.os-qs-more:hover{ background:var(--panel2); color:var(--text); }
.os-qs-more .ic{ width:15px; height:15px; }
.os-qs-foot{
  display:flex; align-items:center; justify-content:space-between; gap:8px;
  padding-top:12px; border-top:1px solid var(--line); font-size:15px; color:var(--muted,#7a8699);
}
.os-qs-bat{ display:flex; align-items:center; gap:8px; font-weight:600; }
.os-qs-bat .ic{ width:24px; height:24px; }
.os-qs-link{
  display:flex; align-items:center; gap:9px; min-height:42px; padding:9px 14px; border-radius:9px;
  border:1px solid var(--line); background:transparent; color:var(--text);
  font:inherit; font-size:15px; font-weight:600; cursor:pointer;
}
.os-qs-link:hover{ border-color:var(--neon,#3ce8ff); }
.os-qs-link .ic{ width:20px; height:20px; }
/* A sub-panel's way back. In the header so it reads as the panel's own title bar, not as a row. */
.os-pop-back{
  display:inline-flex; align-items:center; justify-content:center; width:24px; height:24px;
  margin-inline-end:6px; vertical-align:-6px;
  border:0; border-radius:6px; background:transparent; color:var(--text); cursor:pointer;
}
.os-pop-back:hover{ background:var(--panel2); }
.os-pop-back .ic{ width:15px; height:15px; }
.os-mix-row{ padding:4px 2px; }
.os-mix-body{ flex:1; min-width:0; display:flex; flex-direction:column; gap:2px; }
.os-mix-nm{ font-size:11.5px; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
.os-mix-section{display:flex;flex-direction:column;gap:6px;padding:5px 0 10px;border-bottom:1px solid var(--line)}
.os-mix-section:last-child{border-bottom:0;padding-bottom:2px}
.os-mix-title{font-size:10.5px;font-weight:750;letter-spacing:.08em;text-transform:uppercase;color:var(--muted)}
.os-mix-titlebar{display:flex;align-items:center;justify-content:space-between;gap:8px}.os-mix-titlebar .btn{letter-spacing:0;text-transform:none;display:flex;align-items:center;gap:5px}
.os-mix-device{gap:4px;margin:0}.os-mix-device>span{font-size:11.5px;color:var(--muted)}
.os-mix-device select{width:100%;height:31px;padding:4px 8px;font-size:12px}
.os-mix-master{background:rgba(var(--accent-rgb),.045);border-radius:8px}
.os-bt-tools{display:flex;align-items:center;justify-content:space-between;gap:10px;padding-bottom:10px;border-bottom:1px solid var(--line)}.os-bt-tools label{display:flex;align-items:center;gap:8px;font-weight:650}.os-bt-list{display:flex;flex-direction:column;gap:6px;padding-top:8px}.os-bt-row{display:flex;align-items:center;gap:9px;padding:9px;border:1px solid var(--line);border-radius:10px;background:rgba(var(--accent-rgb),.035)}.os-bt-icon{width:24px;height:24px;display:grid;place-items:center;color:var(--neon)}.os-bt-icon svg{width:18px;height:18px}.os-bt-name{flex:1;min-width:0;display:flex;flex-direction:column;gap:2px}.os-bt-name b{font-size:12.5px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.os-bt-name small{font-size:10.5px;color:var(--muted)}.os-bt-actions{display:flex;gap:5px;flex-wrap:wrap;justify-content:flex-end}.os-bt-actions .btn{padding:6px 8px;font-size:10.5px}

.os-applist-h{
  padding:10px 12px 4px; font-size:11px; letter-spacing:.08em; text-transform:uppercase;
  color:var(--muted,#7a8699);
}

/* A LINUX APP IN A POSTERCHAN WINDOW. The body is a HOLE — the app is a real compositor surface
   held over exactly that rectangle, so anything drawn here would be underneath it and invisible.
   What it does show is what is on screen while the window is being dragged, when the surface is
   stashed and only the frame moves. */
.osw-native .osw-body{
  background:linear-gradient(145deg,rgba(var(--accent-rgb),.08),rgba(var(--accent-rgb),.025)),
             var(--panel,#12101b);
  border-top:1px solid var(--line);
}
.osw-nat-note{
  display:flex; align-items:center; justify-content:center; height:100%;
  color:var(--muted,#9a94aa); font-size:12px; letter-spacing:.04em;
}

/* ── PosterChanOS first run ─────────────────────────────────────────────────────────────────────
 *
 * Above everything, including the desktop (z-index 300) and its popovers (340): this is the screen
 * a machine shows before it will show anything else, so there is nothing legitimate to be behind.
 * `osfr-on` hides the classic client underneath rather than letting it paint through the card. */
.osfr{ position:fixed; inset:0; z-index:400; display:flex; align-items:center; justify-content:center;
       background:radial-gradient(1200px 800px at 50% -10%, rgba(60,232,255,.08), transparent 60%), #05070c;
       padding:24px; overflow:auto; }
html.osfr-on body > .app, html.osfr-on #os-root{ visibility:hidden; }
.osfr-card{ width:min(560px, 100%); border-radius:16px; padding:26px 26px 20px; }
.osfr-dots{ display:flex; gap:7px; justify-content:center; margin-bottom:18px; }
.osfr-dot{ width:7px; height:7px; border-radius:50%; background:var(--line); }
.osfr-dot.done{ background:var(--neon,#3ce8ff); opacity:.45; }
.osfr-dot.on{ background:var(--neon,#3ce8ff); box-shadow:0 0 10px var(--neon,#3ce8ff); }
.osfr-h{ margin:0 0 8px; font-size:23px; line-height:1.2; }
.osfr-sub{ margin:0 0 18px; color:var(--muted,#7a8699); font-size:14px; line-height:1.5; }
.osfr-body{ min-height:56px; }
.osfr-input{ width:100%; }
/* Bounded and scrollable: a laptop in a block of flats sees thirty networks, and a list that grows
   the card past the screen puts the buttons under the bottom edge with no way to reach them. */
.osfr-list{ max-height:42vh; overflow-y:auto; display:flex; flex-direction:column; gap:4px; }
.osfr-row{ display:flex; align-items:center; justify-content:space-between; gap:10px; width:100%;
           padding:9px 11px; border-radius:9px; border:1px solid transparent; background:transparent;
           color:inherit; font:inherit; text-align:start; cursor:pointer; }
.osfr-row:hover{ background:rgba(60,232,255,.10); border-color:var(--neon,#3ce8ff); }
.osfr-row.on{ background:rgba(60,232,255,.16); }
.osfr-row[disabled]{ opacity:.5; cursor:default; }
.osfr-nm{ overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
.osfr-sig{ color:var(--muted,#7a8699); font-size:12px; flex:none; }
.osfr-none,.osfr-note{ color:var(--muted,#7a8699); font-size:13px; line-height:1.5; padding:6px 2px; }
.osfr-foot{ display:flex; gap:10px; justify-content:flex-end; margin-top:20px; flex-wrap:wrap; }

/* ── the terminal's shared history ─────────────────────────────────────────────────────────────
 * A PANEL, not ↑: the shell owns ↑ and knows its own history far better than we do. What it cannot
 * know is what you ran on your other computer, and that is all this offers. */
.tty-hist{ max-height:38vh; overflow-y:auto; display:flex; flex-direction:column; gap:2px;
           padding:6px; border-block-end:1px solid var(--line);
           background:rgba(0,0,0,.35); }
.tty-hist-row{ display:flex; align-items:baseline; justify-content:space-between; gap:12px;
               width:100%; padding:6px 9px; border-radius:7px; border:1px solid transparent;
               background:transparent; color:inherit; text-align:start; cursor:pointer;
               font:inherit; }
.tty-hist-row:hover{ background:rgba(60,232,255,.10); border-color:var(--neon,#3ce8ff); }
.tty-hist-cmd{ font-family:ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; font-size:13px;
               overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
.tty-hist-from{ color:var(--muted,#7a8699); font-size:11px; flex:none; }
.tty-hist-none{ color:var(--muted,#7a8699); font-size:13px; padding:8px; line-height:1.5; }

/* A tray chip that is OFF reads as off rather than as broken — the same weight as the others, in
   the muted colour, so "Tor: off" is a state and not a warning. */
.os-chip.os-off{ opacity:.62; }
.os-chip.on{ color:var(--neon,#3ce8ff); }

/* PosterChanOS virtual machines: a compact libvirt front-end, not virt-manager embedded in a
   window.  Cards remain useful at laptop width and the creation form never hides its action. */
.osw-vms .osw-body{padding:0}.vmui{height:100%;overflow:auto;padding:22px;display:flex;flex-direction:column;gap:16px;background:radial-gradient(circle at 90% 0,rgba(var(--accent-rgb),.11),transparent 34%)}
.vmui-hero{display:grid;grid-template-columns:auto 1fr auto;align-items:center;gap:15px;padding:18px;border:1px solid var(--line);border-radius:16px;background:linear-gradient(135deg,rgba(var(--accent-rgb),.10),var(--panel))}.vmui-heroicon,.vmui-screen{display:grid;place-items:center;width:54px;height:54px;border-radius:13px;background:rgba(var(--accent-rgb),.13);color:var(--accent)}.vmui-heroicon .ic,.vmui-screen .ic{width:29px;height:29px}.vmui-hero h2{font-size:24px;margin:0 0 4px}.vmui-hero p{color:var(--muted);margin:0;line-height:1.4}.vmui-new{min-height:42px}.vmui-formhead{display:flex;flex-direction:column;gap:4px;margin-bottom:15px}.vmui-formhead b{font-size:18px}.vmui-formhead span{color:var(--muted);font-size:12px}
.vmui-top{display:flex;align-items:center;justify-content:space-between;gap:16px}.vmui-top>div{display:flex;flex-direction:column;gap:3px}.vmui-top>div>b{font-size:20px}.vmui-top span,.vmui-note,.vmui-empty span,.vmui-machine span{color:var(--muted);font-size:12px}
.vmui-create{padding:16px;border:1px solid rgba(var(--accent-rgb),.38);border-radius:14px;background:var(--panel);box-shadow:0 12px 40px rgba(0,0,0,.18)}
.vmui-create[hidden]{display:none}.vmui-create label{display:flex;flex-direction:column;gap:6px;font-size:12px;color:var(--muted);margin-bottom:12px}.vmui-pick{display:flex;gap:8px}.vmui-pick .input{flex:1}.vmui-spec{display:grid;grid-template-columns:repeat(3,1fr);gap:10px}.vmui-formacts{display:flex;justify-content:flex-end;gap:8px}
.vmui-list{display:grid;grid-template-columns:repeat(auto-fit,minmax(380px,1fr));gap:12px}.vmui-card{display:flex;flex-direction:column;align-items:stretch;justify-content:space-between;gap:17px;padding:16px;border:1px solid var(--line);border-radius:15px;background:var(--panel);box-shadow:0 8px 25px #0002}
.vmui-machine{display:flex;align-items:center;gap:13px;min-width:180px}.vmui-screen{width:58px;height:48px;color:var(--muted)}.vmui-screen.on{color:#55e39b;background:rgba(85,227,155,.10);box-shadow:inset 0 0 0 1px rgba(85,227,155,.18)}.vmui-machine>div:last-child{display:flex;flex-direction:column;gap:4px}.vmui-machine b{font-size:17px}.vmui-machine span{display:flex;align-items:center;gap:6px}.vmui-machine span i{width:7px;height:7px;border-radius:50%;background:#69717d}.vmui-machine span i.on{background:#55e39b;box-shadow:0 0 8px #55e39b}.vmui-machine small{color:var(--muted);font-size:11px}.vmui-actions{display:flex;justify-content:flex-start;gap:7px;flex-wrap:wrap}.vmui-actions .btn{font-size:12px;padding:8px 11px;min-height:36px;display:inline-flex;align-items:center;gap:6px}.vmui-actions .b-ic{width:15px;height:15px}.vmui-actions .vmui-primary{min-width:110px;justify-content:center}.vmui-empty{grid-column:1/-1;min-height:190px;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:9px;border:1px dashed var(--line);border-radius:15px;text-align:center}.vmui-empty>.ic{width:40px;height:40px;color:var(--muted)}.vmui-note{padding:10px 12px;border-radius:9px;background:rgba(var(--accent-rgb),.07)}
.vmui-section,.vmui-advanced{margin-top:12px;padding:14px;border:1px solid var(--line);border-radius:12px;background:var(--panel)}.vmui-section h3{font-size:14px;margin:0 0 11px}.vmui-section p,.vmui-advanced p{color:var(--muted);font-size:12px;margin:0 0 11px;line-height:1.45}.vmui-section .vmui-spec{grid-template-columns:repeat(3,minmax(0,1fr))}.vmui-section label{display:flex;flex-direction:column;gap:6px;color:var(--muted);font-size:12px}.vmui-section .vmui-check{align-self:end;min-height:38px;justify-content:center;flex-direction:row;align-items:center}.vmui-actions-left{justify-content:flex-start}.vmui-advanced summary{cursor:pointer;font-weight:700;font-size:13px}.vmui-advanced[open] summary{margin-bottom:9px}.vmui-state{min-height:22px;margin-top:10px}
/* Settings is a destination with an explicit Back button, not another dashboard card. Keeping the
   hero/create/list beside it split the window into two competing screens and made hardware errors
   especially confusing. Only the editor remains while that destination is active. */
.vmui.vmui-editing>.vmui-hero,.vmui.vmui-editing>.vmui-create,.vmui.vmui-editing>.vmui-note,.vmui.vmui-editing>.vmui-list{display:none}
.osw-remote .osw-body{padding:0}.pcrd{height:100%;box-sizing:border-box;padding:28px;display:flex;flex-direction:column;gap:18px;background:radial-gradient(circle at 85% 5%,rgba(145,70,255,.18),transparent 38%),linear-gradient(145deg,#0c0d16,#11131f)}
.pcrd-hero{display:flex;align-items:center;gap:16px;padding:20px;border:1px solid rgba(161,113,255,.28);border-radius:18px;background:rgba(20,20,34,.82)}.pcrd-hero>.ic{width:46px;height:46px;color:#c89cff}.pcrd-hero div{display:flex;flex-direction:column;gap:5px}.pcrd-hero b{font-size:22px}.pcrd-hero span{color:var(--muted)}
.pcrd-label{display:flex;flex-direction:column;gap:8px;font-weight:700}.pcrd-label>label{display:flex;flex-direction:column;gap:8px}.pcrd-label .input{font-family:monospace;font-weight:400}.pcrd-note,.pcrd-cap{padding:14px 16px;border-radius:13px;background:rgba(255,255,255,.045);line-height:1.5}.pcrd-cap{display:flex;flex-direction:column;gap:7px}
.pcrd [data-rd-choose][hidden]{display:none!important}.pcrd-status{min-height:20px;color:var(--muted);font-size:13px}.pcrd-status:not(:empty){padding:8px 10px;border-radius:8px;background:rgba(255,255,255,.05)}
.osw-remote .osw-body,.osw-remote .osw-slot{min-height:0}.osw-remote .osw-slot{position:relative}.pcrd-session{position:absolute;inset:0;overflow:hidden;pointer-events:none}.pcrd-session:has(.call-overlay){pointer-events:auto}.pcrd-session>.call-overlay{position:absolute;inset:0;z-index:20}
@media(max-width:720px){.vmui{padding:12px}.vmui-hero{grid-template-columns:auto 1fr}.vmui-hero .vmui-new{grid-column:1/-1}.vmui-list{grid-template-columns:1fr}.vmui-card{align-items:stretch}.vmui-actions{justify-content:flex-start}.vmui-spec{grid-template-columns:1fr}.vmui-top{align-items:flex-start}}

/* ============================ Texts (the phone's messages, archived) =========================
   The web half of the phone shell. The screens on the HANDSET are native Android — that is what
   makes a text arrive when the WebView is dead — so this is the reading-and-answering surface for
   every other device, and it is built out of the same tokens as the rest of the client rather than
   trying to imitate the native one.

   THE SPACING IS LITERAL PIXELS ON THE 2px GRID, and it has to be. This block was written against
   `var(--s1)`/`var(--s2)`/`var(--s3)`, which client.css DEFINES NOWHERE — 16 declarations, every one
   of them invalid at computed-value time and therefore doing nothing at all. So every gap on the
   Texts screen was 0 and every padded row was unpadded: the thread list ran together, the bubbles
   touched, and the compose box sat flush against the conversation. Nothing appears in a console for
   this, and it looks like a design choice. `--s*` is not a token of this stylesheet (the note in
   :root says so: the grid is the invariant, the values are written out) — the same mistake `--fg`
   and `--border` made in the OS shell block. */
.sms-wrap{display:flex;flex-direction:column;height:100%;min-height:0;gap:12px}
.sms-head{display:flex;gap:8px;align-items:center;flex:none}
.sms-head .input{flex:1;min-width:0}
.sms-title{font-size:1.05rem;font-weight:600;color:var(--text);min-width:0;flex:1;display:flex;flex-direction:column;
  overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.sms-title small{font-size:.72rem;font-weight:400;color:var(--muted);overflow:hidden;text-overflow:ellipsis}
.sms-contact-actions{display:flex;gap:6px;flex:none}
@media(max-width:420px){.sms-contact-actions .btn{padding-inline:8px}}
.sms-threads{flex:1;min-height:0;overflow-y:auto;display:flex;flex-direction:column;gap:8px}
.sms-thread{display:flex;gap:12px;align-items:center;width:100%;text-align:left;
  padding:12px;border:1px solid var(--line);border-radius:var(--r);background:var(--panel);
  color:var(--text);cursor:pointer}
.sms-thread:hover{border-color:rgba(var(--accent-rgb),.5)}
/* The avatar is a hue derived from the name, exactly like the native list's — same person, same
   colour, whichever screen you are looking at. */
.sms-av{flex:none;width:40px;height:40px;border-radius:999px;display:flex;align-items:center;
  justify-content:center;font-weight:700;font-size:.8rem;color:#fff;
  background:linear-gradient(135deg,rgba(var(--accent-rgb),.85),rgba(var(--accent2-rgb),.85))}
.sms-body{flex:1;min-width:0}
.sms-row1{display:flex;gap:8px;align-items:baseline}
.sms-who{flex:1;min-width:0;font-weight:600;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.sms-when{flex:none;font-size:.72rem}
.sms-snip{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-size:.82rem}
/* TEXTS IS A CONVERSATION AND SO IS A DM, so it draws with the DM's own `.bubble .in/.out` rather
   than a second set built to the same idea. The rules that were here -- sms-msg, sms-bub, sms-meta
   -- were exactly that second set, and they had already drifted: different padding, no grouping of
   a run of messages, no shared radius. Two implementations of one screen means every later change
   has to be made twice, and the copy nobody remembers is the one that looks wrong.
   `.sms-msgs` keeps only what it needs to be a scroll container; `.dm-msgs` beside it in the markup
   supplies the padding and the scrolling. */
.sms-msgs{flex:1;min-height:0}
/* PICTURE MESSAGES. The bubble's `white-space:pre-wrap` is for TEXT, and it puts a phantom line
   between stacked attachments (the newlines in the template) — so the caption gets its own element
   and the bubble's padding shrinks to a frame once there is a picture in it. */
.bubble.has-att{padding:4px}
/* The caption under an MMS picture. `.b-txt` is the DM's own and already wraps; this only adds the
   padding a bubble in attachment mode gave up. */
.bubble.has-att .b-txt{display:block;padding:4px 8px}
.sms-att{display:block;margin:0;border-radius:calc(var(--r) - 2px);overflow:hidden}
.sms-att+.sms-att{margin-top:4px}
/* max-width, never a fixed one: a bubble is already capped at 78% of the thread and an MMS photo is
   whatever the sender's camera produced. `height:auto` keeps the aspect ratio, and `display:block`
   kills the inline-image descender gap that otherwise looks like a stray pixel row. */
.sms-att-img{display:block;width:100%;max-width:320px;height:auto;border-radius:calc(var(--r) - 2px);
  cursor:zoom-in;background:var(--panel)}
video.sms-att-img,audio.sms-att-img{cursor:default}
audio.sms-att-img{min-width:220px}
.sms-att-open{display:block;width:100%;min-height:38px;border:0;border-top:1px solid var(--line);
  padding:7px 10px;background:var(--panel2);color:var(--cyan);font:600 12px inherit;cursor:pointer}
.sms-att-open:hover,.sms-att-open:focus-visible{background:var(--hover);color:var(--text)}
.sms-compose{display:flex;gap:8px;align-items:center;flex:none}
.sms-compose .input{flex:1;min-width:0}
.sms-attachment-draft{display:flex;align-items:center;justify-content:space-between;gap:10px;margin:0 0 8px;padding:8px 10px;border:1px solid color-mix(in srgb,var(--cyan) 45%,var(--line));border-radius:10px;background:color-mix(in srgb,var(--cyan) 8%,var(--panel))}.sms-attachment-draft>span{display:grid;grid-template-columns:24px minmax(0,1fr);align-items:center;min-width:0}.sms-attachment-draft svg{grid-row:1/3;width:18px;height:18px}.sms-attachment-draft b,.sms-attachment-draft small{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.sms-attachment-draft small{color:var(--muted);font-size:11px}.sms-attachment-draft button{flex:0 0 32px;width:32px;height:32px;border:0;border-radius:8px;background:transparent;color:var(--muted);font-size:20px}.sms-attachment-draft button:hover{background:var(--hover);color:var(--text)}
/* Texts is full-height like Messages: the thread list and the conversation scroll inside themselves,
   so #feed must stop being the timeline's scroll container or the compose box scrolls away with the
   conversation. */
.feed.feed-texts{display:flex;flex-direction:column;min-height:0}
.feed.feed-texts>.sms-wrap{flex:1 1 auto;min-height:0;height:auto;padding:12px}

/* Contacts, read from this device while the server cannot be reached. A NOTICE, not an error: the
   address book is on screen and only the refresh failed, so it sits above the list rather than
   replacing it. Amber, because nothing is wrong with the data — it is merely unconfirmed. */
.ct-stale{padding:8px 14px;border-bottom:1px solid var(--line);
  background:linear-gradient(180deg,rgba(255,190,80,.10),transparent);
  color:var(--muted);display:flex;align-items:center;gap:8px}
.ct-stale::before{content:"";width:7px;height:7px;border-radius:50%;flex:none;
  background:#ffbe50;box-shadow:0 0 8px rgba(255,190,80,.7)}
/* PosterChanOS Task Manager. A private prefix matters: tm-* belongs to the transfer manager. */
.osw-taskmgr .osw-body{padding:0!important;overflow:hidden}
.pctm{height:100%;min-height:430px;display:grid;grid-template-columns:190px 1fr;background:#0b0d12;color:#f5f7fb}
.pctm-nav{padding:18px 10px;background:#10131a;border-right:1px solid #252936;display:flex;flex-direction:column;gap:5px}
.pctm-brand{height:48px;display:flex;align-items:center;gap:11px;padding:0 10px;margin-bottom:12px;font-weight:700}.pctm-brand svg,.pctm-nav button svg{width:21px;height:21px;fill:currentColor}
.pctm-nav button{border:0;background:transparent;color:#aeb5c4;border-radius:8px;padding:11px 12px;display:flex;align-items:center;gap:12px;text-align:left;font:inherit;cursor:pointer}.pctm-nav button:hover{background:#191e28;color:#fff}.pctm-nav button.active{background:#202634;color:#fff;box-shadow:inset 3px 0 var(--cyan)}
.pctm-main{min-width:0;overflow:auto;padding:22px 24px}.pctm-top{display:flex;align-items:center;justify-content:space-between;gap:20px;margin-bottom:20px}.pctm-top h2{font-size:25px;margin:0}.pctm-top p{color:#8f98aa;margin:3px 0 0}.pctm-search{width:min(280px,38vw);background:#151922!important;border-color:#303746!important}
.pctm-summary{display:grid;grid-template-columns:repeat(3,minmax(130px,1fr));gap:10px;margin-bottom:14px}.pctm-summary>div{background:#151922;border:1px solid #292f3c;border-radius:10px;padding:13px 15px}.pctm-summary span{display:block;color:#929bad;font-size:12px}.pctm-summary b{font-size:21px}
.pctm-table{border:1px solid #292f3c;border-radius:10px;overflow:hidden;background:#12151c}.pctm-tr{display:grid;grid-template-columns:minmax(260px,1fr) 72px 105px 120px 92px;align-items:center;min-height:48px;border-bottom:1px solid #222733}.pctm-tr:last-child{border-bottom:0}.pctm-tr>span,.pctm-tr>button{padding:8px 12px}.pctm-th{min-height:38px;background:#171b24;color:#9da6b7;font-size:12px}.pctm-th button{border:0;background:none;color:inherit;text-align:left;cursor:pointer}.pctm-rows>.pctm-tr:hover{background:#191e27}.pctm-tr>span:first-child{display:flex;align-items:center;gap:10px;min-width:0}.pctm-tr>span:first-child>b{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.pctm-appicon{display:grid;place-items:center;flex:0 0 28px;height:28px;border-radius:7px;background:linear-gradient(145deg,#343b4b,#202531);color:#dfe8ff;font-style:normal;font-size:12px}.pctm-heat{background:linear-gradient(90deg,color-mix(in srgb,var(--cyan) 24%,transparent) var(--use),transparent var(--use));height:100%;display:flex;align-items:center}
.pctm-performance{max-width:1100px}.pctm-perfgrid{display:grid;grid-template-columns:1fr 1fr;gap:16px}.pctm-perf{min-height:265px;background:#13171f;border:1px solid #2a303e;border-radius:12px;padding:18px;overflow:hidden}.pctm-perf:first-child{grid-column:1/-1}.pctm-perf header,.pctm-perf footer{display:flex;justify-content:space-between;gap:15px}.pctm-perf header b{font-size:19px}.pctm-perf header small{display:block;color:#8993a6;margin-top:3px}.pctm-perf header strong{font-size:26px}.pctm-graph{display:block;width:100%;height:145px;margin:10px 0;background-image:linear-gradient(#ffffff0a 1px,transparent 1px),linear-gradient(90deg,#ffffff0a 1px,transparent 1px);background-size:25% 25%;border:1px solid #ffffff0d}.pctm-perf footer{color:#929bad;font-size:12px}.pctm-perf footer b{display:block;color:#f4f6fa;font-size:14px;margin-top:2px}
@media(max-width:760px){.pctm{grid-template-columns:58px 1fr}.pctm-brand span,.pctm-nav button span{display:none}.pctm-nav{padding-inline:7px}.pctm-main{padding:16px 12px}.pctm-tr{grid-template-columns:minmax(150px,1fr) 80px 100px 76px}.pctm-tr>*:nth-child(2){display:none}.pctm-summary{grid-template-columns:1fr 1fr}.pctm-summary>div:last-child{grid-column:1/-1}.pctm-perfgrid{grid-template-columns:1fr}.pctm-perf:first-child{grid-column:auto}}
.os-settings-feed{padding:0!important;height:100%;overflow:hidden!important}
.os-settings{display:grid;grid-template-columns:210px 1fr;height:100%;min-height:520px;background:#090b11;color:var(--text)}
.os-set-nav{padding:18px 12px;border-right:1px solid var(--line);background:#0c0f16;box-shadow:8px 0 30px #0002}
.os-set-title{display:flex;gap:9px;align-items:center;font-size:16px;margin:2px 8px 20px}
.os-set-nav button{display:flex;align-items:center;gap:11px;width:100%;min-height:43px;padding:10px 12px;border:0;border-radius:9px;background:none;color:var(--muted);text-align:left;font-weight:600;cursor:pointer}.os-set-nav button .ic{width:19px;height:19px}
.os-set-nav button.on,.os-set-nav button:hover{background:rgba(var(--accent-rgb),.14);color:var(--text)}
.os-set-mobile-nav{display:none}
.os-set-main{padding:26px;overflow:auto;background:radial-gradient(circle at 100% 0,rgba(var(--accent-rgb),.08),transparent 34%)}.os-set-main h2{margin:0;font-size:27px}.os-set-pagehead{display:flex;align-items:center;gap:14px;margin-bottom:20px}.os-set-pagehead>div{display:grid;place-items:center;width:48px;height:48px;border-radius:12px;background:rgba(var(--accent-rgb),.13);color:var(--accent)}.os-set-pagehead>div .ic{width:27px;height:27px}.os-set-pagehead span{display:flex;flex-direction:column;gap:4px}.os-set-pagehead p,.os-set-section-title p,.os-set-cardhead span{margin:0;color:var(--muted);font-size:12px}.os-set-card{padding:18px;border:1px solid var(--line);border-radius:14px;background:var(--panel);box-shadow:0 10px 30px #0002}.os-set-cardhead{display:flex;flex-direction:column;gap:4px}.os-set-cardhead b{font-size:16px}.os-set-section-title{margin:26px 2px 10px}.os-set-section-title h3{margin:0 0 3px;font-size:17px}.os-setting-row{margin-top:9px!important;border-radius:12px!important;background:var(--panel)!important}
.os-display-map{position:relative;margin:20px 0;background:radial-gradient(circle at 50% 50%,rgba(255,255,255,.06),transparent 60%),#05070b;border:1px solid var(--line);border-radius:12px;overflow:hidden}
.os-display{position:absolute;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:3px;border:2px solid #596171;border-radius:7px;background:#171c27;color:var(--text);touch-action:none;cursor:grab;box-shadow:0 8px 24px #0008}
.os-display.selected{border-color:var(--accent);box-shadow:0 0 0 2px rgba(var(--accent-rgb),.25),0 8px 24px #0008}.os-display.off{opacity:.42}
.os-display b{font-size:22px}.os-display span,.os-display small{max-width:95%;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.os-display-controls{display:grid;grid-template-columns:repeat(2,minmax(180px,1fr));gap:13px;padding:16px;border:1px solid var(--line);border-radius:10px;background:rgba(255,255,255,.025)}
.os-display-controls label{display:flex;align-items:center;justify-content:space-between;gap:12px}.os-display-controls select{min-width:145px;background:#111722;color:var(--text);border:1px solid var(--line);border-radius:6px;padding:7px}
.os-set-actions{display:flex;align-items:center;gap:10px;margin-top:16px}
.os-set-control{display:flex;align-items:center;justify-content:space-between;gap:18px;padding:15px 17px;border:1px solid var(--line)}.os-set-control>div{display:flex;flex-direction:column;gap:4px}.os-set-control>div>span,.os-set-control output{color:var(--muted);font-size:12px}.os-set-control>label{display:flex;align-items:center;gap:10px}.os-set-control input[type=range]{width:min(240px,28vw)}.os-set-control select{min-width:150px;padding:8px;background:#111722;color:var(--text);border:1px solid var(--line);border-radius:7px}.os-set-switch span{min-width:24px}.os-about-grid{display:grid;grid-template-columns:repeat(2,minmax(140px,1fr));gap:1px;padding:0;overflow:hidden;background:var(--line)}.os-about-grid>div{display:flex;flex-direction:column;gap:5px;padding:16px;background:var(--panel)}.os-about-grid span{color:var(--muted);font-size:12px}
.os-set-widget-list{margin-top:12px}.os-set-widget-addhead{margin-top:22px}.os-set-widget-grid{display:grid;grid-template-columns:repeat(2,minmax(220px,1fr));gap:8px;margin-top:12px}.os-set-widget-grid .os-wgt-pick{width:100%;min-height:68px}.os-set-widget .danger{color:#ff8f9a}
.os-user-account,.os-update-status{display:flex;align-items:center;gap:16px}.os-user-account>div:nth-child(2),.os-update-status>div:nth-child(2){display:flex;min-width:0;flex-direction:column;gap:5px}.os-user-account code{max-width:100%;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;color:var(--muted)}.os-user-avatar{display:grid;place-items:center;flex:0 0 52px;height:52px;border-radius:50%;background:rgba(var(--accent-rgb),.16);color:var(--accent)}.os-user-avatar .ic{width:28px;height:28px}.os-update-status span{color:var(--muted);font-size:12px;line-height:1.5}.os-update-status .os-set-ready{flex:none}
/* System Settings belongs to the selected desktop experience too; otherwise macOS chrome framed a
   second, unrelated cyberpunk application. Keep these overrides scoped to this isolated document. */
.os-root.os-style-mac .os-settings{background:#ececf0;color:#1d1d1f}.os-root.os-style-mac .os-set-nav{background:rgba(224,224,229,.92);border-right-color:#c6c6ca;box-shadow:none}.os-root.os-style-mac .os-set-nav button{color:#55555c}.os-root.os-style-mac .os-set-nav button.on,.os-root.os-style-mac .os-set-nav button:hover{background:#0a84ff;color:#fff}.os-root.os-style-mac .os-set-main{background:#f5f5f7}.os-root.os-style-mac .os-set-pagehead>div{background:#0a84ff;color:#fff}.os-root.os-style-mac .os-set-pagehead p,.os-root.os-style-mac .os-set-cardhead span,.os-root.os-style-mac .os-set-control>div>span,.os-root.os-style-mac .os-about-grid span,.os-root.os-style-mac .os-update-status span{color:#6e6e73}.os-root.os-style-mac .os-set-card,.os-root.os-style-mac .os-setting-row{background:#fff!important;border-color:#d2d2d7;box-shadow:0 1px 2px #0000000d}.os-root.os-style-mac .os-about-grid{background:#d2d2d7}.os-root.os-style-mac .os-about-grid>div{background:#fff}.os-root.os-style-mac .os-set-control select{background:#fff;color:#1d1d1f;border-color:#c7c7cc}.os-root.os-style-mac .os-display-controls{background:#fff;border-color:#d2d2d7}.os-root.os-style-mac .os-set-ready{background:#dff7e5;color:#167534}
.os-liveusb{background:#11151d}
.os-liveusb-head{display:flex;align-items:center;justify-content:space-between;gap:16px}.os-liveusb-head>div{display:flex;flex-direction:column;gap:4px}.os-liveusb-head span{color:var(--muted);font-size:12px}
.os-liveusb-grid{display:grid;grid-template-columns:1fr 1fr;gap:14px;margin-top:16px}.os-liveusb-grid>div{display:flex;flex-direction:column;gap:12px;padding:15px;border:1px solid #2a303c;border-radius:10px;background:#0b0e14}.os-liveusb-grid h3{margin:0;font-size:16px}.os-liveusb-grid label{display:flex;flex-direction:column;gap:6px;color:var(--muted);font-size:12px}.os-liveusb-grid label:has(input[type=checkbox]){display:block}.os-liveusb-grid select{width:100%;padding:8px;background:#111722;color:var(--text);border:1px solid var(--line);border-radius:6px}.os-path-pick{display:flex;gap:7px}.os-path-pick input{flex:1;min-width:0}.os-liveusb .danger{border-color:#9b4050;color:#ff91a5}.os-liveusb-status{min-height:38px;max-height:120px;margin:14px 0 0;padding:10px;overflow:auto;white-space:pre-wrap;background:#080a0f;border:1px solid #252a35;border-radius:7px;color:#aab3c4}
@media(max-width:760px){.os-settings{grid-template-columns:1fr;min-height:0}.os-set-nav{display:none}.os-set-main{padding:14px 12px}.os-set-mobile-nav{position:sticky;top:-14px;z-index:3;display:flex;flex-direction:column;gap:5px;margin:-14px -12px 16px;padding:12px;background:#0c0f16;border-bottom:1px solid var(--line);color:var(--muted);font-size:12px}.os-set-mobile-nav select{width:100%;min-height:44px;padding:9px 11px;border:1px solid var(--line);border-radius:8px;background:#111722;color:var(--text);font:inherit}.os-display-controls{grid-template-columns:1fr}.os-set-control{align-items:flex-start;flex-direction:column}.os-set-control input[type=range]{width:min(360px,70vw)}.os-about-grid,.os-set-widget-grid{grid-template-columns:1fr}}
@media(max-width:900px){.os-liveusb-grid{grid-template-columns:1fr}}

/* ═══ PosterChan Code ═════════════════════════════════════════════════════════════════════════
 *
 * NOT ONE HARD-CODED COLOUR IN HERE, and that is not tidiness — this client ships seven-plus themes
 * (cherryblossom and professional are LIGHT, win98 has no glow and no radius), and each redefines
 * the same palette variables. A literal hex here would look correct on the dark flagship and wrong
 * everywhere else, which is precisely how a feature ends up "broken" only for the people who
 * changed the theme.
 *
 * The syntax colours are derived from that same palette rather than being a set of their own, so a
 * new theme themes the editor for free and can never leave a token unreadable against its own
 * background. */
:root{
  --code-com: var(--muted);
  --code-str: var(--green);
  --code-kw:  var(--neon2);
  --code-num: var(--amber);
  --code-fn:  var(--neon);
  --code-var: var(--cyan);
  --code-key: var(--neon);
  --code-op:  var(--text);
}

.feed-code{display:flex;flex-direction:column;min-height:0;padding:0;overflow:hidden}
.pcc{display:flex;flex-direction:column;flex:1;min-height:0;background:var(--bg2);color:var(--text)}
.pcc-main{display:flex;flex:1;min-height:0}

/* VS Code-shaped activity rail: switching tools never replaces or strands the sidebar controls. */
.pcc-activity{width:48px;flex:0 0 48px;display:flex;flex-direction:column;align-items:stretch;
  background:var(--bg);border-right:1px solid var(--line)}
.pcc-activity button{position:relative;height:48px;border:0;border-left:2px solid transparent;
  background:transparent;color:var(--muted);cursor:pointer}
.pcc-activity button:hover{color:var(--text);background:var(--panel2)}
.pcc-activity button.on{color:var(--text);border-left-color:var(--neon)}
.pcc-activity .ic{width:22px;height:22px}.pcc-activity em{position:absolute;right:4px;bottom:4px;min-width:16px;
  padding:1px 4px;border-radius:9px;background:var(--neon);color:var(--bg);font:700 10px/14px var(--sans);font-style:normal}

/* ---- file tree ---- */
.pcc-side{width:var(--pcc-side,250px);flex:0 0 var(--pcc-side,250px);min-width:0;
  display:flex;flex-direction:column;background:var(--panel);border-right:1px solid var(--line)}
.pcc-crumbs{display:flex;flex-wrap:wrap;align-items:center;gap:2px;padding:var(--sp-2);
  border-bottom:1px solid var(--line);font-size:var(--fs-0)}
.pcc-crumb{background:none;border:0;color:var(--neon);cursor:pointer;padding:2px 4px;
  border-radius:var(--r-sm);font-size:var(--fs-0);font-family:inherit}
.pcc-crumb:hover{background:var(--panel2)}
.pcc-sep{color:var(--muted);opacity:.6}
.pcc-tree{flex:1;min-height:0;overflow:auto;padding:var(--sp-1)}
.pcc-item{display:flex;align-items:center;gap:var(--sp-2);width:100%;text-align:left;
  background:none;border:0;color:var(--text);cursor:pointer;padding:5px var(--sp-2);
  border-radius:var(--r-sm);font-size:var(--fs-1);font-family:inherit}
.pcc-item:hover{background:var(--panel2)}
.pcc-item.on{color:var(--neon)}
.pcc-nm{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.pcc-ic{flex:none}
.pcc-root{padding:var(--sp-2);border-top:1px solid var(--line);color:var(--muted);
  font-size:var(--fs-0);overflow:hidden;text-overflow:ellipsis;white-space:nowrap;direction:rtl;text-align:left}
.pcc-note{padding:var(--sp-3);color:var(--muted);font-size:var(--fs-1)}
.pcc-note.err{color:var(--danger)}
.pcc-git-head{display:flex;flex-direction:column;gap:3px;padding:10px;border-bottom:1px solid var(--line)}
.pcc-git-head small{overflow:hidden;text-overflow:ellipsis;color:var(--muted)}
.pcc-git-actions{display:grid;grid-template-columns:1fr 1fr;gap:6px;padding:8px}.pcc-git-actions button,.pcc-git-commit button,.pcc-git-file>button{min-height:34px;border:1px solid var(--line);border-radius:7px;background:var(--panel2);color:var(--text);cursor:pointer}
.pcc-git-file{display:grid;grid-template-columns:minmax(0,1fr) 36px 36px;gap:4px;padding:2px 6px}.pcc-git-file>button:first-child{display:flex;align-items:center;gap:7px;min-width:0;padding:5px 7px;text-align:left}.pcc-git-file code{color:var(--neon)}.pcc-git-file span{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.pcc-git-commit{display:grid;grid-template-columns:minmax(0,1fr) auto;gap:5px;padding:9px 7px}.pcc-git-commit input{min-width:0;padding:8px;border:1px solid var(--line);border-radius:7px;background:var(--bg);color:var(--text)}
.pcc-diff-view{display:flex;flex:1;min-height:0;flex-direction:column;background:var(--bg)}
.pcc-diff-view>header{display:flex;align-items:center;gap:10px;min-height:38px;padding:0 10px;border-bottom:1px solid var(--line)}
.pcc-diff-view>header b{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.pcc-diff-view>header span{color:var(--muted);font-size:11px}
.pcc-diff-view>header button{margin-left:auto;border:0;background:none;color:var(--muted);font-size:20px;cursor:pointer}
.pcc-git-diff{flex:1;min-height:0;margin:0;padding:12px 16px;overflow:auto;background:var(--bg);color:var(--text);font:12px/1.5 var(--mono);white-space:pre;tab-size:4}

/* ---- drag handles ---- */
.pcc-grip{flex:none;background:transparent;position:relative;z-index:2}
.pcc-grip-v{width:6px;cursor:col-resize;margin:0 -3px}
.pcc-grip-h{height:6px;cursor:row-resize;margin:-3px 0}
.pcc-grip:hover{background:rgba(var(--accent-rgb),.35)}

/* ---- editor pane ---- */
.pcc-pane{flex:1;min-width:0;display:flex;flex-direction:column;min-height:0}
.pcc-tabs{display:flex;gap:2px;overflow-x:auto;background:var(--panel);border-bottom:1px solid var(--line)}
.pcc-tab{display:flex;align-items:center;gap:var(--sp-1);padding:var(--sp-2) var(--sp-3);
  cursor:pointer;font-size:var(--fs-1);color:var(--muted);border-right:1px solid var(--line);
  white-space:nowrap;flex:none}
.pcc-tab.on{color:var(--text);background:var(--bg2)}
/* The unsaved dot: a MARK, never colour alone — win98's palette has no reliable accent and a
   colour-only cue is invisible to a good share of people in any theme. */
.pcc-tab.dirty .pcc-tabname::after{content:' \25CF';color:var(--amber)}
.pcc-x{background:none;border:0;color:inherit;cursor:pointer;font-size:var(--fs-2);line-height:1;
  padding:0 2px;border-radius:var(--r-sm);opacity:.6}
.pcc-x:hover{opacity:1;background:var(--panel2)}
.pcc-bar{display:flex;align-items:center;gap:var(--sp-2);padding:var(--sp-2);
  background:var(--panel);border-bottom:1px solid var(--line);flex-wrap:wrap}
.pcc-b{font-size:var(--fs-0);padding:4px 10px}
.pcc-grow{flex:1}
.pcc-lang{color:var(--muted);font-size:var(--fs-0);font-family:var(--mono)}
.pcc-blank{flex:1;display:flex;flex-direction:column;align-items:center;justify-content:center;
  gap:var(--sp-2);color:var(--muted)}
.pcc-blank b{color:var(--text);font-size:var(--fs-3)}

/* ---- the two stacked layers ----
 * EVERY METRIC THAT AFFECTS GLYPH POSITION IS DECLARED ONCE, on the class BOTH layers carry. Font,
 * size, line-height, letter-spacing, tab-size, padding, white-space and word-break: if the <pre>
 * and the <textarea> disagree about any single one of them, the colours slide off the letters —
 * progressively, so it looks fine on line 1 and is a mess by line 60. */
.pcc-editwrap{flex:1;min-height:0;display:flex;overflow:hidden;background:var(--bg)}
.pcc-layer{
  margin:0;
  padding:var(--sp-2) var(--sp-3);
  font-family:var(--mono);
  font-size:13px;
  line-height:1.5;
  letter-spacing:0;
  tab-size:4;
  -moz-tab-size:4;
  white-space:pre;
  word-break:normal;
  overflow-wrap:normal;
  border:0;
}
.pcc-edit{position:relative;flex:1;min-width:0}
.pcc-hl{position:absolute;inset:0;overflow:hidden;pointer-events:none;color:var(--text);
  background:var(--bg)}
.pcc-ta{position:absolute;inset:0;width:100%;height:100%;overflow:auto;resize:none;
  background:transparent;
  /* Transparent text over the coloured layer, with a caret that is still visible. `-webkit-text-fill
     -color` is the one the WebView honours; `color` alone leaves the text painted on some builds,
     which shows as a doubled, slightly-offset ghost of every line. */
  color:transparent;-webkit-text-fill-color:transparent;
  caret-color:var(--neon);outline:none}
/* A translucent selection, so the coloured text underneath still reads through it. */
.pcc-ta::selection{background:rgba(var(--accent-rgb),.30);-webkit-text-fill-color:transparent}
.pcc-gutter{flex:none;text-align:right;color:var(--muted);background:var(--panel);
  border-right:1px solid var(--line);overflow:hidden;user-select:none;min-width:3.5em}

/* token colours — see the note at the top of this block.
   Written once for BOTH surfaces that use PCCodeHL: the Code editor's overlay and the git file
   viewer. A second copy scoped to .fb-code would be a second place for a theme to go stale in. */
.pcc-hl .t-com{color:var(--code-com);font-style:italic}
.pcc-hl .t-str{color:var(--code-str)}
.pcc-hl .t-kw{color:var(--code-kw)}
.pcc-hl .t-num{color:var(--code-num)}
.pcc-hl .t-fn{color:var(--code-fn)}
.pcc-hl .t-var{color:var(--code-var)}
.pcc-hl .t-key{color:var(--code-key)}
.pcc-hl .t-dec,.pcc-hl .t-ann{color:var(--code-num)}
.pcc-hl .t-op{color:var(--code-op);opacity:.75}

/* ---- status + terminal ---- */
.pcc-foot{display:flex;align-items:center;gap:var(--sp-2);padding:3px var(--sp-3);
  background:var(--panel);border-top:1px solid var(--line);font-size:var(--fs-0);color:var(--muted)}
.pcc-status.ok{color:var(--green)}
.pcc-status.err{color:var(--danger)}
.pcc-status.warn{color:var(--amber)}
.pcc-pos{font-family:var(--mono)}
.pcc-term{height:var(--pcc-term,260px);flex:0 0 var(--pcc-term,260px);min-height:0;
  border-top:1px solid var(--line);background:var(--bg)}

/* Narrow screens: the tree becomes a strip rather than eating the editor. The desktop shell is
   desktop-only anyway, but the view itself is reachable in a phone browser. */
@media (max-width:720px){
  .pcc-side{width:150px;flex-basis:150px}
  .pcc-grip-v{display:none}
}
/* Built-in CODE editor: fill the modal on desktop and mobile without giving the iframe page scroll. */
.office-head{display:flex;align-items:baseline;justify-content:space-between;gap:12px}.office-head h3{margin:0}
/* var(--line), not var(--border) — this stylesheet has no `--border` token and never has (483
   uses of `--line` against one of `--border`). An undefined custom property makes the whole
   declaration invalid at computed-value time, so the office frame drew an unthemed border. */
/* THE SHEET, NOT JUST THE FRAME. `.modal` is min(720px,96vw); a frame asking for 94vw inside that
   was clipped to 720px and the modal scrolled — a small white rectangle showing the corner of a
   document. The editor gets the screen, and the frame FILLS the sheet rather than naming its own
   size, so the two cannot disagree again. */
.modal.office-modal{width:min(1560px,97vw);max-width:97vw;height:94dvh;max-height:94dvh;
  display:flex;flex-direction:column;gap:8px;padding:14px}
.office-modal > *{flex:0 0 auto}
.office-frame{display:block;flex:1 1 auto;width:100%;height:auto;min-height:0;
  border:1px solid var(--line);border-radius:8px;background:#fff}
.office-launch{display:none}.office-actions{justify-content:flex-end;margin-top:8px}
/* THE SAME EDITOR IN A DESKTOP WINDOW. `.office-modal` above sizes a centred box against the
   VIEWPORT; a window is sized by the user, so this fills whatever the window currently is and lets
   the frame take everything the head and the buttons do not. Without it the iframe keeps the
   modal's viewport-relative height and spills out of a window somebody just made smaller. */
.office-win{display:flex;flex-direction:column;height:100%;min-height:0;padding:10px;gap:8px}
/* A no-feed document lives in `.osw-slot` permanently, including while focused. The generic slot
   is a subdued parked-app snapshot (opacity + outer padding + scrolling); none of those belong on
   a live maximised editor. Keep Office's own deliberate 10px inner spacing. */
.osw-slot.office-win{opacity:1;overflow:hidden;padding:10px}
.office-win > *{flex:0 0 auto}
.office-win .office-frame{flex:1 1 auto;width:100%;height:auto;min-height:0;border:0}
@media(max-width:900px){
  /* Office is an application on a phone/tablet, not a little dialog over Files. `inset:0` also
     avoids the browser toolbar/dynamic-vh black strip that a centred 96dvh box left behind. */
  .modal.office-modal{position:fixed;inset:0;margin:0;width:100vw;max-width:none;height:100dvh;
    max-height:none;border:0;border-radius:0;padding:max(6px,env(safe-area-inset-top))
    max(6px,env(safe-area-inset-right)) max(6px,env(safe-area-inset-bottom))
    max(6px,env(safe-area-inset-left));gap:5px}
  .office-head{display:flex;align-items:center;gap:8px;min-width:0}
  .office-head h3{font-size:1rem;margin:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
  .office-head .muted{display:none}
  .office-actions{margin-top:2px;gap:6px}
  .office-actions .btn{min-height:38px}
}
.os-native-controls{display:inline-flex;align-items:center;gap:1px}.os-native-controls button{width:22px;height:22px;padding:0;border:0;border-radius:4px;background:transparent;color:inherit;line-height:20px}.os-native-controls button:hover{background:rgba(255,255,255,.14)}.os-native-close:hover{background:#c42b1c!important;color:#fff}

/* PREVIEW — the picture/video/PDF viewer (static/js/client/preview.js).
   Two hosts, one look: a desktop window's slot (.pv-win) and a full-screen sheet (.pv-sheet).
   Both are a column whose BODY takes everything the bar does not, so the media is sized by the
   window rather than by the viewport — a viewer that ignores its window is the "tiny white box"
   the office editor was. */
.pv-host{display:flex;flex-direction:column;min-height:0;background:var(--bg);color:var(--text)}
/* Preview owns its black media canvas edge to edge. It is also a permanent `.osw-slot`, so undo
   the parked snapshot's dimming/inset/scroll container rather than showing a grey padded viewer. */
.pv-win{height:100%}
.osw-slot.pv-win{opacity:1;overflow:hidden;padding:0}
.pv-sheet{position:fixed;inset:0;z-index:1200}
.pv-bar{display:flex;align-items:center;gap:10px;padding:8px 10px;flex:0 0 auto;
  border-bottom:1px solid var(--line);background:var(--panel)}
.pv-name{font-weight:600;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;min-width:0}
.pv-size{flex:0 0 auto}
.pv-acts{margin-left:auto;display:flex;gap:6px;flex:0 0 auto}
.pv-body{flex:1 1 auto;min-height:0;display:flex;align-items:center;justify-content:center;
  overflow:auto;background:#000}
.pv-nope{background:var(--bg);padding:16px}
/* FIT, then ACTUAL SIZE on demand. `max-width/height:100%` with `object-fit:contain` is what makes a
   4000px photo readable in a small window; .pv-actual drops the cap so the container scrolls. */
.pv-img{max-width:100%;max-height:100%;object-fit:contain;cursor:zoom-in;
  transition:transform .15s ease}
.pv-img.pv-actual{max-width:none;max-height:none;object-fit:none;cursor:zoom-out}
.pv-img-wrap{padding:0}
.pv-av-wrap{padding:0;position:relative}
.pv-vid{width:100%;height:100%;max-width:100%;max-height:100%;object-fit:contain;display:block;background:#000}
.pv-media-state{position:absolute;inset:0;display:grid;place-items:center;pointer-events:none;
  color:#fff;background:radial-gradient(circle,rgba(0,0,0,.28),rgba(0,0,0,.72));font-size:14px}
.pv-aud{width:min(560px,92%)}
.pv-pdf-body{display:block;background:#202124;padding:10px;overflow:auto}
.pv-pdf-pages{width:100%;display:flex;flex-direction:column;align-items:center;gap:10px;min-height:100%}
.pv-pdf-page{display:block;max-width:100%;height:auto;background:#fff;box-shadow:0 2px 12px #0008}
@media(max-width:640px){.pv-bar{padding:6px 8px;gap:6px}.pv-name{font-size:.95em}}

/* Concord threads. A reply count on a root, and the bar that says you are inside one.
   Deliberately built from the tokens the rest of Concord already uses — a thread is the same
   conversation seen from closer up, not a different surface. */
.cc-thread-open{font-size:11px;padding:2px 8px;border-radius:10px;border:1px solid var(--cc-line,#2a2f3a);
  background:transparent;color:var(--cc-accent,#7aa2f7);cursor:pointer;white-space:nowrap}
.cc-thread-open:hover{background:var(--cc-hover,rgba(122,162,247,.12))}
.cc-thread-bar{display:flex;align-items:center;gap:10px;padding:8px 12px;
  border-bottom:1px solid var(--cc-line,#2a2f3a)}
.cc-thread-bar button{background:transparent;border:0;color:inherit;cursor:pointer;font-size:13px;padding:2px 6px}
.cc-thread-bar b{font-size:13px}
.cc-thread-bar span{opacity:.7;font-size:12px}

/* A POLL, IN CONCORD'S OWN CHAT STYLE — not the timeline's poll card, which is a note with an
   avatar, a header and an action bar, none of which belongs inside a chat bubble. Built from the
   same tokens as the thread controls above, for the same reason: a poll is a message. */
.cc-poll{display:flex;flex-direction:column;gap:6px;margin-top:6px;max-width:420px}
.cc-poll-opt{position:relative;display:flex;align-items:center;gap:8px;width:100%;
  padding:7px 10px;border-radius:8px;border:1px solid var(--cc-line,#2a2f3a);
  background:transparent;color:inherit;cursor:pointer;font-size:13px;text-align:left;overflow:hidden}
.cc-poll-opt:hover:not(:disabled){background:var(--cc-hover,rgba(122,162,247,.12))}
.cc-poll-opt:disabled{cursor:default;opacity:.85}
.cc-poll-opt.voted{border-color:var(--cc-accent,#7aa2f7)}
/* The bar is BEHIND the label, and the label carries its own stacking context — a percentage
   filling the row must never push the text it is describing. */
.cc-poll-bar{position:absolute;left:0;top:0;bottom:0;z-index:0;
  background:var(--cc-hover,rgba(122,162,247,.18));transition:width .18s ease}
.cc-poll-label{position:relative;z-index:1;flex:1;min-width:0;overflow-wrap:anywhere}
.cc-poll-count{position:relative;z-index:1;opacity:.7;font-size:12px;font-variant-numeric:tabular-nums}
.cc-poll-foot{opacity:.7;font-size:12px}
