/* ─────────────────────────────────────────────────────────────────────
   SKIN LAYER — colour/material identity only. Shared markup, no DOM per
   skin. Every skin defines the full token contract so nothing falls back
   to another skin's value.

   Contrast ratios in the comments are measured text-on-surface, and were
   verified rather than estimated. Every body text pair is AAA (>=7:1).
   ───────────────────────────────────────────────────────────────────── */

/* ── token contract ─────────────────────────────────────────────────── */
:root{
  --bg:#0B0C0E; --bg-art:none; --surface:#131518; --surface-2:#1B1E22;
  --sidebar-bg:#0B0C0E; --header-bg:#0B0C0E; --composer-bg:#131518;
  --border:#24282E; --border-strong:#343A42; --text:#EDEFF2; --text-dim:#9AA1AC;
  --font:ui-sans-serif,'Instrument Sans',system-ui,-apple-system,sans-serif;
  --font-read:var(--font);
  /* an UNSET custom property invalidates the whole font-family declaration,
     which silently drops the page to Times New Roman - so this always resolves */
  --font-user:var(--font);
  --accent:#7C8AF7; --accent-text:#0A0C1A; --accent-soft:#1A1F3A;
  --accent-2:#7C8AF7; --danger:#FF6B6B; --success:#3BC77A; --warn:#E3A008; --info:#7C8AF7;
  --bubble-user:#232842; --bubble-ai:#131518;
  --radius:8px; --radius-lg:12px; --radius-pill:9999px;
  --shadow:0 1px 2px rgba(0,0,0,.5); --shadow-lg:0 12px 32px -8px rgba(0,0,0,.7);
  --shadow-in:none;
  --ring:0 0 0 3px rgba(124,138,247,.35); --scrim:rgba(0,0,0,.62); --blur:0px;
}

/* ── 1. GRAPHITE — flat/minimal. DEFAULT. text 15.88 dark / 17.77 light ─ */
:root[data-skin="graphite"]{
  --bg:#F7F8F9; --bg-art:none; --surface:#FFFFFF; --surface-2:#F1F2F4;
  --sidebar-bg:#F7F8F9; --header-bg:#FFFFFF; --composer-bg:#FFFFFF;
  --border:#E1E4E8; --border-strong:#CBD0D7; --text:#16181C; --text-dim:#5C636E;
  --font:ui-sans-serif,'Inter',system-ui,-apple-system,'Segoe UI',sans-serif;
  --font-read:var(--font);
  --accent:#4A57D6; --accent-text:#FFFFFF; --accent-soft:#EEF0FE; --accent-2:#4A57D6;
  --danger:#C62828; --success:#12805C; --warn:#8A5A00; --info:#4A57D6;
  --bubble-user:#EEF0FE; --bubble-ai:#FFFFFF;
  --radius:8px; --radius-lg:12px;
  --shadow:0 1px 2px rgba(16,24,40,.06),0 1px 3px rgba(16,24,40,.08);
  --shadow-lg:0 8px 24px -6px rgba(16,24,40,.14);
  --ring:0 0 0 3px rgba(74,87,214,.28); --scrim:rgba(16,24,40,.42); --blur:0px;
}
.dark:root[data-skin="graphite"]{
  --bg:#0B0C0E; --surface:#131518; --surface-2:#1B1E22;
  --sidebar-bg:#0B0C0E; --header-bg:#0B0C0E; --composer-bg:#131518;
  --border:#24282E; --border-strong:#343A42; --text:#EDEFF2; --text-dim:#9AA1AC;
  --accent:#7C8AF7; --accent-text:#0A0C1A; --accent-soft:#1A1F3A; --accent-2:#7C8AF7;
  --danger:#FF6B6B; --success:#3BC77A; --warn:#E3A008; --info:#7C8AF7;
  --bubble-user:#232842; --bubble-ai:#131518;
  --shadow:0 1px 2px rgba(0,0,0,.5); --shadow-lg:0 12px 32px -8px rgba(0,0,0,.7);
  --ring:0 0 0 3px rgba(124,138,247,.35); --scrim:rgba(0,0,0,.62);
}
[data-skin=graphite] .app-sidebar{border-right:1px solid var(--border);box-shadow:none}
[data-skin=graphite] .nav-item{border-radius:6px}
[data-skin=graphite] .nav-item.is-active{background:var(--accent-soft);box-shadow:inset 2px 0 0 var(--accent)}
[data-skin=graphite] .composer:focus-within{border-color:var(--accent);box-shadow:var(--ring)}
[data-skin=graphite] .msg{border:1px solid var(--border)}

/* ── 2. AURORA — glass. Glass is tinted with the DARK base, not white, so
   the gradient shifts hue not luminance and contrast stays locked.
   Worst-case over the brightest blob overlap: 15.23 dark / 15.92 light. ── */
:root[data-skin="aurora"]{
  --bg:#EEF2FB;
  --bg-art:radial-gradient(52vw 52vw at 12% -8%,rgba(124,92,255,.14),transparent 62%),
           radial-gradient(46vw 46vw at 92% 14%,rgba(34,184,207,.12),transparent 60%),
           radial-gradient(58vw 58vw at 68% 104%,rgba(255,122,182,.10),transparent 62%);
  --surface:rgba(255,255,255,.72); --surface-2:rgba(255,255,255,.86);
  --sidebar-bg:rgba(255,255,255,.60); --header-bg:rgba(255,255,255,.62);
  --composer-bg:rgba(255,255,255,.80);
  --border:rgba(16,24,40,.10); --border-strong:rgba(16,24,40,.20);
  --text:#101828; --text-dim:#4B5768;
  --font:ui-sans-serif,'Inter',system-ui,sans-serif; --font-read:var(--font);
  --accent:#0F5F94; --accent-text:#FFFFFF; --accent-soft:rgba(15,95,148,.12); --accent-2:#0F5F94;
  --danger:#B42318; --success:#067647; --warn:#8A5A00; --info:#0F5F94;
  --bubble-user:rgba(15,95,148,.12); --bubble-ai:rgba(255,255,255,.78);
  --radius:16px; --radius-lg:22px;
  --shadow:0 1px 1px rgba(16,24,40,.05),inset 0 1px 0 rgba(255,255,255,.65);
  --shadow-lg:0 20px 50px -20px rgba(16,24,40,.35),inset 0 1px 0 rgba(255,255,255,.7);
  --ring:0 0 0 3px rgba(15,95,148,.30); --scrim:rgba(12,20,38,.30); --blur:18px;
}
.dark:root[data-skin="aurora"]{
  --bg:#0A0F1A;
  --bg-art:radial-gradient(52vw 52vw at 12% -8%,rgba(76,59,207,.26),transparent 62%),
           radial-gradient(46vw 46vw at 92% 14%,rgba(30,136,168,.20),transparent 60%),
           radial-gradient(58vw 58vw at 68% 104%,rgba(166,63,160,.15),transparent 62%);
  --surface:rgba(11,17,32,.66); --surface-2:rgba(20,28,51,.72);
  --sidebar-bg:rgba(11,17,32,.58); --header-bg:rgba(11,17,32,.60);
  --composer-bg:rgba(20,28,51,.78);
  --border:rgba(255,255,255,.10); --border-strong:rgba(255,255,255,.20);
  --text:#F2F5FA; --text-dim:#A9B4CB;
  --accent:#8FDCF2; --accent-text:#062431; --accent-soft:rgba(143,220,242,.14); --accent-2:#8FDCF2;
  --danger:#FF8A8A; --success:#5FE3A1; --warn:#F5C453; --info:#8FDCF2;
  --bubble-user:rgba(143,220,242,.13); --bubble-ai:rgba(11,17,32,.70);
  --shadow:0 1px 1px rgba(0,0,0,.4),inset 0 1px 0 rgba(255,255,255,.10);
  --shadow-lg:0 24px 60px -24px rgba(0,0,0,.8),inset 0 1px 0 rgba(255,255,255,.14);
  --ring:0 0 0 3px rgba(143,220,242,.32); --scrim:rgba(4,8,18,.55);
}
/* blur only on things that overlay SCROLLING content, never the message list */
[data-skin=aurora] .glass{backdrop-filter:blur(var(--blur)) saturate(160%);
  -webkit-backdrop-filter:blur(var(--blur)) saturate(160%)}
