/* Inter, as next/font does it (10-12, devil-font): font-display swap (optional never showed Inter on a first visit over a real network: the file
   arrives after the ~100ms block period and the whole first page stays in the system font); two files split by unicode-range, latin (108KB, the only one
   preloaded, views.js) and the rest (313KB, fetched only if a page has such a letter); Inter Fallback = Arial resized to Inter's metrics so the swap moves nothing.
   Split with scripts/font-subset.py from InterVariable.693b77d4.woff2 (axes opsz+wght kept). Fallback numbers: next/font's way, from @capsizecss/metrics 3.7.0 -
   size-adjust = (Inter xWidthAvg 978 / 2048) / (Arial 913 / 2048) = 107.12%; ascent 1984/2048, descent 494/2048, lineGap 0, each divided by that.
   (10-17, the light frame's review) Emoji go to the system's colour emoji, as iMessage, ChatGPT and Slack draw reactions: the code points of
   U+2600-27BF that are emoji (Unicode's Emoji property: U+2600 ☀, U+2665 ♥, U+26A0 ⚠, U+2764 ❤) are out of the rest face's range, so a ❤️ no
   longer fetches its 313KB. The other Emoji-property characters Inter draws stay Inter's: # * 0-9 © ® ‼ ⁉ ™, the arrows (↔ ↕ ↖-↙ ↩ ↪ ⬆), ▪ ▶ ◀ ⏏ Ⓜ ⬜. */
@font-face { font-family: "Inter"; src: url("/v2/fonts/InterVariable-rest.ca9b8c57.woff2") format("woff2"); font-weight: 100 900; font-style: normal; font-display: swap; unicode-range: U+0100-0130,U+0132-0148,U+014A-0151,U+0154-01C3,U+01C5-0254,U+0256-027B,U+027E-0284,U+0286-0290,U+0292-02A4,U+02A6-02BA,U+02BD-02C5,U+02C7-02D9,U+02DB,U+02DD-0303,U+0306-0307,U+0309-030A,U+030C,U+030F,U+0313,U+0315,U+031B,U+0323,U+0326-0328,U+032C,U+0337-0338,U+0342-0343,U+0346-036F,U+0374-0376,U+037A-037F,U+0384-038A,U+038C,U+038E-03A1,U+03A3-03D7,U+03DC-03DD,U+03F0-03F6,U+03F9-03FA,U+03FC-0479,U+0480-049D,U+04A0-04FF,U+052F,U+0E3F,U+1D00,U+1D0D,U+1D1B,U+1D43,U+1D47-1D49,U+1D4D,U+1D4F-1D50,U+1D52,U+1D56-1D58,U+1D5B,U+1D62-1D65,U+1D9C,U+1DA0,U+1DBB,U+1DBF-1DF5,U+1DFC-1E9B,U+1E9D-1F15,U+1F18-1F1D,U+1F20-1F45,U+1F48-1F4D,U+1F50-1F57,U+1F59,U+1F5B,U+1F5D,U+1F5F-1F7D,U+1F80-1FB4,U+1FB6-1FC4,U+1FC6-1FD3,U+1FD6-1FDB,U+1FDD-1FEF,U+1FF2-1FF4,U+1FF6-1FFE,U+2070-2071,U+2074-208E,U+2090-209C,U+20A0-20AB,U+20AD-20AF,U+20B1-20B5,U+20B8-20BA,U+20BC-20C0,U+20DB-20DE,U+20E8,U+20F0,U+2100-2101,U+2103,U+2105-2106,U+2109,U+2113,U+2116-2117,U+211E-2121,U+2126,U+212A-212B,U+212E,U+2132,U+213B,U+214D,U+2150-217F,U+2183-2186,U+2189,U+21A9-21AA,U+21B0-21B1,U+21B3-21B5,U+21BA-21BB,U+21D0,U+21D2,U+21D4,U+21DE-21DF,U+21E4-21E5,U+21E7,U+21EA,U+2202,U+2205-2206,U+220F,U+2211,U+221A,U+221E,U+222B,U+2236,U+2248,U+2260,U+2264-2265,U+2295-2298,U+2303-2305,U+2318,U+2325-2327,U+232B,U+2380,U+2387,U+238B,U+23CE-23CF,U+2423,U+2460-2468,U+24B6-24CF,U+24EA,U+25A0-25A2,U+25AA,U+25B2-25B3,U+25B6-25B7,U+25BA-25BD,U+25C0-25C1,U+25C4-25C7,U+25CA-25CB,U+25CF,U+25E6,U+25EF,U+2605-2606,U+263C,U+2661,U+266A-266B,U+2717,U+2756,U+2780-2788,U+27EF,U+27F5-27FA,U+2913,U+2A38,U+2B06,U+2B12-2B13,U+2B1C,U+2B24,U+2C7C,U+2C7F,U+2DFF,U+2E18,U+A69F,U+A7FF,U+A92E,U+E000,U+E002-E05E,U+E06A-E0BD,U+E0C8-E0CC,U+E0DC-E0E6,U+E0F3-E0F5,U+E106,U+E109-E10A,U+E10C-E10F,U+E111-E113,U+E117-E118,U+E121-E122,U+E124,U+E12A-E15E,U+E163,U+E1C3,U+E1D2-E1DF,U+E1E1-E2DC,U+EE01-EE07,U+EE09-EE0A,U+EE0C-EE12,U+EE14,U+EE17,U+EE1D-EE45,U+EE47-EE84,U+EE87-EED6,U+EED8-EEE1,U+F6C3,U+1F12F-1F149,U+1F16A-1F16B,U+1F850,U+1F852; }
@font-face { font-family: "Inter"; src: url("/v2/fonts/InterVariable-latin.a99a3f44.woff2") format("woff2"); font-weight: 100 900; font-style: normal; font-display: swap; unicode-range: U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+0304,U+0308,U+0329,U+2000-206F,U+20AC,U+2122,U+2190-2199,U+2713,U+2212,U+2215,U+FEFF,U+FFFD; }
@font-face { font-family: "Inter Fallback"; src: local("Arial"); ascent-override: 90.44%; descent-override: 22.52%; line-gap-override: 0%; size-adjust: 107.12%; }
/* ---- The scale (css-scale-1016, 10-16; Bonan: "很多前端设计很学生项目"). design/44 set the steps; this writes them as the one
   set of names a rule takes, each value written once. It is a layer over the older tokens in the next :root: the same
   values, merged where two were a hair apart (the inbox's bubble grey and the raised ground two steps up, the panel's line
   and the column's), named for what they are for. The pages move onto it a group at a time: first the inbox, a
   conversation and My agent (eol_research/css-scale-1016); until the last group has moved, the older names stay as they
   are. A rule that needs a value not here is a bug: move it to the nearest step, never add one.
     spacing  a 4px grid in Tailwind's numbers: --sp-1 4, -2 8, -3 12, -4 16, -5 20, -6 24, -8 32, -10 40, -12 48, -16 64.
              Off the grid only the parts design/44 §0 copied whole from their reference: iMessage (a run's 2, a one-line
              bubble 36 high), Cube's menu rows (6 and 14, 10 at the list's ends) and tooltip (6 and 10).
     radius   four: 8 a control; 12 a raised surface (dialog, menu, card, panel); 18 a bubble, the input card, pictures; full.
     type     five: 12 a count badge; 14 meta, buttons, labels; 16 body and items; 18 a dialog's title; 22 a page's title.
     colour   text 1 (what you read), 2 (what supports it), 3 (meta), and words on the accent; grounds 1 (the page), 2
              (raised), 3 (two steps up, a bubble of theirs); tints 1-4, a relative step over any ground (hover, a mark,
              chosen, chosen and pointed at); border (fields, outline buttons) and divider (columns, rows); the accent and
              links; danger, success, online. Each with its light and dark value, chosen by the page's color-scheme. ---- */
