/* ============================================================
   个人作品�?· 样式�?
   纯离线：�?@import、无网络字体、无外部资源
   ============================================================ */

/* ---------- 设计令牌 ---------- */
:root {
  --wrap: 1120px;
  --pad-x: clamp(18px, 4vw, 40px);
  /* 顶栏高度：扁一条。main 的最小高度也用它，两处必须同�?*/
  --head-h: 48px;
  --sp-1: 8px;
  --sp-2: 14px;
  --sp-3: 22px;
  --sp-4: 34px;
  --sp-5: 56px;
  --sp-6: 88px;
  /* 全站统一的圆角：所有圆角矩形都用这一个值，不再分大中小�?
     圆形（头�?圆点/返回顶部按钮）仍然用 50%，不在这里管 */
  --radius: 16px;
  /* Dock 图标的边长和它自己的 R 角。图标是 64 格的 SVG、圆角 rx=13，
     所以图标实际圆角 = 边长 × 13 / 64（视口变窄、图标变小时一起缩） */
  --dock-icon: clamp(48px, 12vw, 58px);
  --dock-icon-radius: calc(var(--dock-icon) * 13 / 64);
  /* Dock 那一条栏自己的圆角：比图标再圆一点（想调大小只改这一个数） */
  --dock-radius: 20px;
  --font-sans: system-ui, -apple-system, "Segoe UI", "Microsoft YaHei UI", "Microsoft YaHei",
               "PingFang SC", "Hiragino Sans GB", Roboto, "Helvetica Neue", Arial, sans-serif;
  --font-mono: ui-monospace, "Cascadia Mono", Consolas, "SFMono-Regular", Menlo, monospace;
  --ease: cubic-bezier(.22, .61, .36, 1);
  --dur: .5s;
  /* 指针配色不跟主题走，而是看鼠标下方是深是浅（JS 算亮度后�?on-dark 类）�?
     只有深浅两档，不参与色相运算，所以不会出现补色�?
     颜色直接由当套配色的强调色算出来（浅色墨 = 提亮，深色墨 = 压暗），
     所以换配色时指针的花色跟着换、看得出来的那种换法�?
     默认那套"森林�?的四个原值在下面�?[data-palette="green"] 里钉住，一点不�?*/
  --cursor-on-dark: color-mix(in srgb, var(--accent) 55%, #ffffff);
  --cursor-on-light: color-mix(in srgb, var(--accent) 45%, #000000);
  --cursor-on-dark-hover: color-mix(in srgb, var(--accent) 35%, #ffffff);
  --cursor-on-light-hover: color-mix(in srgb, var(--accent) 25%, #000000);
  /* 鼠标位置�?1 ~ 1），�?JS 写入，背景靠它做视差 */
  --mx: 0;
  --my: 0;
}

/* 深色（默认） */
:root,
html[data-theme="dark"] {
  --bg: #0a0f0c;
  --bg-soft: #0d1512;
  --surface: rgba(20, 30, 25, .62);
  --surface-solid: #111a15;
  --border: rgba(120, 200, 155, .16);
  --border-strong: rgba(120, 200, 155, .3);
  --text: #e7f2ea;
  --text-muted: #9db2a5;
  --accent: #07c160;
  --accent-ink: #04170d;
  --accent-soft: rgba(7, 193, 96, .14);
  --trace: #35ff9c;                    /* 顶部示波器光束的荧光�?*/
  --shadow: 0 18px 50px rgba(0, 0, 0, .45);
  --code-bg: rgba(0, 0, 0, .35);
  --tag-ink: #07c160;
  /* 悬停预览：深色模式下预览的是浅色模式的样�?*/
  --preview-bg: #f6f8f6;
  --preview-ink: #10201a;
  --grain: .07;                        /* 噪点颗粒的浓�?*/
}

/* 浅色 */
html[data-theme="light"] {
  --bg: #f6f8f6;
  --bg-soft: #eef3ef;
  --surface: rgba(255, 255, 255, .82);
  --surface-solid: #ffffff;
  --border: rgba(12, 80, 46, .14);
  --border-strong: rgba(12, 80, 46, .28);
  --text: #10201a;
  --text-muted: #55685e;
  --accent: #0b7d43;          /* 浅色底上保证 4.5:1 以上对比�?*/
  --accent-ink: #ffffff;
  --accent-soft: rgba(11, 125, 67, .12);
  --trace: #0bbf63;                    /* 浅底上要深一点，否则荧光发散不开就看不清 */
  --shadow: 0 16px 40px rgba(16, 40, 28, .12);
  --code-bg: rgba(16, 40, 28, .06);
  --tag-ink: #0a6c3a;
  /* 悬停预览：浅色模式下预览的是深色模式的样�?*/
  --preview-bg: #0a0f0c;
  --preview-ink: #e7f2ea;
  --grain: .055;                       /* 浅色底上颗粒要淡一点，否则显脏 */
}

/* ---------- 配色风格（设�?App 里切换） ----------
   默认那套"森林�?就是上面�?:root / data-theme 值，不在这里重复写�?
   每种配色只改"彩色"那几个变量（底色、窗口底色、边框、强调色、荧光、指针色），
   文字灰阶和阴影沿用深浅色那套，这样两种模�?× 五种配色都不用各写一遍排版�?
   data-palette �?app.js（和 <head> 里那段引导脚本）写在 <html> �?*/
/* 森林绿：就上面的 :root 值。指针那四个色值钉回原来的�?root 里现在是按强调色算的�?*/
html[data-palette="green"] {
  --cursor-on-dark: #b9ffdd;
  --cursor-on-light: #055f33;
  --cursor-on-dark-hover: #ffffff;
  --cursor-on-light-hover: #022d19;
}
/* 深海�?*/
html[data-palette="ocean"][data-theme="dark"] {
  --bg: #070d14;
  --bg-soft: #0a1520;
  --surface: rgba(16, 30, 44, .62);
  --surface-solid: #0d1822;
  --border: rgba(120, 176, 220, .16);
  --border-strong: rgba(120, 176, 220, .32);
  --text: #e6f0f8;
  --text-muted: #9aafc0;
  --accent: #2f8fe0;
  --accent-ink: #04121f;
  --accent-soft: rgba(47, 143, 224, .16);
  --trace: #4fc3ff;
  --tag-ink: #2f8fe0;
}
html[data-palette="ocean"][data-theme="light"] {
  --bg: #f3f7fb;
  --bg-soft: #e9f1f8;
  --surface: rgba(255, 255, 255, .82);
  --surface-solid: #ffffff;
  --border: rgba(20, 70, 120, .14);
  --border-strong: rgba(20, 70, 120, .28);
  --text: #0e1c28;
  --text-muted: #52697c;
  --accent: #1565c0;             /* 浅底上要压深一点，保证对比�?*/
  --accent-ink: #ffffff;
  --accent-soft: rgba(21, 101, 192, .12);
  --trace: #0d6ec2;
  --tag-ink: #12559f;
}
/* 紫罗�?*/
html[data-palette="violet"][data-theme="dark"] {
  --bg: #0c0913;
  --bg-soft: #120d1c;
  --surface: rgba(32, 24, 48, .62);
  --surface-solid: #171126;
  --border: rgba(178, 150, 240, .18);
  --border-strong: rgba(178, 150, 240, .34);
  --text: #efe9fb;
  --text-muted: #ab9fc4;
  --accent: #a679ff;
  --accent-ink: #170a2b;
  --accent-soft: rgba(166, 121, 255, .18);
  --trace: #c08bff;
  --tag-ink: #a679ff;
}
html[data-palette="violet"][data-theme="light"] {
  --bg: #f8f5fd;
  --bg-soft: #f0eaf9;
  --surface: rgba(255, 255, 255, .85);
  --surface-solid: #ffffff;
  --border: rgba(80, 40, 150, .14);
  --border-strong: rgba(80, 40, 150, .28);
  --text: #1a1026;
  --text-muted: #665b7a;
  --accent: #6b3fc9;
  --accent-ink: #ffffff;
  --accent-soft: rgba(107, 63, 201, .12);
  --trace: #6b3fc9;
  --tag-ink: #5e35b5;
}
/* 琥珀�?*/
html[data-palette="amber"][data-theme="dark"] {
  --bg: #120d06;
  --bg-soft: #191207;
  --surface: rgba(40, 30, 16, .62);
  --surface-solid: #1d1509;
  --border: rgba(240, 190, 120, .18);
  --border-strong: rgba(240, 190, 120, .34);
  --text: #f8efe2;
  --text-muted: #c0ab92;
  --accent: #ffa63d;
  --accent-ink: #2a1500;
  --accent-soft: rgba(255, 166, 61, .16);
  --trace: #ffb85c;
  --tag-ink: #ffa63d;
}
html[data-palette="amber"][data-theme="light"] {
  --bg: #fbf7f0;
  --bg-soft: #f5eee2;
  --surface: rgba(255, 255, 255, .85);
  --surface-solid: #ffffff;
  --border: rgba(140, 80, 10, .14);
  --border-strong: rgba(140, 80, 10, .28);
  --text: #241a0c;
  --text-muted: #75634c;
  --accent: #b45f00;
  --accent-ink: #ffffff;
  --accent-soft: rgba(180, 95, 0, .12);
  --trace: #c96a00;
  --tag-ink: #a85600;
}
/* 玫瑰红 */
html[data-palette="rose"][data-theme="dark"] {
  --bg: #120a0e;
  --bg-soft: #1a0e14;
  --surface: rgba(42, 22, 30, .62);
  --surface-solid: #1e1017;
  --border: rgba(240, 150, 180, .18);
  --border-strong: rgba(240, 150, 180, .34);
  --text: #fbe9f0;
  --text-muted: #c39dab;
  --accent: #ff6b9d;
  --accent-ink: #2b0714;
  --accent-soft: rgba(255, 107, 157, .16);
  --trace: #ff8fb4;
  --tag-ink: #ff6b9d;
}
html[data-palette="rose"][data-theme="light"] {
  --bg: #fdf3f6;
  --bg-soft: #f8e9ee;
  --surface: rgba(255, 255, 255, .85);
  --surface-solid: #ffffff;
  --border: rgba(150, 40, 80, .14);
  --border-strong: rgba(150, 40, 80, .28);
  --text: #26101a;
  --text-muted: #7a5b67;
  --accent: #c2185b;
  --accent-ink: #ffffff;
  --accent-soft: rgba(194, 24, 91, .12);
  --trace: #d81b60;
  --tag-ink: #ad1457;
}

/* ---------- 隐藏皮肤（在内嵌终端里输入暗号解锁，见 app.js 2c 段） ----------
   解锁前 `html[data-palette]` 不会取到这两个值，所以这几条规则平时是死的 */

/* 彩色皮肤：一整套高饱和的极光感。底色偏靛、强调色用洋红。
   光靠一个 --accent 是"彩色"不起来的，所以这套另外覆写顶部光束和 Dock 图标：
   光束走**分段流动的彩带**、图标整体提饱和 —— 这两笔才是"一眼看出是彩色皮肤"的地方 */
html[data-palette="colorful"][data-theme="dark"] {
  --bg: #0a0716;
  --bg-soft: #130c28;
  --surface: rgba(36, 22, 64, .62);
  --surface-solid: #160f2d;
  --border: rgba(190, 150, 255, .20);
  --border-strong: rgba(190, 150, 255, .38);
  --text: #f4ecff;
  --text-muted: #b5a6d8;
  --accent: #ff4fd8;
  --accent-ink: #2b0020;
  --accent-soft: rgba(255, 79, 216, .18);
  --trace: #7dfcff;
  --tag-ink: #ff7ae4;
  --cursor-on-dark: #ffd6f6;
  --cursor-on-light: #7a0053;
}
html[data-palette="colorful"][data-theme="light"] {
  --bg: #fdf6ff;
  --bg-soft: #f6ebff;
  --surface: rgba(255, 255, 255, .86);
  --surface-solid: #ffffff;
  --border: rgba(120, 40, 160, .16);
  --border-strong: rgba(120, 40, 160, .32);
  --text: #1e0f2b;
  --text-muted: #6b5a80;
  --accent: #c2009b;
  --accent-ink: #ffffff;
  --accent-soft: rgba(194, 0, 155, .12);
  --trace: #d81bb0;
  --tag-ink: #a80087;
  --cursor-on-dark: #ffd6f6;
  --cursor-on-light: #6d0050;
}

/* ---- 彩色皮肤的顶部灯束：分段彩带 + 横向流动 ----
   原来那条灯管是一道纵向渐变（束心发白 → 荧光色 → 透明），整根一个色。
   这套把它换成"一段一个色"的彩带，并且让它一直往前流：

   ::before 是那条彩带：宽度 200%（两个视口），横向排 **24 段、每段 15° 色相**
   （所以任意一屏里能看到的 12 段颜色互不相同），动画把它整体左移自身的 50%
   —— 正好一个视口宽 = 12 段 —— 于是接缝处首尾色相接（345° → 0°），循环无痕。
   位移用 transform（合成器上跑），这条管子只有 2px 高，代价可以忽略。
   ::after 是压在彩带上面的那层白色过曝，保留"荧光灯管"的质感，
   不然整根就是一条扁平的彩色胶带，不像灯了。

   聚焦遮罩（聚拢到鼠标的 --line-focus / --gather-x）留在 .top-strip-line 上，
   遮罩对子元素一样生效，所以"灯管收拢到鼠标处"那套效果原样保留 */
html[data-palette="colorful"] .top-strip-line {
  background: none;
  overflow: hidden;
}
html[data-palette="colorful"] .top-strip-line::before {
  content: "";
  position: absolute;
  top: 0;
  bottom: 0;
  left: 0;
  width: 200%;
  background-image: linear-gradient(90deg,
      hsl(  0 95% 62%) 0 4.1667%, hsl( 15 95% 62%) 0 8.3333%,
      hsl( 30 95% 62%) 0 12.5%,   hsl( 45 95% 62%) 0 16.6667%,
      hsl( 60 95% 62%) 0 20.8333%, hsl( 75 95% 62%) 0 25%,
      hsl( 90 95% 62%) 0 29.1667%, hsl(105 95% 62%) 0 33.3333%,
      hsl(120 95% 62%) 0 37.5%,   hsl(135 95% 62%) 0 41.6667%,
      hsl(150 95% 62%) 0 45.8333%, hsl(165 95% 62%) 0 50%,
      hsl(180 95% 62%) 0 54.1667%, hsl(195 95% 62%) 0 58.3333%,
      hsl(210 95% 62%) 0 62.5%,   hsl(225 95% 62%) 0 66.6667%,
      hsl(240 95% 62%) 0 70.8333%, hsl(255 95% 62%) 0 75%,
      hsl(270 95% 62%) 0 79.1667%, hsl(285 95% 62%) 0 83.3333%,
      hsl(300 95% 62%) 0 87.5%,   hsl(315 95% 62%) 0 91.6667%,
      hsl(330 95% 62%) 0 95.8333%, hsl(345 95% 62%) 0 100%);
  animation: strip-flow 18s linear infinite;
}
html[data-palette="colorful"] .top-strip-line::after {
  content: "";
  position: absolute;
  inset: 0;
  background: linear-gradient(180deg,
      rgba(255, 255, 255, .92) 0%,
      rgba(255, 255, 255, .22) 55%,
      rgba(255, 255, 255, 0) 100%);
}
@keyframes strip-flow {
  from { transform: translateX(0); }
  to   { transform: translateX(-50%); }
}
/* 光晕跟着彩带走：box-shadow 没法分段上色，就整体慢慢转色相，
   周期和彩带一致 —— 看着像同一束光在流 */
html[data-palette="colorful"] .top-strip-glow {
  animation: strip-hue 18s linear infinite;
}
@keyframes strip-hue {
  from { filter: hue-rotate(0deg); }
  to   { filter: hue-rotate(360deg); }
}
html[data-palette="colorful"] .dock-icon svg { filter: saturate(1.5) hue-rotate(-6deg); }

/* 黑白皮肤：把色相全部抽掉。强调色用纯白（浅色主题里用近黑），
   顶部光束的渐变本来就由 --trace 算出来，所以自动变成白的；
   剩下两处**不是**由变量上色的（Dock 图标是 SVG 里写死的 fill、菜是真照片），
   各加一层 grayscale 才算真的黑白。
   设置窗口里那几个色点是取色用的，故意保持原色 —— 它们不是界面配色 */
html[data-palette="mono"][data-theme="dark"] {
  --bg: #0a0a0a;
  --bg-soft: #121212;
  --surface: rgba(30, 30, 30, .66);
  --surface-solid: #151515;
  --border: rgba(255, 255, 255, .14);
  --border-strong: rgba(255, 255, 255, .30);
  --text: #ededed;
  --text-muted: #9a9a9a;
  --accent: #ffffff;
  --accent-ink: #0a0a0a;
  --accent-soft: rgba(255, 255, 255, .14);
  --trace: #ffffff;
  --tag-ink: #dcdcdc;
  --cursor-on-dark: #ffffff;
  --cursor-on-light: #0a0a0a;
  --grain: .05;
}
html[data-palette="mono"][data-theme="light"] {
  --bg: #f5f5f5;
  --bg-soft: #ebebeb;
  --surface: rgba(255, 255, 255, .86);
  --surface-solid: #ffffff;
  --border: rgba(0, 0, 0, .14);
  --border-strong: rgba(0, 0, 0, .30);
  --text: #0d0d0d;
  --text-muted: #5c5c5c;
  --accent: #111111;
  --accent-ink: #ffffff;
  --accent-soft: rgba(0, 0, 0, .10);
  --trace: #1a1a1a;
  --tag-ink: #1a1a1a;
  --cursor-on-dark: #ffffff;
  --cursor-on-light: #000000;
}
html[data-palette="mono"] .dock-icon svg { filter: grayscale(1) contrast(1.06); }
html[data-palette="mono"] .dish-img { filter: grayscale(1) contrast(1.05); }

/* ---------- 基础 ---------- */
*, *::before, *::after { box-sizing: border-box; }

html { scroll-behavior: smooth; }
@media (prefers-reduced-motion: reduce) {
  html { scroll-behavior: auto; }
}

/* 这页是一屏内容，不需要右侧滚动条�?
   注意�?隐藏滚动�?而不�?禁止滚动"：滚�?触屏仍可滑动�?
   只是不再占一条竖条（禁止滚动会连带废掉锚点跳转） */
html, body {
  scrollbar-width: none;        /* Firefox */
  -ms-overflow-style: none;     /* 旧版 Edge / IE */
}
html::-webkit-scrollbar,
body::-webkit-scrollbar { width: 0; height: 0; }   /* Chrome / Safari / 新版 Edge */

body {
  margin: 0;
  background: var(--bg);
  color: var(--text);
  font-family: var(--font-sans);
  font-size: 16px;
  line-height: 1.7;
  -webkit-font-smoothing: antialiased;
  text-rendering: optimizeLegibility;
  overflow-x: hidden;
}

/* 顶部光束：模仿示波器的荧光轨迹�?
   束心过曝发白，外面一圈圈由亮到散的光�?—�?这是荧光粉余辉的关键�?
   �?一条均匀的灯�?完全不同：亮度是不均匀、由内向外指数衰减的 */
.top-strip {
  position: fixed;
  top: 0;
  left: 0;
  width: 100%;
  height: 2px;
  z-index: 25;
  pointer-events: none;
}
/* A continuous tube and a feathered pool of light share one focal position.
   Keeping the glow full-width avoids the bright rectangular ends of a shrinking shadow. */
.top-strip-glow {
  position: absolute;
  top: 0;
  left: 0;
  width: 100%;
  height: 92px;
  background:
    radial-gradient(ellipse var(--line-focus) 11px at var(--gather-x, 50%) 0,
      color-mix(in srgb, var(--trace) 28%, transparent) 0%,
      color-mix(in srgb, var(--trace) 12%, transparent) 28%, transparent 78%),
    radial-gradient(ellipse var(--line-focus) 66px at var(--gather-x, 50%) 0,
      color-mix(in srgb, var(--trace) 9%, transparent) 0%,
      color-mix(in srgb, var(--trace) 4%, transparent) 35%, transparent 84%);
  transition: --line-focus 1.2s cubic-bezier(.25,.1,.25,1);
}
.top-strip-line {
  position: absolute;
  inset: 0;
  background: linear-gradient(180deg,
      color-mix(in srgb, var(--trace) 36%, #fff),
      color-mix(in srgb, var(--trace) 78%, transparent) 60%,
      color-mix(in srgb, var(--trace) 36%, transparent));
  -webkit-mask-image: radial-gradient(ellipse var(--line-focus) 100px at var(--gather-x, 50%) 0,
      #000 0%, #000d 16%, #0007 43%, #0003 70%, #0002 100%);
  mask-image: radial-gradient(ellipse var(--line-focus) 100px at var(--gather-x, 50%) 0,
      #000 0%, #000d 16%, #0007 43%, #0003 70%, #0002 100%);
  transition: --line-focus 1.2s cubic-bezier(.25,.1,.25,1);
}
@property --line-focus {
  syntax: "<length>";
  inherits: false;
  initial-value: 3000px;
}
@property --gather-x {
  syntax: "<length-percentage>";
  inherits: true;
  initial-value: 50%;
}
.top-strip { transition: --gather-x .9s cubic-bezier(.25,.1,.25,1); }
.top-strip.is-gathered .top-strip-glow,
.top-strip.is-gathered .top-strip-line { --line-focus: 480px; }
@media (prefers-reduced-motion: reduce) {
  .top-strip, .top-strip-line, .top-strip-glow { transition: none; }
}

/* 噪点颗粒：图片是内联 SVG �?feTurbulence 现场生成的，不请求任何外部资源�?
   saturate 0 把彩色噪声转成灰阶；stitchTiles �?180px 的贴片无缝拼接�?
   静态纹理不参与动画，所以不产生每帧重绘的开销 */
body::after {
  content: "";
  position: fixed;
  inset: 0;
  z-index: -1;
  pointer-events: none;
  opacity: var(--grain);
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='180' height='180'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='.85' numOctaves='3' stitchTiles='stitch'/%3E%3CfeColorMatrix type='saturate' values='0'/%3E%3C/filter%3E%3Crect width='180' height='180' filter='url(%23n)'/%3E%3C/svg%3E");
}

/* 隐藏系统原生光标，页面上的绿点就是光标�?
   只在 html.has-cursor-dot 存在时生�?—�?这个类是 JS 把圆点跑起来之后才加的，
   所�?JS 失效 / 触摸设备 / 减少动效时原生光标原样保留，不会出现"没有光标可用" */
html.has-cursor-dot,
html.has-cursor-dot * { cursor: none !important; }

/* 触屏上长按会弹出系统菜单/呼出菜单，和自绘的右键菜单冲突，一并关�?*/
html {
  -webkit-touch-callout: none;
}
/* 点按/长按不要浏览器默认那层按压高亮（Chrome �?rgba(0,0,0,.18) 的黑色覆盖，
   而且是渐显的，看起来就像"黑色慢慢铺满元素"）�?
   这属性本身是继承的，但个别安卓内核只认写在元素自身的值，所以显式铺一层到 * */
html, body, * {
  -webkit-tap-highlight-color: transparent;
}

/* 这是一�?应用"，不是文档：默认全站不可选中，免得在页面上随手一�?
   就把标题、提示、列表拖出一片蓝色高亮�?
   只有用户自己产生的内容例�?—�?输入框里正在打的字，以及聊天里的消息气泡�?
   输入框必须显式放开：user-select 会往下继承，被禁掉的话框内就没法框�?替换�?*/
* {
  -webkit-user-select: none;
  user-select: none;
}
input,
textarea,
[contenteditable="true"] {
  -webkit-user-select: text;
  user-select: text;
}
.chat-msg {
  -webkit-user-select: text;
  user-select: text;
}

/* 鼠标指针：箭头轮廓�?
   热点（箭尖）在左上角，所以不能用负边距居中，元素左上角直接对齐鼠标坐�?*/
.cursor-glow {
  position: fixed;
  left: 0;
  top: 0;
  width: 18px;
  height: 27px;
  /* viewBox �?-1 -1 20 30，缩�?18×27 后比�?0.9：箭�?(0,0) 落在元素�?0.9px 处，
     所以这里补 -0.9px，让箭尖精确压在光标坐标�?*/
  margin: -0.9px 0 0 -0.9px;
  opacity: 0;
  filter: drop-shadow(0 1px 2px rgba(0, 0, 0, .45));
  transition: opacity .18s var(--ease);
  pointer-events: none;
  /* 指针必须是全站最高层：它下面�?Dock(30)、右键菜�?60)�?
     之前�?40，被菜单�?60 压住 —�?几何上就算只挨着一点，指针也会被菜单盖掉，
     而指针是缓动跟随、右键瞬间常还没落到光标点上 */
  z-index: 80;
  will-change: transform;
}
.cursor-glow.is-on { opacity: 1; }
.cursor-glow svg { display: block; width: 100%; height: 100%; }
.cursor-glow path {
  /* 默认�?下方是浅�?处理；JS 判定下方为深色时给元素加 on-dark */
  fill: var(--cursor-on-light);
  stroke: none;
  transition: fill .15s var(--ease);
}
.cursor-glow.on-dark path { fill: var(--cursor-on-dark); }
/* 悬停可点元素时换成同方向的更�?更浅一�?—�?只换色，不改尺寸
   （改尺寸会让箭头从箭尖向下方突然伸长一截） */
.cursor-glow.on-dark.is-link path { fill: var(--cursor-on-dark-hover); }
.cursor-glow:not(.on-dark).is-link path { fill: var(--cursor-on-light-hover); }

/* 缩放把手上的指针：换成双向箭头�?
   箭头的热点是"箭尖对坐�?，缩放箭头得改成"图标中心对坐�?�?
   所以这里把它从左上角往回收半个身位（容器是 18×27，中心在 (9, 13.5)�?*/
.cursor-glow .cursor-resize {
  --resize-size: 28px;
  position: absolute;
  width: var(--resize-size);
  height: var(--resize-size);
  max-width: none;
  /* Cancel the normal pointer's hotspot offset; keep the resize center on the mouse. */
  left: calc(.9px - var(--resize-size) / 2);
  top: calc(.9px - var(--resize-size) / 2);
  transform-origin: center;
  overflow: visible;
  opacity: 0;
}
.cursor-glow .cursor-resize path {
  stroke: rgba(255, 255, 255, .94);
  stroke-width: 1.5;
  stroke-linejoin: round;
  vector-effect: non-scaling-stroke;
}
.cursor-glow.on-dark .cursor-resize path { stroke: rgba(5, 10, 8, .95); }
.cursor-glow.is-resize .cursor-arrow { opacity: 0; }
.cursor-glow.is-resize .cursor-resize { opacity: 1; }
/* 旋转角要和手势轴一致（rotate 在屏幕上是顺时针，y 轴朝下）�?
   0° �?→←（东西边），90° �?↑↓（南北边）；
   45° �?+x 转到 (cos45, sin45) = 右下，即 ↖↘ �?—�?这是"西北/东南"这一对；
   135° �?+x 转到 (-0.707, 0.707) = 左下，即 ↗↙ �?—�?这是"东北/西南"这一对�?
   这两条曾经写反过：拉东南角却给出 ↗↙ 的箭�?*/
.cursor-glow.is-resize.dir-e, .cursor-glow.is-resize.dir-w { --resize-rot: 0deg; }
.cursor-glow.is-resize.dir-n, .cursor-glow.is-resize.dir-s { --resize-rot: 90deg; }
.cursor-glow.is-resize.dir-nw, .cursor-glow.is-resize.dir-se { --resize-rot: 45deg; }
.cursor-glow.is-resize.dir-ne, .cursor-glow.is-resize.dir-sw { --resize-rot: 135deg; }
.cursor-glow .cursor-resize { transform: rotate(var(--resize-rot, 0deg)); }

img, svg { max-width: 100%; }

a { color: var(--accent); text-decoration: none; }
a:hover { text-decoration: underline; }

h1, h2, h3 { line-height: 1.25; margin: 0; text-wrap: balance; }

code {
  font-family: var(--font-mono);
  font-size: .88em;
  background: var(--code-bg);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  padding: 1px 6px;
}

:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 3px;
  border-radius: var(--radius);
}

.wrap {
  width: 100%;
  max-width: var(--wrap);
  margin-inline: auto;
  padding-inline: var(--pad-x);
}

.skip-link {
  position: absolute;
  left: -9999px;
  top: 0;
  z-index: 50;
  background: var(--accent);
  color: var(--accent-ink);
  padding: 10px 16px;
  border-radius: 0 0 var(--radius) 0;
  font-weight: 600;
}
.skip-link:focus { left: 0; }

/* ---------- 顶栏 ---------- */
.site-head {
  position: sticky;
  top: 0;
  z-index: 20;
  backdrop-filter: blur(14px);
  -webkit-backdrop-filter: blur(14px);
  background: color-mix(in srgb, var(--bg) 78%, transparent);
  border-bottom: 1px solid var(--border);
}
@supports not (background: color-mix(in srgb, red 50%, blue)) {
  .site-head { background: var(--bg-soft); }
}

/* 顶栏内容通栏，不再套 .wrap（那个是 1120px 居中的内容栏）�?
   �?macOS 菜单栏那样：站名贴左缘、状态项贴右缘，中间留白 */
.head-inner {
  display: flex;
  align-items: center;
  gap: var(--sp-3);
  min-height: var(--head-h);
  padding-inline: clamp(14px, 1.6vw, 22px);
}

.brand {
  display: inline-flex;
  align-items: center;
  gap: 10px;
  color: var(--text);
  font-weight: 700;
  letter-spacing: .01em;
  flex: none;
}
.brand:hover { text-decoration: none; }
/* 顶栏这几个链接同理：按住拖动一样会把自绘光标钉住（见 .dock-item 的说明） */
.brand,
.site-nav a,
.nav-compact { -webkit-user-drag: none; }
.site-nav {
  display: flex;
  gap: 4px;
  margin-left: auto;
  min-width: 0;
  overflow-x: auto;
  scrollbar-width: none;
}
.site-nav::-webkit-scrollbar { display: none; }

/* 小屏专用：只在窄屏显示的「顶部」链�?*/
.nav-compact { display: none; }
.site-nav a {
  color: var(--text-muted);
  font-size: 14.5px;
  padding: 7px 12px;
  /* 胶囊形状：两端是半圆，不属于"圆角矩形"，不参与 --radius 的统一 */
  border-radius: 999px;
  white-space: nowrap;
  transition: color .2s var(--ease), background .2s var(--ease);
}
.site-nav a:hover { color: var(--text); background: var(--accent-soft); text-decoration: none; }
.site-nav a[aria-current="true"] { color: var(--accent); background: var(--accent-soft); }

.nav-compact {
  align-items: center;
  color: var(--text-muted);
  font-size: 14.5px;
  padding: 7px 13px;
  border-radius: 999px;      /* �?.site-nav a：胶囊，不参与圆角统一 */
  border: 1px solid var(--border);
}
.nav-compact:hover { color: var(--text); border-color: var(--border-strong); text-decoration: none; }

.theme-btn {
  flex: none;
  position: relative;
  display: inline-grid;
  place-items: center;
  width: 38px; height: 38px;
  padding: 0;
  border: 0;                 /* 去掉外圈圆框 */
  background: none;
  color: var(--text);
  cursor: pointer;
  transition: color .25s var(--ease), transform .2s var(--ease);
}
/* 悬停时图标本身也换成“另一个模式”的颜色，免得白图标落在浅色预览上看不见 */
.theme-btn:hover { color: var(--preview-ink); transform: translateY(-1px); }
.theme-btn:focus-visible { color: var(--preview-ink); }
.theme-btn svg { position: relative; z-index: 1; width: 19px; height: 19px; fill: none; stroke: currentColor; stroke-width: 1.7; stroke-linecap: round; }

/* 鼠标悬停时，从图标位置扩散出一大片“另一个模式”的样子�?
   深色模式下预览浅色底，浅色模式下预览深色�?*/
.theme-glow {
  position: absolute;
  left: 50%;
  top: 50%;
  width: clamp(340px, 52vmin, 560px);
  aspect-ratio: 1;
  border-radius: 50%;
  /* closest-side：让渐变�?100% 正好落在可见圆的边界上（默认 farthest-corner
     会跑到方框最远角，结果边界处还剩一点不透明度，被圆形裁成一圈硬边） */
  background: radial-gradient(circle closest-side,
      color-mix(in srgb, var(--preview-bg) 72%, transparent) 0%,
      color-mix(in srgb, var(--preview-bg) 60%, transparent) 16%,
      color-mix(in srgb, var(--preview-bg) 45%, transparent) 32%,
      color-mix(in srgb, var(--preview-bg) 29%, transparent) 50%,
      color-mix(in srgb, var(--preview-bg) 15%, transparent) 68%,
      color-mix(in srgb, var(--preview-bg) 4%, transparent) 84%,
      transparent 100%);
  opacity: 0;
  transform: translate(-50%, -50%) scale(.1);
  pointer-events: none;
  z-index: 0;
  transition: opacity .3s var(--ease), transform .6s var(--ease);
}
.theme-btn:hover .theme-glow,
.theme-btn:focus-visible .theme-glow {
  opacity: 1;
  transform: translate(-50%, -50%) scale(1);
}

/* 点击切换主题时的平滑过渡：让所有颜色属性渐变，而不是瞬间跳�?*/
html.theme-anim,
html.theme-anim *,
html.theme-anim *::before,
html.theme-anim *::after {
  transition:
    background-color .4s var(--ease),
    background-image .4s var(--ease),
    color .4s var(--ease),
    border-color .4s var(--ease),
    fill .4s var(--ease),
    stroke .4s var(--ease),
    box-shadow .4s var(--ease) !important;
}

/* 注册�?<length> 后才能参与动画插值（否则半径会跳变） */
@property --reveal-r {
  syntax: "<length>";
  inherits: false;
  initial-value: 0px;
}

/* 点击切换主题：新主题以图标为圆心扩散�?
   交界处用 mask 的渐变做羽化，所以黑白相遇处是柔和过渡而不是硬�?*/
::view-transition-old(root),
::view-transition-new(root) {
  animation: none;
  mix-blend-mode: normal;
}
/* 总时长要和下面新快照的动画一致，否则会被 UA 默认�?.25s 截断 */
::view-transition-group(root) {
  animation-duration: .4s;
}
::view-transition-new(root) {
  --theme-feather: clamp(60px, 8vmax, 130px);   /* 交界处渐变带的宽�?*/
  -webkit-mask-image: radial-gradient(circle at var(--theme-x, 50%) var(--theme-y, 0px),
      #000 calc(var(--reveal-r) - var(--theme-feather)), transparent var(--reveal-r));
  mask-image: radial-gradient(circle at var(--theme-x, 50%) var(--theme-y, 0px),
      #000 calc(var(--reveal-r) - var(--theme-feather)), transparent var(--reveal-r));
  -webkit-mask-repeat: no-repeat;
  mask-repeat: no-repeat;
  animation: theme-reveal .4s cubic-bezier(.32, 0, .34, 1) forwards;
}
@keyframes theme-reveal {
  from { --reveal-r: 0px; }
  /* 末尾多扩散一个羽化带的宽度：结束时实心部分正好铺满全屏�?
     若只扩到最远角，那个角的透明度永远差一点，动画一结束就会跳一�?*/
  to   { --reveal-r: calc(var(--theme-r, 100vmax) + var(--theme-feather)); }
}

html[data-theme="dark"] .icon-sun { display: none; }
html[data-theme="light"] .icon-moon { display: none; }
/* 首字母在浅色底上不能用白色，改成黑色 */
html[data-theme="light"] .accent-letter { color: #000; }

/* ---------- 主内容：整块在网页正�?---------- */
/* 顶栏在文档流里占的高度是 --head-h + �?1px 下边框（.site-head �?sticky，还在流里）�?
   少减�?1px，整页就永远多出 1px 可滚区域：滚轮一�?背景"就动一下，
   而这页本来就是一屏内容，看着就像窗口没接住滚�?*/
main {
  min-height: calc(100vh - var(--head-h) - 1px);   /* 减掉顶栏，剩下的才是内容�?*/
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  padding: 0 var(--pad-x) 100px;   /* 底部留出 Dock 的空�?*/
}
/* 移动�?100vh 会把地址栏也算进去，实际比可见高度大，于是永远多出一点可滚动区域�?
   dvh �?动态视口高�?，只算真正可见的那部分，支持时优先用�?*/
@supports (height: 100dvh) {
  main { min-height: calc(100dvh - var(--head-h) - 1px); }
}

/* ---------- Hero ---------- */
.hero { padding: 0; }

.hero-inner { display: block; }

.hero-title {
  font-size: clamp(2.6rem, 7.2vw, 4.6rem);
  font-weight: 800;
  letter-spacing: -.02em;
  line-height: 1.12;
  text-align: center; /* 整行居中 */
  /* 一进站大标题就�?沉在背景�?的水印状态：不再先亮清楚、过 0.9 秒再慢慢暗下去�?
     只留 0.35s �?0 淡到 0.2 的入场，免得首帧硬蹦出来 —�?全程不出现提亮的状�?*/
  animation: title-sink .35s var(--ease) forwards;
}
@keyframes title-sink {
  0%   { opacity: 0; }
  100% { opacity: .2; }   /* 水印状态：和原来动画结束时的值一�?*/
}
/* 站名是标识，不是供复制的内容：顶栏与大标题里�?Mai Machine Net 都不可选中 */
.brand,
.hero-title {
  -webkit-user-select: none;
  user-select: none;
}
/* 大标题三个单词平铺一�?*/
.hero-name {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  justify-content: center;
  column-gap: .5em;   /* 单词之间的空�?*/
  /* 首字�?.accent-letter 单独上色为白色，其余字母用鲜艳的主题�?*/
  color: var(--accent);
}
/* 首字母白色：父级 background-clip:text 会把子元素一起裁成透明�?
   取消继承的背景裁剪，首字母才能显示为实心白色 */
.accent-letter {
  font-style: normal;
  color: #fff;
  -webkit-background-clip: border-box;
  background-clip: border-box;
  background-image: none;
}

/* ---------- Dock（毛玻璃 · 扁平�?---------- */
.dock {
  position: fixed;
  left: 50%;
  bottom: 18px;
  transform: translateX(-50%);
  z-index: 40;
  display: flex;
  align-items: flex-end;
  gap: 12px;
  /* 左右 11px + 1px 边框 = 12px，和 gap 一�?—�?
     否则外侧留白会比图标间距多出 3px，三�?app 整组看起来往里挤 */
  padding: 10px 11px;
  /* Dock 外框圆角（单独一个变量，方便调） */
  border-radius: var(--dock-radius);
  border: 1px solid var(--border);
  background: color-mix(in srgb, var(--surface-solid) 52%, transparent);
  backdrop-filter: blur(20px) saturate(140%);
  -webkit-backdrop-filter: blur(20px) saturate(140%);
  box-shadow: 0 6px 20px rgba(0, 0, 0, .22);
}
@supports not ((backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px))) {
  .dock { background: var(--surface-solid); }
}

.dock-item {
  position: relative;
  display: block;
  line-height: 0;
  /* 按住/长按时别让浏览器把 <a> 当成可拖拽对象：一旦进入原生拖拽，
     pointermove 就不再触发，自绘的那个光标会僵在原地；而系统光标又被
     cursor:none 藏掉了，看起来就像整页卡死。和 .dish-img 同一个理由。
     draggable="false"（app.js 统一加）管标准行为，这条管 Chrome/Safari */
  -webkit-user-drag: none;
  user-select: none;
  /* 手指按在 Dock 上要能拖动排序，别让浏览器当成"开始滚页面"而把指针序列掐掉 */
  touch-action: none;
}
/* 图标和气泡也不能被单独拖起来 */
.dock-icon,
.dock-icon svg,
.dock-label {
  -webkit-user-drag: none;
  user-select: none;
}
/* 拖动排序中：被拖的那个抬到最上面，跟手位置由 JS 写 transform */
.dock-item.is-dragging { z-index: 5; }
.dock-item.is-dragging .dock-icon {
  box-shadow: 0 12px 26px rgba(0, 0, 0, .35);
}
/* 拖动排序：被拖的那个要跟手，绝不能有过渡 */
.dock-item.is-dragging { transition: none; }
/* 其余图标用 transform 平移让位（JS 只改 transform，动画交给这条过渡） */
.dock.is-sorting .dock-item:not(.is-dragging) { transition: transform .16s var(--ease); }
/* 松手后让被拖的图标滑回自己的格子。
   时长要和 app.js 里的 DOCK_SETTLE_MS（220）对上，否则会在动画没跑完时
   就把内联 transform 清掉，看起来是"滑到一半停住" */
.dock-item.is-settling { transition: transform .22s var(--ease); }
/* 有保存过的顺序时先把 Dock 藏起来，等 app.js 排好再显示 —— 否则会先按 HTML
   顺序闪一下再跳成你排的顺序（和主题、配色那两处防闪白同理）。
   这个类是 <head> 里那段引导脚本加的，app.js 排完顺序后摘掉 */
html.dock-pending .dock { visibility: hidden; }
.dock-item:hover { text-decoration: none; }

.dock-icon {
  display: block;
  width: var(--dock-icon);
  border-radius: var(--dock-icon-radius);
  overflow: hidden;
  /* 抬升效果加在图标上，不加�?<a> 上：
     加在 <a> 上会让可点区域跟着上移，鼠标停在图标底部几像素内时会反�?
     触发"进入->离开"，图标高频抖�?*/
  transition: transform .2s var(--ease), box-shadow .2s var(--ease);
}
.dock-item:hover .dock-icon {
  transform: translateY(-6px);
  box-shadow: 0 8px 18px rgba(0, 0, 0, .3);
}
.dock-icon svg { display: block; width: 100%; height: auto; }

/* 气泡提示：同样扁�?*/
.dock-label {
  position: absolute;
  left: 50%;
  bottom: 100%;
  transform: translate(-50%, -8px);
  padding: 5px 11px;
  border-radius: var(--radius);
  border: 1px solid var(--border);
  background: var(--surface-solid);
  color: var(--text);
  font-size: 12.5px;
  line-height: 1.4;
  white-space: nowrap;
  opacity: 0;
  pointer-events: none;
  box-shadow: 0 6px 16px rgba(0, 0, 0, .22);
  transition: opacity .18s var(--ease), transform .18s var(--ease);
}
.dock-item:hover .dock-label { opacity: 1; transform: translate(-50%, -14px); }

/* hidden 属性要能压�?.win-body �?display:flex（UA �?[hidden] 规则权重太低�?*/
[hidden] { display: none !important; }

/* ---------- 应用窗口外壳（聊天窗口与夹菜窗口共用�?---------- */
.chat-win,
.app-win {
  position: fixed;
  left: 0;
  top: 0;
  z-index: var(--window-stack, 24); /* Keep windows below the Dock (30).
                               高于顶栏(20)，因为最小化后那条小条要待在顶栏�?*/
  width: min(520px, calc(100vw - 24px));   /* 右侧要放得下在线列表 */
  max-height: calc(100vh - 24px);
  display: flex;
  flex-direction: column;
  border: 1px solid var(--border);
  border-radius: var(--radius);
  background: var(--surface-solid);
  box-shadow: var(--shadow);
  overflow: hidden;
  /* 触控：从窗口上开始的拖动不要变成"滚动背景页面"�?
     窗口内部真正要滚的地方（消息区、在线列表）自己写着 touch-action: pan-y�?
     按规范它们会自己接管，不受这里影响；app.js 那边还有一道按方向判断的兜�?*/
  touch-action: none;
  opacity: 0;
  visibility: hidden;
  transform: scale(.96);
  transition: opacity .18s var(--ease), transform .18s var(--ease), visibility .18s;
}
.chat-win.is-open,
.app-win.is-open { opacity: 1; visibility: visible; transform: none; }

.chat-win.is-focused,
.app-win.is-focused {
  border-color: color-mix(in srgb, var(--accent) 48%, var(--border));
  box-shadow: var(--shadow), 0 0 0 1px color-mix(in srgb, var(--accent) 18%, transparent);
}
.chat-win .win-title,
.app-win .win-title { color: var(--text-muted); }
.chat-win.is-focused .win-title,
.app-win.is-focused .win-title { color: var(--text); }

/* 最大化/还原时的几何过渡。只�?JS 加上 .is-anim 时生效：
   拖动窗口�?JS 会连续写 left/top，如果常驻过渡，窗口会黏在鼠标后面拖不动�?
   时长要和 JS 里的 BOX_MS�?20）一�?*/
.chat-win.is-anim,
.app-win.is-anim {
  transition: opacity .18s var(--ease), transform .18s var(--ease), visibility .18s,
              left .32s var(--ease), top .32s var(--ease),
              width .32s var(--ease), height .32s var(--ease);
}

/* 小条之间"让位"用的就是上面这套 .is-anim（320ms）。
   注意这里**不能**再写一条 `.is-min { transition: ... }` 去覆盖它：
   两者权重一样，谁在后面谁赢，而 .is-anim 是"这一次要动画"的临时类 ——
   盖掉它小条就变成瞬移了。
   节奏上不会有"点了没反应"的观感：让位的**决定**是当场做的
   （chipWins 只看 is-docking，展开的那条立刻不占格子），
   所以小条在点下去的那一瞬间就开始挪，和窗口长回来同步落地。 */

/* 停靠中的小条压在普通窗口上面：展开动画期间那个正在长大的窗口会从顶栏掠过，
   不抬高这一层的话它会盖住旁边的小条，点谁都点不动（就是"停靠的窗口打不开"） */
.chat-win.is-min,
.app-win.is-min { z-index: var(--window-stack, 26); }

/* 关键：内容宽度一旦被 --freeze-w 钉住就**立刻**生效，不能等 .is-anim 加上才生效。
   挂在 .is-anim 下面会多排一次：第一次强制重排时 is-anim 还没加，内容按"跟随窗口宽度"
   排了一遍；加上类之后内容宽才变成冻结值 -> 布局失效 -> 又得整棵重排一次。
   那次多余的重排就是"停顿"的来源。
   现在设 --freeze-w 的同时就已经钉住：收起时 --freeze-w = 当前宽度，
   等于什么都没改，两次强制重排都直接命中已有布局，几乎零成本；
   整个动画期间窗口宽度怎么变，内容的布局都不用重算。 */
.chat-win .win-body,
.app-win .win-body { width: var(--freeze-w, auto); }

/* 这里刻意**不用 contain**：给窗口加 contain:layout 会让浏览器把整棵内容子树标脏、
   当场重排一次 —— 那是拿"一次启动重排"换每帧的隔离，而我们要省的就是重排。
   内容已经因为 --freeze-w 不随窗口宽度变了，每帧本来就不需要重排。 */

/* 收起过程中把标题淡掉：标题要在"绝对居中"（普通窗口）和"跟着圆点排布"（小条）
   之间切换，而 position / transform 的这种切换没法做过渡 ——
   不淡掉的话，切换那一瞬间标题会跳一下。淡掉之后切换发生在看不见的时候 */
.win-title { transition: opacity .12s var(--ease); }
.chat-win.is-rolling .win-title,
.app-win.is-rolling .win-title { opacity: 0; }

/* 最小化：只留标题栏 */
.chat-win.is-min .win-body,
.app-win.is-min .win-body { display: none; }

/* 最小化后缩进顶栏的那条小条：标题挪到三个圆点右边，窄了就省略号。
   这里**保持绝对定位**（不改成 static）是刻意的：
   标题一旦进入文档流，标题栏的高度就从 31px（12 的圆点 + 上下 9 + 1 边框）
   变成 36px（多出标题那 17px 的行盒）。于是收起动画按 31 飞过去、
   动画结束后那次重排（chipLayoutFor 用 barHeight 量到 36）又把它撑高 5px ——
   看上去就是"缩进顶栏之后又抽一下"。钉成绝对定位，两种状态都是 31px。
   49px = 左内边距 10 + 圆点组 31（12+7+0+7+12 再减去最小化圆点的 -7）+ 间隔 8 */
.chat-win.is-min .win-title,
.app-win.is-min .win-title {
  max-width: none;
  left: 49px;
  right: 10px;
  transform: translateY(-50%);
  text-align: left;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* 最小化/还原�?卷起"过渡。窗体在过渡期间仍然参与布局，靠 .chat-win 自身�?
   overflow:hidden 被裁掉，所以能平滑地卷上去；display:none 是没法动画的�?
   直接切会"�?地消失。淡�?淡入另外给在 .win-body 上（这个过渡必须常驻�?
   否则收起时没过渡、展开时也等不到淡入） */
.win-body { transition: opacity .2s var(--ease); }
.chat-win.is-rolling .win-body,
.app-win.is-rolling .win-body { opacity: 0; }

/* 放大：占满大部分屏幕，消息区跟着长高（需要窗口有确定高度，flex 子项才吃得到余量�?*/
.chat-win.is-max {
  width: min(760px, calc(100vw - 24px));
  height: calc(100vh - 24px);
}
.chat-win.is-max .chat-log { height: auto; flex: 1 1 auto; }

/* 手动拖过边框的窗口：尺寸由内联值说了算，多出来的高度要一路传下去�?
   否则窗体不吃余量、日志又固定 190px，拉高之后底下会空一�?*/
.chat-win.is-sized .win-body { flex: 1 1 auto; }
.chat-win.is-sized .chat-log { height: auto; flex: 1 1 auto; }

/* ---------- 八向缩放把手 ---------- */
/* 完全透明，只负责命中区和光标。必须贴在窗口边框内侧：
   父级�?overflow:hidden，放到外面会被直接裁�?*/
.win-grip {
  position: absolute;
  z-index: 4;
  touch-action: none;       /* 触屏上别变成滚动页面 */
  -webkit-user-select: none;
  user-select: none;        /* 拉边框时不要选中文字 */
}
.win-grip--n, .win-grip--s { left: 12px; right: 12px; height: 7px; cursor: ns-resize; }
.win-grip--w, .win-grip--e { top: 12px; bottom: 12px; width: 7px; cursor: ew-resize; }
.win-grip--n { top: 0; }
.win-grip--s { bottom: 0; }
.win-grip--w { left: 0; }
.win-grip--e { right: 0; }
/* 四角要覆盖到角上，但别盖住标题栏左上角那三个圆点（它们在 (10,9) 起�?2px 见方�?*/
.win-grip--nw, .win-grip--ne, .win-grip--sw, .win-grip--se { width: 12px; height: 12px; }
.win-grip--nw { top: 0; left: 0; cursor: nwse-resize; }
.win-grip--ne { top: 0; right: 0; cursor: nesw-resize; }
.win-grip--sw { bottom: 0; left: 0; cursor: nesw-resize; }
.win-grip--se { bottom: 0; right: 0; cursor: nwse-resize; }
/* 拖拽/缩放中别选中文字 */
.chat-win.is-sizing { user-select: none; }
/* 最小化后缩到顶栏的那条小条只有三十几像素高�? 个把手（7px / 12px）几乎把它的边和�?
   铺满了，碰哪儿都变成缩放。小条状态下把手直接摘掉 —�?display:none 连命中测试都没有�?
   所�?放在把手上就换缩放光�?那段逻辑也不会再被触发；还原成正常窗口时把手自然回来 */
.chat-win.is-min .win-grip,
.app-win.is-min .win-grip { display: none; }

.win-bar {
  position: relative;       /* 让标题能绝对居中 */
  flex: none;
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 9px 10px;
  border-bottom: 1px solid var(--border);
  background: color-mix(in srgb, var(--surface-solid) 78%, transparent);
  -webkit-user-select: none;
  user-select: none;
  cursor: grab;
  touch-action: none;       /* 拖动时不要触发页面滚�?*/
}
.win-bar:active { cursor: grabbing; }

/* macOS 的三个交通灯：左�?关闭 / 最小化 / 放大 */
.win-dots {
  position: relative;
  z-index: 5;               /* 压过缩放把手(4)：圆点边角上被把手抢走点击会很难�?*/
  flex: none;
  display: flex;
  gap: 7px;
}
.win-dot {
  position: relative;
  width: 12px; height: 12px;
  padding: 0;
  display: grid;
  place-items: center;
  border: 0;
  border-radius: 50%;
  cursor: pointer;
  /* 内描边让圆点在浅色背景上也有轮廓，和 macOS 一�?*/
  box-shadow: inset 0 0 0 .5px rgba(0, 0, 0, .22);
  transition: filter .15s var(--ease);
}
.win-dot--close { background: #ff5f57; }
.win-dot--min   { background: #febc2e; }
.win-dot--max   { background: #28c840; }

/* 「最小化」圆点在窗口停靠成小条时收起。
   不能用 display:none —— 那是瞬间的，看着就是"啪"一下没了。
   改成把 width 和 margin-right 也一起过渡掉，它一边淡出缩小，右边的圆点一边滑过来。
   margin-right:-7px 是为了抵消 .win-dots 的 gap(7px)：flex 的 gap 不会因为
   元素宽度变 0 就消失，不补这一下会留下双倍的缝。
   visibility 一起过渡是为了让它走出 Tab 焦点顺序，同时不破坏淡出（离散过渡在
   变 hidden 时会等动画跑完才生效） */
.win-dot--min {
  transition: opacity .18s var(--ease), transform .18s var(--ease),
              width .18s var(--ease), margin-right .18s var(--ease),
              visibility .18s var(--ease), filter .15s var(--ease);
}
.chat-win.is-min .win-dot--min,
.chat-win.is-rolling .win-dot--min,
.app-win.is-min .win-dot--min,
.app-win.is-rolling .win-dot--min {
  width: 0;
  margin-right: -7px;
  opacity: 0;
  transform: scale(.4);
  visibility: hidden;
  pointer-events: none;
}
.win-dot:active { filter: brightness(.82); }
/* 里面的小符号平时不显示，悬停这一组时才浮出来 —�?macOS 就是这个行为 */
.win-dot::after {
  font-size: 8px;
  font-weight: 800;
  line-height: 1;
  color: rgba(0, 0, 0, .55);
  opacity: 0;
  transition: opacity .12s var(--ease);
}
.win-dot--close::after { content: "✕"; }
.win-dot--min::after   { content: "−"; }
.win-dot--max::after   { content: "+"; }
.win-dots:hover .win-dot::after { opacity: 1; }

/* 标题居中，像 macOS 的标题栏；不接收指针事件，免得挡住拖�?*/
.win-title {
  position: absolute;
  left: 50%;
  top: 50%;
  transform: translate(-50%, -50%);
  margin: 0;
  font-size: 13.5px;
  font-weight: 600;
  pointer-events: none;
  max-width: calc(100% - 140px);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.win-body {
  padding: 10px;
  display: flex;
  flex-direction: column;
  gap: 8px;
  min-height: 0;            /* 让里面的日志能正确收缩并滚动 */
}
.chat-log {
  margin: 0;
  padding: 0;
  list-style: none;
  height: 190px;
  overflow-y: auto;
  /* 气泡入场/退场会横向平移 ±10px，不锁死横向就会冒出横向滚动条；
    �?overflow 的规则是"一轴不�?visible 时另一轴自动变 auto"�?
     所以只�?overflow-y 是拦不住横向溢出�?*/
  overflow-x: hidden;
  /* 和整页一致：隐藏滚动条但保留滚动能力�?
     否则动画产生的瞬时溢出会让滚动条忽隐忽现，连带内容抖�?*/
  scrollbar-width: none;
  /* 滚到底之后不�?接力"去滚背景页面（app.js 里还有一道同样的兜底�?
     给不支持 overscroll-behavior 的内核用�?*/
  overscroll-behavior: contain;
  /* 触控：这一块照常上下滑（手指），app.js 那边只有"还能�?时才放行 */
  touch-action: pan-y;
  display: flex;
  flex-direction: column;
  gap: 6px;
}
.chat-log::-webkit-scrollbar { width: 0; height: 0; }
.chat-msg {
  max-width: 84%;
  padding: 6px 10px;
  border-radius: var(--radius);
  font-size: 13.5px;
  line-height: 1.55;
}
.chat-msg--bot {
  align-self: flex-start;
  border: 1px solid var(--border);
  background: var(--bg-soft);
  color: var(--text-muted);
}
.chat-msg--me {
  align-self: flex-end;
  background: color-mix(in srgb, var(--accent) 22%, transparent);
  color: var(--text);
}
/* 对方的“正在输入”提示：套用对方气泡的样式，半透明 + 虚线边，表示还没真的发出来。
   文字固定为“对方正在输入”，宽度由这句话撑开，不再跟随对方打的字数变化 */
.chat-msg--draft {
  opacity: .5;
  border-style: dashed;
  overflow-wrap: anywhere;
  animation: draft-in .18s var(--ease) both;
}
@keyframes draft-in {
  from { opacity: 0; transform: translate(0, 4px) scale(.98); }
  to   { opacity: .5; transform: none; }
}
/* 气泡入场。只有新出现的消息带 .is-new —�?否则每次轮询重画都会全部重播一�?*/
.chat-msg.is-new { animation: bubble-in .22s var(--ease) both; }
.chat-msg--bot.is-new { animation-name: bubble-in-left; }
/* 自己发出去的：弹一下（和收到的"滑入"区分开，发送才有手感） */
.chat-msg--me.is-new { animation: bubble-pop .28s var(--ease) both; }
@keyframes bubble-in {
  from { opacity: 0; transform: translateY(5px) scale(.97); }
  to   { opacity: 1; transform: none; }
}
@keyframes bubble-in-left {
  from { opacity: 0; transform: translate(-8px, 4px) scale(.97); }
  to   { opacity: 1; transform: none; }
}
@keyframes bubble-pop {
  0%   { opacity: 0; transform: translateY(8px) scale(.86); }
  60%  { opacity: 1; transform: translateY(-1px) scale(1.03); }
  100% { opacity: 1; transform: none; }
}

/* 消息作废时的退场：两侧各自往回收，像这段对话被收回去了�?
   时长要和 JS 里的 EXIT_MS 一致（200ms），否则会先重画再播�?*/
.chat-msg.is-out { animation: bubble-out .2s var(--ease) both; }
.chat-msg--bot.is-out { animation-name: bubble-out-left; }
.chat-msg--me.is-out { animation-name: bubble-out-right; }
@keyframes bubble-out {
  to { opacity: 0; transform: translateY(-6px) scale(.92); }
}
@keyframes bubble-out-left {
  to { opacity: 0; transform: translateX(-10px) scale(.92); }
}
@keyframes bubble-out-right {
  to { opacity: 0; transform: translateX(10px) scale(.92); }
}

/* 系统提示（未配对、对话刚开始、出错等）：居中一行淡�?*/
.chat-msg--sys {
  align-self: center;
  max-width: 100%;
  padding: 2px 0;
  background: none;
  border: 0;
  color: var(--text-muted);
  font-size: 12.5px;
  text-align: center;
}
.chat-form { display: flex; gap: 6px; }

/* 聊天态：左边消息、右边在线列�?*/
.chat-body {
  flex-direction: row;
  gap: 10px;
  align-items: stretch;
}
.chat-main {
  flex: 1;
  min-width: 0;
  display: flex;
  flex-direction: column;
  gap: 8px;
}
.chat-side {
  flex: none;
  width: 118px;
  display: flex;
  flex-direction: column;
  gap: 6px;
  padding-left: 10px;
  border-left: 1px solid var(--border);
}
.side-title {
  margin: 0;
  font-size: 11.5px;
  letter-spacing: .08em;
  color: var(--text-muted);
}
.user-list {
  margin: 0;
  padding: 0;
  list-style: none;
  display: flex;
  flex-direction: column;
  gap: 4px;
  overflow-x: hidden;
  overflow-y: auto;
  scrollbar-width: none;
  overscroll-behavior: contain;   /* 同上：滚到头别把背景页面接力滚走 */
  touch-action: pan-y;            /* 触控：这一块照常上下滑（手指） */
}
.user-list::-webkit-scrollbar { width: 0; height: 0; }
.user-item {
  display: flex;              /* 名字 + 状态标签一行排开 */
  align-items: center;
  gap: 6px;
  width: 100%;
  padding: 6px 8px;
  border: 1px solid var(--border);
  border-radius: var(--radius);
  background: var(--bg-soft);
  color: var(--text);
  font: inherit;
  font-size: 13px;
  text-align: left;
  cursor: pointer;
}
.user-item:hover {
  border-color: var(--border-strong);
  background: color-mix(in srgb, var(--accent) 16%, transparent);
}
.user-empty { font-size: 12.5px; color: var(--text-muted); }
.user-name {
  flex: 1 1 auto;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* 填名字那一�?*/
.join-body { gap: 10px; }
.join-tip { margin: 0; font-size: 13px; color: var(--text-muted); }
.join-note,
.chat-note { margin: 0; min-height: 1.2em; font-size: 12.5px; color: var(--accent); }
.chat-input {
  flex: 1;
  min-width: 0;
  padding: 7px 10px;
  border: 1px solid var(--border);
  border-radius: var(--radius);
  background: var(--bg-soft);
  color: var(--text);
  font: inherit;
  font-size: 13.5px;
}
.chat-input:focus { outline: none; border-color: var(--border-strong); }
.chat-send {
  flex: none;
  padding: 7px 13px;
  border: 0;
  border-radius: var(--radius);
  background: var(--accent);
  color: var(--accent-ink);
  font: inherit;
  font-size: 13.5px;
  font-weight: 600;
  cursor: pointer;
  transition: transform .12s var(--ease), opacity .18s var(--ease);
}
.chat-send:disabled { opacity: .45; cursor: default; }
.chat-send:not(:disabled):active { transform: scale(.94); }
/* 发送成功后从按钮向外扩一圈，给一次明确的"已发�?反馈 */
.chat-send.is-sent { animation: send-pulse .38s var(--ease); }
@keyframes send-pulse {
  0%   { box-shadow: 0 0 0 0 color-mix(in srgb, var(--accent) 60%, transparent); }
  100% { box-shadow: 0 0 0 10px transparent; }
}

/* ---------- 群聊（拉人 / 成员 / 5 秒消息） ---------- */
/* 邀请结果只染"名字文字"：绿 = 已经进来，红 = 明确拒绝；边框和底纹一律不动 */
.user-name.name-joined { color: #2fbf5f; }
.user-name.name-declined { color: #ff453a; }
.user-tag {
  flex: none;
  font-size: 11px;
  color: var(--text-muted);
}
/* 正在拉人 / 待确认 / 群聊中：看得到，但点不动 */
.user-item.is-busy { opacity: .5; cursor: default; }
.user-item:disabled { cursor: default; }

/* 勾选行：走 role="checkbox"，不用原生 checkbox（省得和主题打架） */
.user-item--pick .user-check {
  flex: none;
  width: 12px;
  height: 12px;
  display: grid;
  place-items: center;
  border: 1px solid var(--border-strong);
  border-radius: 3px;
  font-size: 9px;
  line-height: 1;
  color: var(--accent-ink);
}
.user-item--pick[aria-checked="true"] .user-check {
  background: var(--accent);
  border-color: var(--accent);
}
.user-item--pick[aria-checked="true"] .user-check::after { content: "✓"; }

/* 侧栏底部按钮：发起群聊 / 开始聊天 / 取消拉人 */
.side-actions {
  flex: none;
  display: flex;
  flex-direction: column;
  gap: 4px;
}
.side-btn {
  padding: 6px 8px;
  border: 1px solid var(--border);
  border-radius: var(--radius);
  background: var(--bg-soft);
  color: var(--text);
  font: inherit;
  font-size: 12.5px;
  cursor: pointer;
}
.side-btn--primary {
  border-color: var(--accent);
  background: var(--accent);
  color: var(--accent-ink);
  font-weight: 600;
}
.side-btn:disabled { opacity: .45; cursor: default; }

/* 聊天成员名单（只有群主拿得到） */
.side-title--sub { margin-top: 2px; }
.member-list {
  margin: 0;
  padding: 0;
  list-style: none;
  display: flex;
  flex-direction: column;
  gap: 3px;
  flex: 0 1 auto;
  max-height: 45%;
  overflow-x: hidden;
  overflow-y: auto;
  scrollbar-width: none;
  overscroll-behavior: contain;
  touch-action: pan-y;
}
.member-list::-webkit-scrollbar { width: 0; height: 0; }
.member-item { display: flex; align-items: center; gap: 6px; font-size: 12.5px; }
.member-name {
  flex: 1 1 auto;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.member-tag { flex: none; font-size: 11px; color: var(--text-muted); }
.chat-side.is-room { width: 156px; }

/* 群聊气泡：名字 + 内容；名字只在这一串里的第一条显示 */
.chat-msg--group {
  position: relative;      /* 背景进度条要绝对定位在它上面 */
  overflow: hidden;        /* 进度条裁进圆角里，不会溢出边框 */
  display: flex;
  flex-direction: column;
  gap: 1px;
}
/* 背景进度条：从左往右逐渐充满，正好走完这条消息剩下的寿命
   （--fill-ms 由 JS 按服务端给的剩余毫秒写进来）。满了的那一瞬间，
   JS 那边同一个计时器就把气泡收掉，所以"满"和"消失"是同一个时刻。
   只用 transform: scaleX 动画，走合成层，不引起重排 */
.chat-msg--group::before {
  content: "";
  position: absolute;
  inset: 0;
  z-index: 0;
  transform-origin: left center;
  transform: scaleX(0);
  pointer-events: none;
  background: var(--fill);
  animation: group-fill var(--fill-ms, 5000ms) linear forwards;
}
.chat-msg--group.chat-msg--bot { --fill: color-mix(in srgb, var(--accent) 34%, transparent); }
.chat-msg--group.chat-msg--me  { --fill: color-mix(in srgb, var(--accent-ink) 22%, transparent); }
/* 名字和正文要压在进度条上面（伪元素是定位元素，会盖住静态内容） */
.chat-msg--group > * { position: relative; z-index: 1; }
@keyframes group-fill {
  from { transform: scaleX(0); }
  to   { transform: scaleX(1); }
}
.msg-name { display: none; font-size: 11.5px; color: var(--text-muted); }
.chat-msg--group.is-lead .msg-name { display: block; }
.msg-text { overflow-wrap: anywhere; white-space: pre-wrap; }

/* 消失：高度收缩到 0（时长要和 JS 里的 GROUP_GONE_MS 一致）。
   关键点是不动 transform、只动盒子尺寸 —— 这样下面那些气泡是被布局平稳推上来的，
   而不是等它摘掉之后"啪"地跳一格；同时淡出，收缩过程看不出被压扁的痕迹。
   边框不参与过渡（JS 里当场置 0）：高度已经钉死，边框收掉不会挪动任何东西，
   但能让终点是干净的 0，摘掉那一刻不会剩下 2px 一跳 */
.chat-msg--group.is-gone {
  opacity: 0;
  pointer-events: none;
  transition: height .19s var(--ease), padding .19s var(--ease),
              margin .19s var(--ease), opacity .15s linear;
}
/* 上一串的最后一条走了之后，下一条会接上名字：让名字淡淡浮现 */
.chat-msg--group.is-lead .msg-name { animation: name-in .16s var(--ease) both; }
@keyframes name-in { from { opacity: 0; } to { opacity: 1; } }

/* 通知条（独立于日志：日志重画不会把它抹掉） */
.notice-strip {
  margin: 0;
  padding: 0;
  list-style: none;
  display: flex;
  flex-direction: column;
  gap: 4px;
}
.notice-strip:empty { display: none; }
.notice-item {
  padding: 4px 8px;
  border: 1px dashed var(--border-strong);
  border-radius: var(--radius);
  background: color-mix(in srgb, var(--accent) 10%, transparent);
  font-size: 12px;
  text-align: center;
  animation: notice-in .18s var(--ease) both;
}
.notice-item.is-out { animation: bubble-out .2s var(--ease) both; }
@keyframes notice-in {
  from { opacity: 0; transform: translateY(-4px); }
  to   { opacity: 1; transform: none; }
}

/* 被拉进群聊时的询问面板 */
.invite-body { gap: 14px; padding: 18px 16px; }
.invite-q { margin: 0; font-size: 14px; font-weight: 600; }
.invite-actions { display: flex; gap: 8px; }
.invite-btn {
  flex: 1;
  padding: 8px 10px;
  border: 1px solid var(--border);
  border-radius: var(--radius);
  background: var(--bg-soft);
  color: var(--text);
  font: inherit;
  font-size: 13.5px;
  font-weight: 600;
  cursor: pointer;
  transition: transform .12s var(--ease);
}
.invite-btn--yes {
  border-color: var(--accent);
  background: var(--accent);
  color: var(--accent-ink);
}
.invite-btn:active { transform: scale(.96); }

/* ---------- 菜窗�?---------- */
/* 外壳复用 .app-win。窗口尺寸跟着图片走（JS 按图片实际像素算好写进内联样式）�?
   比例不合时允许留边。它身上没有 .win-grip 把手，缩放逻辑只在聊天窗口那一边，
   所以用户改不了它的大小 */
.dish-win {
  width: min(260px, calc(100vw - 24px));   /* 还没抽图时：标题�?+ 一个按�?*/
  max-height: calc(100vh - 24px);
}
/* 换图导致窗口改尺寸时临时挂上过渡；拖动时必须没有它，否则窗口会黏在鼠标后�?*/
.dish-win.is-morph {
  transition: opacity .18s var(--ease), transform .18s var(--ease), visibility .18s,
              width .3s var(--ease), height .3s var(--ease),
              left .3s var(--ease), top .3s var(--ease);
}
.dish-body { gap: 10px; }

/* 图片舞台：宽高由 JS 按图片算出来写进内联样式，图片用 object-fit: contain
   完整显示，比例不合就在留边处露出舞台底色。没图时 display:none 不占位置 */
.dish-stage {
  position: relative;
  display: none;
  border-radius: var(--radius);
  background: var(--bg-soft);      /* 留边处的底色，跟着主题�?*/
  border: 1px solid var(--border);
  overflow: hidden;
}
.dish-stage.has-img { display: block; }
.dish-img {
  position: absolute;
  left: 0;
  top: 0;
  width: 100%;
  height: 100%;
  object-fit: contain;
  display: block;
  /* 禁止把图片拖出去（拖到桌�?下载栏就�?保存图片"）�?
     draggable="false" 管标准行为，-webkit-user-drag �?Chrome/Safari */
  -webkit-user-drag: none;
  user-select: none;
  pointer-events: none;
}
/* 换图时淡�?+ 轻微放大，和气泡入场是一套手�?*/
.dish-img.is-new { animation: dish-in .3s var(--ease) both; }
@keyframes dish-in {
  from { opacity: 0; transform: scale(.96); }
  to   { opacity: 1; transform: none; }
}

/* 按下时那层黑的扩散半径。注册成 <percentage> 才能参与过渡插�?
   （不注册的话会一格一格地跳，不是"蔓延"�?*/
@property --press-r {
  syntax: "<percentage>";
  inherits: false;
  initial-value: 0%;
}

.dish-pick {
  position: relative;
  width: 100%;
  padding: 11px 14px;
  border: 0;
  border-radius: var(--radius);
  background-color: var(--accent);
  color: var(--accent-ink);
  font: inherit;
  font-size: 14.5px;
  font-weight: 600;
  cursor: pointer;
  /* 去掉系统按钮外观 + 关掉双击缩放的手势识�?*/
  -webkit-appearance: none;
  appearance: none;
  touch-action: manipulation;
  /* 长按相关时长都收在这几个变量里，想调只改这里 */
  --press-hold: .5s;                              /* 按下多久之后黑才开始蔓延（这段时间里一点黑都没有） */
  --press-dur: 3s;                                /* 铺满时间：黑蔓延到盖满整个按钮用多久 */
  --press-alt-at: 2.9s;                           /* 绿字「加菜」开始浮现的时刻（从按下算起�? "变成加菜"的时刻，
                                                     取在铺满�?0.6s（和原来 4.4/5.0 的比例一致）�?
                                                     这也�?松手算长�?的门槛，app.js 直接读这个字面值�?
                                                     **不能太晚**：浏览器有条硬规�?—�?鼠标按下超过 5 秒后
                                                     激活失效，任何方式都打不开选文件框（连直接点在原生 file
                                                     控件上的真点击、按�?5 秒再松手也不弹）。所�?看见加菜
                                                     再松�?（约 3.2s）必须落�?5s 窗口�?*/
  --press-delay: var(--press-alt-at);             /* 绿字浮现�?transition-delay，就是上面那个时�?*/
  --press-out: .22s;                              /* 松开后收回用多久 */
  --press-abort: 4.8s;                            /* 按住到这个时刻还没松手：这次手势当场作废（见下）�?
                                                     依据是浏览器�?5s 激活窗口：实测 4.6s 还能弹选图框�?
                                                     5.0s 就不弹了，所以留 0.2s 余量 —�?到了这一�?
                                                     鼠标松手也弹不出来了，不如直接作废去夹一�?*/
  /* 黑从一个点�?-press-x / --press-y，由 JS �?pointerdown 时写）开始扩散，
     铺满整个按钮为止。圆心的默认尺寸�?farthest-corner，所�?100% 正好够到最远那个角 */
  background-image: radial-gradient(circle at var(--press-x, 50%) var(--press-y, 50%),
      #000 0, #000 var(--press-r), transparent var(--press-r));
  transition: transform .12s var(--ease),
              background-color .2s var(--ease),
              --press-r var(--press-out) var(--ease);   /* 松开：黑快速收�?*/
}
/* 按住：先�?--press-hold（这段时间算"单击"，一点黑都不出现），
   然后黑在 --press-dur 里匀速蔓延到盖满�?
   半径目标�?100%�? farthest-corner，正好够到最远那个角），
   并且这里必须�?linear：站点那�?--ease 前快后慢，配大半径会提前很久就铺满�?
   文字不跟着淡出 —�?深墨绿的「夹菜」会被黑自然吞掉（黑在字下面），
   绿字「加菜」等到快铺满时才浮现，所以全程只有一张字，不会有重影�?
   单击�? --press-hold）时这一段还没开始就被取消了，所以看不到任何�?*/
.dish-pick:not(:disabled):active,
.dish-pick:not(:disabled).is-press {
  --press-r: 100%;
  transform: scale(.97);
  transition: transform .12s var(--ease),
              background-color .2s var(--ease),
              --press-r var(--press-dur) linear var(--press-hold);
}

/* 两层文案：常态是「夹菜」，按住到门槛时浮现绿色的「加菜」�?
   只动 alt 一层：main 不淡出，而是被黑自然吞掉（黑画在文字下面），
   否则两张半透明的字会一直叠在同一位置，糊成一团�?
   绿字自带一块黑底（药丸形）：加菜比"黑铺�?早出现约 1.5 秒，
   没有黑底时按点靠边的黑圈还没盖到中间，绿字会落在绿按钮上（绿on绿看不见�?*/
.dish-pick-alt {
  position: absolute;
  left: 50%;
  top: 50%;
  transform: translate(-50%, -50%);
  padding: 1px 11px 2px;
  border-radius: 999px;
  background-color: #000;
  color: var(--accent);
  white-space: nowrap;
  opacity: 0;
  transition: opacity .3s var(--ease);   /* 松开：绿字快速退�?*/
}
.dish-pick:not(:disabled):active .dish-pick-alt,
.dish-pick:not(:disabled).is-press .dish-pick-alt {
  opacity: 1;
  transition: opacity .6s var(--ease) var(--press-delay);
}
/* 抽图期间（按钮被禁用）：文字�?JS 换成"夹菜中�?，那层绿的「加菜」要收起�?*/
.dish-pick:disabled .dish-pick-alt { opacity: 0; }
/* 抽图期间（按钮被禁用）：保持实心，只是绿得暗一档�?
   �?background-color 而不�?background 简写，免得把上面那层扩散图一起清�?*/
.dish-pick:disabled {
  cursor: default;
  background-color: color-mix(in srgb, var(--accent) 62%, var(--surface-solid));
  color: color-mix(in srgb, var(--accent-ink) 80%, var(--surface-solid));
}
/* 按住超过 --press-abort 还没松手：这次手势作�?—�?黑当场收回，
   然后�?JS 直接夹一张。鼠标这时�?:active 还按着（那条规则是 100%），
   所以选择器必须跟�?:active / .is-press 才压得住（同样权重、后面的赢） */
.dish-pick.is-abort,
.dish-pick.is-abort:active,
.dish-pick.is-abort.is-press {
  --press-r: 0%;
  transform: none;
  transition: transform .12s var(--ease),
              background-color .2s var(--ease),
              --press-r var(--press-out) var(--ease);
}
.dish-pick.is-abort .dish-pick-alt,
.dish-pick.is-abort:active .dish-pick-alt,
.dish-pick.is-abort.is-press .dish-pick-alt {
  opacity: 0;
  transition: opacity .2s var(--ease);
}

/* ---------- 设置窗口 ---------- */
/* 外壳同样复用 .app-win；和菜窗口一样没�?.win-grip 把手，改不了大小 */
.settings-win {
  width: min(340px, calc(100vw - 24px));
  max-height: calc(100vh - 24px);
}
.settings-body {
  gap: 16px;
  padding: 14px 14px 16px;
}
.set-title {
  margin: 0 0 9px;
  font-size: 12.5px;
  font-weight: 600;
  letter-spacing: .04em;
  color: var(--text-muted);
}
/* 配色：两列小卡片，左边一个色�?+ 名字，选中时描一圈强调色 */
.palette-list {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 8px;
}
.palette-item {
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 9px 10px;
  border: 1px solid var(--border);
  border-radius: var(--radius);
  background: var(--surface-solid);
  color: var(--text);
  font: inherit;
  font-size: 13.5px;
  text-align: left;
  cursor: pointer;
  transition: border-color .16s var(--ease), box-shadow .16s var(--ease), transform .12s var(--ease);
}
.palette-item:hover { border-color: var(--border-strong); }
.palette-item:active { transform: scale(.97); }
.palette-item.is-on {
  border-color: var(--accent);
  box-shadow: 0 0 0 2px var(--accent-soft);   /* 用强调色淡淡的描一�?*/
}
.palette-dot {
  flex: none;
  width: 15px;
  height: 15px;
  border-radius: 50%;
  /* 只在色点自己身上描一道很淡的边，深色浅色底上都看得出边界 */
  box-shadow: inset 0 0 0 1px rgba(0, 0, 0, .22);
}
.palette-name { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }

/* ---------- 右键菜单 ---------- */
.ctx-menu {
  position: fixed;
  left: 0;
  top: 0;
  z-index: 60;              /* 高于指针(40)�?Dock(30) */
  min-width: 172px;
  padding: 6px;
  border-radius: var(--radius);
  border: 1px solid var(--border);
  background: var(--surface-solid);
  box-shadow: var(--shadow);
  opacity: 0;
  visibility: hidden;
  transform: scale(.96);
  transform-origin: top left;
  transition: opacity .12s var(--ease), transform .12s var(--ease), visibility .12s;
  -webkit-user-select: none;
  user-select: none;
}
.ctx-menu.is-open { opacity: 1; visibility: visible; transform: none; }
.ctx-item {
  display: block;
  width: 100%;
  padding: 8px 12px;
  border: 0;
  border-radius: var(--radius);
  background: none;
  color: var(--text);
  font: inherit;
  font-size: 14px;
  text-align: left;
  white-space: nowrap;
  cursor: pointer;
}
.ctx-item:hover,
.ctx-item:focus-visible {
  background: color-mix(in srgb, var(--accent) 18%, transparent);
  color: var(--text);
  outline: none;
}
/* 只需键盘操作时才显示焦点圈，鼠标点开时不显示 */
.ctx-item:focus:not(:focus-visible) { outline: none; }

/* 输入框里右键给复�?粘贴，页面空白处右键给刷新页面，两组互斥 */
.ctx-item--edit { display: none; }
.ctx-menu.is-field .ctx-item--edit { display: block; }
.ctx-menu.is-field .ctx-item--page { display: none; }
.ctx-sep {
  height: 1px;
  margin: 5px 8px;
  background: var(--border);
}

/* ---------- 返回顶部 ---------- */
.to-top {
  position: fixed;
  right: clamp(14px, 3vw, 28px);
  bottom: clamp(14px, 3vw, 28px);
  z-index: 25;
  width: 42px; height: 42px;
  display: grid;
  place-items: center;
  border-radius: 50%;
  border: 1px solid var(--border);
  background: var(--surface-solid);
  color: var(--text);
  cursor: pointer;
  box-shadow: var(--shadow);
  opacity: 0;
  visibility: hidden;
  transform: translateY(8px);
  transition: opacity .25s var(--ease), transform .25s var(--ease), visibility .25s;
}
.to-top.is-visible { opacity: 1; visibility: visible; transform: none; }
.to-top svg { width: 20px; height: 20px; fill: none; stroke: currentColor; stroke-width: 1.8; stroke-linecap: round; stroke-linejoin: round; }

/* ---------- 进场动画 ---------- */
.js .reveal {
  opacity: 0;
  transform: translateY(14px);
  transition: opacity var(--dur) var(--ease), transform var(--dur) var(--ease);
}
.js .reveal.is-in { opacity: 1; transform: none; }

/* ---------- 响应�?---------- */
@media (max-width: 740px) {
  :root { --head-h: 44px; }      /* 窄屏再扁一�?*/
  body { font-size: 15.5px; }
  .head-inner { min-height: var(--head-h); gap: 10px; }
  /* 窄屏隐藏锚点导航，避免顶栏横向溢出；改由「顶部」链接代�?*/
  .site-nav { display: none; }
  .nav-compact { display: inline-flex; margin-left: auto; }
  .theme-btn { margin-left: 0; }
  .hero { padding-top: 34px; }
}

@media (max-width: 420px) {
  .hero-title { font-size: 2.4rem; }
}

/* ---------- 打印 ---------- */
@media print {
  body::after, .top-strip, .site-head, .dock, .to-top { display: none !important; }
  body { background: #fff; color: #000; }
  .js .reveal { opacity: 1; transform: none; }
  .hero-title { animation: none; opacity: 1; }
}

/* ---------- 减少动效 ---------- */
@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: .001ms !important;
    transition-duration: .001ms !important;
  }
  /* 无限循环动画必须彻底停掉�?
     只缩短时长会让它每秒重播无数次，白烧 CPU */
  .top-strip { animation: none !important; }
  /* 标题本来就该�?沉在背景�?的水印状态，这里没有动效可言�?
     关掉动画直接给最终透明度就行（不能�?1，那等于把水印点成了亮字�?*/
  .hero-title { animation: none !important; opacity: .2; }
  .js .reveal { opacity: 1; transform: none; }
  .theme-btn:hover { transform: none; }
}

/* ---------- 页内终端 App（Dock 上第五个应用） ----------
   窗口外壳沿用 .app-win / .win-bar / .win-dots，这里只补终端自己的部分。
   警告色单独取一个变量：深底上用亮黄，浅底上要压深才看得清 */
.term-win {
  --term-warn: #f0b34a;
  --term-bg: #000;
  --term-ink: #e2e6ec;
  --term-muted: #89929f;
  width: min(740px, calc(100vw - 24px));
  height: min(460px, calc(100vh - 96px));
}
html[data-theme="light"] .term-win {
  --term-warn: #f0b34a;
  --term-bg: #000;
  --term-ink: #e2e6ec;
  --term-muted: #89929f;
}
.term-win .win-title { font-family: var(--font-mono); font-size: 12px; font-weight: 400; }

/* 拉边框时别选中文字（和 .chat-win.is-sizing 一致）。
   宽高由 term.js 写内联值，这里不用再管：.term-wrap 本来就是 flex:1，
   拉高之后输出区自己长，拉矮了输出区自己滚 */
.term-win.is-sizing { user-select: none; }

/* ---- 最小化 / 最大化 ----
   几何（left/top/width/height）全由 term.js 算并写内联值；这里只补聊天窗口
   那几条"最小化后长什么样"的规则。.app-win.is-anim 已经提供了几何过渡，
   所以不用再给终端单独写过渡 */
.term-win.is-min .win-body { display: none; }
/* 小条里标题怎么摆由通用的 .app-win.is-min .win-title 负责（终端也是 .app-win）。
   这里以前另抄了一份 position:static 的版本 —— 它排在后面、权重一样，
   于是通用规则被它盖掉，终端的小条又多出那 5px 的高度抖动。删掉，只留一处 */
/* 收起过程中把内容淡掉：display:none 是没法动画的，直接切会"啪"地消失 */
.term-win.is-rolling .win-body { opacity: 0; }
/* 小条只有三十几像素高，7px/12px 的把手几乎把边铺满，碰哪儿都变成缩放 —— 直接摘掉 */
.term-win.is-min .win-grip { display: none; }

/* .win-body 本身没有内边距，滚动也不归它管：
   输出区自己滚，这样滚轮不会被窗口外壳吃掉 */
.term-wrap {
  flex: 1 1 auto;
  min-height: 0;
  display: flex;
  flex-direction: column;
  overflow: hidden;
  background: var(--term-bg);
  color: var(--term-ink);
  font-family: var(--font-mono);
  font-size: 13px;
  font-variant-ligatures: none;
  line-height: 1.6;
}

.term-screen {
  flex: 1 1 auto;
  min-height: 0;
  overflow-y: auto;
  overscroll-behavior: contain;
  /* 窗口外壳是 touch-action:none，这里必须放开，否则手机上滑不动输出区 */
  touch-action: pan-y;
  padding: 14px 16px 18px;
  scrollbar-width: thin;
  scrollbar-color: var(--border-strong) transparent;
}
.term-screen::-webkit-scrollbar { width: 10px; }
.term-screen::-webkit-scrollbar-track { background: transparent; }
.term-screen::-webkit-scrollbar-thumb {
  background: var(--border-strong);
  border-radius: 99px;
  border: 3px solid transparent;
  background-clip: content-box;
}

.term-line { white-space: pre-wrap; overflow-wrap: anywhere; }
/* 空行也要占一行高度 */
.term-line:empty::before { content: "\00a0"; }
.term-line.is-dim { color: var(--term-muted); }
.term-line.is-accent { color: var(--accent); }
.term-line.is-warn { color: var(--term-warn); }
.term-line.is-echo { color: var(--term-ink); }
.term-mini-prompt { color: var(--accent); }
.term-output { min-height: 0; }
.term-screen ::selection { background: color-mix(in srgb, var(--accent) 30%, transparent); }

/* banner 要保留空格，所以用 pre；窄屏让它自己横向滚 */
.term-pre {
  margin: 0 0 6px;
  font: inherit;
  white-space: pre;
  overflow-x: auto;
  color: var(--accent);
}

/* help / man 的两列排版 */
.term-line.is-cmd { display: flex; gap: 12px; align-items: baseline; }
.term-usage { flex: 0 0 auto; }
.term-usage::before { content: "  "; }
.term-desc { color: var(--text-muted); }

.term-form {
  flex: none;
  display: flex;
  align-items: baseline;
  gap: 8px;
  margin: 0;
  padding: 0;
  border: 0;
  background: transparent;
}
/* 输入行不要方框，靠这条上边框的变化表示焦点；光标是 caret，不受全局隐藏鼠标指针影响 */
.term-form:focus-within { border: 0; }
.term-form[hidden] { display: none; }
.term-prompt { display: inline-flex; flex: 0 1 auto; min-width: 0; max-width: 70%; white-space: nowrap; user-select: none; }
.term-prompt > span { flex: none; }
.term-prompt > .term-user { flex: 0 1 auto; min-width: 0; overflow: hidden; text-overflow: ellipsis; }
.term-user, .term-sig { color: var(--accent); }
.term-host { color: var(--accent); }
.term-sep { color: var(--term-muted); }
.term-path { color: #76a9ea; }
html[data-theme="light"] .term-path { color: #76a9ea; }
.term-input {
  flex: 1 1 auto;
  min-width: 0;
  padding: 0;
  border: 0;
  background: transparent;
  color: var(--term-ink);
  font: inherit;
  caret-color: var(--accent);
}
.term-input:focus { outline: none; }
.term-input::placeholder { color: var(--text-muted); opacity: .7; }

@media (max-width: 480px) {
  .term-win { height: min(420px, calc(100vh - 72px)); }
  .term-wrap { font-size: 12px; }
  .term-prompt { font-size: 12px; }
}

@media print {
  .term-win { display: none !important; }
}

/* Records: a quiet journal reader inside the desktop window. */
.records-win { width: min(940px, calc(100vw - 24px)); height: min(630px, calc(100dvh - 112px)); }
.records-win.is-sizing { user-select: none; }
.records-layout { flex: 1; min-height: 0; display: grid; grid-template-columns: 250px minmax(0, 1fr); overflow: hidden; position: relative; }
.records-sidebar { display: flex; flex-direction: column; min-height: 0; border-right: 1px solid var(--border); background: color-mix(in srgb, var(--bg) 40%, var(--surface-solid)); }
.records-heading { display: flex; justify-content: space-between; align-items: center; padding: 22px 20px 16px; }
.records-eyebrow { display: block; font: 10px var(--font-mono); letter-spacing: .18em; color: var(--text-muted); margin-bottom: 6px; }
.records-heading h3 { margin: 0; font-size: 23px; font-weight: 600; letter-spacing: -.04em; }
.records-heading h3 span { margin-left: 10px; font: 11px var(--font-mono); color: var(--text-muted); vertical-align: middle; }
.records-refresh { border: 1px solid var(--border); background: transparent; color: var(--text-muted); border-radius: 50%; width: 28px; height: 28px; font-size: 20px; line-height: 1; padding: 0; cursor: pointer; }
.records-refresh:hover { color: var(--accent); border-color: var(--accent); }
.records-actions { display: flex; align-items: center; gap: 7px; flex-shrink: 0; }
.records-submit-open, .records-example-open { display: inline-flex; align-items: center; justify-content: center; border: 1px solid var(--border); background: var(--accent-soft); color: var(--text); border-radius: 7px; padding: 5px 9px; font: 12px/1.4 var(--font-sans); cursor: pointer; white-space: nowrap; }
.records-submit-open:hover, .records-example-open:hover, .records-example-open[aria-expanded="true"] { border-color: var(--accent); color: var(--accent); }
.records-heading { gap: 8px; }
.records-win.is-example .records-reader > :is(.records-empty, .records-article) { display: none; }
.records-example-panel { padding: 30px 34px 38px; }
.records-example-panel[hidden] { display: none; }
.records-example-tools { display: flex; align-items: center; justify-content: space-between; margin-bottom: 20px; }
.records-example-back { border: 0; padding: 4px 0; background: transparent; color: var(--text-muted); font: 12px var(--font-sans); cursor: pointer; }
.records-example-back:hover { color: var(--accent); }
.records-example-status { color: var(--text-muted); font-size: 12px; line-height: 1.7; }
.records-example-status:empty { display: none; }
.records-reader-tools { display: none; }
.records-submit-overlay { position: absolute; inset: 0; z-index: 3; display: flex; align-items: center; justify-content: center; padding: 18px; background: color-mix(in srgb, var(--bg) 70%, transparent); backdrop-filter: blur(10px); overflow-y: auto; }
.records-submit-overlay[hidden] { display: none; }
.records-submit-card { margin: auto; width: min(390px, 100%); background: var(--surface-solid); color: var(--text); border: 1px solid var(--border); border-radius: 14px; box-shadow: 0 18px 60px #0003; padding: 26px; }
.records-submit-card h3 { margin: 7px 0 12px; font-size: 23px; font-weight: 550; letter-spacing: -.035em; }
.records-submit-card p { font-size: 13px; line-height: 1.8; color: var(--text-muted); }
.records-file-label { display: block; margin: 22px 0 9px; font-size: 13px; }
.records-file-label span { display: block; font-size: 11px; color: var(--text-muted); margin-top: 4px; }
#recordsSubmitAuthor { display: block; width: 100%; }
#recordsSubmitFile, #recordsSubmitImages { display: block; width: 100%; font: 12px var(--font-sans); color: var(--text-muted); }
#recordsSubmitFile::file-selector-button, #recordsSubmitImages::file-selector-button { padding: 8px 11px; margin-right: 10px; border: 1px solid var(--border); border-radius: 6px; background: var(--accent-soft); color: var(--text); cursor: pointer; }
.records-submit-card .records-image-hint { font-size: 11px; margin: 9px 0; }
.records-image-list { list-style: none; padding: 0; margin: 10px 0; max-height: 140px; overflow-y: auto; scrollbar-width: thin; }
.records-image-list:empty { display: none; }
.records-image-list li { border-bottom: 1px solid var(--border); padding: 9px 0; overflow-wrap: anywhere; }
.records-image-list span { display: block; font-size: 11px; color: var(--text-muted); margin-bottom: 5px; }
.records-image-list code { font: 11px/1.7 var(--font-mono); color: var(--text); user-select: text; -webkit-user-select: text; }
.records-clear-images { padding: 0; border: 0; background: none; color: var(--text-muted); font: 11px var(--font-sans); cursor: pointer; }
.records-submit-card .records-submit-status { min-height: 23px; margin: 15px 0; overflow-wrap: anywhere; }
.records-submit-card .records-submit-status.is-error { color: #c55353; }
.records-submit-buttons { display: flex; justify-content: flex-end; gap: 9px; }
.records-submit-buttons button { padding: 8px 14px; font: 12px var(--font-sans); border: 1px solid var(--border); border-radius: 7px; color: var(--text); background: transparent; cursor: pointer; }
.records-submit-buttons .records-submit-send { background: var(--text); color: var(--surface-solid); border-color: var(--text); }
.records-submit-buttons button:disabled { opacity: .45; cursor: wait; }
.records-submit-card :focus-visible, .records-submit-open:focus-visible, .records-example-open:focus-visible, .records-example-back:focus-visible { outline: 2px solid var(--accent); outline-offset: 3px; }
.records-search { display: flex; align-items: center; gap: 8px; margin: 0 14px 14px; padding: 7px 10px; border: 1px solid var(--border); border-radius: 8px; }
.records-search svg { width: 15px; height: 15px; fill: none; stroke: var(--text-muted); stroke-width: 1.6; flex: none; }
.records-search input { width: 100%; min-width: 0; border: 0; outline: 0; padding: 0; background: transparent; color: var(--text); font: 12px var(--font-sans); }
.records-search input::placeholder { color: var(--text-muted); }
.records-search:focus-within { border-color: var(--accent); }
.records-list { overflow-y: auto; min-height: 0; flex: 1; padding: 0 9px 12px; scrollbar-width: thin; touch-action: pan-y; overscroll-behavior: contain; }
.records-entry { display: flex; flex-direction: column; width: 100%; text-align: left; gap: 5px; padding: 13px 12px; margin-bottom: 4px; border: 1px solid transparent; border-radius: 9px; background: transparent; color: var(--text); cursor: pointer; font: inherit; }
.records-entry:hover { background: var(--accent-soft); }
.records-entry.is-selected { background: var(--accent-soft); border-color: color-mix(in srgb, var(--accent) 30%, transparent); }
.records-entry time { font: 10px var(--font-mono); color: var(--text-muted); }
.records-entry strong { font-size: 14px; font-weight: 550; line-height: 1.5; overflow-wrap: anywhere; }
.records-entry-summary { font-size: 11px; color: var(--text-muted); line-height: 1.6; display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; }
.records-list-status { margin: 0; padding: 0 20px 15px; font-size: 12px; color: var(--text-muted); }
.records-list-status:empty { display: none; }
.records-reader { min-width: 0; overflow-y: auto; position: relative; scrollbar-width: thin; touch-action: pan-y; overscroll-behavior: contain; }
.records-empty { min-height: 100%; display: flex; flex-direction: column; align-items: center; justify-content: center; text-align: center; padding: 32px; }
.records-empty-mark { font-size: 36px; font-family: Georgia, serif; color: var(--accent); opacity: .55; }
.records-empty h3 { margin: 10px 0 4px; font-size: 18px; font-weight: 500; }
.records-empty p { margin: 0; font-size: 13px; color: var(--text-muted); }
.records-article { max-width: 740px; margin: 0 auto; padding: 34px clamp(22px, 4vw, 48px) 25px; }
.records-meta { margin: 0 0 12px; font: 10px var(--font-mono); color: var(--text-muted); letter-spacing: .05em; }
.records-article-head h1 { font-size: clamp(23px, 2.6vw, 31px); line-height: 1.45; font-weight: 600; letter-spacing: -.04em; margin: 0; overflow-wrap: anywhere; }
.records-tags { display: flex; flex-wrap: wrap; gap: 6px; margin-top: 14px; }
.records-tags:empty { display: none; }
.records-tags span { font-size: 10px; color: var(--text-muted); border: 1px solid var(--border); border-radius: 5px; padding: 1px 7px; }
.records-article-head { padding-bottom: 24px; margin-bottom: 24px; border-bottom: 1px solid var(--border); }
.markdown-body { font-size: 14px; line-height: 1.95; overflow-wrap: anywhere; user-select: text; -webkit-user-select: text; }
.markdown-body * { user-select: text; -webkit-user-select: text; }
.markdown-body > :first-child { margin-top: 0; }
.markdown-body h1, .markdown-body h2, .markdown-body h3, .markdown-body h4 { line-height: 1.5; margin: 1.7em 0 .65em; font-weight: 600; }
.markdown-body h1 { font-size: 1.65em; }
.markdown-body h2 { font-size: 1.35em; }
.markdown-body h3 { font-size: 1.15em; }
.markdown-body p { margin: 0 0 1.2em; }
.markdown-body ul, .markdown-body ol { padding-left: 1.6em; margin: 0 0 1.2em; }
.markdown-body li { margin: .3em 0; }
.markdown-body li p { margin-bottom: .3em; }
.markdown-body blockquote { margin: 1.3em 0; padding: 3px 0 3px 16px; border-left: 2px solid var(--accent); color: var(--text-muted); }
.markdown-body blockquote > :last-child { margin-bottom: 0; }
.markdown-body pre { margin: 1.3em 0; padding: 15px 17px; overflow-x: auto; background: var(--code-bg); border: 1px solid var(--border); border-radius: 9px; line-height: 1.65; font: 12px/1.7 var(--font-mono); }
.markdown-body pre code { padding: 0; border: 0; background: none; font: inherit; white-space: pre; }
.markdown-body code { font-size: .88em; }
.markdown-body table { display: block; max-width: 100%; overflow-x: auto; border-collapse: collapse; margin: 1.3em 0; font-size: 12px; }
.markdown-body th, .markdown-body td { padding: 7px 12px; border: 1px solid var(--border); }
.markdown-body th { background: var(--accent-soft); font-weight: 600; }
.markdown-body img { display: block; max-width: 100%; height: auto; border-radius: 8px; margin: 1.3em auto; }
.markdown-body hr { border: 0; border-top: 1px solid var(--border); margin: 2em 0; }
.markdown-body a { text-decoration: underline; text-underline-offset: 3px; }
.markdown-body input[type="checkbox"] { margin-right: 6px; accent-color: var(--accent); }
.records-article-foot { font: 9px var(--font-mono); letter-spacing: .15em; color: var(--text-muted); opacity: .6; text-align: center; margin-top: 40px; padding-top: 20px; border-top: 1px solid var(--border); }
.records-back { display: none; }
@media (max-width: 650px) {
  .records-layout { grid-template-columns: minmax(0,1fr); }
  .records-sidebar { border-right: 0; }
  .records-reader { display: none; }
  .records-win.is-reading .records-sidebar { display: none; }
  .records-win.is-reading .records-reader { display: block; }
  .records-win.is-example .records-sidebar { display: none; }
  .records-win.is-example .records-reader { display: block; }
  .records-example-panel { padding: 22px; }
  .records-back { display: block; border: 0; padding: 13px 22px 0; background: transparent; color: var(--text-muted); font: 12px var(--font-sans); cursor: pointer; }
  .records-reader-tools { display: flex; justify-content: space-between; align-items: center; padding-right: 20px; }
  .records-reader-tools .records-actions { margin-top: 12px; }
  .records-article { padding-top: 22px; }
}

/* Journal styling for Chat, Kitchen and Settings. Terminal keeps its own theme. */
.journal-app {
  --app-sidebar: color-mix(in srgb, var(--bg) 40%, var(--surface-solid));
  --app-radius: 8px;
}
.journal-app .app-heading {
  padding: 22px 22px 18px;
  border-bottom: 1px solid var(--border);
  background: var(--app-sidebar);
}
.journal-app .app-eyebrow {
  display: block;
  margin-bottom: 6px;
  color: var(--text-muted);
  font: 10px/1.5 var(--font-mono);
  letter-spacing: .18em;
}
.journal-app .app-heading h3 {
  margin: 0;
  font-size: 23px;
  line-height: 1.45;
  font-weight: 600;
  letter-spacing: -.04em;
}
.journal-app .app-heading p {
  margin: 10px 0 0;
  color: var(--text-muted);
  font-size: 13px;
  line-height: 1.8;
  font-weight: 400;
}
.journal-app .app-field-label {
  margin: 20px 22px 9px;
  font-size: 12px;
  color: var(--text-muted);
}
.journal-app .app-footnote {
  margin: 10px 0 0;
  color: var(--text-muted);
  font-size: 11px;
  line-height: 1.6;
}
.journal-app :is(.chat-input, .chat-send, .side-btn, .invite-btn, .dish-pick) {
  border-radius: var(--app-radius);
  font: 12px/1.6 var(--font-sans);
}
.journal-app .chat-input {
  padding: 9px 11px;
  border: 1px solid var(--border);
  background: transparent;
  color: var(--text);
}
.journal-app .chat-input::placeholder { color: var(--text-muted); }
.journal-app .chat-input:focus { border-color: var(--accent); }
.journal-app :is(.chat-send, .invite-btn--yes) {
  padding: 9px 15px;
  border: 1px solid var(--text);
  background: var(--text);
  color: var(--surface-solid);
  font-weight: 500;
}
.journal-app :is(button, input):focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 3px;
}
.journal-app :is(.join-body, .invite-body) { padding: 0; gap: 0; }
.journal-app .join-body .chat-form { margin: 20px 22px 0; gap: 9px; }
.journal-app :is(.join-note, .chat-note) {
  padding: 12px 22px 20px;
  font-size: 12px;
  line-height: 1.6;
  overflow-wrap: anywhere;
}
.journal-app .invite-actions { margin: 20px 22px 0; gap: 9px; }
.journal-app .invite-btn--no { border-color: var(--border); background: transparent; font-weight: 400; }

/* Conversation: the same quiet heading, divided panes and selected rows as Records. */
.journal-app .chat-body { flex: 1; padding: 0; gap: 0; }
.journal-app .chat-main { min-height: 0; gap: 0; }
.journal-app .chat-heading { padding: 20px 22px 16px; background: transparent; }
.journal-app .chat-log { height: 230px; padding: 18px 20px; gap: 10px; scrollbar-width: thin; }
.journal-app .chat-log::-webkit-scrollbar { width: 4px; }
.journal-app .chat-log::-webkit-scrollbar-thumb { background: var(--border); border-radius: 4px; }
.journal-app .chat-main > .chat-form { padding: 14px 20px 0; border-top: 1px solid var(--border); gap: 8px; }
.journal-app .chat-main > .chat-note { padding: 8px 20px 14px; }
.journal-app .chat-msg { padding: 9px 12px; border-radius: 9px; font-size: 13px; line-height: 1.7; }
.journal-app .chat-msg--bot { background: var(--app-sidebar); color: var(--text); }
.journal-app .chat-msg--me { border: 1px solid color-mix(in srgb, var(--accent) 30%, transparent); background: var(--accent-soft); }
.journal-app .chat-msg--sys { margin: auto 0; padding: 12px 0; font-size: 12px; color: var(--text-muted); }
.journal-app .chat-msg--group.chat-msg--bot { --fill: color-mix(in srgb, var(--accent) 12%, transparent); }
.journal-app .chat-msg--group.chat-msg--me { --fill: color-mix(in srgb, var(--accent) 16%, transparent); }
.journal-app .notice-strip { margin: 12px 20px 0; }
.journal-app .notice-strip:empty { margin: 0; }
.journal-app .notice-item { padding: 8px 10px; border-style: solid; border-radius: var(--app-radius); background: var(--accent-soft); }
.journal-app .chat-side { width: 150px; min-height: 0; padding: 22px 12px 16px; gap: 10px; background: var(--app-sidebar); }
.journal-app .chat-side.is-room { width: 160px; }
.journal-app .chat-side > .app-eyebrow { margin: 0; padding: 0 6px; }
.journal-app .side-title { padding: 0 6px; font-size: 14px; line-height: 1.5; font-weight: 550; letter-spacing: -.02em; color: var(--text); }
.journal-app .side-title--sub { margin-top: 8px; }
.journal-app .user-list { flex: 1; min-height: 0; gap: 4px; }
.journal-app .user-item { padding: 10px 8px; border-color: transparent; border-radius: 9px; background: transparent; font-size: 12px; }
.journal-app .user-item:hover { background: var(--accent-soft); border-color: color-mix(in srgb, var(--accent) 30%, transparent); }
.journal-app .user-empty { padding: 8px 6px; font-size: 12px; }
.journal-app .side-actions { gap: 7px; margin-top: 8px; }
.journal-app .side-btn { padding: 8px; background: transparent; }
.journal-app .side-btn--primary { border-color: var(--border); background: var(--accent-soft); color: var(--text); font-weight: 400; }
.journal-app .side-btn:hover { border-color: var(--accent); color: var(--accent); }
.journal-app .member-item { padding: 5px 6px; font-size: 12px; }

/* Kitchen: centered empty state and a restrained action footer. */
.dish-win.journal-app { width: min(360px, calc(100vw - 24px)); min-width: min(280px, calc(100vw - 24px)); }
/* Docked widths come from the shared top-bar layout, including during transitions. */
.dish-win.journal-app:is(.is-rolling, .is-min) { min-width: 0; }
.journal-app .dish-body { padding: 0; gap: 0; }
.journal-app .dish-stage { flex: none; margin: 20px 20px 0; border-radius: var(--app-radius); background: var(--code-bg); }
.journal-app .dish-empty { min-height: 170px; padding: 26px 20px; display: flex; flex-direction: column; align-items: center; justify-content: center; text-align: center; }
.journal-app .dish-stage.has-img + .dish-empty { display: none; }
.journal-app .dish-empty svg { width: 40px; height: 40px; fill: none; stroke: var(--accent); stroke-width: 2; stroke-linecap: round; stroke-linejoin: round; opacity: .55; }
.journal-app .dish-empty h4 { margin: 10px 0 4px; font-size: 18px; font-weight: 500; }
.journal-app .dish-empty p { margin: 0; color: var(--text-muted); font-size: 13px; }
.journal-app .dish-footer { padding: 18px 20px; }
.journal-app .dish-pick {
  padding: 10px 14px;
  border: 1px solid #07c160;
  background-color: #07c160;
  color: #04170d;
  background-image: radial-gradient(circle at var(--press-x, 50%) var(--press-y, 50%), #0b7d43 0, #0b7d43 var(--press-r), transparent var(--press-r));
}
.journal-app .dish-pick-alt { background-color: #0b7d43; color: #fff; }
.journal-app .dish-pick:disabled { background-color: #07c160; color: #04170d; opacity: .45; }

/* Appearance: palette choices behave visually like selected journal entries. */
.settings-win.journal-app { width: min(380px, calc(100vw - 24px)); }
.journal-app .settings-body { padding: 0; gap: 0; overflow-y: auto; touch-action: pan-y; overscroll-behavior: contain; scrollbar-width: thin; }
.journal-app .set-group { padding: 20px 16px; }
.journal-app .set-title { margin: 0 6px 12px; font-size: 12px; font-weight: 400; letter-spacing: 0; }
.journal-app .palette-list { grid-template-columns: minmax(0, 1fr); gap: 4px; }
.journal-app .palette-item { padding: 12px; gap: 12px; border-color: transparent; border-radius: 9px; background: transparent; font-size: 14px; box-shadow: none; }
.journal-app .palette-item:hover { background: var(--accent-soft); border-color: transparent; }
.journal-app .palette-item.is-on { background: var(--accent-soft); border-color: color-mix(in srgb, var(--accent) 30%, transparent); box-shadow: none; }
.journal-app .palette-item::after { content: ""; margin-left: auto; width: 16px; flex: none; color: var(--accent); font-size: 13px; }
.journal-app .palette-item.is-on::after { content: "✓"; }
.journal-app .palette-dot { width: 16px; height: 16px; }

@media (max-width: 480px) {
  .journal-app .chat-heading { padding: 18px 16px 14px; }
  .journal-app .chat-side, .journal-app .chat-side.is-room { width: 116px; padding: 20px 8px 12px; }
  .journal-app .chat-log { padding: 14px 12px; }
  .journal-app .chat-main > .chat-form { padding: 12px 10px 0; gap: 6px; }
  .journal-app .chat-main > .chat-note { padding: 8px 12px 12px; }
  .journal-app .chat-main .chat-input { padding: 8px; }
  .journal-app .chat-main .chat-send { padding: 8px 10px; }
  .journal-app .notice-strip { margin: 10px 12px 0; }
}

/* Name entry is compact and has no resize/maximize affordances. */
.chat-win.is-joining:not(.is-min):not(.is-rolling) { width: min(390px, calc(100vw - 24px)) !important; height: auto !important; }
.chat-win.is-joining .win-grip, .chat-win.is-joining .win-dot--max { display: none; }
.journal-app.is-joining .join-body .chat-form { margin: 16px 18px 0; }
.journal-app.is-joining .join-note { padding: 6px 18px 12px; min-height: 0; }
.journal-app.is-joining .join-note:empty { padding-bottom: 10px; }

/* Account issuance: a black field with a center-out red light and one key. */
html.account-active { overflow: hidden !important; }
.account-stage { position:fixed; inset:0; z-index:2147483100; background:#000; color:#ff303b; display:grid; place-items:center; outline:none; }
html.account-active .account-stage, html.account-active .account-stage * { cursor:default !important; }
html.account-active .account-stage button { cursor:pointer !important; }
html.account-active .cursor-glow { visibility:hidden !important; }
.account-stage .account-lamp-base { z-index:0; animation:account-lamp-handoff 1.1s ease-out both; translate:none; }
.account-stage .account-lamp-base .top-strip-line { -webkit-mask-image:none; mask-image:none; }
.account-red-light { position:absolute; top:0; left:0; right:0; height:2px; z-index:1; background:linear-gradient(180deg,#c85b69,#d92f43 48%,#650d1e); box-shadow:0 0 2px #ff687880,0 0 7px #ee354b99,0 0 17px #e522394d; transform-origin:center; animation:account-light .8s ease-out both; }
.account-red-light::after { content:""; position:absolute; top:0; left:var(--account-light-x,50%); width:clamp(110px,24vw,360px); height:2px; transform:translateX(-50%); background:linear-gradient(90deg,transparent,#ffb8bf 30%,#fff7f8 50%,#ffb8bf 70%,transparent); box-shadow:0 3px 18px 2px #ff354d33; border-radius:50%; }
.account-key-box { position:relative; z-index:2; display:flex; flex-direction:column; align-items:center; max-width:95vw; animation:account-reveal 1.1s ease both; }
.account-key { font:900 clamp(35px,11vw,125px)/1.2 var(--font-mono); letter-spacing:.10em; color:#ff303b; user-select:text; -webkit-user-select:text; }
.account-stage[data-account-state="key"] .account-key {
  display:flex; gap:.10em; letter-spacing:0; text-shadow:none; filter:none;
}
.account-digit { display:inline-block; color:var(--face-mid,#a5293a); text-shadow:var(--cast-x,0em) .025em 0 #33060e; }
@supports ((-webkit-background-clip:text) or (background-clip:text)) {
  .account-digit {
    background:linear-gradient(var(--face-angle,180deg),var(--face-top,#d95f6a) 0%,var(--face-mid,#a5293a) 43%,var(--face-shadow,#4f101c) 88%);
    -webkit-background-clip:text; background-clip:text; color:transparent; -webkit-text-fill-color:transparent;
    -webkit-text-stroke:.004em #712431;
    text-shadow:none;
    filter:drop-shadow(var(--cast-x,0em) .025em 0 #29050b);
  }
}
.account-copy { margin-top:32px; padding:11px 24px; border:1px solid #ff303b70; border-radius:7px; color:#ff3947; background:#160205; font:500 14px var(--font-sans); }
.account-copy:focus-visible { outline:2px solid #ff3947; outline-offset:5px; }
.account-copy-note { color:#dd6670; font-size:12px; max-width:300px; text-align:center; }
@keyframes account-light { from { transform:scaleX(0); } to { transform:scaleX(1); } }
@keyframes account-lamp-handoff { 0%,75% { opacity:1; } 100% { opacity:0; } }
@keyframes account-reveal { from { opacity:0; transform:translateY(7px); } to { opacity:1; transform:none; } }
@media(prefers-reduced-motion:reduce) { .account-red-light,.account-key-box { animation-duration:.1s; } }
@media(prefers-reduced-motion:reduce) { .account-stage .account-lamp-base { animation-duration:.2s; } }

.localized-file-native { position:absolute !important; width:1px !important; height:1px !important; overflow:hidden; clip-path:inset(50%); opacity:0; pointer-events:none; }
.localized-file-picker { display:flex; gap:10px; align-items:center; font-size:12px; color:var(--text-muted); min-width:0; }
.localized-file-picker button { flex:none; padding:8px 11px; border:1px solid var(--border); border-radius:6px; background:var(--accent-soft); color:var(--text); font:inherit; }
.localized-file-picker span { min-width:0; overflow-wrap:anywhere; }