/* mobile gets the palette, not the effect: 4-8ms/frame on mid-range Android */
@media (max-width:860px){
  [data-skin=aurora] .glass{backdrop-filter:none;-webkit-backdrop-filter:none}
  .dark:root[data-skin=aurora]{--sidebar-bg:#0A0F1A;--header-bg:#0A0F1A;--composer-bg:#141C33}
}
@media (prefers-reduced-transparency:reduce){
  [data-skin=aurora] .glass{backdrop-filter:none;background:var(--bg)}
}

/* ── 3. VELLUM — warm paper. Dark variant is candlelight, not grey.
   Deliberately capped below 21:1; harsh contrast is what it avoids.
   text 13.72 dark / 14.03 light ───────────────────────────────────────── */
:root[data-skin="vellum"]{
  --bg:#F2ECE0; --bg-art:none; --surface:#FBF7EF; --surface-2:#EFE7D8;
  --sidebar-bg:#EDE5D6; --header-bg:#F2ECE0; --composer-bg:#FBF7EF;
  --border:#DED2BC; --border-strong:#C7B79B; --text:#2B2620; --text-dim:#6B6153;
  --font:ui-sans-serif,'Inter',system-ui,sans-serif;
  --font-read:'Iowan Old Style','Palatino Linotype',Palatino,Georgia,ui-serif,serif;
  --accent:#9E4A28; --accent-text:#FFF6EE; --accent-soft:#F0DFD2; --accent-2:#9E4A28;
  --danger:#A32E1F; --success:#3F6B2E; --warn:#8A5A00; --info:#9E4A28;
  --bubble-user:#EAE0CB; --bubble-ai:transparent;
  --radius:5px; --radius-lg:8px;
  --shadow:0 1px 0 rgba(90,70,45,.10); --shadow-lg:0 10px 30px -12px rgba(70,52,30,.32);
  --ring:0 0 0 3px rgba(158,74,40,.26); --scrim:rgba(56,44,28,.38); --blur:0px;
}
.dark:root[data-skin="vellum"]{
  --bg:#16120E; --surface:#1E1915; --surface-2:#2A231C;
  --sidebar-bg:#14100C; --header-bg:#16120E; --composer-bg:#1E1915;
  --border:#372E25; --border-strong:#4C4034; --text:#EDE3D3; --text-dim:#A2947F;
  --accent:#DB9250; --accent-text:#241708; --accent-soft:#2E2317; --accent-2:#DB9250;
  --danger:#F0836B; --success:#8FC97A; --warn:#E3B45C; --info:#DB9250;
  --bubble-user:#33281C; --bubble-ai:transparent;
  --shadow:0 1px 0 rgba(0,0,0,.4); --shadow-lg:0 14px 36px -14px rgba(0,0,0,.75);
  --ring:0 0 0 3px rgba(219,146,80,.30); --scrim:rgba(10,7,4,.62);
}
/* the AI stops being a bubble and becomes prose on a page — the skin's whole
   argument, achieved on shared markup */
[data-skin=vellum] .msg-content{font-family:var(--font-read);font-size:17px;
  line-height:1.72;max-width:66ch;letter-spacing:.005em}
[data-skin=vellum] .msg-ai .msg-content{background:none;border:0;border-radius:0;
  padding-left:0;border-bottom:1px solid var(--border);padding-bottom:1.15rem}
[data-skin=vellum] .msg-user .msg-content{background:var(--bubble-user);border:0;
  box-shadow:inset 3px 0 0 var(--accent)}

/* ── 4. CLAY — neumorphic. Depth lives in shadow; text/borders stay full
   strength. Bubbles are FLAT: 200 messages x 2 blurred shadows would
   stutter on mid-range Android. text 12.47 dark / 12.66 light ─────────── */
:root[data-skin="clay"]{
  --bg:#E4E8EF; --bg-art:none; --surface:#E4E8EF; --surface-2:#EDF0F5;
  --sidebar-bg:#E4E8EF; --header-bg:#E4E8EF; --composer-bg:#E4E8EF;
  --border:#CFD5DF; --border-strong:#B4BDCB; --text:#1E2430; --text-dim:#545C6C;
  --font:ui-rounded,'Nunito',ui-sans-serif,system-ui,sans-serif; --font-read:var(--font);
  --accent:#4A59C4; --accent-text:#FFFFFF; --accent-soft:#DCE1F5; --accent-2:#4A59C4;
  --danger:#A82A1C; --success:#2F7D5B; --warn:#8A5A00; --info:#4A59C4;
  --bubble-user:#DCE1EA; --bubble-ai:#EDF0F5;
  --radius:16px; --radius-lg:24px;
  --shadow:6px 6px 12px rgba(163,172,187,.55),-6px -6px 12px rgba(255,255,255,.90);
  --shadow-in:inset 4px 4px 8px rgba(163,172,187,.55),inset -4px -4px 8px rgba(255,255,255,.90);
  --shadow-lg:14px 14px 30px rgba(150,160,178,.50),-10px -10px 24px rgba(255,255,255,.85);
  --ring:0 0 0 3px rgba(74,89,196,.38); --scrim:rgba(30,36,48,.34); --blur:0px;
}
.dark:root[data-skin="clay"]{
  --bg:#23262C; --surface:#23262C; --surface-2:#2B2F36;
  --sidebar-bg:#23262C; --header-bg:#23262C; --composer-bg:#23262C;
  --border:#33373F; --border-strong:#454A55; --text:#E6E9EF; --text-dim:#98A0AE;
  --accent:#8493FF; --accent-text:#10142B; --accent-soft:#2C3150; --accent-2:#8493FF;
  --danger:#FF7A70; --success:#6FD79E; --warn:#E9BC63; --info:#8493FF;
  --bubble-user:#2A2E36; --bubble-ai:#2B2F36;
  --shadow:6px 6px 12px rgba(13,15,18,.62),-6px -6px 12px rgba(52,57,66,.55);
  --shadow-in:inset 4px 4px 8px rgba(13,15,18,.62),inset -4px -4px 8px rgba(52,57,66,.50);
  --shadow-lg:14px 14px 30px rgba(10,12,15,.70),-10px -10px 24px rgba(52,57,66,.45);
  --ring:0 0 0 3px rgba(132,147,255,.40); --scrim:rgba(10,12,15,.60);
}
[data-skin=clay] .card,[data-skin=clay] .modal-panel,[data-skin=clay] .btn,
[data-skin=clay] .toast{box-shadow:var(--shadow);border:0}
[data-skin=clay] .btn:active,[data-skin=clay] .nav-item.is-active{box-shadow:var(--shadow-in)}
[data-skin=clay] .composer,[data-skin=clay] input,[data-skin=clay] textarea{
  box-shadow:var(--shadow-in);border:0;background:var(--surface)}
[data-skin=clay] .msg-content{box-shadow:none;border:1px solid var(--border)}
/* focus ring stacks ON TOP of the neumorphic shadow — the usual a11y bug, fixed */
[data-skin=clay] :focus-visible{box-shadow:var(--shadow),var(--ring);outline:0}

/* ── 5. VOID — OLED. Every boundary is a line; speaker identity is a
   coloured left rule, which keeps black pixels black (and the panel off).
   text 21.00 both. Offered, not defaulted: white-on-black haloes for some
   readers over long sessions. ─────────────────────────────────────────── */
:root[data-skin="void"]{
  --bg:#FFFFFF; --bg-art:none; --surface:#FFFFFF; --surface-2:#F2F2F2;
  --sidebar-bg:#FFFFFF; --header-bg:#FFFFFF; --composer-bg:#FFFFFF;
  --border:#D6D6D6; --border-strong:#8A8A8A; --text:#000000; --text-dim:#565656;
  --font:ui-sans-serif,'Inter',system-ui,sans-serif; --font-read:var(--font);
  --accent:#00695A; --accent-text:#FFFFFF; --accent-soft:#E2F1EE; --accent-2:#00695A;
  --danger:#C10000; --success:#00695A; --warn:#7A5200; --info:#00695A;
  --bubble-user:#EFEFEF; --bubble-ai:#FFFFFF;
  --radius:4px; --radius-lg:6px; --shadow:none; --shadow-lg:none;
  --ring:0 0 0 2px #FFFFFF,0 0 0 4px #00695A; --scrim:rgba(0,0,0,.55); --blur:0px;
}
.dark:root[data-skin="void"]{
  --bg:#000000; --surface:#000000; --surface-2:#0D0D0D;
  --sidebar-bg:#000000; --header-bg:#000000; --composer-bg:#0D0D0D;
  --border:#2A2A2A; --border-strong:#5A5A5A; --text:#FFFFFF; --text-dim:#A0A0A0;
  --accent:#00E5A0; --accent-text:#00140D; --accent-soft:#04231A; --accent-2:#00E5A0;
  --danger:#FF4D4D; --success:#00E5A0; --warn:#FFC400; --info:#00E5A0;
  --bubble-user:#101010; --bubble-ai:#000000;
  --ring:0 0 0 2px #000000,0 0 0 4px #00E5A0; --scrim:rgba(0,0,0,.80);
}
[data-skin=void] .card,[data-skin=void] .modal-panel,[data-skin=void] .toast,
[data-skin=void] .msg-content,[data-skin=void] .btn{box-shadow:none}
[data-skin=void] .modal-panel,[data-skin=void] .dropdown,[data-skin=void] .toast{
  border:1px solid var(--border-strong)}
[data-skin=void] .msg-content{border:0;border-left:2px solid var(--border);
  border-radius:0;padding-left:.9rem}
[data-skin=void] .msg-user .msg-content{border-left-color:var(--accent)}
[data-skin=void] :focus-visible{box-shadow:var(--ring)!important;outline:0}

/* ── 6. SHERBET — vivid. Gradient confined to the send button + one 2px
   header rule; glow to one element. text 15.94 dark / 16.22 light.
   NOTE: light --accent is 4.69:1 — AA, not AAA. Never set 12px timestamps
   in --accent on this skin; that is what --text-dim is for. ───────────── */
:root[data-skin="sherbet"]{
  --bg:#FFF4F9;
  --bg-art:radial-gradient(60vw 40vw at 100% 0,rgba(214,36,138,.06),transparent 60%),
           radial-gradient(50vw 40vw at 0 100%,rgba(14,116,144,.06),transparent 60%);
  --surface:#FFFFFF; --surface-2:#FFE9F3;
  --sidebar-bg:#FFEAF4; --header-bg:#FFFFFF; --composer-bg:#FFFFFF;
  --border:#F6CFE3; --border-strong:#E9A7C9; --text:#2A1B2E; --text-dim:#6D5473;
  --font:ui-rounded,'Nunito','Baloo 2',ui-sans-serif,system-ui,sans-serif; --font-read:var(--font);
  --accent:#D6248A; --accent-2:#0E7490; --accent-text:#FFFFFF; --accent-soft:#FFE0F0;
  --danger:#D62246; --success:#0E7490; --warn:#8A5A00; --info:#0E7490;
  --bubble-user:#FFE0F0; --bubble-ai:#FFFFFF;
  --radius:14px; --radius-lg:20px;
  --shadow:0 2px 0 rgba(214,36,138,.14); --shadow-lg:0 16px 40px -14px rgba(214,36,138,.35);
  --ring:0 0 0 3px rgba(214,36,138,.32); --scrim:rgba(42,27,46,.38); --blur:0px;
}
.dark:root[data-skin="sherbet"]{
  --bg:#0D0A1A;
  --bg-art:radial-gradient(60vw 45vw at 100% -5%,rgba(255,87,180,.14),transparent 62%),
           radial-gradient(55vw 45vw at -5% 105%,rgba(53,227,255,.12),transparent 62%);
  --surface:#171130; --surface-2:#221A42;
  --sidebar-bg:#120D25; --header-bg:#0D0A1A; --composer-bg:#221A42;
  --border:#33265C; --border-strong:#4B3A82; --text:#F3EEFF; --text-dim:#AB9DD6;
  --accent:#FF57B4; --accent-2:#35E3FF; --accent-text:#22001A; --accent-soft:#2E1140;
  --danger:#FF6B8A; --success:#35E3FF; --warn:#FFC46B; --info:#35E3FF;
  --bubble-user:#3A1140; --bubble-ai:#171130;
  --shadow:0 0 0 1px rgba(255,87,180,.20); --shadow-lg:0 18px 46px -16px rgba(255,87,180,.45);
  --ring:0 0 0 3px rgba(255,87,180,.40); --scrim:rgba(6,4,14,.62);
}
[data-skin=sherbet] .btn-primary,[data-skin=sherbet] .send-btn{
  background:linear-gradient(135deg,var(--accent),var(--accent-2));color:var(--accent-text)}
.dark [data-skin=sherbet] .send-btn{box-shadow:0 0 18px -4px var(--accent)}
[data-skin=sherbet] .app-header{border-bottom:2px solid transparent;
  border-image:linear-gradient(90deg,var(--accent),var(--accent-2)) 1}
[data-skin=sherbet] .msg-user .msg-content{border-bottom-right-radius:4px}
[data-skin=sherbet] .msg-ai .msg-content{border-bottom-left-radius:4px}


/* ── ISLAND DOCK (Deck theme, desktop only) ─────────────────────────────
   Pills that widen on hover to reveal their label, an filled active pill and
   a dot under it. The whole nav is one row, so it is desktop-only by
   construction - 11 items never fit a phone, and below 1024px the Deck rules
   never fire at all, leaving the normal drawer. */
@media (min-width:1024px){
  :root[data-layout="deck"] #left-sidebar{
    /* a ceiling, not a width: the pill takes whatever its label needs up to
       this, so the longest one still fits */
    --dock-item:50px; --dock-open:260px; --dock-gap:6px; --dock-pad:8px;
    position:fixed; left:50%; bottom:20px; top:auto; transform:translateX(-50%);
    flex-direction:row; align-items:center; gap:var(--dock-gap);
    width:auto; height:auto; padding:var(--dock-pad);
    border:1px solid color-mix(in oklab,var(--text) 10%,transparent);
    border-radius:9999px; background:var(--surface);
    box-shadow:0 10px 15px -3px rgb(0 0 0/.10),0 4px 6px -2px rgb(0 0 0/.05),0 20px 25px -5px rgb(0 0 0/.18);
    z-index:50; overflow:visible;
  }
  /* dissolve the two wrappers so nav + footer buttons become one flex row */
  :root[data-layout="deck"] #left-sidebar>div:first-child{display:contents}
  :root[data-layout="deck"] #brand-bar,
  :root[data-layout="deck"] #nav-footer>a{display:none}
  :root[data-layout="deck"] #main-nav,
  :root[data-layout="deck"] #nav-footer{
    display:flex; flex-direction:row; align-items:center; gap:var(--dock-gap);
    padding:0; margin:0; border:0; background:none;
  }
  :root[data-layout="deck"] #nav-footer{
    margin-left:var(--dock-gap); padding-left:var(--dock-gap);
    border-left:1px solid color-mix(in oklab,var(--text) 12%,transparent);
  }

  /* Sized by its own label rather than to a fixed width: max-width does the
     opening and closing, and width:auto stops a short label being stretched.
     The old fixed 150px cut "Manage Sessions" off mid-word, and any single
     number would be wrong for one label or another. */
  :root[data-layout="deck"] .dock-btn{
    position:relative; display:flex; align-items:center; justify-content:flex-start;
    width:auto; max-width:var(--dock-item); height:var(--dock-item); flex-shrink:0;
    padding:0; border-radius:9999px; overflow:hidden;
    background:transparent; color:var(--text-dim);
    transition:max-width .4s cubic-bezier(.25,.8,.25,1), background-color .3s ease, color .3s ease;
  }
  :root[data-layout="deck"] .dock-btn>i,
  :root[data-layout="deck"] .dock-btn>svg{
    min-width:var(--dock-item); width:var(--dock-item); height:var(--dock-item);
    padding:15px; margin:0; flex-shrink:0; z-index:2;
  }
  :root[data-layout="deck"] .dock-btn>span{
    white-space:nowrap; opacity:0; transform:translateX(10px);
    font-size:13px; font-weight:500; padding-right:18px; pointer-events:none;
    transition:opacity .2s ease, transform .4s cubic-bezier(.25,.8,.25,1);
  }
  /* expand on hover AND on keyboard focus - hover-only would strand Tab users */
  :root[data-layout="deck"] .dock-btn:hover,
  :root[data-layout="deck"] .dock-btn:focus-visible{
    max-width:var(--dock-open); background:color-mix(in oklab,var(--text) 12%,transparent); color:var(--text);
  }
  :root[data-layout="deck"] .dock-btn:hover>span,
  :root[data-layout="deck"] .dock-btn:focus-visible>span{
    opacity:1; transform:translateX(0); transition-delay:.05s;
  }
  /* the current destination stays filled and labelled until you reach for another */
  /* --glass-accent-ink is only set while glass is on, and falls back to the
     engine's own fitted ink otherwise - so this is right either way. */
  :root[data-layout="deck"] .dock-btn.is-active{
    max-width:var(--dock-open); background:var(--accent);
    color:var(--glass-accent-ink, var(--accent-text));
  }
  :root[data-layout="deck"] .dock-btn.is-active>span{opacity:1;transform:translateX(0)}
  :root[data-layout="deck"] #left-sidebar:hover .dock-btn.is-active:not(:hover){
    max-width:var(--dock-item); background:transparent; color:var(--text-dim);
  }
  :root[data-layout="deck"] #left-sidebar:hover .dock-btn.is-active:not(:hover)>span{
    opacity:0; transform:translateX(10px);
  }
  :root[data-layout="deck"] .dock-btn.is-active::after{
    content:""; position:absolute; bottom:5px; left:50%; width:5px; height:5px;
    border-radius:50%; background:currentColor; opacity:0;
    transform:translateX(-50%) scale(0);
    transition:transform .4s cubic-bezier(.25,.8,.25,1), opacity .4s ease-out;
  }
  :root[data-layout="deck"] #left-sidebar:hover .dock-btn.is-active:not(:hover)::after{
    opacity:1; transform:translateX(-50%) scale(1);
  }
  /* the dock is the nav, so nothing may sit under it */
  :root[data-layout="deck"] #app{padding-bottom:96px}
  :root[data-layout="deck"] #toast{bottom:96px}
  :root[data-layout="deck"] #sketchboard-sidebar{bottom:96px}
}


/* background art is one fixed layer for every skin — never repaints on scroll */
body{background-color:var(--bg);background-image:var(--bg-art);background-attachment:fixed}

/* ─────────────────────────────────────────────────────────────────────
   LAYOUT LAYER — shell structure only. Shared markup; a layout is one
   attribute on <html>.

   THREE FACTS FROM THE REAL CODE THAT SHAPE ALL OF THIS:
   1. Tailwind Play CDN injects its stylesheet AFTER the inline <style>,
      so on equal specificity Tailwind wins. Every rule below is
      `:root[data-layout=x] #id` = (1,1,1), which beats `.md\:relative`.
      A plain `.sidebar{}` rule would silently do nothing.
   2. `.hf{display:none!important}` is the app's universal hide and beats
      everything here — closed modals/drawers stay closed for free.
   3. The drawers are JS class toggles (`-translate-x-full` on
      #left-sidebar, `translate-x-full` on #sketchboard-sidebar). Never
      write `transform:none` unscoped or the mobile drawer dies.

   THE RULE THAT REMOVES ~90% OF THE MOBILE RISK: every variant lives
   inside @media (min-width:1024px). Below that, no layout rule fires and
   the app is exactly what ships today. A broken variant can never strand
   a phone user.

   DOM TAX (added once, shared by every layout — not per-theme markup):
     #brand-bar #main-nav #nav-footer #main-col #chat-header
     #composer-dock #composer-shell   + data-label="..." on nav buttons
     + <aside id="session-rail"> (display:none except rail/trio)
   ───────────────────────────────────────────────────────────────────── */

/* ── 1. STUDIO — polished classic. DEFAULT. ───────────────────────────
   Fixes what makes today's shell feel generic: one reading measure shared
   by stream AND composer (today the stream is full-bleed but the composer
   is max-w-3xl, so text and input don't align), a real surface hierarchy,
   and nav grouped Chat/Workspace/System instead of eight identical rows.
   Mobile IS today's mobile — zero risk. */
@media (min-width:768px){
  :root[data-layout="studio"] #app{
    display:grid; background:var(--bg);
    grid-template-columns:272px minmax(0,1fr); grid-template-areas:"nav main";
  }
  :root[data-layout="studio"] #left-sidebar{
    grid-area:nav; position:relative; transform:none; width:auto;
    background:var(--sidebar-bg); border-right:1px solid var(--border);
  }
  :root[data-layout="studio"] #main-col{grid-area:main;background:var(--surface);min-width:0}
  /* nav groups with no extra markup */
  :root[data-layout="studio"] #main-nav>button:nth-child(2),
  :root[data-layout="studio"] #main-nav>button:nth-child(6){
    margin-top:.75rem;padding-top:.875rem;border-top:1px solid var(--border);
  }
  :root[data-layout="studio"] #chat-header{
    background:color-mix(in oklab,var(--header-bg) 78%,transparent);backdrop-filter:blur(14px);
  }
  :root[data-layout="studio"] #chat-container{padding-inline:max(2rem,calc((100% - 46rem)/2))}
  :root[data-layout="studio"] #composer-shell{max-width:46rem}
  :root[data-layout="studio"] #composer-dock{
    background:linear-gradient(to top,var(--surface) 68%,transparent);padding-bottom:1.75rem;
  }
  :root[data-layout="studio"] #sketchboard-sidebar{
    border-radius:var(--radius) 0 0 var(--radius);box-shadow:var(--shadow-lg);
  }
}

