/* EarthOnline Bubbles: a copy of EarthOnline Chat's eol_chat/web/public/v2/ui.css as it stood on 2026-10-02 at 23:20,
   unchanged below this comment. Every EarthOnline site uses Chat's controls and type sizes (design/01-bubble-standard.md
   §8): to change one, change Chat's file and copy it again. The notes below are Chat's own.

   EarthOnline Chat: the one set of tokens, type sizes and controls for the whole site (design/05-ui-system.md).
   Loaded first on every page, before style.css and the page groups' sheets; those only lay things out.

   Where the numbers come from (2026-10-02):
   - Controls (Button, Input, Textarea, Label, ToggleGroup, Badge, AlertDialog): shadcn/ui new-york-v4, the real source
     (https://ui.shadcn.com/r/styles/new-york-v4/<name>.json), dark theme (.dark in docs/(root)/theming.mdx).
   - Where Cube's own copy of those controls differs, Cube wins (Bonan 10-02: "我看 cube 里的 button 就一样"):
     cube/apps/web/src/components/ui/alert-dialog.tsx (BTN, CANCEL, ACTION, the dialog box),
     cube/apps/web/src/components/cube/Doc/CredentialsBody.tsx (FIELD_INPUT).
   - Where Bonan's taste (~/.claude/skills/bonan-ui-taste) differs from both, the taste wins: hover changes at once (no
     transition), no shadows, a click puts no ring on a field.
   - Above all of them, this project's own rule (design/00-spec.md §9): black, white and greys; colour only from robots.
     So there is no red or blue control: no destructive variant, and the focus ring is shadcn's grey one.
   - Conversation text (a share's turns, Markdown): ChatGPT, measured in Chrome; the numbers are in 05-ui-system.md §4.

   Class names are fixed; other pages build on them. Change a value here, never a name. */

:root {
  color-scheme: dark;

  /* ---- colour: the page's greys (design/00-spec.md §9, unchanged). Cube's dark zinc greys land on these within a few
     steps of 255, too close to tell apart: zinc-900 #18181b ~ --bg2, zinc-800 #27272a ~ --bg3, zinc-700 #3f3f46 ~ --line,
     zinc-100 #f4f4f5 ~ --fg. ---- */
  --bg: #000000;                    /* the page */
  --bg2: #1d1d1f;                   /* one step up: cards, panels, fields, the outline button, the dialog */
  --bg3: #2a2a2d;                   /* two steps up: hover on a step-up surface, the picked toggle, the badge */
  --fg: #f5f5f7;                    /* text; also the default (primary) button's ground */
  --fg-hover: #d4d4d8;              /* the default button under the pointer: Cube's dark:hover:bg-zinc-300 */
  --on-fg: #18181b;                 /* text on --fg: Cube's dark:text-zinc-900 */
  --muted: #86868b;                 /* secondary text, placeholders */
  --line: #424245;                  /* hairlines; the border of fields, outline buttons and toggle groups */
  --ring: #737373;                  /* focus: shadcn .dark --ring, oklch(0.556 0 0) */
  --ring-soft: rgb(115 115 115 / .5); /* focus halo: shadcn ring-ring/50 */
  --scrim: rgb(0 0 0 / .6);         /* behind a dialog: Cube's dark:bg-black/60 */
  --unread: #FF3B30;                /* unread counts and what needs you: the one red, iOS's and QQ's (10-02 night, Bonan: "未读用红色") */
  --chip: rgb(255 255 255 / .1);    /* small marks drawn on any surface (inline code) */
  --chip2: rgb(255 255 255 / .16);
  --nav: rgb(245 245 247 / .8);

  /* ---- radius ---- */
  --radius: 8px;                    /* buttons, fields, toggles: shadcn rounded-md (0.625rem * .8), Cube rounded-lg */
  --radius-box: 12px;               /* dialog and panels: the taste skill's confirm box, Cube rounded-xl */
  --radius-card: 18px;              /* cards on the page (the lobby's) */
  --radius-full: 9999px;            /* badges, faces */

  /* ---- type: one font, these sizes and no others. Tailwind's steps, the ones shadcn's source names
     (text-xs / sm / base / lg / 2xl / 4xl), each with its own line height. ---- */
  --font: -apple-system, BlinkMacSystemFont, "SF Pro Text", "Segoe UI", Roboto, system-ui, sans-serif;
  --mono: ui-monospace, SFMono-Regular, Menlo, Consolas, "Liberation Mono", monospace;
  --text-xs: 12px;   --lh-xs: 16px;   /* count badges and Badge, nothing else */
  --text-sm: 14px;   --lh-sm: 20px;   /* buttons, header nav, labels, meta, form labels */
  --text-base: 16px; --lh-base: 24px; /* body text, notes, fields on a phone */
  --text-lg: 18px;   --lh-lg: 28px;   /* the title of an item in a list or a card: shadcn typography "Large" */
  --text-2xl: 24px;  --lh-2xl: 32px;  /* a section's title: shadcn typography h3 */
  --text-4xl: 36px;  --lh-4xl: 40px;  /* a page's title: shadcn typography h1 */
  --tracking-tight: -0.025em;         /* titles: shadcn tracking-tight */
  /* every title is 600 (Cube's titles are font-semibold; shadcn's h1 alone is 800) */

  /* ---- conversation text, after ChatGPT (measured 10-02 in Chrome 154 on chatgpt.com/share/…, dark theme, in rem at
     a 16px root; 05-ui-system.md §3) ---- */
  --chat-text: 16px;                /* .markdown: 1rem */
  --chat-lh: 26px;                  /* .markdown: line-height 1.625 */
  --bubble-bg: rgb(50 50 50 / .85); /* the user bubble: --message-surface #323232d9 */
  --bubble-radius: 22px;            /* rounded-[22px] */
  --bubble-pad: 10px 16px;          /* py-2.5 px-4, on 24px lines (leading-6) */

  /* ---- the names the page sheets used before 10-02. Only for sheets not yet moved to the names above
     (public/v2/inbox.css); new rules use the names above. ---- */
  --t-xs: var(--text-xs); --t-m: var(--text-sm); --t-body: var(--text-base); --t-l: var(--text-2xl); --t-title: var(--text-4xl);
  --r-s: 6px; --r-m: var(--radius-box); --r-l: var(--radius-card); --r-pill: var(--radius-full);
}