:root {
  --sp-1: 4px; --sp-2: 8px; --sp-3: 12px; --sp-4: 16px; --sp-5: 20px; --sp-6: 24px; --sp-8: 32px; --sp-10: 40px; --sp-12: 48px; --sp-16: 64px;
  --sp-run: 2px;        /* iMessage: between two messages of one writer's run; X: between the pictures of a tray */
  --sp-bubble-y: 7px;   /* iMessage: a one-line bubble is 36 high (22 + 7 + 7) and a pill */
  --sp-menu-y: 6px; --sp-menu-x: 14px; --sp-menu-end: 10px;  /* Cube's account menu: a row, and the list's first and last */
  --sp-tip-y: 6px; --sp-tip-x: 10px;                         /* Cube's tooltip */

  --radius: 8px; --radius-box: 12px; --radius-lg: 18px; --radius-full: 9999px;

  --text-xs: 12px; --lh-xs: 16px; --text-sm: 14px; --lh-sm: 20px; --text-base: 16px; --lh-base: 24px;
  --text-lg: 18px; --lh-lg: 28px; --text-xl: 22px; --lh-xl: 28px;

  --c-text: light-dark(#18181b, #f5f5f7);
  --c-text-2: light-dark(#71717a, #86868b);
  --c-text-3: light-dark(#8e8e93, #6e6e73);
  --c-text-hover: light-dark(#27272a, #d4d4d8);   /* a solid --c-text ground under the pointer (the default button) */
  --c-text-inverse: light-dark(#ffffff, #18181b); /* words on a --c-text ground */
  --c-on-accent: #ffffff;                         /* words on the accent, on danger, on a coloured face */
  --c-bg: light-dark(#ffffff, #101010);
  --c-bg-2: light-dark(#f4f4f5, #1d1d1f);
  --c-bg-3: light-dark(#e4e4e7, #2a2a2d);
  --c-tint-1: light-dark(rgb(0 0 0 / .04), rgb(255 255 255 / .04)); /* a row under the pointer (Cube's) */
  --c-tint-2: light-dark(rgb(0 0 0 / .06), rgb(255 255 255 / .07)); /* a small mark on any ground: a face's tile, a quoted excerpt */
  --c-tint-3: light-dark(rgb(0 0 0 / .08), rgb(255 255 255 / .1));  /* a chosen row; a face's ring; a line on glass */
  --c-tint-4: light-dark(rgb(0 0 0 / .11), rgb(255 255 255 / .14)); /* chosen and pointed at */
  --c-border: light-dark(#d0d7de, #424245);       /* fields, outline buttons, toggles */
  --c-divider: light-dark(#d0d7de, #2f3336);      /* down a column's sides, between the rows of a list, a glass edge (X's dark line) */
  --c-ring: var(--c-text-3);                      /* a keyboard's focus: the meta grey (shadcn's own #a1a1aa / #737373 until 10-16) */
  --c-ring-soft: color-mix(in srgb, var(--c-text-3) 50%, transparent);
  --c-accent: #3d7bff;                            /* the site's one blue: what needs you, counts, my bubble, the main button */
  --c-accent-hover: color-mix(in srgb, #3d7bff 86%, #000);
  --c-link: light-dark(#2f6ae6, color-mix(in srgb, #3d7bff 75%, #fff));
  --c-danger: #ff3b30;
  --c-danger-hover: color-mix(in srgb, #ff3b30 86%, #000);
  --c-danger-soft: light-dark(#ffebe9, rgb(248 81 73 / .15));   /* a removed line, a red action under the pointer (GitHub Primer's) */
  --c-success: light-dark(#1a7f37, #3fb950);
  --c-success-soft: light-dark(#dafbe1, rgb(46 160 67 / .15));  /* an added line (GitHub Primer's) */
  --c-online: #1be38c;
  --c-scrim: rgb(0 0 0 / .4);
  --c-scroll: color-mix(in srgb, var(--c-text) 30%, transparent);       /* a scrollbar's thumb */
  --c-scroll-hover: color-mix(in srgb, var(--c-text) 50%, transparent);
}
/* 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 {
  /* ---- the theme (10-06, Bonan: a Theme setting in /me, Cube's): Light and Dark, each token below written once with
     both values, light-dark(light, dark), resolved by the page's color-scheme. Chosen (the theme cookie), the server
     writes <html data-theme>; not chosen, the device's own setting decides, as Cube's first visit does. Dark is the
     Lobby as it was; light is the same greys turned over, zinc as in Cube's light theme (the embed's light frame,
     public/v2/embed.css, has used these values since 10-06). Pictures (the login page's dance floor) and the toast,
     dark in every theme as Cube's, keep their own colours. ---- */
  color-scheme: light 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: light-dark(#ffffff, #101010);                /* the page: Threads' ground, measured 10-06 (#101010 dark; it was #000) */
  --bg2: light-dark(#f4f4f5, #1d1d1f);               /* one step up: cards, panels, fields, the outline button, the dialog */
  --bg3: light-dark(#e4e4e7, #2a2a2d);               /* two steps up: hover on a step-up surface, the picked toggle, the badge */
  --fg: light-dark(#18181b, #f5f5f7);                /* text; also the default (primary) button's ground */
  --fg-hover: light-dark(#27272a, #d4d4d8);          /* the default button under the pointer: Cube's dark:hover:bg-zinc-300 */
  --on-fg: light-dark(#ffffff, #18181b);             /* text on --fg: Cube's dark:text-zinc-900 */
  --muted: light-dark(#71717a, #86868b);             /* secondary text, placeholders */
  /* hairlines; the border of fields, outline buttons and toggle groups; in light every divider of the app (rail.css
     --rail-line takes this light half: the column's lines, rows, tabs, modules). Light is GitHub Primer's
     borderColor-default (x-layout-2, 10-09, Bonan: "如果是 light mode，分界线要显眼一些稍微，你看看人家别人的设计"; it was
     #dce2ea, and X's #eff3f4 beside the rail): there at a glance, still quieter than any text. Dark unchanged. */
  --line: light-dark(#d0d7de, #424245);
  --ring: light-dark(#a1a1aa, #737373);              /* focus: shadcn .dark --ring, oklch(0.556 0 0) */
  --ring-soft: light-dark(rgb(161 161 170 / .5), rgb(115 115 115 / .5)); /* focus halo: shadcn ring-ring/50 */
  --scrim: light-dark(rgb(0 0 0 / .3), rgb(0 0 0 / .6)); /* behind a dialog: Cube's bg-black/30, dark:bg-black/60 */
  --unread: #3D7BFF;                /* what needs you and every count: the site blue, the unread dot's (10-12, Bonan: only blue vs grey) */
  --danger: #FF3B30;                /* errors and destructive actions only: iOS red */
  --online: #1BE38C;                /* the online dot on a face: the robots' green (src/robot.js COLORS.green) */
  --online-rim: #fff;               /* the dot's thin rim, so it never melts into a face of its own green (10-06) */
  --chip: light-dark(rgb(0 0 0 / .06), rgb(255 255 255 / .1)); /* small marks drawn on any surface (inline code) */
  --chip2: light-dark(rgb(0 0 0 / .1), rgb(255 255 255 / .16));
  --avatar-ring: light-dark(rgb(0 0 0 / .08), rgb(255 255 255 / .12)); /* the 1px inner ring on every avatar (GitHub, Linear): a dark picture keeps its edge on a dark page; a relative colour, so it is never one step of the ground it sits on */
  --nav: light-dark(rgb(24 24 27 / .8), rgb(245 245 247 / .8));
  /* the feed's panel on a wide window (rail.css), Threads' as measured 10-06 on threads.com: its 1px edge, and the
     line between two rows inside it */
  --panel-line: light-dark(#d5d5d5, #2d2d2d);
  --row-line: light-dark(rgb(0 0 0 / .15), rgb(243 245 247 / .15));
  /* the round send button: zinc-700 under a white arrow in the dark, Cube's zinc-900 in the light */
  --send: light-dark(#18181b, #424245);

  /* ---- 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. ---- */
  /* Cube's type (10-03, Bonan: "这个项目的字体是不是和cube的字体不一样啊，和cube看齐好了"): Inter Variable, the same file
     (Cube app/fonts/InterVariable.woff2, here v2/fonts/), and Cube's fallbacks, Chinese ones included (globals.css --font-sans) */
  --font: "Inter", "Inter Fallback", ui-sans-serif, system-ui, "Segoe UI", Roboto, Arial, "Noto Sans CJK SC", "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei", 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) */

  /* ---- the hierarchy, written once (10-08 polish; Bonan: "缺乏视觉层级是一个通病，解决它"). Sizes and spacing are X's,
     read off x.com's computed styles; every page's own words take one of four levels and one of three colours, and
     nothing else (rail.css "the hierarchy" maps every view onto them). A thing that fits no level is a bug: move it to a
     level, never add one. Buttons keep shadcn's size (--text-sm), a conversation's words ChatGPT's (--chat-text), the
     lobby's search its underline (taste). ----
     level   size/line  weight  colour   what
     title   22/28      700     fg       a page's title, a profile's name, a section's title, a post's title
     item    16/22      700     fg       the title of a row: a post, a person, a conversation, a setting
     body    16/24      400     fg|muted what a row or a page says; muted when it repeats or explains (an excerpt, a bio's handle)
     meta    14/20      400     subtle   when, how many, where from; never a sentence
     (10-08, Bonan: "正文字体可以稍微大一个，16 好了": was 20/24, 15/20, 15/20, 13/16) */
  --fs-meta: 14px;  --lh-meta: 20px;
  --fs-body: 16px;  --lh-body: 24px;
  --fs-item: 16px;  --lh-item: 22px;
  --fs-title: 22px; --lh-title: 28px;
  --level-title: 700 var(--fs-title)/var(--lh-title) var(--font);
  --level-item: 700 var(--fs-item)/var(--lh-item) var(--font);
  --level-body: 400 var(--fs-body)/var(--lh-body) var(--font);
  --level-meta: 400 var(--fs-meta)/var(--lh-meta) var(--font);
  /* a settings row (10-12, Bonan: "那里为什么全都是加粗字"; ChatGPT, X and iOS settings): only a group's heading (.sec-h) is bold; the row's
     name is regular, a vendor's app name one step up, what a row explains is body grey (.me-app-line) */
  --level-row: 400 var(--fs-item)/var(--lh-item) var(--font);
  --level-row-app: 500 var(--fs-item)/var(--lh-item) var(--font);
  /* three text colours: fg (what you read), muted (what supports it), subtle (meta: times and counts) */
  --subtle: light-dark(#8e8e93, #6e6e73);
  /* rhythm: 4 inside a group (a title and its text), 12 between the parts of an item and round a row's words, 24 between
     groups (sections, a head and its tabs' content). A name with its @handle under it: none, their lines touch and the
     handle's line is no taller than the name's (--lh-item), as in a conversation's head and X's list rows (10-09 night,
     Bonan: Who is online's name and handle were too far apart; the conversation's head is right) */
  --sp-1: 4px; --sp-2: 8px; --sp-3: 12px; --sp-4: 16px; --sp-6: 24px;
  /* surfaces: the page (--bg); raised (--bg2: a dialog, a menu, a field); under the pointer one step up (--bg2 on the
     page, --bg3 on a raised surface); chosen one more (--bg3). Lines (--rail-line, rail.css): between the rows of one list
     and down the column's sides only; groups are told apart by space alone, never a line under a title. */
  /* glass, the one frosted material (10-08 polish, Bonan on Search's suggestions: "半透明的，会很漂亮，就和磨砂玻璃一样"): the
     page's ground at 72% over a 20px blur that lifts the colour a little, and the pill's own edge (--panel-line, the title
     pill's). Words on it stay legible over any post. Anything frosted takes these three and no other recipe. */
  /* 10-09 (Bonan: the scrolled "Inbox 50" pill was unreadable, the row under it showing through): 88% of the page's ground, so
     what passes under a pill never competes with its words, in both themes */
  --glass-bg: color-mix(in srgb, var(--bg2) 88%, transparent);
  --glass-blur: blur(24px) saturate(1.4);  /* 10-09 (Bonan: the phone's pill and circle were no shape over the white page): one step up from the page, 24px blur */
  --glass-edge: var(--panel-line);

  /* ---- 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: 24px;                  /* the body level's line (10-08: 16/24) */
  --bubble-bg: light-dark(#f4f4f4, rgb(50 50 50 / .85)); /* the user bubble: ChatGPT's --message-surface, #f4f4f4 light, #323232d9 dark */
  --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)
   one size, 36px, everywhere (design/44-detail-standards.md §7; taste 10-04): .btn-sm is the same size and stays only as a
   name. .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: var(--sp-2);
  height: 36px; padding: 0 var(--sp-4); border: 0; border-radius: var(--radius);
  background: var(--c-text); color: var(--c-text-inverse);
  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(--c-text-hover); }
.btn > svg { flex: none; width: 16px; height: 16px; pointer-events: none; }
.btn:has(> svg) { padding: 0 var(--sp-3); }

.btn-outline { border: 1px solid var(--c-border); background: var(--c-bg-2); color: var(--c-text); }
.btn-outline:hover { background: var(--c-bg-3); }
.btn-ghost { background: transparent; color: var(--c-text-2); }
.btn-ghost:hover { background: var(--c-bg-2); color: var(--c-text); }

.btn-sm { height: 36px; }
.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(--c-ring-soft); }
.btn-outline:focus-visible { border-color: var(--c-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(--c-border); border-radius: var(--radius); background: var(--c-bg-2); color: var(--c-text);
  font: 400 var(--text-base)/var(--lh-base) var(--font); outline: none;
}
.input { height: 36px; padding: var(--sp-1) var(--sp-3); }
.textarea { min-height: 64px; padding: var(--sp-2) var(--sp-3); resize: none; field-sizing: content; }
.input::placeholder, .textarea::placeholder { color: var(--c-text-2); opacity: 1; }
.input:disabled, .textarea:disabled { opacity: .5; cursor: not-allowed; }
/* fields follow the body level at every width (10-08, Bonan: inputs 16; a phone's browser does not zoom into a 16px field) */

/* Label and a field's parts (shadcn ui/label.tsx and its form pattern: label, 8px, field, 8px, description) */
.field { display: grid; gap: var(--sp-2); 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; }

/* ---- Switch: one thing on or off, changed at once (shadcn ui/switch.tsx, new-york v4: a 32 x 18.4 track, a 16px thumb,
   checked bg-primary, unchecked bg-input, the thumb bg-background, in the dark the unchecked thumb foreground and the
   checked one primary-foreground). The taste's: no shadow, no transition (a control in a dialog changes at once).
   <button class="switch" role="switch" aria-checked="true"><span class="switch-thumb"></span></button><label for=…>…</label> */
.switch {
  display: inline-flex; flex-shrink: 0; align-items: center; box-sizing: border-box; width: 32px; height: 18.4px;
  margin: 0; padding: 0; border: 1px solid transparent; border-radius: var(--radius-full);
  background: var(--line); cursor: pointer; outline: none; -webkit-tap-highlight-color: transparent;
}
.switch[aria-checked="true"] { background: var(--fg); }
.switch-thumb { display: block; width: 16px; height: 16px; border-radius: var(--radius-full); background: light-dark(#ffffff, #f5f5f7); pointer-events: none; }
.switch[aria-checked="true"] .switch-thumb { background: var(--on-fg); transform: translateX(14px); }
.switch:focus-visible { border-color: var(--ring); box-shadow: 0 0 0 3px var(--ring-soft); }
.switch:disabled { opacity: .5; cursor: not-allowed; }

/* ---- 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(--c-text);
  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(--c-bg-2); }
.icon-btn > .icon { width: 20px; height: 20px; }
.icon-btn > .count { position: absolute; top: -3px; right: -5px; outline: 2px solid var(--c-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 CSS anchors where supported; otherwise app.js follows
   the trigger and keeps the menu inside the viewport. */
.menu {
  position: fixed; inset: 56px 20px auto auto; width: 240px; margin: 0; padding: 0; overflow: hidden;
  border: 1px solid var(--c-border); border-radius: var(--radius-box); background: var(--c-bg-2); color: var(--c-text);
}
.menu > * + * { border-top: 1px solid var(--c-bg-3); }
/* a row that follows a row has the line too: .menu-row's own border: 0 (below) had beaten the rule above, so only a menu whose rows sat in forms drew its lines (10-07; not the phone's sheet nor a dropdown's list, whose rows are an item list) */
.menu:not(.nav-menu, .dropdown-menu) > :is(.menu-row, form) + .menu-row { border-top: 1px solid var(--c-bg-3); }
.menu form { display: block; margin: 0; }
.menu-row, .menu-head {
  display: flex; align-items: center; gap: var(--sp-2); width: 100%; margin: 0; padding: var(--sp-menu-y) var(--sp-menu-x); border: 0;
  background: none; color: var(--c-text-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(--c-bg-3); }
.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; }
/* the name (whether you are online is the dot on your face beside it, views.js faceDot) */
.menu-head .menu-head-name { display: flex; align-items: center; gap: 8px; min-width: 0; color: var(--fg); font-weight: 600; }
.menu-head .menu-head-name b { min-width: 0; }
@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 (anchor-name: --a) {
  .menu { inset: calc(anchor(bottom, 48px) + 6px) 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: var(--c-accent); color: var(--c-on-accent); }
.btn-blue:hover { background: var(--c-accent-hover); }


/* ---- 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): the globe's menu when signed out. A menu's small title (taste: every popup
   menu has one, small, medium weight, grey); the language in use ticked. Signed in, it is /me's Language section. */
.menu-label { margin: 0; padding: var(--sp-2) var(--sp-menu-x) var(--sp-1); color: var(--c-text-2); font: 500 var(--text-sm)/var(--lh-sm) var(--font); }
.menu-row[aria-current] { color: var(--c-text); }
.menu-check { flex: none; margin-left: auto; }
.segmented > button.segment { border: 0; background: transparent; }

/* 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); }

/* ---- Fade only where it clips (10-11 night, Bonan: "名字那里，包括很多有所谓渐隐的地方，无论能不能全放下，都有渐隐"). Text that ends in
   a fade (never "…") fades only while it really runs past its box; when it fits it is drawn plain. app.js (Fade only where
   it clips) puts .is-clipped on a .fade-clip box exactly while its text is cut (wider than the box; with .fade-lines, taller
   than its lines), at load, on resize and for every node a live refresh adds. The box sets its own --fade (the fade's width,
   default 32px); .fade-lines is a box of several lines whose LAST line fades (--fade-lh: one line's height). Only the mask
   is here: size, layout and cut-off stay each box's own.
   <span class="x fade-clip">…</span> */
.fade-clip.is-clipped { -webkit-mask-image: linear-gradient(to right, #000 calc(100% - var(--fade, 32px)), transparent); mask-image: linear-gradient(to right, #000 calc(100% - var(--fade, 32px)), transparent); }
.fade-clip.fade-lines.is-clipped {
  -webkit-mask-image: linear-gradient(#000, #000), linear-gradient(to right, #000 calc(100% - var(--fade, 40px)), transparent);
  mask-image: linear-gradient(#000, #000), linear-gradient(to right, #000 calc(100% - var(--fade, 40px)), transparent);
  -webkit-mask-size: 100% var(--fade-lh), 100% var(--fade-lh); mask-size: 100% var(--fade-lh), 100% var(--fade-lh);
  -webkit-mask-position: 0 0, 0 var(--fade-lh); mask-position: 0 0, 0 var(--fade-lh);
  -webkit-mask-repeat: no-repeat; mask-repeat: no-repeat;
}

/* ---- 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 the text's colour see-through, 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.
   The mouse on a bar (10-09 night, Bonan: "这个scrollbar不是应该滚动才出现，并且hover又效果么？cursorpionter"): its box gets
   .on-scrollbar (public/app.js, read once a frame from the last move, a mouse only), which shows the thumb and makes the
   pointer a hand (Chrome and Safari take a styled bar's cursor from its box); on the thumb itself, or dragging it, the
   thumb is brighter and a step wider (8 to 10: its clear edge 2 to 1), at once. */
html { scrollbar-gutter: stable; }
/* The homepage (/home, public/landing.css's .landing) keeps the browser's own page bar, its track and thumb following the page's
   color-scheme, as Notion's homepage does (10-11, Bonan: "滚动条那一栏会把header吃掉一部分": the 12px transparent track showed the
   page behind the fixed head). Measured in Chrome: scrollbar-color / scrollbar-width auto do not undo a ::-webkit-scrollbar rule;
   only leaving the root (html and body) out of the two rules below does. Every other box and page is as before. */
:where(:not(:root:has(.landing), body:has(.landing)))::-webkit-scrollbar { width: 12px; height: 12px; }
::-webkit-scrollbar-button { display: none !important; width: 0 !important; height: 0 !important; }
:where(:not(:root:has(.landing), body:has(.landing)))::-webkit-scrollbar-track, :where(:not(:root:has(.landing), body:has(.landing)))::-webkit-scrollbar-corner { background: transparent; }
::-webkit-scrollbar-thumb {
  border: 2px solid transparent; border-radius: var(--radius-full); background-color: transparent; background-clip: padding-box;
  transition: background-color .7s ease;
}
.is-scrolling::-webkit-scrollbar-thumb, .on-scrollbar::-webkit-scrollbar-thumb { background-color: color-mix(in srgb, var(--fg) 30%, transparent); transition: background-color .12s ease; }
::-webkit-scrollbar-thumb:hover, ::-webkit-scrollbar-thumb:active { border-width: 1px; background-color: var(--c-scroll-hover); transition: none; }
.on-scrollbar:not([data-overlay-scroll]) { cursor: pointer; }
/* Firefox has no ::-webkit-scrollbar: its own thin bar, see-through at rest and shown the same way. scrollbar-color is
   inherited, so the boxes inside a shown one say see-through again. */
@supports not selector(::-webkit-scrollbar) {
  * { scrollbar-width: thin; }
  :root, :where(.is-scrolling, .on-scrollbar) * { scrollbar-color: transparent transparent; }
  .is-scrolling, .on-scrollbar { scrollbar-color: color-mix(in srgb, var(--fg) 30%, transparent) transparent; }
}
/* ---- every box's bar, drawn over its edge (scrollbar-unify, 10-10, Bonan: "这个scrollbar又插进去了", of the right column's
   line: its bar kept a 12px lane and its thumb stood under the head). The page's own bar stays the native one above (its
   place kept, html). Every box inside the page draws no bar of its own and keeps no lane (scrollbar-width: none: a
   scrollbar-gutter it asks for is then nothing too), whatever its class, so a new box cannot miss it; public/app.js
   ("Every box's bar") draws the same thumb over the box's last 12px while it scrolls, with the mouse on that strip, or
   dragged: 30% of the text colour, 50% and 10 wide on the thumb itself, the pointer a hand, fading .7s a second after.
   The inbox's list keeps its own drawn thumb (inbox.css, data-overlay-scroll). Opting out: data-native-scroll keeps the
   browser's bar (the rules above); --bar: none draws none (a row of tabs that slides sideways); --bar-top / --bar-bottom
   keep the track off what floats over the box's ends (the column's head). Taste's "scrollbar-gutter: stable" for lists
   is met the same way: nothing appears in the layout when a list starts to scroll, so nothing moves. ---- */
@property --bar { syntax: '<custom-ident>'; inherits: false; initial-value: auto; }
@property --bar-top { syntax: '<length>'; inherits: false; initial-value: 0px; }
@property --bar-bottom { syntax: '<length>'; inherits: false; initial-value: 0px; }
body *:not([data-native-scroll]) { scrollbar-width: none; }
body *:not([data-native-scroll])::-webkit-scrollbar { display: none; width: 0; height: 0; }
.sb-layer { position: fixed !important; inset: 0 !important; z-index: 2147483000; display: block !important; margin: 0 !important; padding: 0 !important; border: 0 !important; background: none !important; overflow: hidden; pointer-events: none; }
.sb-thumb {
  position: absolute; left: 0; top: 0; box-sizing: border-box; border: 2px solid transparent; border-radius: 999px;
  background: color-mix(in srgb, var(--fg) 30%, transparent) padding-box; opacity: 0; transition: opacity .7s ease;
  pointer-events: none; cursor: pointer; touch-action: none;
}
.sb-thumb.is-on { opacity: 1; transition: opacity .12s ease; pointer-events: auto; }
.sb-thumb.is-on:hover, .sb-thumb.is-dragging { border-width: 1px; background-color: color-mix(in srgb, var(--fg) 50%, transparent); }

/* ---- 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: light-dark(#2f6ae6, color-mix(in srgb, #3D7BFF 75%, #fff)); }
/* a theme chosen in Settings (the theme cookie; src/views.js page writes data-theme) over the device's own */
:root[data-theme="light"] { color-scheme: light; }
:root[data-theme="dark"] { color-scheme: dark; }
/* a menu breathes 4px above its first row and below its last (10-02 night, Bonan, of the language menu: "下面没padding不好"),
   and the breath belongs to those two rows, so their hover ground runs to the menu's edge, with no strip left above or
   below it (10-03, Bonan: "menu的第一个和最后一个，hoverbg变色上/下不要留缝，他俩特殊") */
.menu { padding-block: 0; }
.menu > .menu-head:first-child { padding-top: 14px; }
.menu > .menu-label:first-child { padding-top: var(--sp-3); }
.menu > .menu-row:first-child, .menu > form:first-child > .menu-row { padding-top: 10px; }
.menu > .menu-row:last-child, .menu > form:last-child > .menu-row { padding-bottom: var(--sp-menu-end); }
.menu > .menu-head:last-child { padding-bottom: 14px; }
/* the account menu (the one under your face, views.js #me-menu) reads at the rail's size (10-11, Bonan: "这个usermenu里的字怎么大小很诡异":
   its rows were 14px/20 beside the rail's names at 18 and your name at 16). Its rows are the body tier, 16/24 (--text-base, the
   rail's own account button's name), icons 20 (X's menu icons) a 12px gap from the words, 16px in from the edge; the small
   title keeps the small tier (14, grey) on the same left edge. Rows stay 6px above and below, so a row is 36 (the button's
   height), and the lines between rows are as every menu has them. A class of its own: the other menus keep theirs. */
.account-menu .menu-row { gap: var(--sp-3); padding-inline: var(--sp-4); font: 500 var(--text-base)/var(--lh-base) var(--font); }
.account-menu .menu-row > svg { flex: none; width: 20px; height: 20px; }
.account-menu .menu-label { padding-inline: var(--sp-4); }
/* every menu's row icon (10-13, Bonan of the inbox's right-click menu: "这些是否要有icon的"): the account menu's, on every
   menu: 20, Lucide's line, currentColor (a red row's icon is red with its words), 12px from the words. Only the icon: the
   rows keep their own height (an icon of 20 is a small row's line), padding and lines. Not the phone's sheet, a dropdown's
   list or the "who answers" picker (item lists), nor a choice's tick at a row's end (.menu-check). The icons are
   src/views/menu-icons.js; a row whose words flip (Pin / Unpin) has two, and data-alt shows the second. */
.menu:not(.nav-menu, .dropdown-menu, .chatbox-mic-menu) .menu-row > svg:not(.menu-check) { flex: none; width: 20px; height: 20px; }
.menu:not(.nav-menu, .dropdown-menu, .chatbox-mic-menu) .menu-row:has(> svg:not(.menu-check)) { gap: var(--sp-3); }
.menu-row > .menu-icon-alt, .menu-row[data-alt] > .menu-icon:not(.menu-icon-alt) { display: none; }
.menu-row[data-alt] > .menu-icon-alt { display: block; }
/* a destructive row (menu-icons.js MENU_DANGER: Delete, Clear history, Remove, Disconnect): red, icon and words */
.menu .menu-row.is-danger { color: var(--c-danger); }

/* ---- 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; }
/* the About box (setup's first screen, Edit profile): two lines high with its count under it, as X's and Instagram's bio boxes (10-12, bio) */
.textarea[data-about-count] { min-height: calc(2lh + 18px); }
/* (10-09, Bonan, again: "这些输入框右下角这个，是不是说明你打算让他可以拖拽？不允许……项目里的这种输入都是有个 min-height，
   max-height，中间部分可以被撑开") no textarea anywhere has the grip: each has its own min and max and grows between them
   (field-sizing; where a browser has none, public/app.js "Fields that grow") */
textarea { resize: none; }

/* ---- 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(--c-border); border-radius: var(--radius-lg); background: var(--c-bg-2); cursor: text; }
.chatbox-field {
  display: block; box-sizing: border-box; width: 100%; margin: 0; padding: 0 var(--sp-4);
  border: 0; border-top: 10px solid transparent; border-bottom: 10px solid transparent; border-radius: 0;
  background: transparent; color: var(--c-text); 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(--c-text-2); 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-add { color: var(--c-text-2); }
.chatbox-add:disabled { opacity: .4; cursor: default; }
.chatbox-row { display: flex; align-items: center; justify-content: flex-end; gap: var(--sp-2); padding: var(--sp-1) var(--sp-2) var(--sp-2); 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(--send); color: var(--c-on-accent); cursor: pointer; transition: opacity .15s;
  /* (10-09) the row reads [image] [Hand to your agent] from the left, 8px apart, and Send (or Stop) alone at the right
     edge: only it takes the free space (until then the image button and Hand to your agent both did, which floated the
     pill to the middle) */
  margin-left: auto;
}
.chatbox-send:disabled { opacity: .3; cursor: default; }
/* Stop (10-09): the same round button, a filled square, in the send button's place while the owner's agent writes;
   only one of the two shows (public/app.js) */
.chatbox-send[hidden] { display: none; }
/* a tool at the row's left, the send button alone at the right: Cube's chat bar, whose Plug in agent is its PILL made a
   capsule (CubeChatBar.tsx: `${PILL} !rounded-full`, the plug glyph 14px before the words). Here .pill (Cube's PILL) and
   this (10-03, Bonan: "这个style应该和cube里的那个connect agent一样才对，不要自己设计"). The first is Hand to your agent. */
.pill.chatbox-tool { border-radius: var(--radius-full); }
.chatbox-tool svg { flex: none; }

/* ---- 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 dimmed by a plain translucent scrim: ::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: 0;
  overflow: visible; border: 1px solid var(--c-border); border-radius: var(--radius-box);
  background: var(--c-bg-2); color: var(--c-text); box-shadow: none; outline: none;
  contain: layout paint;
}
dialog.dialog.dialog-wide { width: 560px; }
/* Another tab signed in, out, or switched accounts (devil-tabs, 10-12; views.js accountBanner): GitHub's flash banner,
   across the top, a hairline under it, no shadow, shown at once; nothing behind it is out of reach. */
.account-banner {
  position: fixed; top: 0; left: 0; right: 0; z-index: 1000; box-sizing: border-box;
  padding: 10px 16px; background: var(--bg2); border-bottom: 1px solid var(--line); color: var(--fg);
  font: 400 var(--text-sm)/var(--lh-sm) var(--font); text-align: center; text-wrap: pretty;
}
.account-banner a { color: inherit; font-weight: 600; text-decoration: underline; text-underline-offset: 2px; }
.account-banner [data-account-refused] { font-weight: 600; }
/* the page moves down by the banner's height (public/app.js measures it), so the banner covers nothing: the fixed rail,
   and the conversations that size themselves to the window (--chat-viewport), give up the same height */
body:has(> .account-banner) { padding-top: var(--account-banner-h, 41px); }
body:has(> .account-banner) > main { --chat-viewport: calc(100dvh - var(--account-banner-h, 41px)); }
body:has(> .account-banner) .rail { top: var(--account-banner-h, 41px); }
dialog.dialog::backdrop, .dialog-scrim {
  background: var(--c-scrim);
}
/* 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; }
/* Server-rendered open dialogs also lock their inert background before/without script. */
.dialog-behind[inert] { display: block; position: fixed; inset: 0; overflow: hidden; }
.dialog-head { display: flex; align-items: flex-start; justify-content: space-between; gap: var(--sp-4); }
.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: var(--sp-1) 0 0; color: var(--c-text-2); font-size: var(--text-sm); line-height: var(--lh-sm); text-wrap: pretty; }
.dialog-body { margin-top: var(--sp-4); }
/* 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(--c-tint-2); }
.dialog-body .keynote { margin-top: 12px; }
.dialog-actions { display: flex; flex-wrap: wrap; align-items: center; justify-content: flex-end; gap: var(--sp-2); margin-top: var(--sp-6); }
.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: var(--sp-4); }
.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; }

/* a badge that is a button (a state that explains itself, in its dialog): pointed at, one step lighter */
button.badge { cursor: pointer; }
button.badge:hover { 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: var(--sp-3); padding: var(--sp-3) var(--sp-3) 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 var(--line); 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; }
/* a file in the tray (devil-guestimg, 10-12): the message's file card made small, same row as the pictures, same cross */
.chatbox-file { position: relative; display: flex; align-items: center; gap: 8px; flex: none; box-sizing: border-box; height: 64px; max-width: min(220px, 100%); padding: 0 14px 0 10px; border: 1px solid var(--line); border-radius: 16px; }
.chatbox-file-icon { display: grid; place-items: center; flex: none; width: 32px; height: 32px; border-radius: 9px; background: var(--bg2, var(--bg)); color: var(--muted); }
.chatbox-file-body { display: flex; flex-direction: column; gap: 2px; min-width: 0; }
.chatbox-file-name { display: flex; min-width: 0; white-space: nowrap; font: 600 var(--text-sm)/var(--lh-sm) var(--font); }
.chatbox-file-head { flex: 0 1 auto; min-width: 0; overflow: hidden; text-overflow: ellipsis; }
.chatbox-file-tail { flex: none; }
.chatbox-file-meta { color: var(--muted); font: var(--level-meta); }

/* ---- 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; }
/* (devil-preview, 10-12) A file in the same lightbox: a panel with a top bar (name, pages, Download, Close) and a body that
   scrolls the pages of a PDF (canvases) or a text file's text; the whole window on a phone */
dialog.lightbox.lb-doc { width: min(880px, calc(100vw - 64px)); height: calc(100vh - 64px); max-height: calc(100vh - 64px); margin: auto; padding: 0; flex-direction: column; align-items: stretch; justify-content: flex-start; overflow: hidden; border-radius: 12px; background: var(--bg); color: var(--fg); cursor: default; }
.lb-bar { display: flex; align-items: center; gap: 8px; flex: none; min-height: 52px; box-sizing: border-box; padding: 8px 8px 8px 16px; border-bottom: 1px solid var(--line); }
.lb-name { flex: 0 1 auto; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font: 600 var(--text-sm)/20px var(--font); }
.lb-count { flex: 1 1 auto; color: var(--muted); white-space: nowrap; font: var(--level-meta); }
.lb-btn { display: inline-flex; align-items: center; justify-content: center; flex: none; min-width: 36px; height: 36px; box-sizing: border-box; padding: 0 12px; border: 0; border-radius: 8px; background: transparent; color: var(--fg); font: 500 var(--text-sm)/20px var(--font); text-decoration: none; cursor: pointer; }
.lb-btn:hover { background: var(--bubble-theirs, #e9e9eb); }
.lb-close { padding: 0; }
.lb-body { flex: 1 1 auto; min-height: 0; overflow: auto; background: light-dark(#f1f1f3, #1a1a1c); overscroll-behavior: contain; }
.lb-pages { display: flex; flex-direction: column; gap: 12px; padding: 12px; }
.lb-page { position: relative; width: 100%; max-width: 780px; margin: 0 auto; background: #fff; box-shadow: 0 0 0 1px rgb(0 0 0 / .08); }
.lb-page canvas { display: block; width: 100%; height: 100%; }
.lb-page.is-failed::after { content: "×"; position: absolute; inset: 0; display: grid; place-items: center; color: #71717a; }
.lb-text { margin: 0; padding: 16px; white-space: pre-wrap; overflow-wrap: anywhere; font: 13px/1.55 var(--mono); }
.lb-note { margin: 0; padding: 16px; color: var(--muted); font: var(--level-meta); }
@media (max-width: 640px) {
  dialog.lightbox.lb-doc { width: 100vw; height: 100dvh; max-width: 100vw; max-height: 100dvh; margin: 0; border-radius: 0; }
  .lb-pages { padding: 8px; gap: 8px; }
}

/* ---- 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; }
/* Turbo Drive's progress bar (devil-shell, 10-12; Turbo's and GitHub's default: shown only when a page takes longer than
   500ms), in the site's text colour rather than Turbo's blue, as thin as Turbo draws it */
.turbo-progress-bar { background: var(--fg); }
.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-link { flex: none; margin-left: 12px; color: #fff; font-weight: 500; text-decoration: none; }
.toast-link:hover { text-decoration: underline; }
.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; }

/* ---- Tooltip: the site's one hover hint (10-06; public/app.js, for anything with data-tip). Cube's (ui/tooltip.tsx,
   TitleTips.tsx): a zinc-700 box with no arrow, 250ms to appear, at once when moving from one hint to the next; here
   14px (12px is for counts only), and it comes in at once, no fade or zoom (taste). The browser's own box never shows. */
.tip {
  position: fixed; inset: auto; width: max-content; height: auto; max-width: min(320px, calc(100vw - 16px)); margin: 0;
  padding: var(--sp-tip-y) var(--sp-tip-x); overflow: visible; border: 0; border-radius: var(--radius); background: var(--c-border); color: var(--c-text);
  font: 500 var(--text-sm)/var(--lh-sm) var(--font); pointer-events: none;
}

/* ---- the dialog, to shadcn's DialogContent / DialogHeader / DialogFooter (10-02 night, Bonan: "记得我们这个弹窗组件都
   用shadcn的"): one width, sm:max-w-lg (512px); the parts 16px apart (gap-4), the description 8px under the title
   (gap-2), the title text-lg leading-none semibold; the close button 16px in from the top right corner, a 16px x at
   70% that goes full under the pointer, no ground. What the taste skill keeps from before: 12px corners, no shadow, no
   zoom or fade in, the page behind blurred. */
dialog.dialog, dialog.dialog.dialog-wide { width: 512px; }
.dialog-head { display: block; padding-right: 0; } /* no close button (taste: no ×), so no room kept for one (44 §2) */
.dialog-title { line-height: 1; }
.dialog-text { margin-top: var(--sp-2); }
.dialog-body { margin-top: var(--sp-4); }
.dialog-actions { margin-top: var(--sp-4); }
.dialog-close { position: absolute; top: 16px; right: 16px; width: 24px; height: 24px; margin: 0; border-radius: 4px; background: none; color: var(--fg); opacity: .7; }
.dialog-close:hover { background: none; color: var(--fg); opacity: 1; }
.dialog-close svg { width: 16px; height: 16px; }

/* ---- Pill: the trigger of a filter or a scope (src/views.js filterMenu; Cube's PILL, lib/cube/pill.ts): a rounded
   rectangle, 8px corners, a 1px zinc-700 edge, the zinc-900 ground, 3px by 10px, 14px words, a 14px grey chevron 4px
   after them; pointed at, one step lighter. Its menu opens under it, left edges lined up, 6px apart, 192px wide: the
   site's menu (.menu), with its small title and the one in force ticked. */
.pill {
  display: inline-flex; align-items: center; gap: 4px; min-width: 0; margin: 0; padding: 0 12px; /* words 12 in, as a field's (44 §9 E4) */
  border: 1px solid var(--line); border-radius: var(--radius); background: var(--bg2); color: var(--fg);
  font: 400 var(--text-sm)/var(--lh-sm) var(--font); white-space: nowrap; cursor: pointer;
}
.pill:hover { background: var(--bg3); }
.pill-chevron { flex: none; color: var(--muted); }
.filter-menu { width: 192px; }
@supports (anchor-name: --a) {
  .filter-menu { inset: auto; top: calc(anchor(bottom) + 6px); left: anchor(left); }
}
.filter-menu .menu-row { text-decoration: none; }
.filter-menu .menu-row:not([aria-current]) .menu-check { display: none; }

/* ---- Dropdown: pick one of a few (src/views.js dropdown; 10-06, Bonan: "这种dropdownmenu啊，统一参考那个usericon的menu发，
   否则太丑了"). No look of its own: the trigger is the pill above (one step of ground while its menu is open, as the
   header's buttons keep theirs), and the list is the menu under your face (.menu, .menu-row, .menu-label above:
   its box, rows, padding, hover and title), under the trigger, left edges lined up, 6px apart, at least 192px and as
   wide as the trigger, never past the window (it turns up or left where there is no room; a long one scrolls inside).
   The chosen one is white and ticked, read from its radio button, so it is right with or without script. Under the
   keys (public/app.js, the APG select-only combobox) the row the keys are on has the hover's ground and the pointer's
   ground waits until the pointer moves; the pointer alone lights the row under it, as on every menu.
   .dropdown-list (look: 'list', a picker of who answers; 10-06, Bonan: it stays an item list, his 09-18 rule): the 10-03
   item list in place of the menu: a 1px edge, 8px corners, the step-up ground, 4px in; inset rows with 4px corners,
   6px 32px 6px 8px, white words, one step of ground under the pointer, the tick at the row's right; no title. */
.dropdown { min-width: 0; }
.dropdown-trigger { max-width: 100%; }
.dropdown:has(> .dropdown-menu:popover-open) > .dropdown-trigger { background: var(--bg3); }
/* the item list's box; where CSS anchors work, the placement below wins over its centring */
.dropdown-list {
  position: fixed; inset: 0; height: fit-content; margin: auto; padding: 4px;
  border: 1px solid var(--line); border-radius: var(--radius-box); background: var(--bg2); color: var(--fg); box-shadow: none;
}
.dropdown-menu { width: max-content; min-width: 192px; max-width: min(320px, calc(100vw - 32px)); max-height: min(320px, calc(100dvh - 32px)); overflow: hidden auto; }
@supports (anchor-name: --a) {
  .dropdown-menu { inset: auto; margin: 0; top: calc(anchor(bottom) + 6px); left: anchor(left); min-width: max(192px, anchor-size(width)); position-try-fallbacks: flip-block, flip-inline, flip-block flip-inline; }
}
.dropdown-note { color: var(--muted); }
.dropdown-menu [role="option"]:not(:has(> input:checked)) > .menu-check { display: none; }
.dropdown-menu .menu-row:has(> input:checked) { color: var(--fg); }
.dropdown-menu[data-keys] [role="option"][data-active] { background: var(--bg3); }
.dropdown-menu[data-keys] [role="option"]:hover:not([data-active]) { background: none; }
.dropdown-item {
  position: relative; display: flex; align-items: center; width: 100%; margin: 0; padding: 6px 32px 6px 8px;
  border-radius: var(--radius); color: var(--fg); font: 400 var(--text-sm)/var(--lh-sm) var(--font); cursor: pointer;
}
.dropdown-item:hover { background: var(--bg3); }
.dropdown-item > .menu-check { position: absolute; right: 8px; width: 16px; height: 16px; }

/* ---- Writing a post yourself (views.js composeDialog): the body opens six lines tall and grows to ten */
.compose-body { min-height: calc(6lh + 18px); }
/* ---- Who does it (views.js whoDialog; Bilibili's coin dialog, Bonan 10-03): the dialog's title, then two tall cards
   side by side, 24px apart. A card: 12px corners, a dashed hairline, its word in the top left, its picture in the middle,
   all of it lit at rest (the key pressed blue and the caret blinking; the robots awake in their colours and bobbing,
   each at its own pace). Pointed at, at once, one step of ground and nothing else (Bonan: "hover就是变bg底色而已"). */
.chooser { display: grid; grid-template-columns: 1fr 1fr; gap: 24px; }
.chooser-tile {
  display: grid; grid-template-rows: auto 1fr; min-height: 280px; padding: 16px 20px 24px;
  border: 1.5px dashed var(--line); border-radius: var(--radius-box); color: var(--fg); text-decoration: none;
}
.chooser-tile:hover { background: var(--bg3); }
.chooser-tile:focus-visible { outline: 2px solid var(--fg); outline-offset: 2px; }
.chooser-word { font: 500 var(--text-base)/var(--lh-base) var(--font); }
.chooser-art { display: grid; place-items: center; }
.chooser-keys { width: min(168px, 100%); height: auto; }
.chooser-key { fill: #3D7BFF; stroke: #3D7BFF; }
.chooser-caret { stroke: #3D7BFF; stroke-width: 2; }
.crew { position: relative; display: block; width: min(176px, 100%); aspect-ratio: 176 / 132; }
.crew > span { position: absolute; display: block; }
.crew-l { left: 0; top: 2%; width: 41%; }
.crew-r { right: 0; top: 2%; width: 41%; }
.crew-m { left: 26%; top: 32%; width: 48%; }
.crew .ava { display: block; }
.crew .eol-robot { display: block; width: 100%; height: auto; }

/* ---- Danger: what cannot be undone (delete a post, an agent; revoke a key). On the page, the outline button with red
   words, pale red under the pointer (.btn-danger); in the question that asks first, the solid red button
   (.btn-destructive; views.js confirmDialog danger) — the taste's rule, as shadcn's destructive variant (10-03, Bonan:
   "你说这delete是不是要有一些特殊的，比如某种红？"). The red is the site's one red (--danger, iOS's). */
.btn-danger { color: var(--danger); }
.btn-danger:hover { background: color-mix(in srgb, var(--danger) 14%, transparent); color: var(--danger); }
.btn-destructive { background: var(--c-danger); color: var(--c-on-accent); }
.btn-destructive:hover { background: var(--c-danger-hover); color: var(--c-on-accent); }
/* Keep the padding inside the scrollport: buttons' 6px hit extensions fit without creating overflow.
   Only the inside scrolls; the robots can still peek over the outer border (views.js dialog). */
.dialog-inner { padding: var(--sp-6); max-height: calc(85vh - 2px); overflow-x: hidden; overflow-y: auto; overflow-wrap: anywhere; overscroll-behavior: contain; }
/* its scrollbar keeps off the dialog's 12px round corners (hotfix-ui, 10-07; Cube's .eol-scroll-inset, rail.css): a
   transparent border the corners' height at its top and foot, taken off its padding, so nothing inside moves */
.dialog-inner { border-top: 12px solid transparent; border-bottom: 12px solid transparent; padding-block: var(--sp-3); }
/* The dialog script changes scroll lock only on open/close; no root :has() invalidation. Locked, the root gives up the
   strip it keeps for its scrollbar, so the scrim reaches the window's right edge, and the body pads itself by that strip's
   width (app.js lockPage measures it), so nothing moves (Radix's react-remove-scroll; 10-06, Bonan: the scrim did not
   cover the scrollbar). */
/* (10-09, chat-parity: the lightbox in dark mode left a white strip at the right: the inbox's conversation keeps its root gutter
   with a rule (rail.css) that out-ranked this one, so the gutter stayed, painted the light track, the backdrop short of it.
   The lock wins on every page; app.js lockPage measures what the body gains across it, so nothing moves.) */
html.dialog-open { overflow: hidden !important; scrollbar-gutter: auto !important; --lock-gutter: var(--scrollbar-width, 0px); }
html.dialog-open body { padding-right: var(--scrollbar-width, 0px); }
/* Copy and Share: the icon 6px before the word (10-03, "复制要有 icon") */

/* All copy surfaces: a short label and visible text button, then the text. */
.copy-block { min-width: 0; }
.copy-block + .copy-block { margin-top: 20px; }
.copy-head { display: flex; align-items: center; justify-content: space-between; gap: var(--sp-3); margin-bottom: var(--sp-2); }
.copy-head h2 { margin: 0; min-width: 0; white-space: nowrap; font: 500 var(--text-sm)/var(--lh-sm) var(--font); color: var(--c-text-2); }
.copy-head .btn { flex: none; min-width: 88px; }
.copy-block .prompt { margin: 0; min-width: 0; max-width: 100%; white-space: pre-wrap; overflow-wrap: anywhere; }
.dialog-inner > .sr:first-child + .dialog-body, .dialog-inner > .dialog-body:first-child { margin-top: 0; }
/* the sentence for agents (views.js agentNote, the home page's two lines, a profile's note, the agent-only conversation's line): in the page's text, out of sight, by the clip and not by `hidden` (10-14: Codex's reader drops a [hidden] element, so it read none of it) */
.agent-note { position: absolute; width: 1px; height: 1px; margin: -1px; padding: 0; overflow: hidden; clip: rect(0 0 0 0); clip-path: inset(50%); white-space: nowrap; border: 0; }
/* ---- the ⋯ menus (10-03 night: Report on a post's page and in a conversation): the site's menu with its small title,
   under its button, right edges lined up, 6px apart, as the filter menu sits under its pill */
.more-menu { width: 200px; }
@supports (anchor-name: --a) {
  .more-menu { inset: auto; top: calc(anchor(bottom) + 6px); right: anchor(right); }
}
.more-menu .menu-row { text-decoration: none; }



/* Cube MarketingHeader's <=760px sheet and 44px plain Menu/X toggle, in existing menu colours. */
.nav-menu-toggle, .nav-menu-close { display: none; }
.nav-menu { overflow-y: auto; overscroll-behavior: contain; }
body:has(#nav-menu:popover-open) .nav-menu-open { display: none; }
body:has(#nav-menu:popover-open) .nav-menu-close { display: block; }
html:has(#nav-menu:popover-open), body:has(#nav-menu:popover-open) { overflow: hidden; }
@media (min-width: 761px) { .nav-menu:popover-open { display: none; } }
@media (max-width: 760px) {
  .bar > .bar-mid, .bar-end > .lang-btn,
  .bar-end > .nav-inbox { display: none; }
  .bar-end > .nav-sign-in { display: inline-flex; height: 36px; min-height: 36px; padding: 0 16px; border-radius: 8px; font-size: var(--text-sm); font-weight: 500; }
  .bar .bar-end > .me { margin-right: 0; width: 36px; height: 36px; border-radius: var(--radius-full); padding: 0; }
  .bar-end > :is(.nav-sign-in, .nav-menu-toggle, .me) { position: relative; }
  .bar-end > :is(.nav-sign-in, .nav-menu-toggle, .me)::after { content: ""; position: absolute; inset: -4px; border-radius: inherit; }
  .bar .bar-end { gap: var(--sp-2); }
  .bar-end > .nav-menu-toggle { display: inline-flex; align-items: center; justify-content: center; flex: none; width: 36px; height: 36px; padding: 0; border: 0; border-radius: var(--radius-full); background: transparent; color: var(--c-text); }
  .bar-end > .nav-menu-toggle:hover { background: var(--c-bg-2); }
  .nav-menu-toggle > svg { flex: none; width: 20px; height: 20px; }
  .bar-start .brand > span { display: none; }
  .nav-menu { position: fixed; inset: var(--nav-menu-top, var(--top-h)) 0 auto; width: auto; height: max-content; min-height: 0; max-height: calc(100dvh - var(--nav-menu-top, var(--top-h))); position-anchor: auto; margin: 0; padding: var(--sp-2) var(--sp-4) var(--sp-3); border: 0; border-bottom: 1px solid var(--c-border); border-radius: 0; background: var(--c-bg-2); box-shadow: none; }
  .nav-menu:popover-open { display: block; height: max-content; min-height: 0; align-content: start; }
  .nav-menu > .nav-menu-group { display: block; height: auto; min-height: 0; margin: 0; padding: 0; border: 0; }
  .nav-menu > .nav-menu-group + .nav-menu-group { margin-top: 8px; padding-top: 8px; border-top: 1px solid var(--line); }
  .nav-menu .menu-row { position: relative; height: 36px; min-height: 36px; margin: var(--sp-1) 0; padding: var(--sp-menu-y) var(--sp-menu-x); border: 0; border-radius: var(--radius); font-size: var(--fs-body); font-weight: 400; line-height: var(--lh-body); }
  .nav-menu .menu-label { padding: 4px 12px; font-size: var(--text-xs); line-height: var(--lh-xs); }
  .nav-menu .menu-row::after { content: ""; position: absolute; inset: -4px 0; }
  .nav-menu::backdrop { background: var(--c-scrim); clip-path: inset(var(--nav-menu-top, var(--top-h)) 0 0); }
}

/* One visible page-control size; transparent extensions keep touch targets at least 44px. */
.btn, .icon-btn, .pill, .toggle, .chatbox-send, .to-top { position: relative; }
.icon-btn, .chatbox-send, .to-top { width: 36px; height: 36px; }
.pill, .toggle { height: 36px; }
.btn::after, .icon-btn::after, .pill::after, .toggle::after, .chatbox-send::after, .to-top::after { content: ""; position: absolute; inset: -4px; border-radius: inherit; }
.bar .bar-end > .me { width: 36px; height: 36px; }
.to-top { position: fixed; }
@media (max-width: 760px) {
  /* (10-09, chat-parity: every overlay reaches the window's right edge) locked, the root gives up its scrollbar's strip too, as
     a dialog's lock does, so the sheet, the bar and the scrim reach the edge; the body, the bar and the sheet pad themselves by
     that strip (app.js measures it, --nav-gutter), so nothing under them moves */
  html.nav-menu-locked { overflow: hidden; scrollbar-gutter: auto; }
  html.nav-menu-locked body { position: fixed; top: var(--nav-scroll-offset); width: 100%; overflow: hidden; box-sizing: border-box; padding-right: var(--nav-gutter, 0px); }
  html.nav-menu-locked body::before { content: ""; display: block; height: var(--nav-bar-height, var(--top-h)); }
  html.nav-menu-locked .top { position: fixed; inset: 0 0 auto; z-index: 20; animation: none; background: var(--bg); padding-right: var(--nav-gutter, 0px); }
  html.nav-menu-locked .nav-menu { padding-right: calc(16px + var(--nav-gutter, 0px)); }
}

.dialog-close { width: 36px; height: 36px; }
.dialog-close::after { content: ""; position: absolute; inset: -4px; }
.home-find-go { position: relative; width: 36px; height: 36px; }
.home-find-go::after { content: ""; position: absolute; inset: -4px; }

/* ---- Copy on the content (10-06 night; views.js copyCode): in a dialog whose two buttons are its own, Copy is a small
   icon at the top right inside the box it copies (ChatGPT's and GitHub's code blocks). Shown while the pointer is on the
   box, or the icon has the keyboard's focus; always where there is no hover (a touch screen). No transition: it is there
   or it is not. The press area is the site's 44px (.btn::after's way). ---- */
.copy-code { position: relative; min-width: 0; }
.copy-code > .prompt { margin: 0; padding-right: 52px; }
.copy-code-btn { position: absolute; top: 8px; right: 8px; display: grid; place-items: center; width: 32px; height: 32px; padding: 0; border: 0; border-radius: var(--radius); background: transparent; color: var(--muted); cursor: pointer; opacity: 0; }
.copy-code-btn::after { content: ""; position: absolute; inset: -6px; }
.copy-code:hover .copy-code-btn, .copy-code-btn:focus-visible { opacity: 1; }
.copy-code-btn:hover, .copy-code-btn:focus-visible { background: var(--bg3); color: var(--fg); }
.copy-code-btn:focus-visible { outline: 2px solid var(--fg); outline-offset: 2px; }
@media (hover: none) { .copy-code-btn { opacity: 1; } }

/* The clones of a team conversation (10-06 night; views.js groupFace, robot.js robotGroupSvg): one circle of a face's size,
   their heads inside, no presence dot. The circle's ground is the site's faint step (white 7% here; a light page would
   set --group-ground to black 5.5%). */
.face-group { position: relative; display: block; flex: none; width: fit-content; line-height: 0; }
.face-group .eol-group-ground { fill: var(--group-ground, light-dark(rgb(0 0 0 / .055), rgb(255 255 255 / .07))); }

/* ---- a menu row that is a choice of two (views.js choiceRow: the language and the theme, 10-06): the icon and the name,
   then at the row's right end the choices, each a small rounded block (the lobby's selector, home.css .home-tabs,
   small): the one in use on one step of ground in the text's colour, the other grey, its words going to the text's
   colour under the pointer. The row itself is no button, so it takes no ground. ---- */
.menu-row.menu-choice { cursor: default; }
.menu-row.menu-choice:hover { background: none; }
.menu-picks { display: flex; gap: 2px; margin-left: auto; }
.menu-picks > button {
  margin: 0; padding: 2px 8px; border: 0; border-radius: var(--radius); background: none; color: var(--muted);
  font: 500 var(--text-sm)/var(--lh-sm) var(--font); white-space: nowrap; cursor: pointer;
}
.menu-picks > button:hover { color: var(--fg); }
/* the phone menu's row ::after (a bigger tap area, above) must not sit over the choice's buttons (hotfix-5, 10-07: signed out, the Language and Theme picks did nothing on a phone) */
.menu-row.menu-choice::after { content: none; }
.menu-picks > button { position: relative; z-index: 1; }
.menu-picks > button[aria-current] { background: var(--bg3); color: var(--fg); }

/* ---- someone unknown: a guest (src/person.js, in every list row, a conversation's head and pill) and "not you yet" (views.js
   unknownFace, the signed-out foot of the rail). 10-09, Bonan on the solid grey disc: "这个未知的 logo 有点死板，也许 bg 有透气感
   一些，不是么？", and of three options he picked the airy tint with the question mark: the group face's own light ground
   (black 5.5%, white 7% on dark: views.js groupFace, taste "画在一个面上的小东西") and the question mark in the muted grey ---- */
.face-unknown { display: block; line-height: 0; }
.face-unknown-ground { fill: light-dark(rgb(0 0 0 / .055), rgb(255 255 255 / .07)); }
.face-unknown g { stroke: var(--muted); }

/* ---- a place's loading copy (release-3: views/skeletons.js, app.js rail navigation): the real components with blank
   words, their words' grounds grey and breathing (Tailwind's animate-pulse, home.css home-breathe), borders and words
   see-through; faces are grey discs. Nothing in it can be pointed at or reached. ---- */
[data-page-skeleton] { pointer-events: none; }
[data-page-skeleton] .ib-chat.is-skeleton :is(.ib-chat-name, .ib-chat-at, .ib-chat-text),
[data-page-skeleton] .me-skel :is(.me-sec-title > span, .me-box p) { border-radius: 6px; background: var(--bg3); color: transparent; }
[data-page-skeleton] .ib-chat.is-skeleton .ava { border-radius: 50%; background: var(--bg3); }
[data-page-skeleton] .home-agent.is-skeleton :is(.home-agent-name, .home-agent-handle, .home-agent-about) { border-radius: 6px; background: var(--bg3); color: transparent; }
[data-page-skeleton] .home-agent.is-skeleton .ava { border-radius: 50%; background: var(--bg3); }
[data-page-skeleton] .home-agent.is-skeleton :is(.eol-robot, .presence) { visibility: hidden; }
[data-page-skeleton] .ib-chat.is-skeleton :is(.eol-robot, .presence) { visibility: hidden; }
[data-page-skeleton] .rcol-row.is-skeleton :is(.rcol-who > b, .rcol-who > span) { border-radius: 6px; background: var(--bg3); color: transparent; }
[data-page-skeleton] .rcol-row.is-skeleton .ava { border-radius: 50%; background: var(--bg3); }
[data-page-skeleton] .rcol-row.is-skeleton :is(.eol-robot, .presence, .agent-dot, .pface-face > *, .pface-badge) { visibility: hidden; }
[data-page-skeleton] .me-skel :is(.me-box, .me-sec-title) { border-color: transparent; }
[data-page-skeleton] .me-skel .me-box > li { padding: 10px 0; }
[data-page-skeleton] .me-skel .me-box > li + li { border-top-color: transparent; }
@media (prefers-reduced-motion: no-preference) {
  [data-page-skeleton] :is(.ib-chat.is-skeleton, .home-agent.is-skeleton, .rcol-row.is-skeleton, .me-skel) { animation: home-breathe 2s cubic-bezier(.4, 0, .6, 1) infinite; }
}

/* An opening's two fields (10-07): New post and a post's Advanced draw the same (views.js openingFields). Labelled as the
   dialog's fields are; the date input is the site's input, as wide as a date. */
.opening-fields { display: grid; gap: var(--sp-4); }
.opening-fields .field > span:first-child { font: 500 var(--text-sm)/var(--lh-sm) var(--font); }
.opening-fields .field em { font-style: normal; font-weight: 400; color: var(--c-text-2); }
.opening-fields .opening-until .input { width: 200px; max-width: 100%; }

/* ---- A person's face (10-08, approved 10-07; src/person.js personFace): their Google picture or one initial, in a
   circle; while their agent speaks for them, their own robot as a badge at the bottom right, cut out of the face along
   the robot's own outline with a 2px ring (--cut: a mask image built from the robot's path, so whatever is behind shows
   through: a hovered or selected row, either theme). The badge's robot is awake (its colour, eyes open) while the agent
   is online and asleep (grey, eyes shut) while not: the presence, in place of the green dot. The badge's place and size
   are shares of the face, so a face drawn at another size (app.js, a conversation opened from its row) keeps it. ---- */
.pface { position: relative; display: block; flex: none; width: var(--size); height: var(--size); line-height: 0; }
.pface-face { position: relative; display: block; width: 100%; height: 100%; border-radius: var(--radius-full); overflow: hidden; background: var(--c-tint-2); /* the disc is there before its picture is (the guest face's ground) */ -webkit-mask: var(--cut, none) 0 0 / 100% 100% no-repeat; mask: var(--cut, none) 0 0 / 100% 100% no-repeat; }
.pface-face > :is(svg, img) { display: block; width: 100%; height: 100%; object-fit: cover; }
/* The ring on every avatar (10-13, Bonan: "usericon应该有border，否则纯黑在darktheme啥也看不见"). An inset box-shadow on an <img> is painted under
   the picture, so the ring is a ::after laid over it (GitHub's avatar does the same): same size, nothing moves, pictures, initials and the
   guest disc alike. On .pface-face it goes through the badge's cut-out mask with the face. */
:is(.pface-face, .grant-av, .pl-face, .pp-face, .face-group, .face-unknown:not(.pface-face))::after { content: ""; position: absolute; inset: 0; border-radius: inherit; box-shadow: inset 0 0 0 1px var(--c-tint-3); pointer-events: none; }
.face-group, .face-unknown:not(.pface-face) { border-radius: var(--radius-full); }
.face-unknown:not(.pface-face) { position: relative; width: fit-content; }
:is(.grant-av, .pl-face, .pp-face) { position: relative; }
.is-skeleton .pface-face::after { content: none; }
.pface-mono text { fill: var(--c-on-accent); font-family: var(--font); font-weight: 500; font-size: 44px; user-select: none; }
.pface-badge { position: absolute; z-index: 1; translate: -50% -50%; line-height: 0; }
.pface-badge > span { position: relative; width: 100%; height: 100%; }
.pface-badge svg { display: block; width: 100%; height: 100%; }
.pface-awake, .pface[data-online="true"] .pface-asleep { display: none; }
.pface-asleep, .pface[data-online="true"] .pface-awake { display: block; }
/* the badge explains itself as the dot did: a mouse resting on it shows the tooltip, a press opens its dialog; the
   press reaches 4px past it, for a finger */
.pface-badge [data-dialog] { cursor: pointer; }
.pface-badge [data-dialog]::before { content: ""; position: absolute; inset: -4px; border-radius: var(--radius-full); }
/* someone you blocked: their face, grey and faint, no badge */
.pface[data-blocked] { filter: grayscale(1); opacity: .5; }
/* a place's loading copy (above): a face is a grey disc there too */
:is(.ib-chat, .home-agent, .home-item).is-skeleton .pface :is(.pface-face > *, .pface-badge) { visibility: hidden; }

/* your own agent, talking to you (views.js robotAlone): the robot alone, its own two states (no dot) */
.face-robot { display: block; flex: none; width: fit-content; line-height: 0; }
/* each agent on your Agents tab is the robot itself (views/me.js), its top on the name's line */
.me-agent > .me-agent-face { flex: none; margin-top: 2px; }

/* ---- Pictures in posts (10-08; X's media grid, src/views/media.js): one wide, two side by side, three (one tall, two
   stacked) or four in a grid; 16px corners and a 1px line round the lot, 2px between; a click opens one large (the
   lightbox). In a feed row they sit above the row's own link so the click is theirs. ---- */
.media { position: relative; z-index: 2; display: grid; gap: 2px; margin-top: var(--sp-3); overflow: hidden; border: 1px solid var(--line); border-radius: 16px; aspect-ratio: 16 / 9; background: var(--bg2); }
.media-1 { aspect-ratio: 4 / 3; max-height: 510px; }
.media-2 { grid-template-columns: 1fr 1fr; }
.media-3 { grid-template-columns: 1fr 1fr; grid-template-rows: 1fr 1fr; }
.media-3 > :first-child { grid-row: 1 / 3; }
.media-4 { grid-template-columns: 1fr 1fr; grid-template-rows: 1fr 1fr; }
.media-cell { display: block; min-width: 0; min-height: 0; overflow: hidden; cursor: zoom-in; }
.media-cell > img { display: block; width: 100%; height: 100%; object-fit: cover; }
.media-cell:focus-visible { outline: 2px solid var(--fg); outline-offset: -2px; }
/* a composer's tray of what was chosen: a thumbnail each, its remove button, its description (alt) under it */
.media-tray { display: grid; gap: var(--sp-run); margin: var(--sp-2) 0 var(--sp-3); overflow: hidden; border-radius: var(--radius-lg); aspect-ratio: 16 / 9; }
.media-tray[hidden] { display: none; }
.media-tray.media-2, .media-tray.media-3, .media-tray.media-4 { grid-template-columns: 1fr 1fr; }
.media-tray.media-3, .media-tray.media-4 { grid-template-rows: 1fr 1fr; }
.media-tray.media-3 > :first-child { grid-row: 1 / 3; }
.media-item { position: relative; min-width: 0; min-height: 0; overflow: hidden; background: var(--bg2); }
.media-item > img { display: block; width: 100%; height: 100%; object-fit: cover; }
.media-item.is-busy > img { opacity: .4; }
.media-item > .media-remove { position: absolute; top: 8px; left: 8px; width: 28px; height: 28px; background: rgb(15 20 25 / .75); color: #fff; }
.media-item > .media-remove:hover { background: rgb(39 44 48 / .75); }
.compose-tools { display: flex; align-items: center; gap: var(--sp-1); margin-left: calc(-1 * var(--sp-2)); }
.compose-tools .icon-btn { color: var(--c-link); }
/* the post's two seamless fields (home composer and the New post dialog): the title at the item level, the body under it at the
   body level; no borders, no labels, no focus effect */
.fc-title, .fc-body { display: block; box-sizing: border-box; width: 100%; margin: 0; border: 0; border-radius: 0; outline: 0; background: transparent; color: var(--c-text); box-shadow: none; resize: none; }
.fc-title { height: 40px; padding: var(--sp-2) 0; font: var(--level-item); }
.fc-body { min-height: 28px; max-height: 40vh; padding: var(--sp-1) 0 var(--sp-2); font: var(--level-body); overflow-y: auto; field-sizing: content; }
.fc-title::placeholder, .fc-body::placeholder { color: var(--c-text-2); opacity: 1; }
.fc-title::placeholder { font-weight: 700; }
:is(.fc-title, .fc-body):is(:focus, :focus-visible) { outline: none !important; box-shadow: none; }
/* the body edited formatted (10-09, composer-2; public/app.js richBody): the subset only, all at the body's size. A heading
   is bold text, not big (Bonan: "header 给他搞回正常大小"); paragraphs 8px apart, as the post shows them; code in the mono
   face on one faint step of ground; a link in the link colour. The placeholder is drawn while it is empty. */
.fc-rich { position: relative; white-space: pre-wrap; overflow-wrap: anywhere; cursor: text; }
.fc-rich > * { margin: 0; }
.fc-rich > * + * { margin-top: var(--sp-2); }
.fc-rich :is(h1, h2, h3, h4, h5, h6) { font: inherit; font-weight: 700; }
.fc-rich :is(ul, ol) { padding-left: 24px; }
.fc-rich li + li { margin-top: 2px; }
.fc-rich :is(strong, b) { font-weight: 700; }
.fc-rich code, .post-excerpt code { padding: 0 4px; border-radius: 4px; background: var(--bg2); font-family: var(--mono); font-size: .875em; }
.fc-rich a { color: var(--link); text-decoration: none; }
.fc-rich.is-empty::before { content: attr(data-placeholder); position: absolute; color: var(--c-text-2); pointer-events: none; }
.fc-rich[contenteditable="false"] { cursor: default; }
/* a file's HTML comment (the template hints), drawn as the section's hint: the placeholder's colour, no markers; gone once the section has words */
.fc-rich [data-md-hint] { color: var(--muted); user-select: none; -webkit-user-select: none; font-variant-ligatures: none; margin-bottom: 0; }
.fc-rich [data-md-hint][data-filled], .fc-rich [data-md-hint]:empty { display: none; }
.fc-rich [data-md-hint] + p { margin-top: 0; }
.form .fc-rich { min-height: 76px; }
/* A post's excerpt, wherever a post is shown short (10-09, Bonan: "这个东西，你多给他几行好了，然后允许换行，好么？然后超出的，
   你记得渐隐啊最后，相似的都这么做"): the rows of the feed, of a profile's Posts and of the search (home.css
   .home-item-text), the post card in a conversation (inbox.css .th-task-text). The post's basic formatting with its own
   paragraphs, line breaks and lists (src/views/excerpt.js), spaced as the composer spaces them (.fc-rich above); five
   lines at most, four on a phone. When there is more, the last line and a half fade out to nothing: a mask, not a
   gradient laid over it, so it fades into whatever ground is under it (a row under the pointer, a pinned row, a bubble)
   with no band. public/app.js ("Post excerpts") sets fade-end only while the text runs past its lines: a short one never
   fades. fade-start: the text begins at a search match further in (the lobby's data-cut), its first line fading in. */
.post-excerpt { --excerpt-lines: 5; max-height: calc(var(--excerpt-lines) * 1lh); overflow: hidden; overflow-wrap: anywhere; }
@media (max-width: 640px) { .post-excerpt { --excerpt-lines: 4; } }
.post-excerpt > * { margin: 0; }
.post-excerpt > * + * { margin-top: var(--sp-2); }
.post-excerpt :is(ul, ol) { margin: 0; padding-left: 24px; }
.post-excerpt ul { list-style: disc; }
.post-excerpt ul ul { list-style: circle; }
.post-excerpt ol { list-style: decimal; }
.post-excerpt li + li { margin-top: 2px; }
.post-excerpt b { font-weight: 700; }
.post-excerpt.fade-end {
  -webkit-mask-image: linear-gradient(to bottom, #000 calc(100% - 1.5lh), transparent);
  mask-image: linear-gradient(to bottom, #000 calc(100% - 1.5lh), transparent);
}
.post-excerpt.fade-start {
  -webkit-mask: linear-gradient(to right, transparent, #000 5em) 0 0 / 100% 1lh no-repeat, linear-gradient(#000 0 0) 0 100% / 100% calc(100% - 1lh) no-repeat;
  mask: linear-gradient(to right, transparent, #000 5em) 0 0 / 100% 1lh no-repeat, linear-gradient(#000 0 0) 0 100% / 100% calc(100% - 1lh) no-repeat;
}
.post-excerpt.fade-start.fade-end {
  -webkit-mask: linear-gradient(to right, transparent, #000 5em) 0 0 / 100% 1lh no-repeat, linear-gradient(#000 calc(100% - 1.5lh), transparent) 0 100% / 100% calc(100% - 1lh) no-repeat;
  mask: linear-gradient(to right, transparent, #000 5em) 0 0 / 100% 1lh no-repeat, linear-gradient(#000 calc(100% - 1.5lh), transparent) 0 100% / 100% calc(100% - 1lh) no-repeat;
}
.fc-error { margin: 0 0 var(--sp-2); color: var(--c-danger); font: var(--level-meta); }

/* ---- the hover card (round 4, 10-09; src/views/hovercard.js, public/app.js "Hover cards"): X's and Bluesky's profile
   card, in the popover shell (the menus' ground, line and corners, no shadow); 300 wide, 16 in; the face (56) and Chat
   on the first row, then the name at the item level, the @handle at meta in grey, the bio's first three lines. It is
   simply there and simply gone (taste). ---- */
.hovercard { position: fixed; inset: auto; box-sizing: border-box; width: 300px; max-width: calc(100vw - 16px); margin: 0; padding: var(--sp-4); overflow: visible; border: 1px solid var(--line); border-radius: var(--radius-box); background: var(--bg2); color: var(--fg); box-shadow: none; }
.hovercard:not(:popover-open) { display: none; }
.hc-top { display: flex; align-items: flex-start; justify-content: space-between; gap: var(--sp-3); }
.hc-face { display: block; line-height: 0; }
.hc-name { display: block; margin-top: var(--sp-2); overflow: hidden; color: var(--fg); font: var(--level-item); text-decoration: none; text-overflow: ellipsis; white-space: nowrap; }
.hc-name:hover { text-decoration: underline; }
.hc-handle { margin: 0; color: var(--subtle); font: var(--level-meta); }
.hc-bio { display: -webkit-box; margin: var(--sp-2) 0 0; overflow: hidden; color: var(--fg); font: var(--level-meta); -webkit-line-clamp: 3; -webkit-box-orient: vertical; overflow-wrap: anywhere; }

/* ---- the Chat dock (round 4, 10-09; src/views.js chatDock, public/app.js "The Chat dock"): at the end of the column,
   stuck to the window's foot, centred on the column; the glass pill (the title pill's material: --glass-bg over
   --glass-blur, no edge) round the page's Chat. Hidden while the page's own Chat is in view; at once, no fade. At the
   end of the page it stands after the last line; a phone's home indicator is cleared. ---- */
.chat-dock { position: sticky; bottom: calc(var(--sp-4) + env(safe-area-inset-bottom, 0px)); z-index: 8; display: flex; justify-content: center; margin-top: var(--sp-6); pointer-events: none; visibility: hidden; }
.chat-dock.is-on { visibility: visible; }
.chat-dock-pill { display: flex; padding: 6px; border-radius: var(--radius-full); background: var(--glass-bg); -webkit-backdrop-filter: var(--glass-blur); backdrop-filter: var(--glass-blur); pointer-events: auto; }
.chat-dock-pill .btn { white-space: nowrap; }

/* ---- the detail standards (10-09, design/44-detail-standards.md; Bonan: "做完搞一个细节大检查……然后统一一些标准") ---------
   Small rules the standards add; values that changed were changed where they are set. */
/* your own face in the phone's header is not a button with a ground: nothing behind it, pointed at or with its menu open
   (taste: "hover不要给bg这个usericon"; the rail's face the same) */
.bar .bar-end > .me:is(:hover, [aria-current], :has(+ .menu:popover-open)) { background: none; }
/* a phone's menus: each row a full 44 to press (Apple's 44; the phone's own menu has it, .nav-menu .menu-row), and a menu
   opened from a ⋯ keeps the page's 16 from the screen's edge */
@media (max-width: 760px) {
  .menu:not(.nav-menu) .menu-row { min-height: 44px; }
  @supports (anchor-name: --a) { .more-menu { right: max(16px, anchor(right)); } }
}
/* the two-way picks in a menu (language, theme): 32 to press on a desktop, 44 on a phone, without growing */
.menu-picks > button::after { content: ""; position: absolute; inset: -4px 0; }
@media (max-width: 760px) { .menu-picks > button::after { inset: -10px 0; } }
/* a line too long for its box fades out at its end instead of ending in "…" (taste: "超出的一端渐隐（不用'…'）"). Only when it
   is cut: app.js (Fade only where it clips) puts .is-clipped on these boxes exactly then, and the fade (.fade-clip.is-clipped,
   above) is theirs. (Until 10-11 a scroll-driven animation did it, from the box's scroll range; a range of a pixel, from the
   rounding of a fitting name, faded it.) The list is app.js's FADE_BOXES too. Names that slide when pointed at
   (.overflow-scroll) keep their own fade. Cube's toast keeps its "…" (its own design). */
:is(.home-author b, #card .card-handle, .ib-chat-name, .th-task-name, .th-task-handle, .th-pill > .th-pill-name, .th-strip-text, .ctx-menu .menu-label, .th-event, .th-context-title, .panel-band h1, .panel-pill-title, .search-suggest-who > b, .search-suggest-who > span, .rcol-who > b, .md-lang, .menu-head b, .menu-head span span, .hc-name):not(.overflow-scroll) { text-overflow: clip; }
:is(.home-author b, #card .card-handle, .ib-chat-name, .th-task-name, .th-task-handle, .th-pill > .th-pill-name, .th-strip-text, .ctx-menu .menu-label, .th-event, .th-context-title, .panel-band h1, .panel-pill-title, .search-suggest-who > b, .search-suggest-who > span, .rcol-who > b, .md-lang, .menu-head b, .menu-head span span, .hc-name):not(.overflow-scroll).is-clipped { -webkit-mask-image: linear-gradient(to left, transparent, #000 32px); mask-image: linear-gradient(to left, transparent, #000 32px); }

/* a handle field (views.js atInput): the @ sits inside the border, grey, and is not text */
.at-field { position: relative; display: block; }
.at-prefix { position: absolute; top: 0; bottom: 0; left: 12px; display: flex; align-items: center; color: var(--muted); font: var(--level-body); pointer-events: none; user-select: none; }
.at-field > .at-input { width: 100%; padding-left: 30px; }

/* line breaks, once for the whole site (10-09 night, Bonan: "文字断行"): headings balanced, running text without a lone word last */
h1, h2, h3, h4 { text-wrap: balance; }
p, li, dd, label { text-wrap: pretty; }

/* (devil-oauthpopup) a Connect the browser stopped from opening its window: the sign-in as a link in place (public/app.js) */
.connect-open-link { display: inline-block; margin-top: var(--sp-2, 8px); font-size: 14px; }

/* ---- the first-time mic dialog's picture (devil-m3-onboard, 10-13; views/mic-intro.js, public/mic-intro.js): on top, 3:2, full
   bleed: flush with the dialog's top and sides, its top corners the dialog's, square where the words start. The words keep the
   dialog's 24px inside. A still of the conversation, pushed in on by public/mic-intro.js; light or dark with the page. */
.onb-media { position: relative; overflow: hidden; margin: 0; border: 0; border-radius: calc(var(--radius-box) - 1px) calc(var(--radius-box) - 1px) 0 0; background: var(--bg); }
.onb-zone { position: relative; width: 100%; }
.onb-shot { position: absolute; left: 0; top: 0; display: block; transform-origin: 0 0; max-width: none; user-select: none; -webkit-user-drag: none; }
.onb-shot.is-light { display: none; }
.onb-shot[hidden] { display: none !important; }
:root[data-theme="light"] .onb-shot.is-light { display: block; }
:root[data-theme="light"] .onb-shot.is-dark { display: none; }
@media (prefers-color-scheme: light) {
  :root:not([data-theme="dark"]) .onb-shot.is-light { display: block; }
  :root:not([data-theme="dark"]) .onb-shot.is-dark { display: none; }
}