/* ── 2. RAIL — 64px icon rail + 264px session panel + conversation.
   Labels come from data-label on :hover AND :focus-visible so keyboard
   users get them too. Needs #session-rail. Mobile drawer is itself
   rail+list (64+236=300 of 360) — different feel, still fits. */
@media (min-width:1024px){
  :root[data-layout="rail"] #app{
    display:grid;grid-template-columns:64px var(--rail-w,264px) minmax(0,1fr);
    grid-template-areas:"rail list main";
    transition:grid-template-columns .42s cubic-bezier(.2,0,0,1);
  }
  :root[data-layout="rail"] #left-sidebar{
    grid-area:rail;position:relative;transform:none;width:64px;
    background:var(--bg);border-right:1px solid var(--border);
  }
  :root[data-layout="rail"] #main-nav,:root[data-layout="rail"] #nav-footer{
    display:flex;flex-direction:column;gap:.25rem;padding:.5rem;border-top:0;
  }
  :root[data-layout="rail"] #main-nav>button,
  :root[data-layout="rail"] #nav-footer>button{
    width:48px;height:44px;padding:0;justify-content:center;position:relative;
  }
  /* hide label spans only — NOT the logo glyph */
  :root[data-layout="rail"] #main-nav>button>span,
  :root[data-layout="rail"] #nav-footer>button>span,
  :root[data-layout="rail"] #brand-bar h1,
  :root[data-layout="rail"] #nav-footer>a{display:none}
  :root[data-layout="rail"] #brand-bar{justify-content:center;padding:0}
  :root[data-layout="rail"] [data-label]:hover::after,
  :root[data-layout="rail"] [data-label]:focus-visible::after{
    content:attr(data-label);position:absolute;left:calc(100% + 10px);
    padding:.3rem .55rem;border-radius:6px;font-size:11px;white-space:nowrap;
    background:var(--text);color:var(--bg);box-shadow:var(--shadow-lg);z-index:60;
  }
  :root[data-layout="rail"] #session-rail{
    grid-area:list;display:flex;flex-direction:column;min-height:0;
    background:var(--surface-2);border-right:1px solid var(--border);
  }
  :root[data-layout="rail"] #main-col{grid-area:main;background:var(--surface);min-width:0}
  :root[data-layout="rail"] #chat-container{padding-inline:2rem}
}

/* ── 3. TRIO — desktop-messenger three columns; a 4th appears >=1440px
   and the sketchboard stops being a drawer. The :has() pair lets the
   existing toggleSketchboard() keep working while docked. */
@media (min-width:1024px){
  :root[data-layout="trio"] #app{
    display:grid;grid-template-columns:236px var(--rail-w,312px) minmax(0,1fr);
    grid-template-areas:"nav list main";
    transition:grid-template-columns .42s cubic-bezier(.2,0,0,1);
  }
  :root[data-layout="trio"] #left-sidebar{
    grid-area:nav;position:relative;transform:none;width:auto;
    background:var(--sidebar-bg);border-right:1px solid var(--border);
  }
  :root[data-layout="trio"] #session-rail{
    grid-area:list;display:flex;flex-direction:column;min-height:0;
    background:var(--surface-2);border-right:1px solid var(--border);
  }
  :root[data-layout="trio"] #main-col{grid-area:main;background:var(--surface);min-width:0}
  :root[data-layout="trio"] #brand-bar,
  :root[data-layout="trio"] #session-rail>header,
  :root[data-layout="trio"] #chat-header{height:64px;flex:none}
  :root[data-layout="trio"] #chat-container{padding-inline:1.5rem}
  :root[data-layout="trio"] #composer-shell{max-width:none}
}
@media (min-width:1440px){
  :root[data-layout="trio"] #app{
    grid-template-columns:236px var(--rail-w,312px) minmax(0,1fr) 320px;
    grid-template-areas:"nav list main sketch";
  }
  :root[data-layout="trio"] #sketchboard-sidebar{
    grid-area:sketch;position:relative;transform:none;width:auto;
    box-shadow:none;border-left:1px solid var(--border);
  }
  :root[data-layout="trio"] #sketchboard-sidebar.translate-x-full{display:none}
  :root[data-layout="trio"] #app:has(#sketchboard-sidebar.translate-x-full){
    grid-template-columns:236px var(--rail-w,312px) minmax(0,1fr) 0;
  }
}

/* ── 4. ZEN — focus mode. Chrome fades; nav parks off-canvas behind a
   12px edge strip. DISCOVERABILITY IS NOT HOVER-ONLY: the mobile
   hamburger is un-hidden on desktop (always visible + focusable),
   :focus-within reveals the nav for Tab users, and the hamburger acts as
   a pin. Hover is a shortcut, never the mechanism. */
@media (min-width:1024px){
  :root[data-layout="zen"] #app{display:block;background:var(--bg)}
  :root[data-layout="zen"] #main-col{height:100%;background:transparent}
  :root[data-layout="zen"] #left-sidebar{
    position:fixed;inset-block:0;left:0;width:272px;z-index:40;
    transform:translateX(calc(-100% + 12px));
    transition:transform .22s cubic-bezier(.2,.8,.2,1);
    background:var(--sidebar-bg);box-shadow:var(--shadow-lg);
  }
  :root[data-layout="zen"] #left-sidebar:hover,
  :root[data-layout="zen"] #left-sidebar:focus-within,
  :root[data-layout="zen"] #left-sidebar:not(.-translate-x-full){transform:translateX(0)}
  :root[data-layout="zen"] #chat-header>div>button:first-child{display:flex}
  :root[data-layout="zen"] #chat-header{
    height:52px;border-bottom:0;background:transparent;backdrop-filter:none;
    width:min(42rem,100%);margin-inline:auto;opacity:.55;transition:opacity .2s;
  }
  :root[data-layout="zen"] #chat-header:hover,
  :root[data-layout="zen"] #chat-header:focus-within{opacity:1}
  :root[data-layout="zen"] #chat-container{
    padding-inline:max(1.5rem,calc((100% - 42rem)/2));padding-block:2.5rem;line-height:1.75;
  }
  :root[data-layout="zen"] #composer-shell{max-width:42rem}
  :root[data-layout="zen"] #composer-dock{
    background:linear-gradient(to top,var(--bg) 70%,transparent);padding-bottom:2rem;
  }
  :root[data-layout="zen"] #summarize-banner,
  :root[data-layout="zen"] #load-more-banner{top:52px}
}

/* ── 5. DECK — floating cards + bottom dock. The trick that needs no
   markup change: `#left-sidebar>div{display:contents}` dissolves the
   wrapper so #main-nav and #nav-footer become row siblings in the pill.
   DOCK DOES NOT SHIP ON MOBILE: 11 items x 40px = 440 > 360. Mobile
   keeps the drawer but retains the card look. */
@media (min-width:1024px){
  :root[data-layout="deck"] body{
    background:radial-gradient(120% 80% at 50% -20%,
      color-mix(in oklab,var(--accent) 14%,var(--bg)),var(--bg));
  }
  :root[data-layout="deck"] #app{display:block;padding:18px 18px 96px}
  :root[data-layout="deck"] #main-col{
    height:100%;background:var(--surface);border:1px solid var(--border);
    border-radius:calc(var(--radius)*2);box-shadow:var(--shadow-lg);overflow:hidden;
  }
  /* nav for Deck is the island dock defined below */
  :root[data-layout="deck"] #sketchboard-sidebar{
    top:18px;right:18px;bottom:96px;border-radius:calc(var(--radius)*2);
    border:1px solid var(--border);box-shadow:var(--shadow-lg);
  }
  /* #toast is bottom-32 md:bottom-24 and would sit BEHIND the dock */
  :root[data-layout="deck"] #toast{bottom:92px}
}

/* ── 6. CONSOLE — dense command centre. `#main-col{display:contents}`
   dissolves <main> so its header/stream/composer join the #app grid —
   that is how the status band spans full width with zero markup change.
   Two consequences straight out of the code: the banners are
   `absolute top-16` inside <main>, so once <main> is display:contents
   they resolve against #app and must be repositioned; and .msg-row has
   contain-intrinsic-size:auto 90px while dense rows are ~48px, which
   makes the scrollbar jitter on fast scroll unless overridden. */
@media (min-width:1024px){
  :root[data-layout="console"]{--radius:5px}
  :root[data-layout="console"] #app{
    display:grid;font-size:13px;
    grid-template-columns:208px minmax(0,1fr) 300px;
    grid-template-rows:44px minmax(0,1fr) auto;
    grid-template-areas:"nav bar sketch" "nav stream sketch" "nav dock sketch";
  }
  :root[data-layout="console"] #main-col{display:contents}
  :root[data-layout="console"] #left-sidebar{
    grid-area:nav;position:relative;transform:none;width:auto;
    background:var(--sidebar-bg);border-right:1px solid var(--border);
  }
  :root[data-layout="console"] #brand-bar{height:44px;padding-inline:.75rem}
  :root[data-layout="console"] #main-nav{padding:.375rem;gap:1px}
  :root[data-layout="console"] #main-nav>button,
  :root[data-layout="console"] #nav-footer>button{
    padding:.3125rem .5rem;font-size:12px;border-radius:4px;
  }
  :root[data-layout="console"] #chat-header{
    grid-area:bar;height:44px;padding-inline:.875rem;position:static;
    border-bottom:1px solid var(--border);
  }
  :root[data-layout="console"] #chat-container{grid-area:stream;padding:.75rem 1rem;min-width:0}
  :root[data-layout="console"] #chat-container>.msg-row+.msg-row{margin-top:.5rem}
  :root[data-layout="console"] .msg-row{contain-intrinsic-size:auto 48px}
  :root[data-layout="console"] #composer-dock{grid-area:dock;padding:.625rem 1rem 1rem}
  :root[data-layout="console"] #composer-shell{max-width:none}
  :root[data-layout="console"] #sketchboard-sidebar{
    grid-area:sketch;position:relative;transform:none;width:auto;
    box-shadow:none;border-left:1px solid var(--border);
  }
  :root[data-layout="console"] #sketchboard-sidebar.translate-x-full{display:none}
  :root[data-layout="console"] #app:has(#sketchboard-sidebar.translate-x-full){
    grid-template-columns:208px minmax(0,1fr) 0;
  }
  :root[data-layout="console"] #summarize-banner,
  :root[data-layout="console"] #load-more-banner{top:44px;left:208px;right:300px}
  :root[data-layout="console"] #modal-backdrop{align-items:flex-start;padding-top:11vh}
}
/* Console must NEVER lower #chat-input / #new-pin-input below 16px on
   mobile: they carry `text-base md:text-sm` precisely so iOS does not zoom
   the page on focus. user-scalable=no in the viewport meta is ignored by
   modern iOS Safari and will not save you. */


/* ── 7. LEDGER — the admin-dashboard shape: one full-width bar across the
   top, navigation beneath it on the left, content in a framed card.
   display:contents on the sidebar and its wrapper dissolves both, so the
   brand bar, the nav and the footer become grid items of #app and the bar
   can span the whole width without a second DOM. */
@media (min-width:1024px){
  :root[data-layout="ledger"] #app{
    display:grid; background:var(--bg);
    grid-template-columns:var(--nav-w,17rem) minmax(0,1fr);
    grid-template-rows:56px minmax(0,1fr) auto;
    grid-template-areas:"brand brand" "nav main" "foot main";
  }
  /* only the first wrapper: #nav-footer is a #left-sidebar>div too, and
     dissolving it drops the account row, Nuke Server and Log out into
     implicit rows at the bottom of the page */
  :root[data-layout="ledger"] #left-sidebar,
  :root[data-layout="ledger"] #left-sidebar>div:first-child{display:contents}
  :root[data-layout="ledger"] #brand-bar{
    grid-area:brand; height:56px; padding-inline:1.25rem;
    background:var(--surface); border-bottom:1px solid var(--border);
  }
  :root[data-layout="ledger"] #main-nav{
    grid-area:nav; padding:.875rem; background:var(--sidebar-bg);
    border-right:1px solid var(--border); overflow-y:auto;
  }
  :root[data-layout="ledger"] #nav-footer{
    grid-area:foot; padding:.75rem .875rem; background:var(--sidebar-bg);
    border-right:1px solid var(--border); border-top:1px solid var(--border);
  }
  :root[data-layout="ledger"] #main-col{
    grid-area:main; min-width:0; background:var(--bg);
    padding:1rem 1.25rem 0; display:flex; flex-direction:column;
  }
  /* the conversation is a card, the way a dashboard puts every panel in one */
  :root[data-layout="ledger"] #chat-header{
    border:1px solid var(--border); border-bottom:0;
    border-radius:var(--radius-lg) var(--radius-lg) 0 0;
    background:var(--header-bg); flex:none;
  }
  :root[data-layout="ledger"] #chat-container{
    border-inline:1px solid var(--border); background:var(--surface);
    padding-inline:max(1.5rem,calc((100% - var(--measure,46rem))/2));
  }
  :root[data-layout="ledger"] #composer-dock{
    border:1px solid var(--border); border-top:0;
    border-radius:0 0 var(--radius-lg) var(--radius-lg);
    background:var(--composer-bg); padding-bottom:1rem; margin-bottom:1.25rem;
  }
  :root[data-layout="ledger"] #composer-shell{max-width:var(--measure,46rem)}
  :root[data-layout="ledger"] #load-more-banner,
  :root[data-layout="ledger"] #summarize-banner{top:56px; left:var(--nav-w,17rem)}
  :root[data-layout="ledger"] #sketchboard-sidebar{top:56px}
}

/* ── 8. SPLIT — the desktop-messenger shape: a wide conversation list that
   shows what each chat is, and the chat itself filling the rest. The list is
   the same #session-rail the Rail and Trio layouts use. */
@media (min-width:1024px){
  :root[data-layout="split"] #app{
    display:grid; background:var(--bg);
    grid-template-columns:76px var(--rail-w,340px) minmax(0,1fr);
    grid-template-areas:"rail list main";
    transition:grid-template-columns .42s cubic-bezier(.2,0,0,1);
  }
  :root[data-layout="split"] #left-sidebar{
    grid-area:rail; position:relative; transform:none; width:76px;
    background:var(--bg); border-right:1px solid var(--border);
  }
  :root[data-layout="split"] #main-nav,
  :root[data-layout="split"] #nav-footer{
    display:flex; flex-direction:column; gap:.375rem; padding:.5rem; border-top:0;
  }
  :root[data-layout="split"] #main-nav>button,
  :root[data-layout="split"] #nav-footer>button{
    width:60px; height:52px; padding:0; position:relative;
    flex-direction:column; gap:3px; justify-content:center; font-size:9px;
  }
  :root[data-layout="split"] #main-nav>button>span,
  :root[data-layout="split"] #nav-footer>button>span{
    font-size:9px; line-height:1; max-width:56px; overflow:hidden;
    text-overflow:ellipsis; white-space:nowrap; margin:0;
  }
  :root[data-layout="split"] #brand-bar{justify-content:center;padding:0;height:60px}
  :root[data-layout="split"] #brand-bar h1,
  :root[data-layout="split"] #nav-footer>a{display:none}
  :root[data-layout="split"] #session-rail{
    grid-area:list; display:flex; flex-direction:column; min-height:0;
    background:var(--surface-2); border-right:1px solid var(--border);
  }
  :root[data-layout="split"] #main-col{grid-area:main;background:var(--surface);min-width:0}
  :root[data-layout="split"] #chat-container{padding-inline:2.5rem}
  :root[data-layout="split"] #composer-shell{max-width:none}
}