/* ---- Button (shadcn ui/button.tsx; Cube alert-dialog.tsx BTN / CANCEL / ACTION) -------------------------------------
   <button class="btn">                default: the one main action of an area
   <button class="btn btn-outline">    everything beside it (Cancel, Copy, Delete, Save in a row)
   <button class="btn btn-ghost">      a quiet action that needs no box (Sign out)
   sizes: (none) 36px, .btn-sm 32px (rows, dialogs, the header), .btn-lg 40px. .btn-block takes the full width.
   An <a> can wear it too. One default button per area at most. */
.btn {
  display: inline-flex; flex-shrink: 0; align-items: center; justify-content: center; gap: 8px;
  height: 36px; padding: 0 16px; border: 0; border-radius: var(--radius);
  background: var(--fg); color: var(--on-fg);
  font: 500 var(--text-sm)/var(--lh-sm) var(--font);
  white-space: nowrap; text-decoration: none; cursor: pointer; user-select: none; outline: none;
  -webkit-tap-highlight-color: transparent;
}
.btn:hover { background: var(--fg-hover); }
.btn > svg { flex: none; width: 16px; height: 16px; pointer-events: none; }
.btn:has(> svg) { padding: 0 12px; }

.btn-outline { border: 1px solid var(--line); background: var(--bg2); color: var(--fg); }
.btn-outline:hover { background: var(--bg3); }
.btn-ghost { background: transparent; color: var(--muted); }
.btn-ghost:hover { background: var(--bg2); color: var(--fg); }

.btn-sm { height: 32px; gap: 6px; padding: 0 12px; }
.btn-sm:has(> svg) { padding: 0 10px; }
.btn-lg { height: 40px; padding: 0 24px; }
.btn-lg:has(> svg) { padding: 0 16px; }
.btn-block { display: flex; width: 100%; }

.btn:disabled, .btn[aria-disabled="true"] { opacity: .5; pointer-events: none; }
/* the keyboard's ring (shadcn: focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50).
   A pointer leaves no ring. */
.btn:focus-visible { box-shadow: 0 0 0 3px var(--ring-soft); }
.btn-outline:focus-visible { border-color: var(--ring); }

/* ---- Input and Textarea (shadcn ui/input.tsx, ui/textarea.tsx; Cube FIELD_INPUT) -----------------------------------
   16px text on a phone so it does not zoom, 14px from 768px up (shadcn: text-base md:text-sm). A solid ground and a
   hairline; clicking in changes nothing, the caret is the mark (taste: "我讨厌点击有特效的输入框"). */
.input, .textarea {
  display: block; width: 100%; min-width: 0; margin: 0;
  border: 1px solid var(--line); border-radius: var(--radius); background: var(--bg2); color: var(--fg);
  font: 400 var(--text-base)/var(--lh-base) var(--font); outline: none;
}
.input { height: 36px; padding: 4px 12px; }
.textarea { min-height: 64px; padding: 8px 12px; resize: vertical; field-sizing: content; }
.input::placeholder, .textarea::placeholder { color: var(--muted); opacity: 1; }
.input:disabled, .textarea:disabled { opacity: .5; cursor: not-allowed; }
@media (min-width: 768px) { .input, .textarea { font-size: var(--text-sm); line-height: var(--lh-sm); } }

