/* companion — dark, portrait-forward, sidebar-first.

   The shape is taken from the candy.ai survey in docs/candy-ai-ui-survey.md: a left
   sidebar as the primary IA, a three-column chat, and character cards that are the
   portrait rather than a thumbnail beside text. What is NOT taken from it is the
   commercial furniture — no countdown badges, no token balance, no blur-to-unlock.
   Where their Premium slot sits, this puts the real dollar ledger. */

:root{
  /* SWOONLY.AI brand identity (2026-09-22) — LIGHT-first, bright + coral/peach.
     Replaces the old dark #0e0f16 + pink→magenta theme. Palette from the Swoonly Brand Kit
     (memory reference_companion_swoonly_brand_kit): coral #FE7C6C (primary), peach #FDD89A
     (secondary), warm off-white ground, charcoal text. */
  --bg:#FAFAF8; --bg-2:#F4F1EC; --panel:#ffffff; --panel-2:#FBF8F3; --line:#E7E3DC;
  --ink:#1a1a1a; --ink-2:#3d3d40; --muted:#7a7a82; --off:#a8a29a;
  --accent:#FE7C6C; --accent-deep:#F0604E; --peach:#FDD89A; --ok:#3f9d77;
  --danger:#e0574a;
  --grad:linear-gradient(135deg,#FE7C6C 0%,#FDD89A 100%);
  --grad-soft:linear-gradient(135deg,rgba(254,124,108,.14),rgba(253,216,154,.22));
  /* Aliases some page-scoped styles reference with a DARK fallback (var(--card-bg,#1a1b24),
     var(--panel2,…)). Defining them here maps every such surface onto the light theme in one
     place, instead of editing each template. */
  --card-bg:#ffffff; --panel2:#ffffff; --card-border:var(--line);
  --font:'DM Sans',-apple-system,BlinkMacSystemFont,"Segoe UI",Helvetica,Arial,sans-serif;
  --side:246px;
  --r:14px;
  /* The sticky topbar's height. Since the scroll moved to `.viewport` (2026-09-08 gallery
     scroll fix), the topbar and `main` scroll together, so any sticky element INSIDE main
     that must clear the topbar offsets its `top` by this — otherwise it slides up to the
     same top:0 as the topbar and hides behind it. Kept in sync with .topbar min-height. */
  --topbar-h:56px;
}
*{box-sizing:border-box}
/* `hidden` MUST actually hide. The UA stylesheet's `[hidden]{display:none}` loses to any
   author rule that sets `display` on the same element — origin beats specificity — so
   `.suggest{display:flex}` quietly made every `el.hidden = true` a no-op. Measured
   2026-09-04: after `hidden = true` the suggestion chip still computed `display:flex`
   and 97.7px tall. Craig had to report "the prompt doesnt go away" twice, because the
   first fix moved a line that had never done anything. Global, so it cannot recur. */
[hidden]{display:none!important}


html,body{height:100%}
body{margin:0;background:var(--bg);color:var(--ink);font:15px/1.55 var(--font);
  display:flex;min-height:100%}
a{color:var(--accent);text-decoration:none}
a:hover{text-decoration:underline}
:focus-visible{outline:2px solid var(--accent);outline-offset:2px;border-radius:4px}
@media(prefers-reduced-motion:reduce){*{animation:none!important;transition:none!important}}

/* ---- shell ---------------------------------------------------------------- */
.sidebar{width:var(--side);flex:none;background:var(--bg-2);
  border-right:1px solid var(--line);display:flex;flex-direction:column;
  padding:1rem .75rem;position:sticky;top:0;height:100vh;overflow:hidden;z-index:30}
/* Fixed regions never shrink; only the character list scrolls (below). Without this a
   long My-AI list overflowed the auto-pinned footer and rendered on top of it. */
.brand,.nav,.nav-buy,.nav-auth,.sitefooter{flex:none}
.brand{display:flex;align-items:center;gap:.55rem;color:var(--ink);font-weight:700;
  font-size:1.06rem;letter-spacing:.01em;padding:.35rem .55rem 1rem}
.brand:hover{text-decoration:none}
/* The brand is the Swoonly "Horizontal Logo — Icon Left" lockup from the Brand Kit: the icon
   (two chat bubbles + negative-space heart, heart takes --bg-2) plus the Swoonly.ai wordmark,
   as ONE inline SVG so they stay a single unit. */
.brandlogo{height:68px;width:auto;max-width:100%;display:block;flex:none}

.nav{display:flex;flex-direction:column;gap:.12rem}
.navlink{display:flex;align-items:center;gap:.7rem;padding:.55rem .6rem;border-radius:10px;
  color:var(--ink-2);font-size:.92rem;font-weight:500}
.navlink i{font-style:normal;width:1.1rem;text-align:center;color:var(--muted);font-size:1rem}
.navlink:hover{background:var(--panel);color:var(--ink);text-decoration:none}
.navlink.on{background:var(--grad-soft);color:var(--ink)}
.navlink.on i{color:var(--accent)}

.navsection{margin-top:1.1rem;display:flex;flex-direction:column;gap:.35rem;
  flex:1 1 auto;min-height:0}
.navhead{color:var(--off);font-size:.68rem;text-transform:uppercase;letter-spacing:.09em;
  padding:0 .6rem;font-weight:700}
.navchars{display:flex;flex-direction:column;gap:.1rem;
  flex:1 1 auto;min-height:0;overflow-y:auto;overscroll-behavior:contain;
  scrollbar-width:thin;scrollbar-color:var(--line) transparent}
.navchars::-webkit-scrollbar{width:8px}
.navchars::-webkit-scrollbar-thumb{background:var(--line);border-radius:4px}
.navchar{display:flex;align-items:center;gap:.6rem;padding:.35rem .6rem;border-radius:10px;
  color:var(--ink-2);font-size:.9rem}
.navchar:hover{background:var(--panel);color:var(--ink);text-decoration:none}
.navchar.on{background:var(--panel);color:var(--ink)}
.navchar img,.navchar .ini{width:26px;height:26px;border-radius:50%;object-fit:cover;
  flex:none;background:var(--panel-2);display:grid;place-items:center;font-size:.72rem;
  color:var(--muted);font-weight:700}
.navchar.on img,.navchar.on .ini{box-shadow:0 0 0 2px var(--accent)}
/* A new message waiting in a thread he is not in: a dot on the avatar, same yellow as the
   list pill and the home badge. */
.navpic{position:relative;flex:none;display:inline-flex}
.navdot{position:absolute;right:-2px;top:-2px;width:10px;height:10px;border-radius:50%;
  background:#f5c542;box-shadow:0 0 0 2px var(--bg)}
.navdot[hidden]{display:none}
.navchar.has-unseen .navchar-name{color:var(--ink);font-weight:600}
.navchar-name{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}

.sidefoot{margin-top:auto;padding-top:1rem;display:flex;flex-direction:column;gap:.5rem;flex:none}
.ledger{display:flex;flex-direction:column;gap:.05rem;padding:.6rem .7rem;border-radius:12px;
  background:var(--panel);border:1px solid var(--line);color:var(--ink)}
.ledger:hover{border-color:var(--accent);text-decoration:none}
.ledger-n{font-weight:700;font-variant-numeric:tabular-nums}
.ledger-l{font-size:.72rem;color:var(--muted)}
.sidenote{margin:0;font-size:.7rem;line-height:1.45;color:var(--off);padding:0 .3rem}

/* The shell owns the window height so there is exactly ONE scrolling region on screen.
   Craig, 2026-09-04: "scrolling on this page is fucked… The chat window scrolls, the
   whole page scrolls, its impossible to control." That was mine: making the chat card
   "3x as long" set the log to min(174vh,1860px), which is TALLER THAN THE WINDOW, so the
   document had to scroll to reveal a box that also scrolled inside itself. Two nested
   scrollers with no visible boundary is not a tall card, it is a trap. */
.viewport{flex:1;min-width:0;display:flex;flex-direction:column;
  height:100vh;height:100dvh;overflow-y:auto}
.topbar{display:flex;align-items:center;gap:1rem;padding:.75rem 1.4rem;
  border-bottom:1px solid var(--line);background:rgba(255,255,255,.82);
  backdrop-filter:blur(10px);position:sticky;top:0;z-index:20;min-height:56px}
.topttl{font-weight:600;font-size:1rem}
.topright{margin-left:auto;display:flex;align-items:center;gap:1rem;font-size:.87rem}
.topright a{color:var(--muted)}
.topright a:hover{color:var(--accent)}
/* The RIGHT PANE (`.viewport`) owns the scroll, not this centered column. `main` is
   capped at 1320px and centred, so when it owned `overflow-y:auto` the wheel only worked
   over the middle of a wide window — the empty side margins are `.viewport`, which had
   nothing to scroll (Craig, 2026-09-08: "you have to be in the middle to scroll"). Moving
   the scroll up to the full-width viewport means the wheel works anywhere in the pane.
   The chat page is unaffected: at >=901px `main.mainchat` is overflow:hidden and fills
   the viewport exactly (nothing for the viewport to scroll) and its rails scroll on their
   own; stacked under 901px it scrolls the page, which is what it did before. */
main{padding:1.4rem;max-width:1320px;width:100%;margin:0 auto;flex:1;min-width:0;
  min-height:0}
/* The chat page fills the window instead of scrolling it: the columns scroll, the
   document does not. Every other page keeps scrolling in `main` as before.
   Only while the columns are SIDE BY SIDE. Under 900px the rail stacks UNDER the
   conversation, so a locked height splits one screen between two stacked panels and the
   log collapses — measured at 390x844: a 32px log. Narrow layouts scroll the page, which
   is the right behaviour when the content is a column. */
@media(min-width:901px){
  main.mainchat{display:flex;flex-direction:column;overflow:hidden}
  .mainchat .chat-wrap{flex:1;min-height:0;align-items:stretch}
  .mainchat .convcol,.mainchat .side{min-height:0;overflow-y:auto}
  /* `overflow-y:auto` alone was NOT enough and the way it failed is worth knowing: both
     rails are flex columns, so their panels shrank to fit instead of overflowing —
     scrollHeight equalled clientHeight, the column reported "no overflow", and the
     portrait card was rendered 32px tall when it wanted 860. Nothing scrolled because
     there was, technically, nothing to scroll. `flex:none` makes each panel keep its
     own height, which is what gives the column something to scroll. */
  .mainchat .convcol > *,.mainchat .side > *{flex:none}
  .mainchat .chatmain{min-height:0}
}

.burger{display:none;cursor:pointer;font-size:1.15rem;color:var(--ink-2)}
/* A bar that exists only to hold the burger. At desktop widths the sidebar is
   always on screen, so there is nothing for it to do. */
@media(min-width:901px){.topbar.narrowonly{display:none}}
.scrim{display:none}
@media(max-width:900px){
  .sidebar{position:fixed;left:0;top:0;transform:translateX(-100%);transition:transform .2s}
  #navtoggle:checked ~ .sidebar{transform:none;box-shadow:0 0 60px rgba(0,0,0,.6)}
  #navtoggle:checked ~ .scrim{display:block;position:fixed;inset:0;z-index:25;
    background:rgba(0,0,0,.5)}
  .burger{display:block}
  main{padding:1rem}
}

/* ---- primitives ----------------------------------------------------------- */
.panel{background:var(--panel);border:1px solid var(--line);border-radius:var(--r);
  padding:1.1rem 1.2rem;margin-bottom:1.1rem;overflow-x:auto}
.panel.narrow{max-width:640px}
h1{font-size:1.5rem;margin:.1rem 0 .9rem;letter-spacing:-.01em}
h2{font-size:1.02rem;margin:.1rem 0 .7rem}
.muted{color:var(--muted)}
.small{font-size:.85rem}
.error{background:rgba(224,87,74,.1);border:1px solid rgba(224,87,74,.32);color:#b23b2f;
  padding:.6rem .85rem;border-radius:10px}
.btn{display:inline-block;background:var(--grad);color:#fff;border:0;
  padding:.58rem 1.15rem;border-radius:10px;font:inherit;font-weight:600;cursor:pointer;
  box-shadow:0 4px 14px rgba(184,86,159,.22)}
.btn:hover{filter:brightness(1.08);text-decoration:none}
.btn:disabled{opacity:.45;cursor:default;box-shadow:none;filter:none}
.btn.ghost{background:var(--panel-2);color:var(--ink);border:1px solid var(--line);
  box-shadow:none}
.btn.ghost:hover{border-color:var(--accent)}
.btn.danger-btn{background:var(--danger);color:#fff;box-shadow:none}
.btn.small-btn{padding:.32rem .65rem;font-size:.8rem}
button.link{background:none;border:0;color:var(--muted);cursor:pointer;font:inherit;
  font-size:.82rem;padding:.2rem}
button.link:hover{color:var(--ink)}
button.link.danger:hover{color:var(--danger)}
.stack{display:flex;flex-direction:column;gap:.8rem}
.row{display:flex;gap:.8rem}
.row>label{flex:1}
label{display:flex;flex-direction:column;gap:.3rem;font-size:.82rem;color:var(--muted);
  text-transform:none}
input,textarea,select{background:var(--panel-2);border:1px solid var(--line);
  color:var(--ink);border-radius:10px;padding:.55rem .7rem;font:inherit;width:100%}
textarea{resize:vertical}
input:focus,textarea:focus,select:focus{outline:0;border-color:var(--accent)}
label.inline{flex-direction:row;gap:.45rem;align-items:center}
label.inline input[type=checkbox]{width:auto}
code{background:var(--panel-2);border-radius:5px;padding:.05rem .3rem;font-size:.85em}
summary{cursor:pointer;color:var(--muted);font-size:.88rem}
.mt{margin-top:.9rem}
.note{font-size:.8rem;color:var(--muted);margin-top:.5rem}
.nowrap{white-space:nowrap}

/* ---- character grid — the card IS the portrait ---------------------------- */
/* candy.ai's home is a wall of faces with the name and age burned into the bottom of
   each. A 56px thumbnail beside a line of text is a file manager; this is the product. */
.cards{list-style:none;padding:0;margin:0 0 1.2rem;display:grid;
  grid-template-columns:repeat(auto-fill,minmax(212px,1fr));gap:1rem}
.card{position:relative;margin:0;border-radius:var(--r);overflow:hidden;
  background:var(--panel);border:1px solid var(--line);aspect-ratio:3/4;
  display:flex;transition:transform .16s,border-color .16s}
.card:hover{transform:translateY(-3px);border-color:var(--accent)}
.card-link{position:absolute;inset:0;display:flex;flex-direction:column;
  justify-content:flex-end;color:#fff;padding:.85rem}
.card-link:hover{text-decoration:none}
.card-art{position:absolute;inset:0;width:100%;height:100%;object-fit:cover;
  background:var(--panel-2)}
.card-art.placeholder{display:grid;place-items:center;font-size:3.4rem;font-weight:700;
  color:var(--off);background:radial-gradient(circle at 50% 35%,var(--panel-2),var(--bg-2))}
/* The scrim exists so the name is legible over any portrait, light or dark. The
   bright light-UI theme does NOT reach inside the card: the overlay always sits on
   this dark gradient, so its text stays light regardless of --ink. Deepened after the
   Swoonly light flip so a light portrait (white top, pale wall) can't wash the name out. */
.card-scrim{position:absolute;inset:0;
  background:linear-gradient(to top,rgba(8,9,14,.95) 0%,rgba(8,9,14,.78) 26%,rgba(8,9,14,.32) 52%,rgba(8,9,14,0) 78%)}
.card-body{position:relative;display:flex;flex-direction:column;gap:.15rem;min-width:0}
.card-name{font-weight:700;font-size:1.06rem;letter-spacing:-.01em;color:#fff}
.card-name .age{color:rgba(255,255,255,.82);font-weight:500}
.clip{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-size:.82rem;
  color:var(--ink-2)}
/* Card overlay text is light on the scrim; .clip/.tag are dark elsewhere (tables, panels). */
.card .clip{color:rgba(255,255,255,.85)}
.preview{font-style:italic;color:var(--muted)}
.cardtags{position:relative;display:flex;flex-wrap:wrap;gap:.25rem;margin-top:.4rem}
.tag{background:rgba(255,255,255,.12);backdrop-filter:blur(4px);border-radius:999px;
  padding:.1rem .5rem;font-size:.68rem;color:var(--ink);white-space:nowrap}
.card .tag{background:rgba(255,255,255,.2);color:#fff}
.cardbadge{position:absolute;top:.6rem;right:.6rem;z-index:2;display:flex;gap:.3rem}
.cardbadge span{background:rgba(8,9,14,.7);backdrop-filter:blur(6px);border-radius:999px;
  padding:.12rem .5rem;font-size:.68rem;color:rgba(255,255,255,.9);font-variant-numeric:tabular-nums}
/* "Create character" sits in the grid as a card, the way theirs does. */
.card.newcard{border-style:dashed;background:transparent}
.card.newcard .card-link{align-items:center;justify-content:center;gap:.5rem;
  color:var(--muted);text-align:center}
.card.newcard:hover .card-link{color:var(--accent)}
.newplus{font-size:2rem;line-height:1}

.hero{display:flex;align-items:flex-end;justify-content:space-between;gap:1rem;
  flex-wrap:wrap;margin-bottom:1.1rem}
.hero h1{margin:0}
.hero p{margin:.25rem 0 0;color:var(--muted);font-size:.9rem}
.sectionhead{display:flex;align-items:baseline;justify-content:space-between;gap:1rem;
  margin:1.4rem 0 .7rem}
.sectionhead h2{margin:0}

.status{list-style:none;padding:0;margin:0;display:flex;flex-wrap:wrap;gap:.5rem;
  font-size:.84rem}
.status li{display:flex;gap:.5rem;align-items:center;background:var(--panel-2);
  border:1px solid var(--line);border-radius:999px;padding:.3rem .7rem}
.dot{width:8px;height:8px;border-radius:50%;flex:none}
.dot.ok{background:var(--ok)} .dot.off{background:var(--off)}

/* ---- chat: three columns -------------------------------------------------- */
/* Their layout, and it earns its keep: threads on the left, the conversation in the
   middle, and who you are talking to on the right where you can see her while you type. */
.chat-wrap{display:grid;grid-template-columns:260px minmax(0,1fr) 300px;gap:1rem;
  align-items:start}
/* A group room has no conversations column, so the thread takes the wide track and
   the roster sits in the 300px rail — same proportions as the 1:1 chat's main+side. */
.chat-wrap.grouproom{grid-template-columns:minmax(0,1fr) 300px}
@media(max-width:1180px){.chat-wrap{grid-template-columns:minmax(0,1fr) 290px}
  .convcol{display:none}}
@media(max-width:900px){.chat-wrap{grid-template-columns:minmax(0,1fr)}
  .chat-wrap.grouproom{grid-template-columns:minmax(0,1fr)}
  .side{display:none}}

.convcol{position:sticky;top:72px}
@media(min-width:901px){.mainchat .convcol{position:static;top:auto}}
.convsearch{margin-bottom:.6rem}
.convlist{display:flex;flex-direction:column;gap:.15rem;max-height:70vh;overflow-y:auto}
.conv{display:flex;gap:.6rem;align-items:center;padding:.5rem;border-radius:11px;
  color:var(--ink);min-width:0}
.conv:hover{background:var(--panel-2);text-decoration:none}
.conv.on{background:var(--grad-soft)}
.conv img,.conv .ini{width:38px;height:38px;border-radius:50%;object-fit:cover;flex:none;
  background:var(--panel-2);display:grid;place-items:center;color:var(--muted);
  font-weight:700;font-size:.8rem}
.conv-body{min-width:0;flex:1;display:flex;flex-direction:column}
.conv-top{display:flex;justify-content:space-between;gap:.5rem;align-items:baseline}
.conv-name{font-weight:600;font-size:.9rem;overflow:hidden;text-overflow:ellipsis;
  white-space:nowrap}
.conv-when{font-size:.7rem;color:var(--off);flex:none}
/* New messages waiting in a thread he is not in. Same yellow as the home card's badge, so
   "new" means one thing everywhere. `hidden` wins over this display via [hidden] rule. */
.conv-unseen{flex:none;min-width:1.15rem;height:1.15rem;padding:0 .35rem;border-radius:.6rem;
  background:#f5c542;color:#222;font-size:.68rem;font-weight:700;line-height:1.15rem;text-align:center}
.conv-unseen[hidden]{display:none}
.conv.has-unseen .conv-prev{color:var(--fg);font-weight:600}
.conv.has-unseen .conv-name{color:var(--fg)}
.conv-prev{font-size:.78rem;color:var(--muted);overflow:hidden;text-overflow:ellipsis;
  white-space:nowrap}

.chatmain{display:flex;flex-direction:column;padding:0;overflow:visible}
.chathead{display:flex;align-items:center;gap:.75rem;padding:.85rem 1.1rem;
  border-bottom:1px solid var(--line)}
.chathead img,.chathead .ini{width:42px;height:42px;border-radius:50%;object-fit:cover;
  flex:none;background:var(--panel-2);display:grid;place-items:center;font-weight:700;
  color:var(--muted)}
.chathead-body{min-width:0;flex:1}
.chathead-name{font-weight:700;font-size:1rem;display:block}
.chathead-sub{font-size:.78rem;color:var(--muted)}
.chathead-acts{display:flex;gap:.4rem;align-items:center}
#historylink{position:relative}
.starcount{position:absolute;top:-.35rem;right:-.5rem;font-size:.62rem;line-height:1;
  padding:.1rem .3rem;border-radius:999px;background:#f5c542;color:#222;font-weight:700}
/* `hidden` must beat the class display — see reference_hidden_attribute_loses_to_class_display */
.starcount[hidden]{display:none}
.iconbtn{display:grid;place-items:center;width:36px;height:36px;border-radius:50%;
  background:var(--panel-2);border:1px solid var(--line);color:var(--ink-2);
  cursor:pointer;font-size:.95rem;padding:0;flex:none}
.iconbtn:hover{border-color:var(--accent);color:var(--accent);text-decoration:none}
.iconbtn.callbtn-round{background:var(--grad);border:0;color:#fff}
.iconbtn.callbtn-round:hover{color:#fff;filter:brightness(1.08)}

/* Craig, 2026-09-04: "make the chat card longer please? It's too short. 3x as long."
   Literally 3x what it was — 58vh/620px became 174vh/1860px. That is taller than any
   screen, which is the point: the card no longer scrolls its own little window, the
   PAGE scrolls and you see three screens of conversation. The consequence is that the
   composer would sit below the fold, so it is stuck to the bottom of the viewport
   (below) — a chat you have to scroll down to type into would be a worse trade than
   the short card was. */
/* No fixed height: the log takes whatever the window leaves after the header and the
   composer, so it is as long as the screen allows and is the only thing that scrolls.
   `min-height:0` is load-bearing — a flex item's default `min-height:auto` refuses to
   shrink below its content, which is how a scrollable child silently grows its parent
   and hands the document a second scrollbar. */
.log{display:flex;flex-direction:column;gap:0;flex:1;min-height:0;
  overflow-y:auto;padding:1rem 1.1rem}
/* iMessage spacing: a sender change gets the wide gap, a same-sender run stacks tight.
   Margins rather than `gap` so the two cases can differ. The universal rule has zero
   specificity ON PURPOSE: .daymark, .msg.grouped and `.log > :first-child{margin-top:auto}`
   all outrank it. The picker is an absolutely positioned child of the log and a margin
   would shift where openPicker put it, so it is excused. Apple publishes no gap values —
   8px/2px are a design choice. */
.log > * + *{margin-top:8px}
.log > .rxpicker{margin-top:0}
.log > .msg.user + .msg.user,.log > .msg.assistant + .msg.assistant{margin-top:2px}
/* Stacked layout (under 900px the rail sits BELOW the conversation): the log is a fixed
   slice of the window again, but one that FITS in it — never the 174vh that started
   this. Written AFTER the rule above on purpose: `flex:1` sets flex-basis:0 and beats a
   `height` declared earlier in the file, so an earlier media block silently did nothing
   and the log grew to 80,688px. */
@media(max-width:900px){.log{flex:none;height:min(58vh,560px)}}
/* With a card this tall, a short thread would float at the top of a large empty box.
   Pushing the FIRST child down with auto margin sits the conversation at the bottom,
   where a texting app puts it. Deliberately not `justify-content:flex-end`, which does
   the same thing until the content overflows and then clips the oldest messages out of
   reach — the scrollbar exists but cannot reach them. */
.log > :first-child{margin-top:auto}
.msg{padding:8px 14px;border-radius:20px;max-width:70%;position:relative;font-size:17px;line-height:22px}
/* The words live in their own span, and ONLY the span is pre-wrap. The bubble used to be
   pre-wrap itself, so every newline and indent in the template between its controls
   (edit, star, speak, delete, react) rendered as a blank line INSIDE the bubble: sixteen
   of them, ~385 px of empty pink under a one-line message (Craig, 2026-09-05). Nothing
   outside .msgtext may ever be whitespace-significant. */
.msgtext{white-space:pre-wrap;overflow-wrap:break-word;word-wrap:break-word}
.msg.user{align-self:flex-end;background:var(--grad);color:#fff}
.msg.assistant{align-self:flex-start;background:var(--panel-2);border:0}
/* group.html still puts the ▶ INSIDE the bubble (group.html:45 template, :268 stream both
   append a direct-child .speak); keep the room for it there, and only there. */
.msg.assistant:has(> .speak){padding-right:2.1rem}
.msg.media{padding:0;max-width:70%;background:none;border:0}
.msg.media img,.msg.media video{width:100%;border-radius:20px;display:block;background:#000}
/* A bare <audio> has no shape of its own; keep a bubble behind the player. */
.msg.media:has(audio){padding:4px 6px;background:var(--panel-2)}
.msg.media audio{width:260px;max-width:100%;display:block}
.sysnote{align-self:center;font-size:.76rem;color:var(--muted);font-style:italic;
  padding:.25rem .8rem;border:1px dashed var(--line);border-radius:999px}
.thinking{opacity:.6;font-style:italic}
/* iMessage's separators: a centred small grey caption — 'Today', 'Yesterday', a short date
   — followed by the time of the cluster's first message (third-party description; Apple
   documents no format). 11/13 = Apple Caption 2; which style Messages uses is not
   verified. margin-bottom is 0 because the `.log > * + *` rule gives the next item 8px. */
.daymark,.timemark{display:flex;justify-content:center;align-items:baseline;gap:.3em;
  margin:14px 0 0;color:var(--muted);font-size:11px;line-height:13px;
  text-transform:none;letter-spacing:0}
.daymark span{font-weight:600}
/* The time is stamped into data-time by stampTimes() in chat.html and painted here, so the
   marker's textContent stays exactly the label — pollNew (chat.html:1546-1549) and the
   earlier loader (:1608-1614) dedupe day markers by textContent. */
.daymark[data-time]::after,.timemark[data-time]::after{content:attr(data-time);font-weight:400}
.speak{position:absolute;right:.4rem;bottom:.35rem;background:none;border:0;
  color:var(--off);cursor:pointer;font-size:.78rem;line-height:1;padding:.15rem}
.speak:hover{color:var(--accent)}
.speak:disabled{opacity:.5;cursor:default}
.msgdel{position:absolute;top:.15rem;right:.3rem;margin:0;opacity:0;visibility:hidden;
  transition:opacity .12s,visibility .12s}
.msg:hover .msgdel,.msgdel:focus-within,.msg:focus-within .msgdel,.msg.acts-open .msgdel{opacity:1;visibility:visible}
.msgdel button{font-size:.72rem;line-height:1;padding:.1rem .2rem}
.msg.user .msgdel{right:auto;left:.3rem}
/* Edit sits beside delete and reveals with it: both adjust a message that exists.
   A repair control on a bubble is fine on hover — the ALWAYS-visible tools are the
   threadbar's (regenerate / suggest / continue / rewind / break). */
.msgedit{position:absolute;top:.15rem;right:1.6rem;background:none;border:0;
  font-size:.72rem;line-height:1;padding:.1rem .2rem;cursor:pointer;color:var(--muted);
  opacity:0;visibility:hidden;transition:opacity .12s,visibility .12s}
.msg:hover .msgedit,.msgedit:focus,.msg:focus-within .msgedit,.msg.acts-open .msgedit{opacity:1;visibility:visible}
.msgedit:hover{color:var(--accent)}
.msg.user .msgedit{right:auto;left:1.6rem}
.msgeditor{display:block;width:100%;min-width:0;min-height:3.2em;resize:vertical;
  font:inherit;color:inherit;background:rgba(0,0,0,.12);border:1px solid var(--line);
  border-radius:10px;padding:.4rem .55rem;white-space:pre-wrap}
.msg.user .msgeditor{background:rgba(255,255,255,.14);color:#fff}
/* Favourite. Faint but ALWAYS there while unset — Craig could not find a hover-only
   control once already (the react button) and a star is an invitation, not a repair
   tool; full on hover, and a SET star stays lit with a yellow bar on the bubble. */
.msgstar{position:absolute;top:.15rem;right:2.9rem;background:none;border:0;
  font-size:.78rem;line-height:1;padding:.1rem .2rem;cursor:pointer;color:var(--muted);
  opacity:.35;transition:opacity .12s}
.msg:hover .msgstar,.msgstar:focus,.msgstar.is-on{opacity:1}
.msgstar.is-on{color:#f5c542}
.msgstar:hover{color:var(--accent)}
.msg.user .msgstar{right:auto;left:2.9rem}
.msg.starred{box-shadow:inset 3px 0 0 #f5c542}
.msg.user.starred{box-shadow:inset -3px 0 0 #f5c542}
/* Scene anchor + Learned Context panels */
.sceneform{display:flex;gap:.4rem;margin-top:.3rem}
.sceneform input{font-size:.83rem;flex:1}
.proposal{background:var(--panel-2);border-left:2px solid var(--accent);border-radius:8px;
  padding:.4rem .6rem;margin-bottom:.5rem}
.proposalbtns{display:flex;gap:.8rem}
.proposalbtns form{margin:0}
.learnedform{display:flex;flex-direction:column;gap:.25rem}
.learnedform label{color:var(--muted);margin-top:.3rem}
.learnedform textarea{font-size:.82rem;resize:vertical;min-height:2.6em}
.learnedfoot{display:flex;justify-content:space-between;align-items:center;gap:.5rem;
  margin-top:.3rem}
/* Image form additions: enhance beside the free text; persona avatar row in settings */
.promptrow{display:flex;gap:.4rem;align-items:center}
.promptrow input{flex:1;min-width:0}
.promptrow .link{white-space:nowrap}
.avatarrow{display:flex;gap:.7rem;align-items:flex-start;margin-top:.5rem;flex-wrap:wrap}
.avatarrow .avatar{width:56px;height:56px;border-radius:50%;object-fit:cover;
  border:1px solid var(--line)}
.avatarform{display:flex;flex-direction:column;gap:.3rem}
.avatarform input[type=file]{font-size:.8rem}
.cardbadge .unseen{background:#f5c542;color:#222;font-weight:700}
form.inline{display:inline}
/* History view */
.history{display:grid;grid-template-columns:220px 1fr;gap:1rem;align-items:start}
.histdays{display:flex;flex-direction:column;gap:.15rem;position:sticky;
  top:calc(var(--topbar-h) + 1rem)}
.histday{display:flex;justify-content:space-between;gap:.5rem;padding:.35rem .55rem;
  border-radius:8px;color:inherit;text-decoration:none;font-size:.86rem}
.histday:hover{background:var(--panel-2)}
.histday.on{background:var(--panel-2);box-shadow:inset 2px 0 0 var(--accent)}
.histhead{display:flex;justify-content:space-between;align-items:baseline;gap:.6rem}
.histlog{max-height:none}
@media (max-width:760px){.history{grid-template-columns:1fr}.histdays{position:static}}
/* Kindroid's rule: a tweaked message shows it was tweaked, to you — never to her. */
.msg.edited::after{content:'edited';display:block;font-size:.62rem;color:var(--muted);
  margin-top:.25rem;letter-spacing:.02em}
.msg.user.edited::after{color:rgba(255,255,255,.7)}
.msg.user .who{color:rgba(255,255,255,.75)}
.who{display:block;font-size:.7rem;color:var(--accent);font-weight:700;
  letter-spacing:.02em;margin-bottom:.15rem}

/* Sticky so the taller log above cannot push the input off-screen. It needs its own
   opaque background — a transparent sticky bar would have messages scrolling through
   the text you are typing. */
.composerwrap{border-top:1px solid var(--line);padding:.75rem 1.1rem 1rem;
  position:sticky;bottom:0;z-index:5;background:var(--panel);
  border-bottom-left-radius:inherit;border-bottom-right-radius:inherit}
/* The turn status floats ABOVE the composer instead of sitting in it. Craig,
   2026-09-04: "every time i click the record button the screen jerks around and the
   button loses contact, then i have to click it again to record." Measured: writing
   "recording… release to send" into an empty #note grew the composer by 32px and moved
   the mic button 32px UP — nearly its own height (36px) — so the cursor was left outside
   the button, mouseleave fired, and the recording stopped the instant it started.
   Out of flow, it cannot move anything, whatever it says or how long that is. */
/* No `position:relative` override any more: it silently cancelled the sticky above, so at
   phone widths the composer sat 66px below the visible edge of main. #note is in flow. */
/* Out of flow it covered the last 28 px of the thread instead (Craig, 2026-09-05 19:37:
   "Bottom of chat is always covered by the thinking bar, need to have both full
   visible" — measured: #note 673–701, .log ends 705). So: IN flow, but at a FIXED
   height — 1.9rem whether empty or full, one line, ellipsised — which keeps the 2026-09-04
   guarantee (nothing moves when it fills) and gives the thread its own bottom edge. */
#note{display:flex;align-items:center;justify-content:center;min-height:1.9rem;
  max-height:1.9rem;box-sizing:border-box;margin:0;line-height:1.2;
  text-align:center;pointer-events:none;overflow:hidden;text-overflow:ellipsis;
  white-space:nowrap}
#note:not(:empty){background:var(--panel);border:1px solid var(--line);
  border-radius:999px;padding:.2rem .7rem}
.composer{display:flex;gap:6px;align-items:flex-end;margin-top:.55rem;position:relative}
.composer input{flex:1;border-radius:999px;padding:.65rem 1rem}
/* iMessage's compose row: '+' to the left of the text field (Apple, iOS 26 guide), the
   field a pill that grows with the text, the Send arrow inside its right end. Apple does
   not publish the field's metrics; these make a one-line pill 38px tall around 17pt text
   (22 line + 5+5 textarea padding + 2+2 pill padding + 1+1 border). `.composer input` is
   kept verbatim above for group.html's plain <input> composer. */
.composer .plus{width:34px;height:34px;margin-bottom:2px;font-size:1.35rem;line-height:1;color:var(--ink-2)}
.composer .plus[aria-expanded="true"]{border-color:var(--accent);color:var(--accent)}
/* The tools behind the '+': a popover ABOVE the row, out of flow, so opening it moves
   nothing in the composer — same reasoning as #note. [hidden] beats display:flex. */
.extras{position:absolute;left:0;bottom:calc(100% + 8px);z-index:6;display:flex;gap:6px;
  padding:6px;background:var(--panel);border:1px solid var(--line);border-radius:24px;
  box-shadow:0 6px 20px rgba(0,0,0,.35)}
.field{flex:1;min-width:0;display:flex;align-items:flex-end;gap:2px;
  background:var(--panel-2);border:1px solid var(--line);border-radius:19px;
  padding:2px 2px 2px 12px}
.field:focus-within{border-color:var(--accent)}
.field textarea{flex:1;min-width:0;width:auto;background:none;border:0;border-radius:0;
  margin:0;padding:5px 0;font:inherit;font-size:17px;line-height:22px;color:var(--ink);
  resize:none;max-height:110px;overflow-y:auto}
.field textarea:focus{outline:0;border:0}
.field .iconbtn{width:30px;height:30px;border:0;background:none;font-size:1rem}
.field .sendbtn{width:30px;height:30px;padding:0;border-radius:50%;display:grid;
  place-items:center;font-size:1.1rem;font-weight:700;line-height:1;box-shadow:none;flex:none}
.composer .mic.recording{background:var(--danger);color:#fff;border-color:transparent;
  animation:pulse 1.1s ease-in-out infinite}
@keyframes pulse{50%{filter:brightness(1.3)}}
.sendbtn{border-radius:999px;padding:.6rem 1.3rem}
.threadbar{display:flex;justify-content:space-between;align-items:center;
  margin:.2rem 0 .5rem;font-size:.79rem;flex-wrap:wrap;row-gap:.2rem;column-gap:.7rem}
.threadbar form{margin:0}
.threadbar .link{white-space:nowrap}

/* suggestion chips above the composer, as on their chat */
.chips{display:flex;flex-wrap:wrap;gap:.35rem}
.chip{background:var(--panel-2);border:1px solid var(--line);color:var(--ink-2);
  border-radius:999px;padding:.28rem .7rem;font:inherit;font-size:.79rem;cursor:pointer}
.chip:hover{border-color:var(--accent);color:var(--accent);text-decoration:none}
.chip.on{border-color:var(--accent);color:var(--accent)}
.chip:disabled{opacity:.4;cursor:default}
a.chip{display:inline-block;text-decoration:none}
a.chip:hover{text-decoration:none}
.chip.ghostchip{padding:.28rem .6rem}
.chips.starters{margin:.5rem 0 .2rem}
.chipsets{display:flex;flex-direction:column;gap:.6rem}
.chipset{display:flex;gap:.6rem;align-items:baseline}
.chiplabel{flex:0 0 4.7rem;color:var(--off);font-size:.7rem;text-transform:uppercase;
  letter-spacing:.06em;padding-top:.3rem}
.suggest{display:flex;gap:.5rem;align-items:center;flex-wrap:wrap;margin-top:.7rem;
  padding:.6rem .75rem;background:var(--panel-2);border:1px dashed var(--accent);
  border-radius:12px}
.suggest input{flex:1;min-width:160px;font-size:.86rem}
/* The scene box is a textarea now (Craig: 'make the video prompt approval larger, so I
   can easily edit'). Full-width so the length toggle + Generate wrap beneath it. */
.suggest textarea{flex:1 1 100%;min-width:160px;font-size:.86rem;min-height:3.6rem;
  line-height:1.4;padding:.4rem .55rem}
.suggest .lenrow{display:inline-flex;gap:.35rem}
#askrow{margin:.5rem 0 .2rem;align-items:center}

/* ---- right rail: the character card --------------------------------------- */
/* NOT sticky: this column is ~2900px of panels and sticking a column taller than the
   viewport just pins it at the top and does nothing, while breaking the scroll. */
.side{display:flex;flex-direction:column;gap:1rem}
.side .panel{margin:0}
.railcard{padding:0;overflow:hidden}
.railart{width:100%;aspect-ratio:3/4;object-fit:cover;display:block;background:var(--panel-2)}
.railart.placeholder{display:grid;place-items:center;font-size:3.6rem;font-weight:700;
  color:var(--off)}
.railbody{padding:.9rem 1rem 1.05rem}
.railname{font-weight:700;font-size:1.1rem;margin:0 0 .1rem}
.railtag{color:var(--muted);font-size:.84rem;margin:0 0 .8rem}
dl.about{display:grid;grid-template-columns:auto 1fr;gap:.35rem .8rem;margin:.2rem 0 .8rem}
dl.about dt{color:var(--off);font-size:.68rem;text-transform:uppercase;
  letter-spacing:.06em;align-self:baseline;white-space:nowrap}
dl.about dd{margin:0;font-size:.85rem;color:var(--ink-2)}
.side .gallery{display:grid;grid-template-columns:1fr 1fr;gap:.4rem}
.side .gallery img{width:100%;border-radius:9px;display:block;aspect-ratio:1;object-fit:cover}
.facts{list-style:none;padding:0;margin:0;font-size:.82rem;
  display:flex;flex-direction:column;gap:.35rem}
.facts li{display:flex;justify-content:space-between;gap:.5rem;
  background:var(--panel-2);border-radius:8px;padding:.35rem .55rem}
.facts li.pinned{border-left:2px solid var(--accent)}
.factbtns{display:flex;gap:.1rem;flex:none}
.pin{font-size:.75rem}
.addfact{display:flex;gap:.4rem;margin-top:.6rem}
.addfact input{font-size:.83rem}
.clips{display:flex;flex-direction:column;gap:.5rem;margin-top:.6rem}
.clips video{width:100%;border-radius:10px;display:block;background:#000}

/* ---- disclosure + crisis -------------------------------------------------- */
/* Quiet but permanent; the crisis box is loud and only appears when it should. */
.disclose{font-size:.76rem;color:var(--off);border-left:2px solid var(--line);
  padding:.15rem .7rem;margin:0 0 .9rem}
.crisis{background:var(--grad-soft);border:1px solid var(--accent);border-radius:var(--r);
  padding:.9rem 1.1rem;margin-bottom:1rem;font-size:.9rem}
.crisis ul{margin:.6rem 0 .4rem;padding-left:1.1rem;display:flex;
  flex-direction:column;gap:.3rem}
.crisis a{text-decoration:underline}

/* ---- tables, tiles, gallery ---------------------------------------------- */
.tiles{display:flex;gap:.8rem;flex-wrap:wrap;margin-top:.9rem}
.tile{background:var(--panel-2);border:1px solid var(--line);border-radius:12px;
  padding:.75rem .95rem;display:flex;flex-direction:column;gap:.15rem;min-width:170px}
.tile-n{font-size:1.5rem;font-weight:700;font-variant-numeric:tabular-nums}
.grid{width:100%;border-collapse:collapse;font-size:.86rem}
.grid th,.grid td{text-align:left;padding:.4rem .5rem;border-bottom:1px solid var(--line)}
.grid th{color:var(--off);font-weight:600;font-size:.72rem;text-transform:uppercase;
  letter-spacing:.05em}
.grid td.n,.grid th.n{text-align:right;font-variant-numeric:tabular-nums}
.grid td.clip{max-width:22ch}
.est{font-size:.66rem;color:var(--muted);margin-left:.3rem;border:1px solid var(--line);
  border-radius:4px;padding:0 .2rem;vertical-align:middle}
.barcell{width:34%}
.bar-fill{display:block;height:8px;border-radius:4px;background:var(--grad);min-width:2px}
.tiles-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(var(--tile-min,215px),1fr));
  gap:.85rem;margin-top:.9rem}
.mtile{margin:0;background:var(--panel-2);border:1px solid var(--line);
  border-radius:12px;overflow:hidden;display:flex;flex-direction:column}
.mtile img,.mtile video{width:100%;display:block;background:#000}
.mtile audio{width:100%;padding:.5rem}
.mtile figcaption{padding:.5rem .6rem;display:flex;flex-direction:column;gap:.1rem;
  font-size:.79rem;min-width:0}
/* Gallery tile size. `--tile-min` (set by the [data-tile] class the toggle applies)
   drives the column width of both gallery grids; tilesize.js persists the choice per
   viewer. `.mgrid` was previously undefined, so the per-character gallery rendered each
   tile full-width and stacked — "too big to manage" (Craig 2026-09-08). */
.mgrid{display:grid;grid-template-columns:repeat(auto-fill,minmax(var(--tile-min,190px),1fr));
  gap:.85rem;margin:.9rem 0}
[data-tilegrid][data-tile="s"]{--tile-min:120px}
[data-tilegrid][data-tile="m"]{--tile-min:190px}
[data-tilegrid][data-tile="l"]{--tile-min:280px}
[data-tilegrid][data-tile="xl"]{--tile-min:460px}
.viewbar{display:flex;align-items:center;gap:.4rem;flex-wrap:wrap;margin-top:.6rem}
.filter{margin-bottom:.8rem}

/* ---- settings / lists ----------------------------------------------------- */
ul.plain{list-style:none;margin:0;padding:0}
.row-item{display:flex;justify-content:space-between;align-items:flex-start;gap:1rem;
  background:var(--panel-2);border:1px solid var(--line);border-radius:12px;
  padding:.75rem .85rem}
.row-item.is-active{border-color:var(--accent)}
.row-actions{display:flex;gap:.4rem;flex-shrink:0}
.row-actions form{margin:0}
.row-actions .btn.ghost.small-btn{text-decoration:none}
.pill{display:inline-block;margin-left:.4rem;padding:.05rem .5rem;border-radius:999px;
  background:var(--grad);color:#fff;font-size:.68rem;font-weight:700;vertical-align:middle}
fieldset.fs{border:1px solid var(--line);border-radius:12px;padding:.9rem 1rem;margin:0;
  display:flex;flex-direction:column;gap:.8rem}
fieldset.fs legend{padding:0 .4rem;color:var(--off);font-size:.72rem;
  text-transform:uppercase;letter-spacing:.06em}
.stack-thumb{display:flex;align-items:center;justify-content:flex-start;overflow:visible}
.stack-thumb img,.stack-thumb .ini{width:30px;height:30px;border-radius:50%;
  object-fit:cover;border:2px solid var(--panel);margin-left:-10px;
  background:var(--panel-2);display:flex;align-items:center;justify-content:center;
  font-size:.72rem;color:var(--muted)}
.stack-thumb img:first-child,.stack-thumb .ini:first-child{margin-left:0}
.thumb{width:56px;height:56px;border-radius:10px;object-fit:cover;flex:none;
  background:var(--panel-2);display:grid;place-items:center;font-weight:700;color:var(--muted)}

/* ---- call ---------------------------------------------------------------- */
.callwrap{display:grid;grid-template-columns:minmax(0,1fr) 320px;gap:1.1rem;
  align-items:start}
@media(max-width:900px){.callwrap{grid-template-columns:minmax(0,1fr)}}
.callstage{display:flex;flex-direction:column;align-items:center;text-align:center;
  gap:.4rem;padding:2rem 1.2rem}
.callstage h1{margin:.5rem 0 0}
.callface{width:200px;height:200px;border-radius:50%;object-fit:cover;
  border:3px solid var(--line);background:var(--panel-2);transition:border-color .2s}
.callface.placeholder{display:grid;place-items:center;font-size:4rem;font-weight:700;
  color:var(--off)}
.callface.talking{border-color:var(--accent);animation:ring 1.4s ease-in-out infinite}
@keyframes ring{50%{box-shadow:0 0 0 12px rgba(224,103,154,.15)}}
.callstate{color:var(--muted);font-size:.9rem;margin:.1rem 0;min-height:1.3em}
.calltimer{font-variant-numeric:tabular-nums;font-size:1.7rem;font-weight:600;margin:.1rem 0}
.callbtns{display:flex;gap:.6rem;flex-wrap:wrap;justify-content:center;margin-top:.7rem}
.callbtn{padding:.65rem 1.7rem}
.callbtn.end{background:var(--danger);color:#fff;box-shadow:none}
.callbtn.held{background:var(--grad);color:#fff}
.level{width:180px;height:4px;border-radius:2px;background:var(--line);overflow:hidden;
  margin-top:.4rem}
.level-fill{height:100%;width:0;background:var(--grad);transition:width .06s linear}
.calllog{height:auto;max-height:44vh;padding:0}
.calllog .msg{max-width:92%}

/* ---- model chooser (settings) -------------------------------------------
   Two options that are NOT equivalent, so they are laid out as full cards with
   their trade-offs on the face rather than as a bare pair of radio labels. The
   selected one carries the accent border; nothing else on the page does. */
.choice{display:flex;gap:.7rem;align-items:flex-start;padding:.85rem .95rem;
  border:1px solid var(--line);border-radius:var(--r);background:var(--bg);
  cursor:pointer}
.choice:hover{border-color:var(--accent-deep)}
.choice.is-active{border-color:var(--accent);
  box-shadow:inset 0 0 0 1px color-mix(in srgb,var(--accent) 35%,transparent)}
.choice input{margin-top:.28rem;accent-color:var(--accent);flex:none}
.choice > span{display:block}
.choice b{margin-right:.4rem}
.choice .pill{margin-right:.3rem}
.choice .muted{margin-top:.35rem}
.pill.warn{background:color-mix(in srgb,var(--danger) 22%,transparent);
  color:var(--danger);border-color:color-mix(in srgb,var(--danger) 45%,transparent)}
.warn{color:var(--danger)}

/* ---- per-character gallery ---------------------------------------------- */
.galhead{display:flex;flex-wrap:wrap;gap:1rem;align-items:center;
  justify-content:space-between}
.galwho{display:flex;gap:.9rem;align-items:center}
.galwho img,.galwho .ini{width:64px;height:64px;border-radius:50%;object-fit:cover;
  flex:none;display:grid;place-items:center;font-size:1.6rem;
  background:var(--panel-2);color:var(--accent)}
.galhead h1{margin:0 0 .15rem;font-size:1.25rem}
.galfilter{display:flex;gap:.4rem;flex-wrap:wrap}
.chip{display:inline-block;padding:.25rem .7rem;border-radius:999px;font-size:.8rem;
  border:1px solid var(--line);color:var(--muted);background:var(--bg)}
.chip:hover{border-color:var(--accent-deep);text-decoration:none}
.chip.on{border-color:var(--accent);color:var(--accent)}
.styleform .choice{padding:.6rem .7rem}
.styleform .choice b{font-size:.95rem}

/* Voice audition. Cards are uniform so the ear, not the layout, does the comparing. */
.vgrid { display: grid; grid-template-columns: repeat(auto-fill, minmax(210px, 1fr)); gap: .75rem; }
.vcard { display: flex; flex-direction: column; gap: .45rem; padding: .7rem;
         border: 1px solid var(--line); border-radius: 8px; }
.vname { font-weight: 600; font-variant-numeric: tabular-nums; }
.vcard audio { width: 100%; }
.vassign { display: flex; gap: .4rem; }
.vassign select { flex: 1; min-width: 0; }
.quoteline { font-style: italic; opacity: .85; }
ul.plain { list-style: none; padding: 0; margin: .4rem 0 0; display: flex;
           flex-direction: column; gap: .3rem; }
.caps { text-transform: uppercase; letter-spacing: .06em; opacity: .7; margin: 0 0 .5rem; }

/* A reply that came as several messages. The follow-ups sit tighter to the one above,
   the way a texting client groups a burst from the same person. */
.msg.grouped { margin-top: 2px; }

/* Picture pickers + generation progress. */
.picks { display: grid; grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); gap: .5rem; }
.pick { display: flex; flex-direction: column; gap: .2rem; min-width: 0; }
.pick > span { font-size: .72rem; text-transform: uppercase; letter-spacing: .06em; opacity: .65; }
.pick select { width: 100%; min-width: 0; }
.preview { opacity: .8; font-style: italic; margin: .1rem 0 0; }
.prog { margin: .5rem 0 0; }
.prog .bar { height: 5px; border-radius: 3px; background: var(--line); overflow: hidden; }
.prog .bar i { display: block; height: 100%; width: 0; border-radius: 3px;
               background: var(--accent, #b4235a); transition: width .35s ease; }

/* An image in the thread opens full size; keep the link invisible as a link. */
.msg.media a { display: block; border: 0; line-height: 0; }
.gallery a { display: block; border: 0; line-height: 0; }
.msg.media a:hover img, .gallery a:hover img { opacity: .92; }

/* The slow burn. A ladder rather than a bar: the stages are named states, not a
   percentage, and drawing them as one would invent precision that isn't there. */
.ladder { display: flex; flex-direction: column; gap: .25rem; margin: .5rem 0; }
.rung { display: flex; align-items: center; gap: .5rem; font-size: .82rem; opacity: .45; }
.rung i { width: 8px; height: 8px; border-radius: 50%; background: var(--line); flex: none; }
.rung.done { opacity: .7; }
.rung.done i { background: var(--accent, #b4235a); opacity: .55; }
.rung.now { opacity: 1; font-weight: 600; }
.rung.now i { background: var(--accent, #b4235a); box-shadow: 0 0 0 3px rgba(180,35,90,.18); }
.rpform { margin: 0; }

/* Media-frequency slider. The range input is the control; the sentence under it is the
   real limit, spelled out, because "Often" alone tells the user nothing about what
   they have actually chosen. */
.freq { display: flex; flex-direction: column; gap: .4rem; }
.freq input[type=range] { width: 100%; accent-color: var(--accent); }
.freq p { margin: 0; }

/* Reference-image picker. The current one is marked, because "which image is she
   actually being generated from" is the question this panel exists to answer. */
/* The drop target. Dashed, because a dashed edge reads as "put something here" in a
   way a solid panel does not, and it must not look like another button. */
.dropzone { display: flex; align-items: center; gap: .6rem; text-align: left;
  border: 2px dashed var(--line); border-radius: 12px; padding: .8rem .9rem;
  color: var(--muted); font-size: .85rem; cursor: pointer; background: var(--panel-2);
  transition: border-color .12s, background .12s, color .12s; }
.dropzone:hover, .dropzone:focus-visible { border-color: var(--accent); color: var(--ink-2); }
.dropzone.over { border-color: var(--accent); background: var(--grad-soft);
  color: var(--ink); }
.dropzone.busy { opacity: .6; pointer-events: none; }
.dropmark { font-size: 1.4rem; line-height: 1; flex: none; }
#refnote:empty { display: none; }
#refnote.bad { color: var(--danger); }
.refform input[type=file] { font: inherit; }
/* The portrait chooser (2026-09-07). ONE prominent grid over EVERY image — Craig: "more
   prominent, load all images to choose the portrait." Replaces the old 54px reference
   strip and the capped six-slot avatars panel. The current portrait is shown large beside
   the add-image form; below, a responsive grid where the thumb IS the make-portrait button
   and each cell also carries `show` (display face) and, for uploads, `remove`. */
.portrait-lead { display: flex; gap: 1rem; align-items: flex-start; flex-wrap: wrap;
  margin: .3rem 0 1rem; }
.portrait-current { margin: 0; flex: none; }
.portrait-current img { width: 150px; height: 200px; object-fit: cover; border-radius: 12px;
  border: 3px solid var(--accent); display: block; }
.portrait-current figcaption { text-align: center; font-size: .68rem; color: var(--muted);
  margin-top: .35rem; text-transform: uppercase; letter-spacing: .05em; }
.portrait-lead .refform { flex: 1 1 260px; min-width: 220px; margin: 0; }
.pickhead { margin-bottom: .4rem; }

.refpick.grid { display: grid; gap: .6rem;
  grid-template-columns: repeat(auto-fill, minmax(94px, 1fr)); }
.refpick.grid.scroll { max-height: 62vh; overflow-y: auto; overscroll-behavior: contain;
  padding: .15rem .3rem .15rem 0; }
.refpick form { margin: 0; }
.refcell { position: relative; display: flex; flex-direction: column; gap: .2rem;
  line-height: 1.2; }
.refthumb { padding: 0; border: 2px solid transparent; border-radius: 10px; background: none;
  cursor: pointer; line-height: 0; width: 100%; }
.refthumb img { width: 100%; aspect-ratio: 3 / 4; object-fit: cover; border-radius: 8px;
  display: block; }
.refthumb:hover { border-color: var(--accent); }
.refthumb.is-current, .refcell.is-current .refthumb { border-color: var(--accent);
  box-shadow: 0 0 0 3px color-mix(in srgb, var(--accent) 28%, transparent); cursor: default; }
/* Role chips over the top corners: portrait (the reference) and shown (the display face). */
.refcell .tag { position: absolute; top: 6px; z-index: 1; font-size: .58rem; font-weight: 700;
  padding: .1rem .32rem; border-radius: 6px; text-transform: uppercase; letter-spacing: .04em;
  line-height: 1.25; }
.refcell .tag.ref { left: 6px; background: var(--accent); color: #fff; }
.refcell .tag.disp { right: 6px; background: #f5c542; color: #222; }

/* Video reference picker (in #vidform): smaller radio thumbnails than the portrait grid.
   The radio is visually hidden; the checked thumbnail carries the accent ring. */
.vidrefpick.grid { grid-template-columns: repeat(auto-fill, minmax(62px, 1fr)); gap: .4rem;
  margin-top: .2rem; }
.vidrefpick.grid.scroll { max-height: 30vh; }
.vidrefcell { position: relative; margin: 0; cursor: pointer; line-height: 0; display: block; }
.vidrefcell input { position: absolute; opacity: 0; width: 0; height: 0; }
.vidrefcell img { width: 100%; aspect-ratio: 3 / 4; object-fit: cover; border-radius: 8px;
  display: block; border: 2px solid transparent; }
.vidrefcell:hover img { border-color: var(--accent); }
.vidrefcell:has(input:checked) img { border-color: var(--accent);
  box-shadow: 0 0 0 3px color-mix(in srgb, var(--accent) 28%, transparent); }
.vidrefcell .tag.ref { position: absolute; top: 5px; left: 5px; z-index: 1; font-size: .52rem;
  font-weight: 700; padding: .08rem .28rem; border-radius: 5px; text-transform: uppercase;
  letter-spacing: .04em; background: var(--accent); color: #fff; line-height: 1.2; }
.refacts { display: flex; justify-content: space-between; align-items: center; gap: .3rem; }
.refacts form { margin: 0; }
.refacts .link { font-size: .7rem; padding: 0; white-space: nowrap; }
.refacts .link.is-on { color: var(--accent); cursor: default; }
.refacts .refremove .link { color: var(--muted); }
.refacts .refremove .link:not([disabled]):hover { color: var(--danger); }

/* The in-thread progress bubble for media SHE is sending. Shaped like one of her
   message bubbles because that is where it sits and what it will be replaced by. */
.msg.mediabar { display: flex; align-items: center; gap: .55rem; max-width: 70%;
  font-size: .82rem; color: var(--muted); }
.mediabar .mlabel { white-space: nowrap; }
.mediabar .mtrack { flex: 1; min-width: 70px; height: 4px; border-radius: 999px;
  background: var(--line); overflow: hidden; }
.mediabar .mtrack i { display: block; height: 100%; width: 0;
  background: var(--grad); transition: width .4s ease; }
.mediabar .mtime { font-variant-numeric: tabular-nums; opacity: .75; }
/* Indeterminate: motion instead of a number, for work that reports no stages. */
.mediabar.indeterminate .mtrack i { width: 40%; animation: mslide 1.1s ease-in-out infinite; }
@keyframes mslide { 0% { margin-left: -40%; } 100% { margin-left: 100%; } }
@media (prefers-reduced-motion: reduce) {
  .mediabar.indeterminate .mtrack i { animation: none; width: 100%; opacity: .5; }
}

/* There is no Stop button. Craig, 2026-09-04: "no stop button", "No cancelling of
   media" — Send only ever sends, and nothing in the page ends work that has started. */

/* Emoji reactions. Asked for twice, and not found twice — first as a hover-only
   control, then as a faint ☺ tucked in the corner beside the delete cross: Craig,
   2026-09-04, "Where are the emojis on the messages?" and then "WHERE ARE THE FUCKING
   EMOJIS?!" Subtlety was the bug, so this is now a real button that sits OUTSIDE the
   bubble on the thread's own margin, at full strength, with a colour emoji and a plus.
   Delete stays hidden-until-hover because it is destructive and should not invite a
   click; reacting is the opposite. */
/* Still outside the bubble, still at full strength — now the FIRST item of the .acts rail
   beside the bubble (see the iMessage block at the end of the file) rather than the one
   positioned control. No scale on hover: a control must not move under the pointer. */
.react { background: var(--panel-2); border: 1px solid var(--line); border-radius: 999px;
  cursor: pointer; font-size: .95rem; line-height: 1; padding: .2rem .4rem;
  display: flex; align-items: center; gap: .1rem; flex: none; opacity: 1;
  transition: background .12s, border-color .12s; }
.react > span { font-size: .8rem; color: var(--muted); font-weight: 700; }
.react:hover, .react:focus { border-color: var(--accent); background: var(--panel); }
/* A bubble is 78% of the log at most, so there is always room beside it — but never let
   the button be the thing that creates a horizontal scrollbar. */
.log { overflow-x: hidden; }

/* The "load earlier messages" control, at the top of a paginated thread. Centred and
   quiet: it is a way back into history, not an action the conversation is about. */
.earlier{display:flex;justify-content:center;padding:.4rem 0 .8rem}
.earlier button{font-size:.82rem}

/* Reactions sit like Tapbacks: a small strip overlapping the bubble's TOP corner on the
   side opposite the tail, out of flow — a bubble is as tall as its words again. Apple:
   Tapbacks are 'attached to a specific message' and stack; which corner is not verified,
   'opposite the tail' is the brief's call. Template whitespace makes .rxs non-empty, so
   the room above is reserved by `:has(> .rx)` (end of file), not `:not(:empty)`. */
.rxs { display: flex; flex-wrap: nowrap; gap: 2px; position: absolute; top: -12px; z-index: 1; }
.msg.assistant .rxs { right: -6px; }
.msg.user .rxs { left: -6px; }
.rx { background: var(--panel); border: 1px solid var(--line); border-radius: 999px;
  padding: 1px 6px; font-size: 13px; line-height: 18px; cursor: pointer;
  box-shadow: 0 1px 3px rgba(0,0,0,.35); }
.rx:hover { border-color: var(--accent); }
.rx[disabled] { opacity: .5; cursor: default; }

.rxpicker { position: absolute; z-index: 20; display: flex; gap: .1rem;
  background: var(--panel); border: 1px solid var(--line); border-radius: 999px;
  padding: .2rem .3rem; box-shadow: 0 6px 20px rgba(0,0,0,.35); }
.rxpicker button { background: none; border: 0; cursor: pointer; font-size: 1.05rem;
  line-height: 1; padding: .2rem; border-radius: 50%; }
.rxpicker button:hover { background: var(--panel-2); transform: scale(1.15); }
/* The picker is positioned against the log, so the log must be its containing block. */
.log { position: relative; }

/* A reply she SPOKE. The player right below is the message; this pill only carries the
   delete and react controls, so it stays quiet. */
.msg.spoken { background: var(--panel-2); border: 1px dashed var(--line);
  color: var(--muted); font-size: .8rem; padding: .35rem .8rem; }
.spokenmark { opacity: .85; }

/* Bubbles held back while she records a voice note. display:none rather than opacity —
   a half-visible message is still a message the user reads before the audio lands. */
.msg.hushed { display: none !important; }

/* ---- iMessage grammar (2026-09-05), appended last so it wins ties by order ---------- */
/* Tail on the LAST bubble of a run only. `:has(+ …)` looks at the next sibling in the log:
   a same-sender bubble that is not hidden (.hushed) and not the progress bar continues the
   run; anything else (the other sender, .daymark, .timemark, .sysnote) ends it. The typing
   bubble is excluded and gets the thought-bubble trail below instead. Browsers without
   :has() match nothing here and simply show every bubble tail-less and fully rounded.
   The shape is a masked SVG so the fill is a token: --accent-deep is the 100% stop of
   --grad, i.e. the colour already at the sent bubble's bottom-right corner; --panel-2 is
   the received bubble itself. 16px box, 8px overlapping the corner (squares the small
   radius under it, as Kraft's recipe does), 8px beyond the edge. Apple publishes none. */
.log > .msg.user:not(.media):not(.spoken):not(:has(+ .msg.user:not(.hushed)))::before,
.log > .msg.assistant:not(.media):not(.spoken):not(.mediabar):not(.thinking):not(:has(+ .msg.assistant:not(.hushed):not(.mediabar)))::before{
  content:"";position:absolute;bottom:0;width:16px;height:16px;pointer-events:none}
.log > .msg.user:not(.media):not(.spoken):not(:has(+ .msg.user:not(.hushed)))::before{
  /* --grad is 135deg coral→PEACH, so the sent bubble's bottom-right corner is peach now
     (was --accent-deep pre-Swoonly). The tail fill must match that corner or it detaches. */
  right:-8px;background:var(--peach);
  -webkit-mask:url("data:image/svg+xml;utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Cpath d='M0 0H8C8 9 11 14 16 16H0Z'/%3E%3C/svg%3E") center/16px 16px no-repeat;
  mask:url("data:image/svg+xml;utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Cpath d='M0 0H8C8 9 11 14 16 16H0Z'/%3E%3C/svg%3E") center/16px 16px no-repeat}
.log > .msg.assistant:not(.media):not(.spoken):not(.mediabar):not(.thinking):not(:has(+ .msg.assistant:not(.hushed):not(.mediabar)))::before{
  left:-8px;background:var(--panel-2);
  -webkit-mask:url("data:image/svg+xml;utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Cpath d='M16 0H8C8 9 5 14 0 16H16Z'/%3E%3C/svg%3E") center/16px 16px no-repeat;
  mask:url("data:image/svg+xml;utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Cpath d='M16 0H8C8 9 5 14 0 16H16Z'/%3E%3C/svg%3E") center/16px 16px no-repeat}

/* Typing indicator: three dots blinking in sequence in her bubble while nothing has
   arrived. `:empty` matches only until the first token is painted — chat.html adds
   .thinking when the bubble is created (:1380) and removes it on the first token (:1413),
   so that is exactly the window. One element draws all three dots via box-shadow; the 1s
   cycle in thirds is the Fusco replica's timing. The two-circle trail stands in for the
   tail (the tail rule above excludes .thinking). The generic .thinking rule is untouched
   for group.html's text-streaming bubble; reduced-motion at the top of the file stills
   the animation. */
.msg.assistant.thinking:has(> .msgtext:empty){opacity:1;font-style:normal}
.msg.assistant.thinking > .msgtext:empty{display:inline-block;width:8px;height:8px;margin:7px 12px;
  border-radius:50%;background:var(--muted);
  box-shadow:-12px 0 0 var(--off),12px 0 0 var(--off);animation:tdots 1s infinite ease-in-out}
@keyframes tdots{
  0%,100%{background:var(--off);box-shadow:-12px 0 0 var(--muted),12px 0 0 var(--off)}
  33%{background:var(--muted);box-shadow:-12px 0 0 var(--off),12px 0 0 var(--off)}
  66%{background:var(--off);box-shadow:-12px 0 0 var(--off),12px 0 0 var(--muted)}}
.log > .msg.assistant.thinking::before{content:"";position:absolute;left:-2px;bottom:-2px;
  width:12px;height:12px;border-radius:50%;background:var(--panel-2);
  box-shadow:-7px 7px 0 -3px var(--panel-2);pointer-events:none}

/* The controls iMessage does not have live in a rail OUTSIDE the bubble on the side away
   from the tail, out of flow, anchored on its first row so a second row (phones) can appear
   below without moving the first. React stays at full strength (hover-only was the bug,
   twice); star and speak rest faint and go full on hover/focus/tap; edit and delete are
   hidden at rest on a pointer screen and reveal on hover, focus-within, or a tap on the
   bubble (.acts-open — the web stand-in for iMessage's touch-and-hold). On a screen with
   no hover they rest faint instead: never opacity 0 where nothing can hover. */
.acts{position:absolute;top:50%;transform:translateY(-12px);display:flex;align-items:center;
  gap:2px;white-space:nowrap;z-index:1}
.msg.assistant .acts{left:100%;margin-left:.4rem}
.msg.user .acts{right:100%;margin-right:.4rem;flex-direction:row-reverse}
.acts .msgstar,.acts .speak,.acts .msgedit,.acts .msgdel{position:static;top:auto;right:auto;
  left:auto;bottom:auto;margin:0;transform:none}
.acts .msgstar,.acts .speak{font-size:.9rem;opacity:.45;color:var(--muted)}
.acts .msgstar:hover,.acts .speak:hover{color:var(--accent)}
.acts .msgstar.is-on{color:#f5c542}
.acts .msgedit,.acts .msgdel button{font-size:.85rem}
.msg:hover .acts .msgstar,.msg:hover .acts .speak,.msg:focus-within .acts .msgstar,
.msg:focus-within .acts .speak,.msg.acts-open .acts .msgstar,.msg.acts-open .acts .speak,
.acts .msgstar.is-on{opacity:1}
/* Edit and delete are VISIBLE at rest, on every screen. The restyle shipped them
   visibility:hidden until hover on pointer devices; Craig, 2026-09-05 20:10: "Emojis are
   missing again" — the same complaint as the react button on 2026-09-04 (memory:
   hover-reveal hides the feature). Faint at rest, full on hover/focus/tap. */
.acts .msgedit,.acts .msgdel{opacity:.45;visibility:visible}
.acts .msgedit:hover,.acts .msgdel:hover,.msg:hover .acts .msgedit,.msg:hover .acts .msgdel,
.msg.acts-open .acts .msgedit,.msg.acts-open .acts .msgdel{opacity:1}
@media(max-width:900px){
  /* ~85px beside a 70% bubble at 375px: react+star+speak on the first row, edit+delete on
     a 16px second row, the rail hung from the bubble's TOP so the second row ends inside a
     one-line bubble's 38px + 2px gap (24 + 1 + 16 = 41: one px over on the rare pair of
     consecutive one-line messages that BOTH carry controls — not measured). */
  .acts{top:0;transform:none;flex-wrap:wrap;width:84px;justify-content:flex-start;row-gap:1px}
  .acts .msgedit,.acts .msgdel button{font-size:.75rem;line-height:16px;padding:0 .15rem}
}

/* Room above a bubble that carries Tapbacks, so the strip does not sit on the bubble
   above. Beats the 2px/8px sibling rules by specificity and order; spares the first
   child's margin-top:auto. */
.log > .msg:not(:first-child):has(> .rxs > .rx){margin-top:16px}
/* 'Delivered' under the LAST sent item only (Apple: the sender sees 'Delivered' below
   messages they've sent; last-only per third-party). Pure CSS: the last .msg.user in the
   log is the one with no later .msg.user sibling — a sent photo counts, as in iMessage.
   Caption 2 (11/13); which style Messages uses is not verified. The bubble reserves 15px
   below so the caption never overlaps what follows. Not in the call transcript, not in
   the history view. */
.log:not(.calllog):not(.histlog) > .msg.user:not(:has(~ .msg.user)){margin-bottom:15px}
.log:not(.calllog):not(.histlog) > .msg.user:not(:has(~ .msg.user))::after{content:'Delivered';position:absolute;
  right:0;top:100%;margin:2px 0 0;display:block;font-size:11px;line-height:13px;
  color:var(--muted);letter-spacing:0;white-space:nowrap}
.log:not(.calllog):not(.histlog) > .msg.user.edited:not(:has(~ .msg.user))::after{content:'edited · Delivered'}

/* ---- character profile: /character/<cid> ----------------------------------------
   Craig, 2026-09-05: "EVerything that isn't necessary to the chat from the furthest right
   column needs to be moved to a model profile page." Six tabs; every panel is the chat
   rail's own markup, so the rail rules (.picks .slots .freq .choice .refpick .facts .ladder
   .status …) style them unchanged. NO display rule on .tabpanel, on purpose: the switcher
   uses el.hidden, and [hidden]{display:none!important} (top of this file) must be the only
   thing deciding whether a tab shows — an author `display` here would be the exact bug
   tests/test_template_ids.py pins. The scroll container is `.viewport` (topbar + main scroll
   together), so this sticks at top:var(--topbar-h) to clear the topbar rather than slide
   behind it — before the 2026-09-08 scroll fix `main` scrolled and top:0 sat below it. */
.profile{display:flex;flex-direction:column;gap:1rem}
.prohead .btns{margin-left:auto;display:flex;gap:.5rem;flex-wrap:wrap}
.tabs{position:sticky;top:var(--topbar-h);z-index:5;display:flex;gap:.25rem;padding:.35rem 0;
  background:var(--bg);border-bottom:1px solid var(--line)}
.tab{flex:none;padding:.5rem .9rem;border-radius:999px;color:var(--muted);font-size:.88rem;
  white-space:nowrap}
.tab:hover{color:var(--ink);background:var(--panel);text-decoration:none}
.tab[aria-selected=true]{background:var(--grad-soft);color:var(--ink)}
.tabpanel > .panel{margin:0 0 1rem}
/* .side .gallery is scoped to the rail and would not reach this page. */
.profile .gallery{display:grid;grid-template-columns:repeat(auto-fill,minmax(120px,1fr));gap:.5rem}
.profile .gallery img{width:100%;aspect-ratio:1;object-fit:cover;border-radius:9px;display:block}
/* Six pills do not fit 343px; two visible rows beat a strip that hides Behaviour and
   Budget off-screen. Widths not measured — the 375px browser check asserts it. */
@media(max-width:600px){.tabs{flex-wrap:wrap;gap:.2rem}.tab{padding:.4rem .65rem;font-size:.82rem}}

/* Ask-her pair in the chat rail (2026-09-05 23:04) — two buttons, one row, wrap when narrow. */
.askrow{display:flex;gap:.5rem;flex-wrap:wrap}
.askrow .btn{flex:1 1 auto}

/* profile: language-model + reply-shape rows (2026-09-06) */
.modelrow{display:flex;flex-wrap:wrap;gap:.6rem 1rem;align-items:end;margin:.5rem 0}
.modelrow select{max-width:min(100%,26rem)}

/* The compose-box emoji palette (Craig, 2026-09-06: "HOW CAN I SEND AN EMOJI IN MY
   MESSAGES?!? INSIDE MY SENTENCES?!?"). Toggled by #emojibtn; anchored to the
   position:relative .composer so it floats above the pill. A pick inserts at the caret
   (see chat.html) and the panel stays open for adding several. */
.emojibar { position: absolute; bottom: 100%; left: 0; margin-bottom: .45rem;
  display: flex; flex-wrap: wrap; gap: .05rem; width: 20rem; max-width: 88vw;
  max-height: 15rem; overflow-y: auto; overscroll-behavior: contain; z-index: 30;
  background: var(--panel); border: 1px solid var(--line); border-radius: .7rem;
  padding: .4rem; box-shadow: 0 8px 28px rgba(0,0,0,.28); }
.emojibar button { background: none; border: 0; cursor: pointer; font-size: 1.05rem;
  line-height: 1; padding: .15rem .18rem; border-radius: .3rem; }
.emojibar button:hover { background: var(--panel-2); transform: scale(1.15); }
.emojibtn { font-size: 1.15rem; }

/* P8 — what she remembered this turn, under her last bubble; and the memory lanes */
.recalled{margin:2px 0 6px 8px;font-size:.78rem;color:var(--muted,#8a8f98);max-width:70%}
.recalled summary{cursor:pointer;list-style:none;opacity:.8}
.recalled summary::before{content:"▸ ";}
.recalled[open] summary::before{content:"▾ ";}
.recalled ul{margin:4px 0 0 0;padding-left:1rem}
.recalled li{margin:2px 0}
.lane{display:inline-block;font-size:.68rem;padding:0 .35em;border-radius:6px;background:rgba(127,127,127,.18);vertical-align:middle;text-transform:uppercase;letter-spacing:.03em}
.facts li.lane-her .lane{background:rgba(220,120,160,.22)}
.facts li.lane-event .lane{background:rgba(120,170,220,.22)}
.facts li.muted > span:first-child{opacity:.45}
.weightform select{font-size:.75rem;padding:0 .2em}

.prewrap{white-space:pre-wrap}

.lore li.muted{opacity:.55}
.loreform .row label.small{align-self:end}

/* Generate Media studio — one panel, three studios switched by a pill tab bar (Image /
   Video / Voice note). Craig 2026-09-08: consolidate the create-image/video/voice sections
   into one organised "Generate Media" area. Matches the profile `.tab` pill language. */
.mediatabs{display:flex;gap:.35rem;flex-wrap:wrap;margin:.4rem 0 1rem}
.mediatab{flex:none;padding:.45rem .95rem;border:1px solid var(--line);border-radius:999px;
  background:transparent;color:var(--muted);font-size:.9rem;cursor:pointer;line-height:1.3}
.mediatab:hover{color:var(--ink);background:var(--panel)}
.mediatab.on{background:var(--grad-soft);color:var(--ink);border-color:transparent}
.mediapane[hidden]{display:none}
.mediapane > h3:first-child{margin-top:0}