/* ── 9. CANVAS — no side navigation at all: the nav becomes a horizontal
   strip under the header and the conversation gets the full width. For
   people who keep the sketchboard open and want it beside the text. */
@media (min-width:1024px){
  :root[data-layout="canvas"] #app{
    display:grid; background:var(--bg);
    grid-template-rows:52px minmax(0,1fr);
    grid-template-columns:minmax(0,1fr);
    grid-template-areas:"bar" "main";
  }
  /* only the first wrapper: #nav-footer is a #left-sidebar>div too, and
     dissolving it drops the account row, Nuke Server and Log out into
     implicit rows at the bottom of the page */
  :root[data-layout="canvas"] #left-sidebar,
  :root[data-layout="canvas"] #left-sidebar>div:first-child{display:contents}
  :root[data-layout="canvas"] #brand-bar{display:none}
  :root[data-layout="canvas"] #main-nav{
    grid-area:bar; display:flex; flex-direction:row; align-items:center;
    gap:.25rem; padding:.375rem .75rem; overflow-x:auto;
    background:var(--sidebar-bg); border-bottom:1px solid var(--border);
  }
  :root[data-layout="canvas"] #main-nav>button{
    width:auto; flex:none; padding:.4rem .7rem; margin:0; border-top:0;
  }
  :root[data-layout="canvas"] #nav-footer{
    grid-area:bar; justify-self:end; align-self:center;
    display:flex; flex-direction:row; gap:.25rem; padding:.375rem .75rem;
    border-top:0; background:transparent;
  }
  :root[data-layout="canvas"] #nav-footer>button{width:auto;padding:.4rem .6rem}
  :root[data-layout="canvas"] #nav-footer>button>span,
  :root[data-layout="canvas"] #nav-footer>a{display:none}
  :root[data-layout="canvas"] #main-col{grid-area:main;background:var(--surface);min-width:0}
  :root[data-layout="canvas"] #chat-container{padding-inline:3rem}
  :root[data-layout="canvas"] #composer-shell{max-width:none}
  :root[data-layout="canvas"] #load-more-banner,
  :root[data-layout="canvas"] #summarize-banner{top:52px;left:0}
  :root[data-layout="canvas"] #sketchboard-sidebar{top:52px}
}

/* ── 10. TILES — every region is its own rounded panel on a tinted ground.
   The most decorated of the ten, and the one that shows a wallpaper best,
   because the gaps let it through. */
@media (min-width:1024px){
  :root[data-layout="tiles"] #app{
    display:grid; background:var(--bg); gap:12px; padding:12px;
    grid-template-columns:var(--nav-w,17rem) minmax(0,1fr);
    grid-template-areas:"nav main";
  }
  :root[data-layout="tiles"] #left-sidebar{
    grid-area:nav; position:relative; transform:none; width:auto;
    background:var(--surface); border:1px solid var(--border);
    border-radius:var(--radius-lg); overflow:hidden; box-shadow:var(--shadow);
  }
  :root[data-layout="tiles"] #main-col{
    grid-area:main; min-width:0; background:transparent;
    display:flex; flex-direction:column; gap:12px;
  }
  :root[data-layout="tiles"] #chat-header{
    flex:none; border:1px solid var(--border); border-radius:var(--radius-lg);
    background:var(--header-bg); box-shadow:var(--shadow);
  }
  :root[data-layout="tiles"] #chat-container{
    border:1px solid var(--border); border-radius:var(--radius-lg);
    background:var(--surface); box-shadow:var(--shadow);
    padding-inline:max(1.5rem,calc((100% - var(--measure,46rem))/2));
  }
  :root[data-layout="tiles"] #composer-dock{
    flex:none; border:1px solid var(--border); border-radius:var(--radius-lg);
    background:var(--composer-bg); box-shadow:var(--shadow); padding:.75rem;
  }
  :root[data-layout="tiles"] #composer-shell{max-width:var(--measure,46rem)}
  :root[data-layout="tiles"] #sketchboard-sidebar{
    top:12px; bottom:12px; right:12px; border-radius:var(--radius-lg);
    border:1px solid var(--border); box-shadow:var(--shadow-lg);
  }
  :root[data-layout="tiles"] #load-more-banner,
  :root[data-layout="tiles"] #summarize-banner{left:calc(var(--nav-w,17rem) + 24px)}
}

/* ── STYLE OPTIONS ──────────────────────────────────────────────────────
   Each theme declares which of these it exposes; the values arrive as custom
   properties or data-attributes on <html>. Defaults are written so a theme
   that does NOT expose an option still renders correctly. */
:root{ --measure:46rem; --nav-w:17rem; --deck-gap:18px; }

/* reading width: the message column and the composer must share one measure,
   otherwise text and the box you type into do not line up */
@media (min-width:768px){
  :root[data-layout="studio"] #chat-container,
  :root[data-layout="zen"] #chat-container{
    padding-inline:max(1.5rem,calc((100% - var(--measure))/2));
  }
  :root[data-layout="studio"] #composer-shell,
  :root[data-layout="zen"] #composer-shell{ max-width:var(--measure) }
  /* "Full" removes the cap rather than centring on a huge measure */
  :root[data-layout="studio"] #chat-container[style],
  :root[data-layout="zen"] #chat-container[style]{}
}

/* sidebar width */
@media (min-width:768px){
  :root[data-layout="studio"] #app{ grid-template-columns:var(--nav-w) minmax(0,1fr) }
}
@media (min-width:1024px){
  :root[data-layout="console"] #app{ grid-template-columns:var(--nav-w) minmax(0,1fr) 300px }
  :root[data-layout="console"] #app:has(#sketchboard-sidebar.translate-x-full){
    grid-template-columns:var(--nav-w) minmax(0,1fr) 0;
  }
  :root[data-layout="console"] #summarize-banner,
  :root[data-layout="console"] #load-more-banner{ left:var(--nav-w) }
}

/* separation: how surfaces are told apart */
:root[data-depth="flat"] .msg-content,
:root[data-depth="flat"] #composer-shell>div,
:root[data-depth="flat"] .modal-content{ box-shadow:none!important; border-color:transparent!important }
:root[data-depth="flat"] #left-sidebar,
:root[data-depth="flat"] #chat-header{ border-color:transparent!important }
:root[data-depth="shadow"] .msg-content,
:root[data-depth="shadow"] #composer-shell>div{ box-shadow:var(--shadow); border-color:transparent }
:root[data-depth="shadow"] .modal-content{ box-shadow:var(--shadow-lg) }

/* density: one scale, applied where it is felt */
:root[data-density="compact"]{ font-size:13px }
:root[data-density="compact"] #chat-container{ padding-top:.5rem;padding-bottom:.5rem }
:root[data-density="compact"] #chat-container>*+*{ margin-top:.75rem }
:root[data-density="compact"] .msg-content{ padding:.6rem .8rem }
:root[data-density="compact"] #main-nav>button,
:root[data-density="compact"] #nav-footer>button{ padding-top:.3rem;padding-bottom:.3rem }
:root[data-density="compact"] #chat-header,
:root[data-density="compact"] #brand-bar{ height:52px }

/* motion: a user-facing switch, separate from the OS preference, because the
   reason to turn it off here is a very long scrollback rather than vestibular
   sensitivity */
:root[data-motion="reduced"] *,
:root[data-motion="reduced"] *::before,
:root[data-motion="reduced"] *::after{
  animation-duration:.001ms!important; animation-iteration-count:1!important;
  transition-duration:.001ms!important; scroll-behavior:auto!important;
}

/* deck: card inset + which end the dock lives on */
@media (min-width:1024px){
  :root[data-layout="deck"] #app{ padding:var(--deck-gap) var(--deck-gap) 96px }
  :root[data-layout="deck"] #sketchboard-sidebar{ top:var(--deck-gap);right:var(--deck-gap) }
  :root[data-layout="deck"][data-dockpos="top"] #left-sidebar{ top:20px;bottom:auto }
  :root[data-layout="deck"][data-dockpos="top"] #app{ padding:96px var(--deck-gap) var(--deck-gap) }
  :root[data-layout="deck"][data-dockpos="top"] #toast{ bottom:32px }
  :root[data-layout="deck"][data-dockpos="top"] #sketchboard-sidebar{ top:96px;bottom:var(--deck-gap) }
}


/* ── PRESS FEEDBACK ─────────────────────────────────────────────────────
   scale(0.96) on press, 150ms ease-out. A CSS transition rather than a
   keyframe so releasing mid-press returns smoothly instead of snapping.
   Named properties only - transition:all would also animate layout.
   Excluded: .dock-btn, which runs its own width transition and would fight
   this, and any control marked data-static. */
#app button:not(:disabled):not(.dock-btn):not([data-static]),
#login-screen button:not(:disabled),
#modal-backdrop button:not(:disabled):not([data-static]){
  transition-property:scale,opacity,background-color,border-color,color,box-shadow;
  transition-duration:150ms;
  transition-timing-function:ease-out;
}
#app button:not(:disabled):not(.dock-btn):not([data-static]):active,
#login-screen button:not(:disabled):active,
#modal-backdrop button:not(:disabled):not([data-static]):active{ scale:.96 }

/* the theme swap injects this; see toggleTheme */
.rp-no-transition,.rp-no-transition *,
.rp-no-transition *::before,.rp-no-transition *::after{
  transition:none!important; animation:none!important;
}

/* Touch targets. Measured on the key form: Save was 36px and Browse 38px,
   both under the 44px a finger needs. Only applied to coarse pointers, so the
   desktop density the design was drawn at is untouched. .dock-btn sets its own
   size and is excluded. */
@media (pointer:coarse){
  #app button:not(.dock-btn):not(.rp-switch),
  #modal-backdrop button:not(.rp-switch),
  #login-screen button,
  #app select, #modal-backdrop select{ min-height:44px }
  /* A button pinned into a corner cannot answer a min-height by growing:
     it is positioned from the top, so it grows downward out of the card it
     belongs to. Measured: the pencil and the bin on a session card ran 7px
     past the bottom of a 46px card, which is why they read as sitting
     under the title rather than beside it. They keep their drawn size and
     take the finger area from a pseudo-element instead. */
  #app [class~="absolute"] button, #modal-backdrop [class~="absolute"] button{
    min-height:0; position:relative;
  }
  #app [class~="absolute"] button::after,
  #modal-backdrop [class~="absolute"] button::after{
    content:''; position:absolute; inset:-10px;
  }
  /* same bargain for the switches: 44x24 as drawn, 44 tall to the finger */
  .rp-switch{ position:relative }
  .rp-switch::after{ content:''; position:absolute; inset:-10px }
  #modal-backdrop input:not([type=checkbox]):not([type=range]),
  #app input:not([type=checkbox]):not([type=range]){ min-height:44px }
}


/* ── A SWITCH IS A DRAWING, NOT A BUTTON ────────────────────────────────
   Its size is the design: a 44x24 pill with a 20px knob inset 2px. Stated here
   so a rule meant for tap targets cannot turn it into a 44x44 circle again,
   which is exactly what happened. */
.rp-switch{ width:2.75rem; height:1.5rem; flex:0 0 auto; padding:0; border:0 }

/* ── GLASS OFF THE THINGS THAT MOVE ─────────────────────────────────────
   A backdrop-filter re-samples what is behind it on every frame it moves. On a
   phone the message list is the one surface that moves constantly, and eleven
   filtered bubbles scrolling is the most expensive thing on the page. They keep
   their fill, their rim and their design; what they give up is a blur of a
   wallpaper that is sliding past anyway.

   The chrome keeps its glass. The header, the sidebar and the composer do not
   move, so their filter is composited once and reused. */
@media (pointer:coarse) and (max-width:900px){
  #chat-container .msg-content{
    backdrop-filter:none!important; -webkit-backdrop-filter:none!important;
  }
  /* There used to be a fill here too, firming plain bubbles to 88% to make up
     for the blur. It carried #chat-container, which put it above the opacity
     slider's rule - so on a phone the slider did nothing - and it painted both
     sides in the surface colour, which took the accent off your own bubbles.
     The fill is the slider's to decide; only the blur is dropped. */
}


/* ── MODALS ON A PHONE ──────────────────────────────────────────────────
   vh is the large viewport - the page as it would be if the address bar and
   the navigation bar were hidden. They are not hidden, so 95vh on a phone is
   taller than the screen, and #modal-backdrop is fixed inset-0, which measures
   the same large viewport and then centres the modal inside it. The overflow
   comes out of both ends at once: the title and the close button above the top
   edge, the save button cut off below the bottom.

   svh, not dvh: dvh follows the bars as they slide away, which resizes a
   dialog while the page moves behind it. svh is the small viewport - what is
   visible with the bars shown - so a modal that fits with them showing fits at
   every other moment as well. The vh line stays as the fallback.

   Attribute selectors because the class names contain brackets: a class
   selector would need them escaped, and a backslash in this file has to be
   doubled to survive the template literal. */
#modal-backdrop{ height:100vh; height:100dvh }
/* Cap against the box, not against a guess at the box.
   The backdrop is the visible area now, so a modal that may not exceed its own
   parent needs no viewport unit at all - which also means it cannot be wrong
   about one. The svh caps stay underneath as a floor for the same intent, and
   because a percentage max-height needs the parent to have a definite height,
   which is exactly what the line above gives it. */
[class~="max-h-[95vh]"]{ max-height:95vh; max-height:95svh }
[class~="max-h-[88vh]"]{ max-height:88vh; max-height:88svh }
[class~="max-h-[60vh]"]{ max-height:60vh; max-height:60svh }
#modal-backdrop > .modal-content{ max-height:100% }
:root[data-layout="console"] #modal-backdrop{ padding-top:11vh; padding-top:11svh }

/* A 500px floor is taller than the visible area of a small phone, and a floor
   beats the cap above it, so the editor grew until it pushed its own buttons
   off screen. */
[class~="min-h-[500px]"]{ min-height:min(500px, 46svh) }

/* Nothing lands under a home indicator or a rounded corner. max() so a device
   without safe areas keeps exactly the padding the design already had. */