/* Label and a field's parts (shadcn ui/label.tsx and its form pattern: label, 8px, field, 8px, description) */
.field { display: grid; gap: 8px; min-width: 0; }
.label { display: flex; align-items: center; gap: 8px; font: 500 var(--text-sm)/1 var(--font); color: var(--fg); user-select: none; }
.field-note { margin: 0; color: var(--muted); font-size: var(--text-sm); line-height: var(--lh-sm); }

/* ---- ToggleGroup: pick one of a few values (shadcn ui/toggle-group.tsx, variant outline, size sm, no spacing) --------
   <div class="toggle-group" role="group" aria-label="…"><button class="toggle" aria-pressed="true">…</button>…</div>
   The options share one outline; the picked one is one step darker than hover (taste: hover one step, picked one more). */
.toggle-group { display: inline-flex; width: fit-content; align-items: center; border-radius: var(--radius); }
.toggle-group form { display: contents; }
.toggle {
  display: inline-flex; flex-shrink: 0; align-items: center; justify-content: center; gap: 8px;
  height: 32px; min-width: 0; padding: 0 12px; margin: 0;
  border: 1px solid var(--line); border-left-width: 0; border-radius: 0; background: transparent; color: var(--fg);
  font: 500 var(--text-sm)/var(--lh-sm) var(--font); white-space: nowrap; cursor: pointer; outline: none;
}
.toggle-group > .toggle:first-child, .toggle-group > :first-child .toggle { border-left-width: 1px; border-radius: var(--radius) 0 0 var(--radius); }
.toggle-group > .toggle:last-child, .toggle-group > :last-child .toggle { border-radius: 0 var(--radius) var(--radius) 0; }
.toggle:hover { background: var(--bg2); }
.toggle[aria-pressed="true"] { background: var(--bg3); cursor: default; }
.toggle:focus-visible { position: relative; z-index: 1; border-color: var(--ring); box-shadow: 0 0 0 3px var(--ring-soft); }
.toggle:disabled { opacity: .5; pointer-events: none; }

/* ---- Segmented: which of two or three places you are in (the header's Lobby | Inbox) ---------------------------------
   <nav class="segmented"><a class="segment" href="…" aria-current="page">…</a><a class="segment" href="…">…</a></nav>
   After the picture Bonan gave (10-02): a capsule; the place you are in is a solid capsule inside it, the others are
   words only. Measures are shadcn's Tabs (ui/tabs.tsx: the list 36px with 3px inside, the triggers 14px medium, the
   others muted until pointed at), in capsules, the parts equally wide as in a segmented control. Black, white and
   grey: the capsule and the picked part are see-through white, so they look the same on the page and on the header's
   glass (taste: a small thing on a surface takes a see-through colour). No shadow, nothing moves. Two classes, so no
   rule for links in a nav (style.css) reaches it. */
.segmented { display: inline-grid; grid-auto-flow: column; grid-auto-columns: 1fr; align-items: center; height: 36px; padding: 3px; border-radius: var(--radius-full); background: var(--chip); }
.segmented > .segment {
  display: inline-flex; align-items: center; justify-content: center; gap: 6px;
  height: 30px; min-width: 0; padding: 0 16px; border-radius: var(--radius-full); color: var(--muted);
  font: 500 var(--text-sm)/var(--lh-sm) var(--font); white-space: nowrap; text-decoration: none; cursor: pointer;
  -webkit-tap-highlight-color: transparent;
}
.segmented > .segment:hover { color: var(--fg); }
.segmented > .segment[aria-current] { background: var(--chip2); color: var(--fg); }

/* ---- Badge (shadcn ui/badge.tsx, variant secondary): a word about a thing (Lobby, Link only, Official, New) --------- */
.badge {
  display: inline-flex; width: fit-content; flex-shrink: 0; align-items: center; justify-content: center; gap: 4px;
  padding: 2px 8px; border: 1px solid transparent; border-radius: var(--radius-full);
  background: var(--bg3); color: var(--fg);
  font: 500 var(--text-xs)/var(--lh-xs) var(--font); white-space: nowrap; vertical-align: middle;
}

/* ---- Icon button: a round 32px button that is only an icon or a face (the header's inbox, and you) -----------------
   <a class="icon-btn" href="…"><svg class="icon">…</svg><span class="sr">Name</span></a>
   No ground at rest; pointed at, on the page it leads to, or while its menu is open: one step of ground. A count
   (.count, the unread mark) sits on its top right corner, cut out of the icon by a ring of the page's black (Reddit). */
.icon-btn {
  position: relative; display: inline-grid; place-items: center; flex: none; width: 32px; height: 32px; padding: 0;
  border: 0; border-radius: var(--radius-full); background: transparent; color: var(--fg);
  text-decoration: none; cursor: pointer; -webkit-tap-highlight-color: transparent;
}
.icon-btn:hover, .icon-btn[aria-current], .icon-btn:has(+ .menu:popover-open) { background: var(--bg2); }
.icon-btn > .icon { width: 20px; height: 20px; }
.icon-btn > .count { position: absolute; top: -3px; right: -5px; outline: 2px solid var(--bg); }

/* ---- Menu: a short list of actions that pops up under what opened it (the account menu under your face) -------------
   Cube's one menu grammar (cube lib/cube/menu.ts; taste skill "菜单长什么样"): a box with a 1px edge, 12px corners and no
   padding; its rows run edge to edge with a hairline between each, and pointing at a row fills the whole row at once.
   Rows are 6px above and below and 14px in; the first row may be a taller head (who you are). It appears at once, with
   no fade or zoom; Esc and a click anywhere else close it (the popover attribute does both, no script).
   <button popovertarget="m">…</button>
   <div class="menu" id="m" popover><a class="menu-head" href="…">…</a><form …><button class="menu-row">…</button></form></div>
   Placed under its button, right edges lined up, by anchor positioning where the browser has it, else at the header's
   right end. */