#modal-backdrop{
  padding-bottom:max(0.5rem, env(safe-area-inset-bottom, 0px));
  padding-left:max(0.5rem, env(safe-area-inset-left, 0px));
  padding-right:max(0.5rem, env(safe-area-inset-right, 0px));
}
@media (min-width:640px){
  #modal-backdrop{
    padding-bottom:max(1rem, env(safe-area-inset-bottom, 0px));
    padding-left:max(1rem, env(safe-area-inset-left, 0px));
    padding-right:max(1rem, env(safe-area-inset-right, 0px));
  }
}

/* images carry a 1px outline for consistent depth - pure black/white at low
   opacity, never a tinted neutral, which would read as dirt on the edge */
#app img:not([src^="data:image/svg"]){ outline:1px solid oklch(0 0 0/.1); outline-offset:-1px }
.dark #app img:not([src^="data:image/svg"]){ outline-color:oklch(1 0 0/.1) }


/* ── COLOUR ENGINE ──────────────────────────────────────────────────────
   The Colour tab writes its result as inline custom properties on <html>,
   so it layers over whichever skin is active rather than replacing it.

   --accent stays a SOLID: Tailwind maps it to background-color AND to color,
   and color-mix() fails silently to transparent when the referenced var holds
   a gradient. The gradient rides as a background-image instead, which paints
   over the solid on exactly the elements that fill with the accent. */
.bg-accent{ background-image:var(--accent-grad,none) }

/* swatches in Appearance > Colour */
.sw{ position:relative; width:100%; aspect-ratio:1/1; border-radius:var(--radius);
     border:1px solid var(--border); overflow:hidden; padding:0;
     transition-property:box-shadow,border-color,scale; transition-duration:150ms;
     transition-timing-function:ease-out }
.sw:active{ scale:.96 }
.sw.is-on{ border-color:var(--accent); box-shadow:var(--ring) }
.sw.is-on::after{ content:''; position:absolute; right:5px; bottom:5px; width:10px; height:10px;
     border-radius:9999px; background:var(--accent); outline:2px solid var(--surface) }
/* a palette card shows the skin it would switch you to, not a label */
.skin-card{ display:flex; flex-direction:column; gap:6px; padding:8px; text-align:left;
     border:1px solid var(--border); border-radius:var(--radius-lg); background:var(--surface-2);
     transition-property:border-color,box-shadow,scale; transition-duration:150ms;
     transition-timing-function:ease-out }
.skin-card:active{ scale:.96 }
.skin-card.is-on{ border-color:var(--accent); box-shadow:var(--ring) }
.skin-strip{ display:flex; height:22px; border-radius:6px; overflow:hidden; border:1px solid var(--border) }
.skin-strip>span{ flex:1 }




/* ── CUSTOM BUBBLE AND EXTENSIONS ───────────────────────────────────────
   Both are real elements inside .msg-content rather than pseudo-elements:
   every design already spends ::before and ::after on its own ornament, and
   a video cannot be a background-image at any rate.

   The media sits at z-index:-1 inside the bubble's own stacking context, so
   it paints over the fill and under the text. The wash on top of it is what
   makes light text readable over a bright photo, and dark text over a dark
   one - which is why its colour and strength are both the reader's to set. */
.msg-content > .bub-layer{
  position:absolute; inset:0; z-index:-1; overflow:hidden;
  border-radius:inherit; pointer-events:none;
}
.bub-layer > img, .bub-layer > video{
  display:block; width:100%; height:100%;
  object-fit:var(--bub-fit,cover);
  opacity:var(--bub-media-op,1);
  filter:blur(var(--bub-media-blur,0px)) saturate(var(--bub-media-sat,1));
}
.bub-layer > .bub-tint{
  position:absolute; inset:0;
  background:var(--bub-tint,#000000);
  opacity:var(--bub-tint-op,0);
}
/* a glyph pinned outside the corner, so it never lands on the message */
.msg-content > .bub-ext{
  position:absolute; z-index:2; pointer-events:none; line-height:1;
  font-size:var(--bub-ext-size,20px);
  filter:drop-shadow(0 1px 2px rgba(0,0,0,.28));
}
/* Inside the edge, not over it. Most designs clip their own overflow to keep
   a texture or a radius honest, and a mark hung outside the box was simply
   sliced in half by whichever design was in use. */
.bub-ext[data-corner="tl"]{ top:3px; left:5px }
.bub-ext[data-corner="tr"]{ top:3px; right:5px }
.bub-ext[data-corner="bl"]{ bottom:3px; left:5px }
.bub-ext[data-corner="br"]{ bottom:3px; right:5px }

/* ── BUBBLE TEXT LEGIBILITY ─────────────────────────────────────────────
   A scenery or pattern fill has dark and light passages, so ink of either
   polarity meets its opposite somewhere behind the text. A hairline halo in
   the opposite polarity lifts the glyphs off whatever is behind them.

   Applied only where a design paints an image AND declares no text-shadow of
   its own, decided per design in dressRow() - a design that already handles
   its own contrast keeps what it chose. The halo colour follows the ink, so
   dark text gets a light halo and light text a dark one. */
[data-bubble] .msg-content[data-halo]{
  text-shadow:
    0 0 2px var(--bub-halo),
    0 0 4px var(--bub-halo),
    0 1px 1px var(--bub-halo);
}

/* ── BUBBLE PICKER ──────────────────────────────────────────────────────
   The cards are a mock, not a live render: the bubble rules are scoped to
   :root, so nothing inside the page can wear a different bubble than the page
   itself. Hovering a card puts the real bubble on <html> instead, which
   previews it on the actual conversation behind the modal - a truer preview
   than any thumbnail, and it costs one attribute write. */
.bub-card{ display:flex; flex-direction:column; gap:5px; padding:7px; text-align:left;
  border:1px solid var(--border); border-radius:var(--radius-lg); background:var(--surface-2);
  transition-property:border-color,box-shadow,scale; transition-duration:150ms;
  transition-timing-function:ease-out }
.bub-card:active{ scale:.96 }
.bub-card.is-on{ border-color:var(--accent); box-shadow:var(--ring) }
/* 4 across on a desktop, 3 on a phone: the designs are the point, so they get
   the space rather than the labels */
#bub-grid{ display:grid; gap:8px; grid-template-columns:repeat(3,minmax(0,1fr)) }
@media (min-width:640px){ #bub-grid{ grid-template-columns:repeat(4,minmax(0,1fr)) } }

/* The preview holds REAL .msg-content elements so each card shows its own
   design, animations and all.

   It is SCALED, not shrunk. Overriding font-size alone leaves every ornament at
   full size - a design's decoration is written in pixels, and Fluffy Cloud's
   clouds are 40 and 59px across - so the trimming rounds a bubble a third of
   the size and swamps the card. Laying the bubbles out at their real size and
   scaling the whole stage keeps the ornament in proportion to what it
   decorates, which is what makes a thumbnail the thing rather than a version of
   it with one measurement changed. */
.bub-demo{ position:relative; border-radius:8px;
  background:var(--surface); overflow:hidden; height:62px }
.bub-demo>.bub-stage{
  position:absolute; inset:0;
  display:flex; flex-direction:column; gap:7px; justify-content:center;
  padding:12px 14px;
  width:calc(100% / var(--bub-zoom,0.42));
  height:calc(100% / var(--bub-zoom,0.42));
  transform:scale(var(--bub-zoom,0.42));
  transform-origin:top left;
}
.bub-demo .msg-content{ font-size:13px; line-height:1.45; padding:7px 12px;
  /* A real message is around 180px wide, and every ornament is positioned for
     one - Fluffy Cloud's clouds sit at left:24 and left:50. A two-word demo
     collapses to 55px and the ornament lands beside the bubble instead of on
     it, which is what made the preview look broken while the design was fine
     in use. */
  min-width:150px; max-width:86%; width:max-content; white-space:nowrap;
  overflow:hidden; text-overflow:ellipsis; margin:0 }
.bub-demo .msg-content[data-side="ai"]{ align-self:flex-start }
.bub-demo .msg-content[data-side="user"]{ align-self:flex-end }
/* A card previews ITS OWN design, so the user's overrides must not leak in.
   Setting a custom property to `initial` makes it guaranteed-invalid, which
   sends every var() reading it to the authored fallback. */
.bub-neutral{
  --bub-ai-bg:initial; --bub-ai-tx:initial;
  --bub-user-bg:initial; --bub-user-tx:initial;
  --bub-sp:initial; --bub-font:initial;
  --b1:initial;--b2:initial;--b3:initial;--b4:initial;
  --b5:initial;--b6:initial;--b7:initial;--b8:initial;
}
.bub-name{ font-size:10px; font-weight:600; line-height:1.2; overflow:hidden;
  text-overflow:ellipsis; white-space:nowrap }
.bub-cat{ font-size:9px; color:var(--text-dim); line-height:1.2 }
.chip{ padding:4px 9px; border-radius:9999px; border:1px solid var(--border);
  font-size:11px; color:var(--text-dim); white-space:nowrap; flex-shrink:0;
  transition-property:border-color,background-color,color,scale; transition-duration:150ms;
  transition-timing-function:ease-out }
.chip:active{ scale:.96 }
.chip.is-on{ border-color:var(--accent); background:var(--accent-soft); color:var(--text); font-weight:600 }


/* ── THEME MOCKUPS ──────────────────────────────────────────────────────
   A miniature of each layout, built from the same tokens the app uses, so it
   tracks the palette and the accent instead of going stale the moment either
   changes. Proportions are the real ones: the sidebar column here is the same
   fraction of the width that --nav-w is at a typical desktop size. */
/* Its own ladder, not the app's. --step is a mix toward the text colour, which
   is the one thing guaranteed to be far from the background in either mode, so
   a single set of rules reads in both. The app's real surfaces sit within
   1.02:1 of each other by design; at this size that is one flat block. */
.tm{
  --tm-1:color-mix(in oklab, var(--text) 18%, var(--bg));
  --tm-2:color-mix(in oklab, var(--text) 28%, var(--bg));
  --tm-3:color-mix(in oklab, var(--text) 40%, var(--bg));
  --tm-4:color-mix(in oklab, var(--text) 58%, var(--bg));
  --tm-edge:color-mix(in oklab, var(--text) 34%, transparent);
  position:relative; display:grid; gap:2px; width:100%; aspect-ratio:16/10;
  padding:2px; border:1px solid var(--tm-edge); border-radius:6px;
  background:var(--bg); overflow:hidden;
}
.tm>span{ border-radius:3px; min-width:0 }
.tm-nav,.tm-rail,.tm-panel{ background:var(--tm-3) }
.tm-list{ background:var(--tm-2); border:1px solid var(--tm-edge) }
.tm-main{
  display:flex; flex-direction:column; gap:3px; padding:3px;
  background:var(--tm-1); min-width:0;
}
.tm-bar{ height:9px; flex:none; border-radius:2px; background:var(--tm-3) }
.tm-msg{ height:8px; flex:none; border-radius:3px }
.tm-msg.a{ width:56%; align-self:flex-start; background:var(--tm-3);
           border:1px solid var(--tm-edge) }
.tm-msg.u{ width:44%; align-self:flex-end; background:var(--accent);
           background-image:var(--accent-grad,none) }
.tm-inp{ margin-top:auto; height:11px; flex:none; border-radius:3px;
         background:var(--tm-2); border:1px solid var(--tm-edge) }
/* the nav rows, so a sidebar reads as a sidebar and not as a grey block */
.tm-nav{ display:flex; flex-direction:column; gap:2px; padding:3px }
.tm-nav>i{ display:block; height:5px; border-radius:2px; background:var(--tm-4) }
.tm-nav>i:first-child{ background:var(--accent) }
.tm-rail{ display:flex; flex-direction:column; gap:3px; padding:3px; align-items:center }
.tm-rail>i{ display:block; width:7px; height:7px; border-radius:2px; background:var(--tm-4) }
.tm-rail>i:first-child{ background:var(--accent) }
.tm-list{ display:flex; flex-direction:column; gap:2px; padding:3px }
.tm-list>i{ display:block; height:6px; border-radius:2px; background:var(--tm-4); opacity:.75 }
/* Deck floats the card and puts navigation in a dock */
.tm[data-mock="deck"]{ padding:5px 5px 14px }
.tm-dock{
  position:absolute; left:50%; bottom:3px; transform:translateX(-50%);
  display:flex; gap:2px; padding:2px 3px; border-radius:9999px;
  background:var(--surface-2); border:1px solid var(--border);
}
.tm-dock>i{ display:block; width:5px; height:5px; border-radius:9999px; background:var(--border-strong) }
.tm-dock>i:first-child{ background:var(--accent) }
/* Zen hides the chrome; the mockup should too */
.tm[data-mock="zen"] .tm-bar{ opacity:.35 }
.tm[data-mock="console"] .tm-main{ gap:2px; padding:2px }
.tm[data-mock="console"] .tm-msg{ height:6px }
.tm-band{ height:5px; flex:none; border-radius:2px; background:var(--accent); opacity:.5 }
/* Ledger and Canvas put a bar across the whole width, so it spans the grid */
.tm-top{ grid-column:1/-1; height:8px; border-radius:2px; background:var(--surface-2);
         border:1px solid var(--border) }
.tm-top.strip{ display:flex; align-items:center; gap:2px; height:10px; padding:0 2px }
.tm-top.strip>i{ display:block; width:9px; height:4px; border-radius:1px; background:var(--border-strong); opacity:.55 }
.tm-top.strip>i:first-child{ background:var(--accent); opacity:1 }
.tm[data-mock="ledger"],.tm[data-mock="canvas"]{ grid-template-rows:auto 1fr }
.tm[data-mock="canvas"] .tm-main{ grid-column:1/-1 }
.tm-list.wide>i{ height:8px }
/* Tiles: gaps on the ground, every region its own panel */
.tm[data-mock="tiles"]{ gap:3px; padding:3px }
.tm-nav.tile,.tm-main.tiled{ border:1px solid var(--tm-edge); border-radius:4px }
.tm-main.tiled{ background:transparent; gap:3px; padding:0; border:0 }
/* Each tile sits straight on the mockup's ground, so it is the ground it has to
   read against - not a main panel, which this layout does not have. */
.tm-main.tiled>span{ border:1px solid var(--tm-edge); background:var(--tm-2) }
.tm-main.tiled>.tm-msg{ border:0 }


/* ── FITTING THE WINDOW ─────────────────────────────────────────────────
   A grid with no declared rows gets one auto-sized row, and an auto row grows
   to fit its content. With a long conversation inside, that pushed the
   composer and the sidebar footer off the bottom of the window on five of the
   ten layouts.

   Bounding the row stops the growth. min-height:0 is the other half: a flex
   item refuses to shrink below its own content unless it is told it may, so
   without it the scrolling message list still forces its parent taller. */
@media (min-width:768px){
  :root[data-layout="studio"] #app,
  :root[data-layout="rail"] #app,
  :root[data-layout="trio"] #app,
  :root[data-layout="split"] #app,
  :root[data-layout="tiles"] #app{
    grid-template-rows:minmax(0,1fr);
    height:100%;
    min-height:0;
  }
}
/* the scroller and everything that has to stay put around it */
#main-col{ min-height:0 }
#chat-container{ min-height:0 }
#composer-dock{ flex:0 0 auto }
/* the sidebar has the same problem: a long nav list pushed the footer, which
   is where the account and Log out live, past the bottom edge */
#left-sidebar{ min-height:0 }
#left-sidebar>div:first-child{ min-height:0; display:flex; flex-direction:column }
#main-nav{ min-height:0; overflow-y:auto; overscroll-behavior:contain }
#nav-footer{ flex:0 0 auto }


/* ── DOCK: TASKBAR ──────────────────────────────────────────────────────
   The second shape the dock can take: a bar across the bottom edge rather
   than an island floating above it. Squared off, full width, resting on a
   hairline - the shape a desktop taskbar has, and the one that reads as part
   of the window rather than on top of it.

   Labels stay open here. A taskbar has room for them, and hiding them behind
   a hover would be the island's behaviour wearing the wrong shape. */
@media (min-width:1024px){
  :root[data-layout="deck"][data-dockstyle="taskbar"] #left-sidebar{
    left:0; right:0; bottom:0; top:auto;
    transform:none; width:100%; height:46px;
    padding:0 10px; gap:2px;
    border:0; border-top:1px solid var(--border); border-radius:0;
    background:color-mix(in oklab,var(--surface) 94%,transparent);
    backdrop-filter:blur(14px);
    box-shadow:0 -1px 0 color-mix(in oklab,var(--text) 4%,transparent);
    justify-content:flex-start;
  }
  :root[data-layout="deck"][data-dockstyle="taskbar"] .dock-btn{
    width:auto; max-width:none; height:34px;
    padding:0 10px 0 8px; border-radius:6px; gap:7px;
  }
  /* padding:0 is the point here. The island gives its icons 15px of padding
     inside a 50px box; shrinking the box to 18px without clearing that leaves
     a border-box element whose drawing area is zero, and the icon disappears
     while still taking up space. */
  :root[data-layout="deck"][data-dockstyle="taskbar"] .dock-btn>i,
  :root[data-layout="deck"][data-dockstyle="taskbar"] .dock-btn>svg{
    min-width:18px; width:18px; height:18px; margin:0; padding:0;
  }
  :root[data-layout="deck"][data-dockstyle="taskbar"] .dock-btn>span{
    opacity:1; transform:none; padding:0; font-size:12px;
  }
  /* the running-app underline, which is what a taskbar uses instead of a fill */
  :root[data-layout="deck"][data-dockstyle="taskbar"] .dock-btn.is-active{
    background:color-mix(in oklab,var(--accent) 14%,transparent);
    color:var(--text); max-width:none;
  }
  :root[data-layout="deck"][data-dockstyle="taskbar"] .dock-btn.is-active::after{
    content:''; position:absolute; left:8px; right:8px; bottom:0; top:auto;
    height:2px; width:auto; border-radius:2px; background:var(--accent); opacity:1;
  }
  :root[data-layout="deck"][data-dockstyle="taskbar"] #left-sidebar:hover .dock-btn.is-active:not(:hover){
    max-width:none;
  }
  :root[data-layout="deck"][data-dockstyle="taskbar"] #left-sidebar:hover .dock-btn.is-active:not(:hover)>span{ opacity:1 }
  :root[data-layout="deck"][data-dockstyle="taskbar"] #nav-footer{
    margin-left:auto; padding-left:8px;
    border-left:1px solid var(--border);
  }
  /* the card sits above the bar rather than clearing a floating island */
  :root[data-layout="deck"][data-dockstyle="taskbar"] #app{ padding-bottom:58px }
  :root[data-layout="deck"][data-dockstyle="taskbar"] #toast{ bottom:58px }
  :root[data-layout="deck"][data-dockstyle="taskbar"] #sketchboard-sidebar{ bottom:46px }
}