.menu {
  position: fixed; inset: 56px 20px auto auto; width: 240px; margin: 0; padding: 0; overflow: hidden;
  border: 1px solid var(--line); border-radius: var(--radius-box); background: var(--bg2); color: var(--fg);
}
.menu > * + * { border-top: 1px solid var(--bg3); }
.menu form { display: block; margin: 0; }
.menu-row, .menu-head {
  display: flex; align-items: center; gap: 10px; width: 100%; margin: 0; padding: 6px 14px; border: 0;
  background: none; color: var(--fg-hover); font: 500 var(--text-sm)/var(--lh-sm) var(--font);
  text-align: left; text-decoration: none; cursor: pointer;
}
.menu-head { padding: 10px 14px; color: var(--fg); }
.menu-row:hover, .menu-head:hover { background: var(--bg3); }
.menu-head > span { display: grid; min-width: 0; }
.menu-head b, .menu-head span span { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.menu-head b { font-weight: 600; }
.menu-head span span { color: var(--muted); font-weight: 400; }
@media (max-width: 640px) { .menu { inset: 56px 12px auto auto; } }
/* the page names the anchor (anchor-name on the button, position-anchor on the menu); without one, the fallbacks hold */
@supports (position-anchor: --a) {
  .menu { inset: calc(anchor(bottom, 48px) + 8px) anchor(right, 20px) auto auto; }
}

/* ---- To top: the round button that takes a long list back to its top -------------------------------------------------
   Cube's "to the latest" in its chat (CubeChatPopover), pointing up: 32px round, a 1px edge, the dialog's ground, a
   16px arrow; pointed at, one step of ground and white. Centred at the foot of the window, 24px up. It shows once the
   page has scrolled more than a window's height and goes when it is back near the top (app.js), at once, no fade.
   <button class="to-top" type="button" data-to-top aria-label="Back to top" hidden>…</button> */
.to-top {
  position: fixed; left: 50%; bottom: 24px; z-index: 5; display: grid; place-items: center; width: 32px; height: 32px;
  margin: 0 0 0 -16px; padding: 0; border: 1px solid var(--line); border-radius: var(--radius-full);
  background: var(--bg2); color: var(--fg-hover); cursor: pointer;
}
.to-top:hover { background: var(--bg3); color: var(--fg); }
.to-top[hidden] { display: none; }

/* ---- the one coloured button: the robots' blue (src/robot.js), white words. For the one action a page exists for,
   Talk to (10-02 night, Bonan: "talk to 应该是蓝色 button"). Pointed at, one step darker.
   <a class="btn btn-blue" href="…">Talk to Maya's agent</a> */
.btn-blue { background: #3D7BFF; color: #fff; }
.btn-blue:hover { background: color-mix(in srgb, #3D7BFF 86%, #000); }


/* ---- A name that may run long (10-02 night, Bonan: "agent的name有maxwidth，hover会滚动，当然width给大，只是应对极端情况";
   his way with extreme cases: a generous limit the usual never meets, and the rare one still readable). One line in a
   box whose max-width the page sets; when it does not fit, its end fades out, and pointed at, it slides along to show
   the rest and back, slowly (app.js, transform only), both ends fading while it moves. Nothing slides for someone who
   asked for less motion.
   <span class="overflow-scroll" data-overflow-scroll><span>…</span></span> */
.overflow-scroll { display: block; min-width: 0; overflow: hidden; white-space: nowrap; }
.overflow-scroll > span { display: inline-block; }
.overflow-scroll.is-clipped { -webkit-mask-image: linear-gradient(to left, transparent, #000 32px); mask-image: linear-gradient(to left, transparent, #000 32px); }
.overflow-scroll.is-sliding { -webkit-mask-image: linear-gradient(to right, transparent, #000 24px, #000 calc(100% - 24px), transparent); mask-image: linear-gradient(to right, transparent, #000 24px, #000 calc(100% - 24px), transparent); }
/* room above and below for accents and descenders (the g of "agent"), which the clip would otherwise cut; the line
   sits where it did */
.overflow-scroll { padding-block: .15em; margin-block: -.15em; }

/* ---- the language (views.js langMenu, langRow): the globe's menu when signed out, a row of your menu when signed in.
   A menu's small title (taste: every popup menu has one, small, medium weight, grey); the language in use ticked; the
   row is a label and a small segmented control (Cube's account menu, its Appearance and Language rows). */
.menu-label { margin: 0; padding: 8px 14px 4px; color: var(--muted); font: 500 var(--text-sm)/var(--lh-sm) var(--font); }
.menu-row[aria-current] { color: var(--fg); }
.menu-check { flex: none; margin-left: auto; }
.menu-pick { display: flex; align-items: center; justify-content: space-between; gap: 12px; padding: 8px 14px; color: var(--fg-hover); font: 500 var(--text-sm)/var(--lh-sm) var(--font); }
.segmented > button.segment { border: 0; background: transparent; }
.segmented-sm { height: 28px; padding: 2px; }
.segmented-sm > .segment { height: 24px; padding: 0 10px; }

/* the same for a title of several lines (data-overflow-scroll="y", the page sets its max-height in lines): past its last
   line it fades out at the foot, and pointed at, it slides up to show the rest and back, both ends fading meanwhile
   (10-02 night, Bonan, of a task's title: "如果超长，我们怎么办呢？他自己有不显示scrollbar的滚动策略？"). No scrollbar. */
.overflow-scroll-y { display: block; overflow: hidden; }
.overflow-scroll-y > span { display: block; }
.overflow-scroll-y.is-clipped { -webkit-mask-image: linear-gradient(to bottom, #000 calc(100% - 1lh), transparent); mask-image: linear-gradient(to bottom, #000 calc(100% - 1lh), transparent); }
.overflow-scroll-y.is-sliding { -webkit-mask-image: linear-gradient(to bottom, transparent, #000 .6lh, #000 calc(100% - .6lh), transparent); mask-image: linear-gradient(to bottom, transparent, #000 .6lh, #000 calc(100% - .6lh), transparent); }

/* ---- Scrollbars: Cube's, everywhere (10-02 night, Bonan: "这个项目里的scrollbar，都参考cube里的，不要出现那种一旦
   scrollbar出现导致页面滚动"; then "我滚动的时候出现，hover出现"; "scrollbar稍微瘦一点点"). Cube's (globals.css body +
   .eol-scroll-hover): the page always keeps the bar's room, so nothing moves sideways when a page grows long enough to
   scroll; the bar is 12px with a 2px clear edge round the thumb and no arrow buttons; the thumb is invisible at rest,
   shows while its box scrolls and under the pointer, in see-through white, and fades a moment after scrolling stops.
   A box that scrolls gets .is-scrolling (public/app.js); the page's own bar, which Chrome does not redraw for a class,
   gets the same thumb from a style app.js puts in while the page scrolls (tested 10-02: a class leaves it unpainted,
   a style swap paints it). scrollbar-width / scrollbar-color are not set: with them, Chrome draws the classic bar with
   arrows and ignores these rules. Firefox keeps its own thin bar. */
html { scrollbar-gutter: stable; }
::-webkit-scrollbar { width: 12px; height: 12px; }
::-webkit-scrollbar-button { display: none !important; width: 0 !important; height: 0 !important; }
::-webkit-scrollbar-track, ::-webkit-scrollbar-corner { background: transparent; }
::-webkit-scrollbar-thumb {
  border: 2px solid transparent; border-radius: 999px; background-color: transparent; background-clip: padding-box;
  transition: background-color .7s ease;
}
.is-scrolling::-webkit-scrollbar-thumb, ::-webkit-scrollbar-thumb:hover { background-color: rgb(255 255 255 / .3); transition: background-color .12s ease; }

/* ---- two more tokens, 10-02 night ----
   --text-3xl: the step between a section's 24px and a page's 36px (shadcn typography h2, text-3xl): a task's title
   (Bonan: "那个task的title，在task详情页可以大些", after 36px was too big and 24px too small).
   --link: what a name that leads somewhere turns to under the pointer (Bonan: "我hover到maya agent的时候，这种东西应
   变蓝的"): the robots' blue (src/robot.js) with a quarter white, so it reads on the dark grounds. */
:root { --text-3xl: 30px; --lh-3xl: 36px; --link: color-mix(in srgb, #3D7BFF 75%, #fff); }
/* a menu breathes 4px above its first row and below its last (10-02 night, Bonan, of the language menu: "下面没padding不好") */
.menu { padding-block: 4px; }

/* ---- Textarea, 10-02 night (Bonan, of About on /me: "怎么能自己拖拽大小呢？太蠢了，固定大小，然后多出来就自动扩张，有一个
   maxheight，scrollbar参考cube模式"): no grip to drag; three lines high to start, it grows with what is written
   (field-sizing: content) up to ten lines, and past that it scrolls inside, with the site's scrollbar (above). */
.textarea { resize: none; min-height: calc(3lh + 18px); max-height: calc(10lh + 18px); overflow-y: auto; }

/* ---- Chatbox: the box a message is written in (10-02 night, Bonan: "对话框抄袭cube的chatbox") -------------------------
   Cube's CubeChatBar (components/cube/Doc/CubeChatBar.tsx), without what is Cube's own (pickers, modes, queue). A card:
   a 1px edge (dark:border-zinc-700, which is --line here), 16px corners (rounded-2xl), the step-up ground (zinc-900,
   --bg2), and no change on focus (Cube: "one edge always"). The text on top: 16px on 24px lines, 16px in, 10px above and
   below (Cube's eol-scroll-inset), one line at first, growing with what is written to twelve, then scrolling inside.
   Under it a row with the send button at its right end: 32px round, zinc-700 ground, a white arrow up (lucide
   arrow-up, 16px, stroke 2.5); at .3 opacity while there is nothing to send. Enter sends, Shift+Enter is a new line, and
   a click anywhere on the card puts the cursor in the text (public/app.js).
   <form class="chatbox"><textarea class="chatbox-field" rows="1" placeholder="Message"></textarea>
     <div class="chatbox-row"><button class="chatbox-send" type="submit" aria-label="Send"><svg>…</svg></button></div></form> */
.chatbox { display: block; border: 1px solid var(--line); border-radius: 16px; background: var(--bg2); cursor: text; }
.chatbox-field {
  display: block; box-sizing: border-box; width: 100%; margin: 0; padding: 0 16px;
  border: 0; border-top: 10px solid transparent; border-bottom: 10px solid transparent; border-radius: 0;
  background: transparent; color: var(--fg); font: 400 var(--text-base)/var(--lh-base) var(--font);
  resize: none; outline: none; box-shadow: none; field-sizing: content;
  min-height: calc(1lh + 20px); max-height: calc(12lh + 20px); overflow-y: auto;
}
.chatbox-field::placeholder { color: var(--muted); opacity: 1; }
/* the card is the field: no ring of its own (the site's keyboard ring, style.css :focus-visible, would otherwise win) */
.chatbox .chatbox-field:focus, .chatbox .chatbox-field:focus-visible { outline: none; box-shadow: none; }
.chatbox-row { display: flex; align-items: center; justify-content: flex-end; gap: 8px; padding: 4px 8px 8px; cursor: text; }
.chatbox-send {
  display: grid; place-items: center; flex: none; width: 32px; height: 32px; padding: 0; border: 0;
  border-radius: var(--radius-full); background: var(--line); color: #fff; cursor: pointer; transition: opacity .15s;
}
.chatbox-send:disabled { opacity: .3; cursor: default; }

/* ---- Dialog: the site's one dialog (views.js dialog / confirmDialog / agentDialog) ------------------------------------
   10-02 night, Bonan: "这个项目弹窗应该统一 component，背景直接模糊掉". Every dialog is this one: a question before
   something that cannot be undone (delete a post, revoke a key), a text to give your agent, the welcome form.
   shadcn's Dialog and AlertDialog, which share one box (DialogContent / AlertDialogContent: p-6, rounded, border, a
   text-lg semibold title, a muted text-sm description, the footer's buttons at the bottom right, 8px apart), in the
   taste skill's shell: a 1px edge, the step-up ground, 12px corners, no shadow. 400px wide; .dialog-wide 560px, for a
   text to copy. At most 85% of the window tall, its inside scrolling. The close button at the top right on a dialog
   that holds content; a question has its own Cancel instead (shadcn: AlertDialog has no close button).
   Behind it the page is blurred under a dim: ::backdrop once the script has made it modal, .dialog-scrim when the
   server drew it open (a page of its own, ?confirm=…, which works without script). It appears at once: no fade, no zoom.
   Esc, Cancel or the close button, and a click outside close it (public/app.js).
   <dialog class="dialog" id="x" aria-labelledby="x-title"><div class="dialog-head"><h2 class="dialog-title" id="x-title">…</h2>
     <button class="dialog-close" type="button" data-dialog-close aria-label="Close">…</button></div>
     <p class="dialog-text">…</p><div class="dialog-body">…</div><div class="dialog-actions">…</div></dialog> */
dialog.dialog {
  width: 400px; max-width: calc(100vw - 32px); max-height: 85vh; margin: auto; padding: 24px;
  overflow-y: auto; overscroll-behavior: contain; border: 1px solid var(--bg3); border-radius: var(--radius-box);
  background: var(--bg2); color: var(--fg); box-shadow: none; outline: none;
}
dialog.dialog.dialog-wide { width: 560px; }
dialog.dialog::backdrop, .dialog-scrim {
  background: rgb(0 0 0 / .4); -webkit-backdrop-filter: blur(12px); backdrop-filter: blur(12px);
}
/* drawn open by the server: over everything, centred, with the page behind it inert (views.js page) */
.dialog-scrim { position: fixed; inset: 0; z-index: 20; }
dialog.dialog[open]:not(:modal) { position: fixed; inset: 0; z-index: 21; height: fit-content; }
.dialog-behind { display: contents; }
.dialog-head { display: flex; align-items: flex-start; justify-content: space-between; gap: 16px; }
.dialog-title { margin: 0; font: 600 var(--text-lg)/var(--lh-lg) var(--font); letter-spacing: var(--tracking-tight); }
.dialog-close {
  flex: none; display: grid; place-items: center; width: 28px; height: 28px; margin: 0 -6px 0 0; padding: 0;
  border: 0; border-radius: var(--radius); background: transparent; color: var(--muted); cursor: pointer;
}
.dialog-close:hover { background: var(--bg3); color: var(--fg); }
.dialog-text { margin: 4px 0 0; color: var(--muted); font-size: var(--text-sm); line-height: var(--lh-sm); text-wrap: pretty; }
.dialog-body { margin-top: 16px; }
/* a text to copy, on the dialog's ground: a see-through step up (taste: a small thing on a surface) */
.dialog-body .prompt { margin: 0; background: var(--chip); }
.dialog-body .keynote { margin-top: 12px; }
.dialog-actions { display: flex; flex-wrap: wrap; align-items: center; justify-content: flex-end; gap: 8px; margin-top: 24px; }
.dialog-actions form { display: contents; }
/* a form in a dialog (the welcome): shadcn's field pattern, 16px between fields; why it was refused over the fields */
.dialog-form { display: grid; gap: 16px; }
.dialog-form .field > span:first-child { font: 500 var(--text-sm)/var(--lh-sm) var(--font); }
.dialog-form .field em { font-style: normal; font-weight: 400; color: var(--muted); }
.dialog-error { margin: 0; color: var(--fg); font-size: var(--text-sm); line-height: var(--lh-sm); }
dialog.dialog:focus-visible { outline: none; }

/* ---- Tip: a small popover that says what something means (views.js stateTip: a state, pressed) ----------------------
   10-02 night, Bonan: "这些 offline 啥的，点击弹一个小的，解释我们怎么定义 offline 的". shadcn's Popover (PopoverContent:
   w-72, p-4, rounded-md, border) in the taste skill's shell: a 1px edge, the step-up ground, 12px corners, no shadow;
   it appears at once. 288px wide, 14px text. Under what opened it, left edges lined up (anchor positioning; it flips
   above or leftward when there is no room); where the browser has no anchor positioning, in the middle of the window.
   Esc and a click anywhere else close it (the popover attribute does both). A link in it is the site's link blue.
   <button class="badge" popovertarget="x" style="anchor-name:--x">Offline</button>
   <span class="tip" id="x" popover style="position-anchor:--x">Offline: …</span> */
.tip {
  position: fixed; inset: 0; width: 288px; max-width: calc(100vw - 32px); height: fit-content; margin: auto;
  padding: 12px 14px; border: 1px solid var(--line); border-radius: var(--radius-box); background: var(--bg2);
  color: var(--fg); font: 400 var(--text-sm)/var(--lh-sm) var(--font); text-align: left; text-wrap: pretty;
  white-space: normal; box-shadow: none;
}
.tip:not(:popover-open) { display: none; }
.tip:popover-open { display: block; }
@supports (position-anchor: --a) {
  .tip { inset: auto; top: anchor(bottom); left: anchor(left); margin: 8px 0 0; position-try-fallbacks: flip-block, flip-inline, flip-block flip-inline; }
}
.tip a { display: block; width: fit-content; margin-top: 8px; color: var(--link); text-decoration: none; }
.tip a:hover { text-decoration: underline; }
/* a badge that is a button (a state that explains itself): pressed for its tip; pointed at, one step lighter */
button.badge { cursor: pointer; }
button.badge:hover, button.badge:has(+ .tip:popover-open) { background: var(--line); }

/* the pictures waiting to be sent, in the card over the text (Cube's CubeChatBar): 64px squares, 16px corners, a 1px
   zinc-700 edge, 12px apart and 12px in; a 20px white round cross on each top right corner, half outside, to take
   one out */
.chatbox-pics { display: flex; flex-wrap: wrap; gap: 12px; padding: 12px 12px 0; }
.chatbox-pics[hidden] { display: none; }
.chatbox-pic { position: relative; flex: none; width: 64px; height: 64px; }
.chatbox-pic img { display: block; width: 100%; height: 100%; object-fit: cover; border: 1px solid #3f3f46; border-radius: 16px; }
.chatbox-pic-out {
  position: absolute; top: -6px; right: -6px; display: grid; place-items: center; width: 20px; height: 20px; padding: 0;
  border: 0; border-radius: var(--radius-full); background: #fff; color: #111827; cursor: pointer;
  box-shadow: 0 4px 6px -1px rgb(0 0 0 / .1), 0 2px 4px -2px rgb(0 0 0 / .1);
}
.chatbox-pic-out:hover { background: #f3f4f6; }

/* ---- Lightbox: a picture large, over 75% black, 32px from the window's edges, 8px corners (Cube's ChatPictures);
   a click anywhere or Esc closes it (public/app.js) */
dialog.lightbox { max-width: 100vw; max-height: 100vh; width: 100vw; height: 100vh; margin: 0; padding: 32px; border: 0; background: transparent; cursor: zoom-out; display: flex; align-items: center; justify-content: center; }
dialog.lightbox:not([open]) { display: none; }
dialog.lightbox::backdrop { background: rgb(0 0 0 / .75); }
dialog.lightbox img { display: block; max-width: 100%; max-height: 100%; border-radius: 8px; object-fit: contain; }
dialog.lightbox:focus-visible { outline: none; }

/* ---- Toast: Cube's deck (lib/cube/toast.ts, decided.ts, components/cube/Doc/DecisionToast.tsx; 10-02 night, Bonan:
   "cube的toast显示逻辑，就是最上面出现，你也抄过来，以后可能能用上"). One door: public/app.js eolToast(label, text,
   { icon, key }). Only Cube's three kinds of toast: a receipt for what the person just did (a success they can see
   gets none, a failure always one, a slow or unseen one always one), work running in the background, a condition that
   lasts. At the very top of the window, centred, 8px down, 480px wide; each a 36px pill, dark in every theme, a 1px
   zinc-700 edge, a small shadow (Cube's shadow-md); a 14px icon, the label in medium white, the rest in grey, both cut
   to one line. Newest in front; the two behind peek 8px lower and 5% smaller, more are hidden; under the pointer the
   deck fans out 44px apart and every clock stops. Each stays 5s; swiped sideways past a third of its width (or flicked)
   it goes. It comes in from 8px above over 150ms and leaves over 160ms, opacity and transform only; with reduced motion
   it just appears and goes.
   <ol class="toast-deck" role="status" aria-live="polite"><li class="toast"><span class="toast-main">
     <svg class="toast-icon">…</svg><b class="toast-label">Key revoked</b><span class="toast-text">…</span></span></li></ol> */
.toast-deck {
  position: fixed; top: 8px; left: 50%; z-index: 60; width: min(480px, calc(100vw - 32px)); height: 36px;
  margin: 0; padding: 0; list-style: none; transform: translateX(-50%); pointer-events: none;
}
.toast-deck:empty { display: none; }
.toast {
  position: absolute; inset: 0 0 auto; display: flex; align-items: center; height: 36px; padding: 0 12px 0 14px;
  border: 1px solid #3f3f46; border-radius: var(--radius-full); background: rgb(24 24 27 / .95); color: #f4f4f5;
  box-shadow: 0 4px 6px -1px rgb(0 0 0 / .1), 0 2px 4px -2px rgb(0 0 0 / .1);
  font: 400 var(--text-sm)/var(--lh-sm) var(--font); transform-origin: top center; touch-action: pan-y; user-select: none;
  transition: transform .16s ease-out, opacity .16s ease-out;
}
.toast-main { display: flex; flex: 1; align-items: center; gap: 6px; min-width: 0; }
.toast-icon { flex: none; width: 14px; height: 14px; }
.toast-label { flex: none; max-width: 192px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-weight: 500; }
.toast-text { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; color: #a1a1aa; }

.toast.is-leaving { opacity: 0; pointer-events: none; }
@media (prefers-reduced-motion: reduce) { .toast { transition: none; } }
/* stacked, a card behind shows only its edge: its words would show through the front one's 95% ground */
.toast[data-behind] .toast-main { opacity: 0; }