/* ── FRAMED CONVERSATION ────────────────────────────────────────────────
   Ledger and Tiles used to draw a border around the header, the message list
   and the composer, which walls the conversation off from the column it sits
   in and costs a few pixels on every side for nothing. It is a choice now,
   and the default is to let the conversation fill its space. */
@media (min-width:1024px){
  :root[data-layout="ledger"]:not([data-frame="on"]) #chat-header,
  :root[data-layout="ledger"]:not([data-frame="on"]) #chat-container,
  :root[data-layout="ledger"]:not([data-frame="on"]) #composer-dock{
    border-inline:0; border-radius:0;
  }
  :root[data-layout="ledger"]:not([data-frame="on"]) #chat-header{ border-top:0 }
  :root[data-layout="ledger"]:not([data-frame="on"]) #composer-dock{ border-bottom:0; margin-bottom:0 }
  :root[data-layout="ledger"]:not([data-frame="on"]) #main-col{ padding:0 }

  :root[data-layout="tiles"]:not([data-frame="on"]) #chat-header,
  :root[data-layout="tiles"]:not([data-frame="on"]) #chat-container,
  :root[data-layout="tiles"]:not([data-frame="on"]) #composer-dock{
    border:0; border-radius:0; box-shadow:none;
  }
  :root[data-layout="tiles"]:not([data-frame="on"]) #main-col{ gap:0 }
  :root[data-layout="tiles"]:not([data-frame="on"]) #chat-header{ border-bottom:1px solid var(--border) }
}


/* ── ROOM FOR A BREAKOUT ────────────────────────────────────────────────
   .msg-row carries content-visibility:auto for scrolling performance, and
   that brings paint containment with it: the moment a row is off screen and
   comes back, anything painted outside its padding box is clipped. A sun
   cresting the top of a bubble, an ear, a paw - all of them live exactly
   there.

   Padding gives the containment box the room to hold them. The matching
   negative margin takes the space back out of the layout, so nothing moves.
   Only rows that are actually wearing a design pay for it. */
.msg-row[data-bubble]{
  padding-top:34px;
  margin-top:-34px;
  padding-inline:14px;
  margin-inline:-14px;
  contain-intrinsic-size:auto 124px;
}


/* ── WHEN A WALLPAPER IS SET ────────────────────────────────────────────
   #app and #main-col each paint an opaque fill across the whole window, so a
   wallpaper behind them - picture or video - was covered completely. These
   soften the two large surfaces, and the chrome around them, so it shows.

   Small surfaces are deliberately left alone. A bubble, an input, a menu keeps
   its solid fill, because that is what the text is read against; only the big
   empty areas become glass.

   How much is a setting: --wall-veil is the share of the surface that stays.
   The Dim slider still governs the picture itself; this governs how much of
   the app is in front of it. */
:root[data-wallpaper] #app{ background:transparent }

/* ONE pane, not a stack of bands. Veiling the header, the message list and the
   composer separately meant three layers of the same fill on top of each other,
   and every boundary between them showed as a step in opacity - the seams above
   the composer and under the header. #main-col alone carries the veil now and
   everything inside it is transparent, so the conversation reads as a single
   sheet over the wallpaper. --wall-veil is how much of the app stays in front;
   at 0% the picture is simply there. */
:root[data-wallpaper] #main-col{
  background:color-mix(in oklab, var(--surface) var(--wall-veil,0%), transparent);
}
:root[data-wallpaper] #chat-container,
:root[data-wallpaper] #load-more-banner{
  background:transparent; box-shadow:none;
}
/* A scrim, not a panel. The title and the composer chrome are dark text and
   without something behind them they are invisible over a dark picture - the
   title measured rgb(22,24,28) on the video. A flat fill is what drew the seams
   in the first place, so this starts at the window edge and dissolves to
   nothing across the height of the bar: there is no boundary to see because the
   gradient never arrives at one. */
:root[data-wallpaper] #chat-header{
  background:linear-gradient(to bottom,
    color-mix(in oklab, var(--surface) 34%, transparent),
    color-mix(in oklab, var(--surface) 12%, transparent) 60%,
    transparent);
  box-shadow:none;
}
/* Nothing behind the composer. A scrim here is a pale band straight across the
   picture, and the input card has its own fill, so the chrome it was meant to
   support was never actually unsupported. */
:root[data-wallpaper] #composer-dock{
  background:transparent;
  box-shadow:none;
}
:root[data-wallpaper] #chat-header{
  border-bottom-color:transparent;
  backdrop-filter:none; -webkit-backdrop-filter:none;
}
/* the composer's fade strip is a solid --surface gradient; over a wallpaper it
   is just a bar across the picture */
:root[data-wallpaper] #composer-dock>div[style*="linear-gradient"]{ background-image:none!important }

/* The nav is its own region and has to stay readable, so it keeps a fill - but
   only one. #nav-footer was in this list too, painting a second copy of the
   same colour over the sidebar's, which is what made the account row look like
   a solid white card sitting in the middle of the glass. */
:root[data-wallpaper] #left-sidebar,
:root[data-wallpaper] #session-rail{
  background:color-mix(in oklab, var(--sidebar-bg) var(--wall-veil,0%), transparent);
  /* No blur here. These are the two tallest elements on screen, and a
     backdrop-filter on something this size over a playing video is what Chrome
     splits into tiles - the seam down the sidebar, shimmering as each frame
     recomposites. Blurring is what liquid glass is for, so it lives in the
     glass rules and nowhere else; with glass off these keep their fill and stop
     touching the backdrop at all. */
}
:root[data-wallpaper] #nav-footer,
:root[data-wallpaper] #main-nav,
:root[data-wallpaper] #left-sidebar>div{ background:transparent }
:root[data-wallpaper] #nav-footer{ border-top-color:color-mix(in oklab,var(--border) 55%,transparent) }

/* Deck floats its nav already; it only needs to stop being opaque. */
:root[data-wallpaper][data-layout="deck"] #left-sidebar{
  background:color-mix(in oklab, var(--surface) var(--wall-veil,0%), transparent);
}

/* No legibility rule for the messages themselves on purpose: a message always
   paints its own opaque fill - the surface for theirs, the accent for yours, or
   whatever a design specifies - so the text never lands on the wallpaper,
   whatever the veil is set to. */

/* ── LIQUID GLASS ───────────────────────────────────────────────────────
   The reference builds a signed-distance field per element, derives surface
   normals, refracts through them by Snell's law and feeds the result to
   feDisplacementMap as a canvas-generated displacement image. That is the
   right way to get true refraction, and it re-runs whenever the backdrop
   repaints - affordable for a page with a dozen panels, not for a
   conversation with hundreds of bubbles on a playing video.

   What survives that trade is the part the eye actually reads as glass, and
   it costs one composited layer: a blurred and saturated backdrop, a thin
   tint over it, a bright rim along the top edge where light would catch, and
   a darker one along the bottom where it falls away. */
:root[data-glass="on"]{
  /* Half a pixel by default. The refraction is the whole point and a real blur
     erases it, so this is only here to take the hard edge off the bent image -
     but how much frost you want is a preference, so it is settable. */
  --glass-blur:0.5px;
  --glass-sat:1.4;
  /* thin: chrome, where there is nothing to read but labels */
  /* of --glass-base, not --surface: see glassBase(). The fallback keeps this
     sane for the moment before the first measurement lands. */
  --glass-fill:color-mix(in oklab, var(--glass-base, var(--surface)) 15%, transparent);
  --glass-nav:color-mix(in oklab, var(--glass-base-nav, var(--sidebar-bg)) 15%, transparent);
  /* thicker: surfaces carrying words. The bevel is only the outer ~30px, so
     the refraction still happens at the rim - filling the middle costs it
     nothing and gives the text a ground. */
  --glass-read:color-mix(in oklab, var(--glass-base, var(--surface)) 64%, transparent);
  --glass-rim:rgba(255,255,255,.6);
  --glass-under:rgba(255,255,255,.14);
  --glass-hair:rgba(255,255,255,.26);
  --glass-drop:0 8px 30px rgba(0,0,0,.16), 0 1px 2px rgba(0,0,0,.08);
}
:root[data-glass="on"].dark{
  --glass-fill:color-mix(in oklab, var(--glass-base, var(--surface)) 14%, transparent);
  --glass-nav:color-mix(in oklab, var(--glass-base-nav, var(--sidebar-bg)) 14%, transparent);
  --glass-read:color-mix(in oklab, var(--glass-base, var(--surface)) 58%, transparent);
  --glass-rim:rgba(255,255,255,.28);
  --glass-under:rgba(255,255,255,.06);
  --glass-hair:rgba(255,255,255,.13);
  --glass-drop:0 10px 34px rgba(0,0,0,.5), 0 1px 2px rgba(0,0,0,.34);
}
/* A 30% tint is not enough to read small dark labels against a photograph, and
   thickening it would put the frost back. A shadow costs the glass nothing and
   is what the reference does with its own text. */
:root[data-glass="on"] #left-sidebar,
:root[data-glass="on"] #chat-header,
:root[data-glass="on"] #session-rail,
:root[data-glass="on"] #sketchboard-sidebar,
:root[data-glass="on"] .msg-row:not([data-bubble]) .msg-content{
  text-shadow:0 0 1px color-mix(in oklab, var(--bg) 96%, transparent),
              0 0 3px color-mix(in oklab, var(--bg) 92%, transparent),
              0 0 7px color-mix(in oklab, var(--bg) 78%, transparent),
              0 1px 2px color-mix(in oklab, var(--bg) 88%, transparent);
}
/* your own messages are white on the accent, so their shadow has to be dark
   whatever the theme is doing */
:root[data-glass="on"] .msg-row:not([data-bubble]) .msg-content[data-side="user"]{
  text-shadow:0 1px 10px rgba(0,0,0,.45), 0 0 2px rgba(0,0,0,.35);
}
/* the one recipe, shared */
:root[data-glass="on"] #chat-header,
:root[data-glass="on"] #composer-shell>div,
:root[data-glass="on"] #sketchboard-sidebar,
:root[data-glass="on"] #left-sidebar,
:root[data-glass="on"] #session-rail,
:root[data-glass="on"] #send-btn,
:root[data-glass="on"] #scroll-btn{
  backdrop-filter:blur(var(--glass-blur)) saturate(var(--glass-sat));
  -webkit-backdrop-filter:blur(var(--glass-blur)) saturate(var(--glass-sat));
  background:var(--glass-fill);
  box-shadow:inset 0 1px 0 var(--glass-rim),
             inset 0 -1px 0 var(--glass-under),
             inset 0 0 0 1px var(--glass-hair),
             var(--glass-drop);
  border-color:transparent;
}
:root[data-glass="on"] #left-sidebar,
:root[data-glass="on"] #session-rail,
:root[data-glass="on"] #sketchboard-sidebar{ background:var(--glass-nav) }
/* the two you read from */
:root[data-glass="on"] .msg-row:not([data-bubble]) .msg-content,
:root[data-glass="on"] #composer-shell>div{ background:var(--glass-read) }
/* The message bubble has the recipe but not the drop shadow. --glass-drop is
   34px at half opacity: behind a panel that is what lifts it off the page,
   behind a bubble sitting on a photograph it is a dark smear across whatever
   the picture was doing there, and the backdrop blur underneath compounds it.
   A bubble was never floating above anything - it reads as its own object from
   its shape. The rim stays, because that is the part that says glass. */
:root[data-glass="on"] .msg-row:not([data-bubble]) .msg-content{
  backdrop-filter:blur(var(--glass-blur)) saturate(var(--glass-sat));
  -webkit-backdrop-filter:blur(var(--glass-blur)) saturate(var(--glass-sat));
  box-shadow:inset 0 1px 0 var(--glass-rim),
             inset 0 0 0 1px var(--glass-hair);
  border-color:transparent;
}
/* and a bubble wearing a design keeps the design's own depth, minus nothing */
/* Your own messages keep their accent. The neutral fill above outranks the
   bg-accent class they carry, and with text-onaccent still white that left
   white text on white glass - the two sides became one. */
:root[data-glass="on"] .msg-row:not([data-bubble]) .msg-content[data-side="user"]{
  background:color-mix(in oklab, var(--accent) 76%, transparent);
}
/* a button keeps its accent; glass only softens and lights the edge */
:root[data-glass="on"] #send-btn,
:root[data-glass="on"] #scroll-btn{
  background:color-mix(in oklab, var(--accent) 78%, transparent);
}
:root[data-glass="on"] .dock-btn:hover,
:root[data-glass="on"] .dock-btn.is-active{
  background:color-mix(in oklab, var(--accent) 26%, transparent);
}
/* A bubble wearing a design gets the blur and nothing else. Its surface and
   its depth are the design's to decide, and glass has no business repainting
   them - the fill above is for a bubble that has no design of its own. */
:root[data-glass="on"] .msg-row[data-bubble] .msg-content{
  backdrop-filter:blur(var(--glass-blur)) saturate(var(--glass-sat));
  -webkit-backdrop-filter:blur(var(--glass-blur)) saturate(var(--glass-sat));
}
:root[data-glass="on"] #chat-header{ border-bottom-color:transparent }

/* ── GLASS: BUTTONS AND THE SKETCHBOARD ─────────────────────────────────
   A flat accent pill sitting next to a bubble that refracts its backdrop is
   what gives the whole material away, so the buttons are glass as well: the
   accent thinned enough to see through, the same rim, and - because they are
   in the engine's selector list - the same refracted edge rather than a
   translucent fill doing an impression of one. */

/* ── THE PILLS INSIDE THE DOCK ──────────────────────────────────────────
   No backdrop-filter of their own. The bar they sit in already has one, and
   nesting them means the inner filter blurs an already-blurred backdrop -
   which Chrome bands along the layer boundaries, and that banding is the
   striping across the dock. A pill on frosted glass wants a tint on top of
   the frost, not a second helping of it. */
:root[data-glass="on"] .dock-btn:hover,
:root[data-glass="on"] .dock-btn.is-active{
  background:color-mix(in oklab, var(--text) 12%, transparent);
  box-shadow:inset 0 1px 0 var(--glass-rim), inset 0 0 0 1px var(--glass-hair);
  border-color:transparent;
}
/* Whatever is currently open. Same reasoning as the accent pill: the ink the
   colour engine fitted to a solid accent is not the ink for a thinned one, and
   this is the rule that dresses the button you just touched. */
:root[data-glass="on"] .dock-btn.is-active{
  background:color-mix(in oklab, var(--accent) 72%, transparent);
  color:var(--glass-accent-ink, var(--accent-text));
}

/* The pill clips its own label while the width animates. Without this the
   label is painted by a layer that has not been re-clipped yet, and it shows
   outside the pill for a frame or two - the ghosting in the dock. */
:root[data-layout="deck"] .dock-btn{ contain:paint }

:root[data-glass="on"] #think-btn{
  backdrop-filter:blur(var(--glass-blur)) saturate(var(--glass-sat));
  -webkit-backdrop-filter:blur(var(--glass-blur)) saturate(var(--glass-sat));
  background:color-mix(in oklab, var(--surface) 22%, transparent);
  box-shadow:inset 0 1px 0 var(--glass-rim), inset 0 0 0 1px var(--glass-hair);
  border-color:transparent;
}
/* Whatever is currently open, and anything that carries the accent.

   88%, not 58%. The label on these is text-onaccent - the ink the colour engine
   fitted against the accent at full strength - and thinning the accent moves
   the surface out from under that decision. In dark mode the fitted ink is
   near-black, because this accent is light; composite it at 58% over a dark bar
   and the ink is left on something far too dark for it. Measured: rgb(10,10,10)
   on the Current Chat pill while every other label was rgb(242,245,250).

   The right ink cannot be worked out here - part of what it sits on is a
   wallpaper no computed style can report - so the fill stays close enough to
   the accent for the ink already fitted to it to remain true. The blur, the rim
   and the refracted edge are all still there. */
:root[data-glass="on"] #main-nav .dock-btn.bg-accent,
:root[data-glass="on"] #nav-footer .dock-btn.bg-accent,
:root[data-glass="on"] #sketchboard-sidebar button.bg-accent,
:root[data-glass="on"] .modal-content button.bg-accent,
:root[data-glass="on"] #send-btn,
:root[data-glass="on"] #scroll-btn{
  background:color-mix(in oklab, var(--accent) 72%, transparent);
  /* not text-onaccent: that ink was fitted to the accent at full strength, and
     this is not that surface. See glassAccentInk(). */
  color:var(--glass-accent-ink, var(--accent-text));
  box-shadow:inset 0 1px 0 rgba(255,255,255,.45),
             inset 0 0 0 1px rgba(255,255,255,.28),
             var(--glass-drop);
}

/* The sketchboard's title bar and its fields had no fill of their own, so the
   wallpaper came through them unbroken - a header you cannot read. They are the
   thicker glass, like the message bubbles, for the same reason. */
:root[data-glass="on"] #sketchboard-sidebar>div:first-child{
  background:color-mix(in oklab, var(--sidebar-bg) 82%, transparent);
  backdrop-filter:blur(10px) saturate(var(--glass-sat));
  -webkit-backdrop-filter:blur(10px) saturate(var(--glass-sat));
  border-bottom-color:var(--border);
  box-shadow:inset 0 -1px 0 var(--glass-under);
}
:root[data-glass="on"] #new-pin-input,
:root[data-glass="on"] #sketchboard-sidebar .rs-search,
:root[data-glass="on"] #sketchboard-sidebar input[type="text"]{
  background:color-mix(in oklab, var(--surface) 84%, transparent);
  border-color:var(--border);
  color:var(--text);
}

/* The bar itself is a scrim that fades to nothing, which is right for a bar and
   useless for the words on it - the persona name, the session line and the
   Sketchboard pill were left sitting on the wallpaper. The text gets a chip;
   the bar stays open. */
:root[data-wallpaper] #header-persona-btn,
:root[data-wallpaper] #chat-header>button:last-of-type{
  /* No backdrop-filter on a chip this size. It samples a strip a few pixels
     wide, which is not glass by any useful definition, and each one is another
     composited layer whose edges can show. A slightly heavier fill says the
     same thing for nothing. */
  background:color-mix(in oklab, var(--surface) 76%, transparent);
  box-shadow:inset 0 0 0 1px color-mix(in oklab, var(--border) 70%, transparent);
}
:root[data-wallpaper] #header-persona-btn{ border-radius:12px }

/* The nav has no fill of its own to speak of now, so its labels are read
   against the wallpaper. This is the same shadow the header gets under glass,
   applied whenever there is a picture back there at all. */
:root[data-wallpaper] #left-sidebar,
:root[data-wallpaper] #session-rail{
  text-shadow:0 0 1px color-mix(in oklab, var(--bg) 96%, transparent),
              0 0 3px color-mix(in oklab, var(--bg) 92%, transparent),
              0 0 7px color-mix(in oklab, var(--bg) 78%, transparent),
              0 1px 2px color-mix(in oklab, var(--bg) 88%, transparent);
}

/* The composer's placeholder is Tailwind's gray-400, picked against an opaque
   white card. Over glass it is barely there, and neither is what you type. */
:root[data-glass="on"] #chat-input,
:root[data-wallpaper] #chat-input{ color:var(--text) }
:root[data-glass="on"] #chat-input::placeholder,
:root[data-wallpaper] #chat-input::placeholder{
  color:color-mix(in oklab, var(--text) 62%, transparent);
  opacity:1;
}
:root[data-glass="on"] #composer-shell>div{ color:var(--text) }

/* ── GLASS: THE FLOATING SURFACES ───────────────────────────────────────
   A menu, a modal, a popover and the toast sit on top of everything, so they
   are where glass reads strongest - and they were the ones still painting
   flat. */
:root[data-glass="on"] #global-dropdown,
:root[data-glass="on"] #think-picker,
:root[data-glass="on"] .modal-content,
:root[data-glass="on"] #toast{
  backdrop-filter:blur(var(--glass-blur)) saturate(var(--glass-sat));
  -webkit-backdrop-filter:blur(var(--glass-blur)) saturate(var(--glass-sat));
  background:var(--glass-fill);
  box-shadow:inset 0 1px 0 var(--glass-rim),
             inset 0 0 0 1px var(--glass-hair),
             var(--glass-drop);
  border-color:transparent;
}
/* A menu is a panel, not chrome. Thinned to the header's fill it does show the
   refraction beautifully and its coloured rows - the destructive ones, which
   are the ones you least want to misread - stop being legible against a
   photograph. A bar carrying two short labels can afford that; a list of
   commands cannot. So this keeps the fill the bubbles use, and the header and
   the sidebar stay thin. */
:root[data-glass="on"] #global-dropdown,
:root[data-glass="on"] #think-picker{
  background:var(--glass-read);
}
/* a row you are pointing at should still separate from the rest */
:root[data-glass="on"] #global-dropdown button:hover,
:root[data-glass="on"] #think-picker button:hover{
  background:color-mix(in oklab, var(--text) 14%, transparent);
}
/* the toast is an accent chip, not a panel */
:root[data-glass="on"] #toast{
  background:color-mix(in oklab, var(--accent) 74%, transparent);
}
/* A field is where you read back what you typed, so it cannot be a window.
   Every input in a modal is bg-transparent, which is right on an opaque card
   and wrong the moment the card is glass: the conversation behind it shows
   straight through the box, and the two-factor form ended up with message
   bubbles legible inside the username field. */
:root[data-glass="on"] .modal-content input:not([type="range"]):not([type="checkbox"]):not([type="radio"]),
:root[data-glass="on"] .modal-content textarea,
:root[data-glass="on"] .modal-content select,
:root[data-wallpaper] .modal-content input:not([type="range"]):not([type="checkbox"]):not([type="radio"]),
:root[data-wallpaper] .modal-content textarea,
:root[data-wallpaper] .modal-content select{
  background:var(--surface-2);
  border-color:var(--border);
}

/* The modals stay thicker, and it is not an oversight. A menu is six words per
   row; a settings panel is forms, labels, inputs and paragraphs, and one you
   have to squint through is not a better settings panel. */
:root[data-glass="on"] .modal-content{
  background:color-mix(in oklab, var(--glass-base, var(--surface)) 78%, transparent);
}

/* ── THE MESSAGE MENU BUTTON ────────────────────────────────────────────
   Three dark dots and nothing behind them. On an opaque page that reads fine;
   over a wallpaper they land on whatever the picture happens to be doing there
   and disappear. A round chip is enough - and being in the engine's selector
   list, its edge refracts like everything else. */
:root[data-wallpaper] .msg-more,
:root[data-glass="on"] .msg-more{
  /* one of these per message, at 24px each - a fill, not a filter */
  background:color-mix(in oklab, var(--surface) 82%, transparent);
  box-shadow:inset 0 0 0 1px color-mix(in oklab, var(--border) 60%, transparent),
             0 2px 8px rgba(0,0,0,.14);
  color:var(--text);
}
/* it only appears on hover, so it may as well answer when pointed at */
:root[data-wallpaper] .msg-more:hover,
:root[data-glass="on"] .msg-more:hover{
  background:color-mix(in oklab, var(--surface) 88%, transparent);
}

/* ── THE MESSAGES PASS UNDER THE HEADER ─────────────────────────────────
   #chat-container is the scroller, and it began below the header, so its own
   overflow clipped every message dead level with the header's bottom edge -
   a straight cut, with nothing ever reaching the glass to be refracted by it.

   Pulling the scroller up behind the header and padding its content back down
   by the same amount leaves the layout exactly where it was and lets a message
   scroll underneath, where the header's backdrop-filter bends it like it bends
   everything else.

   Not for the layouts that place the header with grid-area: there the header is
   not a flex sibling of the scroller and a negative margin means something else
   entirely. */
:root[data-wallpaper]:not([data-layout="console"]):not([data-layout="ledger"]):not([data-layout="canvas"]) #chat-container{
  margin-top:calc(0px - var(--hdr-h));
  padding-top:var(--hdr-h);
  scroll-padding-top:var(--hdr-h);
}
/* Measured, not assumed - see watchHeaderHeight(). 4rem is only what it is
   before the first measurement lands. */
:root{ --hdr-h:4rem }

/* ── CONSOLE: THE PERSONA CHIP IN A 44px BAR ────────────────────────────
   Console draws a 44px bar and the chip is a 32px avatar stacked over two
   lines of text, which wants about 48. Something has to give, and in a bar
   this dense it is the session line - the name is what you actually read. */
:root[data-layout="console"] #header-persona-btn{ padding-block:2px }
:root[data-layout="console"] #header-persona-btn>div:last-child>div{ display:none }
:root[data-layout="console"] #header-avatar{ width:24px; height:24px }
:root[data-layout="console"] #chat-header{ overflow:hidden }

/* ── ZEN: A HEADER YOU CAN SEE ──────────────────────────────────────────
   Zen fades its bar to .55 and brings it back on hover. Against a plain
   background that is a nice piece of restraint. Over a wallpaper it is dark
   text at half strength on a photograph, which is not restraint, it is
   invisible - and you cannot hover something you cannot find. */
:root[data-layout="zen"] #chat-header{ opacity:1 }

/* ── THE CORNERS THE THEME ASKED FOR ────────────────────────────────────
   Corners is a theme option, and two things were ignoring it: the header,
   which had no radius at all, and the persona list, which is a fixed 8px. At
   Round or Pill everything else curves and those two stay square. */
:root[data-wallpaper] #chat-header,
:root[data-glass="on"] #chat-header{
  border-radius:calc(var(--radius) * 1.2);
}
#global-dropdown{ border-radius:calc(var(--radius) * 1.2) }
#global-dropdown button:first-child{ border-top-left-radius:var(--radius); border-top-right-radius:var(--radius) }
#global-dropdown button:last-child{ border-bottom-left-radius:var(--radius); border-bottom-right-radius:var(--radius) }

/* ── NO SHADOW BEHIND THE COMPOSER ──────────────────────────────────────
   It reads as a card lifted off the page, which is the one thing the composer
   is not: it is part of the frame. */
#composer-shell>div{ box-shadow:none }
:root[data-glass="on"] #composer-shell>div{
  box-shadow:inset 0 1px 0 var(--glass-rim), inset 0 0 0 1px var(--glass-hair);
}

/* ── THE CONVERSATION LIST ──────────────────────────────────────────────
   Hidden by default and revealed only by the two layouts that reserve a
   column for it. Everywhere else the same conversations are one click away in
   Manage Sessions, and a third column on a layout that did not plan for one
   would be worse than no column at all. */
#session-rail{ display:none }
@media (min-width:1024px){
  :root[data-layout="rail"] #session-rail,
  :root[data-layout="trio"] #session-rail,
  :root[data-layout="split"] #session-rail{ display:flex }
}
/* Trio hides the rail's own header because its nav column already carries the
   brand bar; Rail keeps it, because at 64px its nav has room for nothing. */
:root[data-layout="rail"] #session-rail>header{ background:var(--surface-2) }

/* ── THE COLUMN, AND WHAT IT DOES ───────────────────────────────────────
   One width for the chat list, a wider one for a docked panel, and zero when
   the conversation is asked for on its own. grid-template-columns animates, so
   a single transition on #app moves the whole layout and nothing has to be
   choreographed element by element. */
@media (min-width:1024px){
  :root[data-layout="rail"]{ --rail-w:264px }
  :root[data-layout="trio"]{ --rail-w:312px }
  :root[data-layout="split"]{ --rail-w:340px }
  :root[data-layout="rail"][data-rail="panel"],
  :root[data-layout="trio"][data-rail="panel"],
  :root[data-layout="split"][data-rail="panel"]{ --rail-w:min(460px, 40vw) }
  :root[data-layout="rail"][data-rail="off"],
  :root[data-layout="trio"][data-rail="off"],
  :root[data-layout="split"][data-rail="off"]{ --rail-w:0px }

  :root[data-layout="split"] #session-rail{ overflow:hidden }
  :root[data-layout="split"] #chat-container{
    transition:padding .42s cubic-bezier(.2,0,0,1);
  }
  :root[data-layout="split"][data-rail="off"] #chat-container{
    padding-inline:max(2rem, calc((100% - 50rem) / 2));
  }

  /* The grids themselves are declared once, with the layouts - see
     --rail-w there. Only the widths it takes live here.
     So the contents clip rather than squeeze while the column closes: */
  :root[data-layout="rail"] #session-rail,
  :root[data-layout="trio"] #session-rail{ overflow:hidden }
  :root[data-rail="off"] #session-rail{ border-right-width:0 }

  /* With the column gone the conversation has the whole width, which is more
     than a line of text should ever be. It settles in the middle instead. */
  :root[data-layout="rail"] #chat-container,
  :root[data-layout="trio"] #chat-container{
    transition:padding .42s cubic-bezier(.2,0,0,1);
  }
  :root[data-layout="rail"][data-rail="off"] #chat-container,
  :root[data-layout="trio"][data-rail="off"] #chat-container{
    padding-inline:max(2rem, calc((100% - 50rem) / 2));
  }
}
@media (prefers-reduced-motion: reduce){
  :root[data-layout="rail"] #app, :root[data-layout="trio"] #app,
  :root[data-layout="split"] #app,
  :root[data-layout="rail"] #chat-container, :root[data-layout="trio"] #chat-container,
  :root[data-layout="split"] #chat-container{
    transition:none;
  }
}

/* A panel that has been moved into the column is not a floating card any more:
   no width of its own, no radius, no shadow, no height cap. Addressed through
   #rail-panel so it outranks the utility classes still on the element without
   a pile of !important. */
#rail-panel{ overflow:hidden }
#rail-panel > .modal-content{
  position:static; width:100%; max-width:none; height:100%; max-height:none;
  margin:0; border:0; border-radius:0; box-shadow:none; background:transparent;
  /* A flex column, but sized with flex:1 0 auto on the panes below - the
     no-shrink part is what makes one declaration right for both kinds of
     panel. Some bring their own scroll container and some do not; a pane
     that could shrink would squeeze a long panel into the column and clip
     it, which is why this was display:block before. Basis auto starts a
     pane at its content height, grow fills a short column, and no-shrink
     lets a long one run past the bottom - where overflow-y:auto here
     scrolls the panel, exactly as it always did. */
  display:flex; flex-direction:column; overflow-y:auto; overscroll-behavior:contain;
}
#rail-panel > .modal-content > div:first-child{
  /* the panel's own title row lines up with the chat header beside it, and
     stays there while the rest scrolls - in a column this narrow it is the only
     thing saying which panel you are in */
  position:sticky; top:0; z-index:2;
  min-height:4rem;
  background:color-mix(in oklab, var(--sidebar-bg) 92%, transparent);
  backdrop-filter:blur(10px);
  -webkit-backdrop-filter:blur(10px);
}
/* Any scroll container the panel brought with it stops being one: with the
   panel itself scrolling, a nested scroller just traps the wheel halfway down.
   .rs-scroll-list is here because it sets overflow-y in its own class instead
   of wearing the utility, so it used to slip past this rule and keep a 340px
   cap inside a column nine hundred pixels tall. */
#rail-panel > .modal-content .overflow-y-auto,
#rail-panel > .modal-content .rs-scroll-list{
  overflow:visible; max-height:none;
}
/* The footer bar. In a modal it marks where the panel ends and the page
   resumes; in a column nothing resumes, so it is a tinted strip under something
   that already finished. The buttons keep their spacing, the bar does not. */
#rail-panel > .modal-content > div:last-child{
  background:transparent; border-top:0;
}


/* ── THE PINNED FOOTER ──────────────────────────────────────────────────
   Sticky rather than fixed: fixed would take it out of the panel and leave it
   floating over the conversation when the panel is a column. Sticky pins it to
   the bottom edge of whichever box is scrolling - the modal in one case, the
   docked panel in the other - which is the same result in both without either
   knowing about the other. */
#keys-footer{
  position:sticky; bottom:0; z-index:2;
  background:color-mix(in oklab, var(--surface-2) 94%, transparent);
  backdrop-filter:blur(10px); -webkit-backdrop-filter:blur(10px);
}
/* The docked rule above strips the fill from a panel's last child, because a
   footer bar under finished content is just a tinted strip. This one has rows
   sliding underneath it, so it keeps its fill and its edge. Two ids, so it
   outranks that rule without !important. */
#rail-panel > .modal-content > #keys-footer{
  background:color-mix(in oklab, var(--surface-2) 94%, transparent);
  border-top:1px solid var(--border);
}


/* ── DOCKED: THE BODY FILLS THE COLUMN ──────────────────────────────────
   A panel used to stop wherever its content stopped, which in a 910px column
   meant a form of four short boxes sitting in the top third with three hundred
   pixels of nothing under the Save row. Measured across every dockable panel:
   360 to 625px unused.

   min-height rather than height, because a panel with more content than the
   column - Advanced AI is 1000px of it - still has to run past the bottom and
   let the panel scroll, which is what it already does. 4rem is the sticky
   title row above this pane. */
/* Every pane shares out the column; see the note on the panel above for why
   this grows without being allowed to shrink. */
/* Only a pane with something worth growing grows. Letting every pane take a
   share turned Account Settings - three single-line inputs and a paragraph -
   into three inputs spread down 900px with holes between them, which is worse
   than the empty space it replaced. A pane earns the slack by holding a text
   box or a list; everything else keeps its natural height and the column
   simply ends where the content does. */
#rail-panel > .modal-content > *{ flex:0 0 auto }
#rail-panel > .modal-content > .overflow-y-auto,
#rail-panel > .modal-content > div:has(textarea),
#rail-panel > .modal-content > div:has(.rs-scroll-list){ flex:1 0 auto }
/* and a bar is a bar even when it holds one of those */
#rail-panel > .modal-content > :first-child,
#rail-panel > .modal-content > [class*="border-t"],
#rail-panel > .modal-content > #keys-footer{ flex:0 0 auto }
#rail-panel > .modal-content > .overflow-y-auto{
  display:flex; flex-direction:column;
}

/* Any wrapper that holds a text box shares out the height, and the box fills
   its wrapper. This is what the persona form's right-hand column already did
   for itself; stating it here gets every other panel the same treatment
   without each one having to declare it. */
#rail-panel > .modal-content div:has(> textarea){
  flex:1 1 0; display:flex; flex-direction:column; min-height:104px;
}
#rail-panel > .modal-content textarea{ flex:1 1 auto; min-height:76px }

/* The persona form is two columns side by side. The row holding them takes
   what is left of the column, so both sides run to the same depth instead of
   the left one finishing halfway up. */
#rail-panel > .modal-content > #personas-form-view > div:first-of-type{
  flex:1 1 auto; min-height:0;
}
#rail-panel > .modal-content > #personas-form-view > div:first-of-type > div{
  display:flex; flex-direction:column; gap:1rem;
}
/* space-y-* spaces siblings with margins. With gap doing that job the two
   stack, and the left column drifts a row lower than the right. */
#rail-panel > .modal-content > #personas-form-view > div:first-of-type > div > * + *{
  margin-top:0;
}
/* the buttons stay a button-sized strip at the bottom of whatever is above */
#rail-panel > .modal-content > .overflow-y-auto > div:last-child{
  flex:0 0 auto;
}

/* ── HOW SOLID THE DEFAULT BUBBLE IS ────────────────────────────────────
   A bubble with no design takes its fill from a utility class, so there is no
   colour token to put an alpha on the way a design's fill has. The fill is
   restated here instead, at whatever the slider asked for.

   Written to outrank the glass recipe, which also sets a background on this
   element - and deliberately not touching backdrop-filter, so a translucent
   bubble is still glass, just more of it. */
:root[data-plainalpha] .msg-row:not([data-bubble]) .msg-content{
  background:color-mix(in oklab, var(--glass-base, var(--surface)) var(--bub-plain,100%), transparent);
}
:root[data-plainalpha] .msg-row:not([data-bubble]) .msg-content[data-side="user"]{
  background:color-mix(in oklab, var(--accent) var(--bub-plain,100%), transparent);
}

/* ── NO RING ON THE CHAT BOX ────────────────────────────────────────────
   The Void and Clay skins draw a focus ring on anything :focus-visible, and a
   text box matches that even when it is clicked - so every tap on the chat bar
   drew an accent rectangle around it. Where you are typing is already obvious:
   the caret is blinking in it. The ring stays everywhere else, which is where
   someone moving through the page with a keyboard actually needs it.

   The ring is taken out by making it transparent rather than by clearing the
   shadow: Clay gives this box a pressed-in shadow of its own, and clearing
   box-shadow on focus made it pop out every time you started typing. The
   skins' rules still run; what they draw is simply nothing. */
#chat-input{ --ring:0 0 0 0 transparent }
#chat-input:focus, #chat-input:focus-visible{ outline:none !important }
/* Clay presses this box in, and its focus rule swaps that for a raised shadow -
   the box popped out the moment you tapped it. It keeps one face now, typing
   or not. */
[data-skin=clay] #chat-input:focus-visible{ box-shadow:var(--shadow-in) }

/* ── NO SHADOW ON A MESSAGE ─────────────────────────────────────────────
   126 of the 240 designs declare their own outer box-shadow. Removing it from
   the plain bubble and leaving the designs to theirs is why it kept coming
   back: for most of the catalogue nothing had changed.

   !important because a design's own rules can outrank anything written here,
   and this has to hold for all of them rather than for whichever ones lose a
   specificity contest.

   filter is left alone on purpose: several designs build their shape out of
   drop-shadow() - the paw's rim, the cloud's edge - and stripping that would
   take the drawing with it. */
#chat-container .msg-content{ box-shadow:none !important }
/* glass keeps its rim, which is an inset and not a shadow cast on anything */
:root[data-glass="on"] #chat-container .msg-content{
  box-shadow:inset 0 1px 0 var(--glass-rim, transparent),
             inset 0 0 0 1px var(--glass-hair, transparent) !important;
}

/* ── WITHOUT LIQUID GLASS ───────────────────────────────────────────────
   Glass earns its transparency with a blur: there is something happening
   between you and the wallpaper, so a thin surface still reads as a surface.
   Turn it off and a thin surface is just a hole. Measured with the wallpaper
   on and glass off, the header and the sidebar were both at alpha 0 while the
   menus were at 0.72 - the quiet backdrops invisible and the readable panels
   nearly solid, which is backwards.

   So the chrome gets a floor it cannot go below, and the menus get a fill that
   still lets the picture show. The header and the sidebar share one number
   because they were deliberately put in step with each other.

   And no backdrop-filter in here. Blurring is the thing that was switched off,
   and on areas this size over a playing video it is what Chrome splits into
   tiles - which is the vertical seam, and the shimmer along it as every frame
   recomposites. A surface reads as a surface here because it is opaque enough
   to, not because something is happening behind it. */
:root[data-wallpaper]:not([data-glass="on"]) #chat-header{
  background:color-mix(in oklab, var(--glass-base, var(--surface)) var(--chrome-veil,62%), transparent);
}
:root[data-wallpaper]:not([data-glass="on"]) #left-sidebar,
:root[data-wallpaper]:not([data-glass="on"]) #session-rail{
  background:color-mix(in oklab, var(--glass-base-nav, var(--sidebar-bg)) var(--chrome-veil,62%), transparent);
}
/* A menu is a list you read off, so it keeps more than the chrome does - but
   not so much that the wallpaper stops existing behind it. */
:root[data-wallpaper]:not([data-glass="on"]) #global-dropdown,
:root[data-wallpaper]:not([data-glass="on"]) #think-picker{
  background:color-mix(in oklab, var(--glass-base, var(--surface)) 94%, transparent);
}
@media (prefers-reduced-transparency: reduce){
  :root[data-glass="on"]{ --glass-blur:0px; --glass-sat:1;
    --glass-fill:var(--surface); --glass-nav:var(--sidebar-bg); }
}

/* ── UI ZOOM ────────────────────────────────────────────────────────────
   Tailwind's spacing and type scales are rem-based, so moving the root font
   size scales padding, gaps, type and radii together. Widths declared in px
   are converted to rem below so they come along too.
   The floor exists because #chat-input carries text-base md:text-sm
   specifically to stay >=16px on phones - below that iOS Safari zooms the
   page on focus - so the scale is never allowed to shrink it under 16px. */
:root{ --ui-scale:1; }
/* The sketchboard drawer parks itself one width off the right edge, and it is
   position:absolute, so it extends the document box and the whole page can be
   dragged 320px sideways into nothing. body already hides its own overflow;
   the root did not. Using clip rather than hidden keeps the root from becoming a
   scroll container, which would break position:fixed children. */
html{ overflow-x:clip }
html{ font-size:calc(16px * var(--ui-scale)) }
@media (max-width:767px){
  /* keep the composer at a real 16px no matter how far the user zooms out */
  #chat-input,#new-pin-input{ font-size:max(16px, 1rem) }
}
