/* ============================================================
   组件：消息条目 / 输入区 / 面板 / 登录门 / toast
   ============================================================ */

/* ============================================================
   对话条目
   ============================================================ */
.thread-inner > .entry + .entry{ margin-top:26px; }

/* ---------- 助手侧：一根金色渐变竖线 + 宋体 ---------- */
.entry[data-role="assistant"]{
  display:grid;
  grid-template-columns:1px minmax(0,1fr);
  gap:18px;
}
.e-mark{
  background:linear-gradient(var(--gold), transparent 88%);
  opacity:.5;
  box-shadow:0 0 8px var(--gold-glow);
}
.entry[data-role="assistant"] .e-body{
  font-family:var(--serif);
  font-size:var(--step-read);
  line-height:2;
  letter-spacing:.03em;
  color:var(--text-soft);
  text-shadow:var(--ink-shadow);
}
.entry[data-role="assistant"] .e-body p + p{ margin-top:14px; }

/* ---------- 每条助手消息底下的操作行（复制 / 重试） ----------
   刻意做得比正文安静：它是**附加动作**，不是内容的一部分 ——
   给每条消息都拉一条分隔线会把线程切碎。

   只在**真有指针**的设备上默认藏起来（鼠标进到这条消息上才浮出来）。
   触摸屏上 `(hover:hover)` 不匹配，于是永远显示 —— 那台设备本来就没有"悬停"
   这个动作，藏起来就等于没有。键盘走 :focus-within，Tab 到就出现。 */
.entry-actions{
  display:flex; flex-wrap:wrap; gap:7px;
  margin-top:11px;
  transition:opacity var(--t) var(--ease);
}
.entry-actions .suggest-chip{
  padding:4px 10px;
  font-family:var(--sans);
  font-size:var(--step-micro);
  letter-spacing:.1em;
}
@media (hover:hover) and (pointer:fine){
  .entry-actions{ opacity:0; }
  .entry:hover .entry-actions,
  .entry:focus-within .entry-actions{ opacity:1; }
}

/* 「已停止」—— 被用户打断的那条回复下面的一行小字。
   选择器刻意写这么长：上面那条 `p + p` 的 margin-top:14px
   会盖掉一个单类的 .entry-note，两段的回复上就会多出 5px。 */
.entry[data-role="assistant"] .e-body .entry-note{
  margin-top:9px;
  font-family:var(--sans);
  font-size:var(--step-micro);
  letter-spacing:var(--track-caps);
  color:var(--text-faint);
}

/* ---------- 用户侧：右对齐的实面块 ---------- */
.entry[data-role="user"]{
  display:flex; justify-content:flex-end;
}
.entry[data-role="user"] .e-body{
  max-width:min(560px, 86%);
  padding:11px 15px;
  background:var(--space-3);
  border:1px solid var(--hair);
  border-right-color:var(--gold-line);
  font-size:var(--step-body);
  line-height:1.8;
  color:var(--text);
}

/* 流式吐字时的光标 */
.stream-caret{
  display:inline-block;
  width:1px; height:1em;
  margin-left:2px;
  vertical-align:-.12em;
  background:var(--gold);
  animation:caret 1.05s steps(1) infinite;
}
@keyframes caret{ 50%{ opacity:0 } }

/* ---------- 思考中：一行极淡宋体 + 一颗呼吸的金点 ---------- */
.thinking{
  display:flex; align-items:center; gap:9px;
  font-family:var(--serif);
  font-size:var(--step-body);
  letter-spacing:.06em;
  color:var(--text-faint);
}
.pulse-dot{
  width:5px; height:5px; border-radius:50%;
  background:var(--gold);
  box-shadow:0 0 8px var(--gold-glow);
  animation:breathe 1.6s var(--ease) infinite;
}
@keyframes breathe{
  0%,100%{ opacity:.25; transform:scale(.8) }
  50%    { opacity:1;   transform:scale(1.15) }
}
@media(prefers-reduced-motion:reduce){
  .pulse-dot{ animation:none; opacity:.8 }
  .stream-caret{ animation:none }
}

/* ============================================================
   行情数字
   ------------------------------------------------------------
   ⚠️ 这两条**不是卡片的残骸**，动它们之前先看右面板：
   js/ui/panel.js 的自选股在渲染 .q-price / .q-chg，
   .watch-quote[data-dir]（下面"右面板"那节）给它们上色。
   （卡片那一整套 —— .card / .doc-outline / .quote-card .q-row /
   .q-spark / .quote-card .q-foot / .steps —— 2026-10-09 删了，
   理由见 js/ui/thread.js 的文件头。这三个类是被右面板留下的。）
   ============================================================ */
.q-price{
  font-family:var(--mono);
  font-size:var(--step-body);
  font-variant-numeric:tabular-nums;
  color:var(--text);
  text-align:right;
}
.q-chg{
  font-family:var(--mono);
  font-size:var(--step-small);
  font-variant-numeric:tabular-nums;
  min-width:74px;
  text-align:right;
}

/* ============================================================
   建议 chip
   ============================================================ */
.suggest-chip{
  border:1px solid var(--hair);
  background:transparent;
  padding:7px 13px;
  font-size:var(--step-small);
  letter-spacing:.06em;
  color:var(--text-quiet);
  transition:color var(--t) var(--ease), border-color var(--t) var(--ease),
             background var(--t) var(--ease);
}
.suggest-chip:hover{
  color:var(--gold);
  border-color:var(--gold-line);
  background:var(--gold-dim);
}

/* ============================================================
   输入区
   ============================================================ */
.composer{
  display:flex; align-items:flex-end; gap:10px;
  background:var(--surface);
  border:1px solid var(--hair);
  padding:11px 12px 11px 16px;
  transition:border-color var(--t) var(--ease), box-shadow var(--t) var(--ease);
}
.composer:focus-within{
  border-color:var(--gold-line);
  box-shadow:0 0 0 1px var(--gold-dim), 0 0 22px rgba(242,192,120,.07);
}

.composer textarea{
  flex:1; min-width:0;
  border:0; background:transparent; resize:none;
  padding:5px 0;
  max-height:168px;               /* 约 6 行，再高就内部滚 */
  font-size:var(--step-body);
  line-height:1.7;
  color:var(--text);
  overflow-y:auto;
}
.composer textarea::placeholder{ color:var(--text-faint); }
.composer textarea:focus{ outline:none; }

.send{
  flex:none;
  border:1px solid var(--hair-strong);
  background:transparent;
  padding:8px 15px;
  font-size:var(--step-label);
  letter-spacing:var(--track-label);
  color:var(--gold);
  transition:background var(--t) var(--ease), border-color var(--t) var(--ease),
             color var(--t) var(--ease), opacity var(--t) var(--ease);
}
.send:hover:not(:disabled){
  background:var(--gold-dim);
  border-color:var(--gold);
}
.send:disabled{ opacity:.3; cursor:not-allowed; }

/* 忙的时候这个键变成「停止」。**必须和「送出」看得出区别** ——
   否则一个可点的键看起来还是那个按不动的主按钮。
   用中性色而不是金色：停止是收尾动作，不该跟主按钮抢视觉权重。 */
.send[data-mode="stop"]{
  color:var(--text-soft);
  border-color:var(--hair-strong);
}
.send[data-mode="stop"]:hover:not(:disabled){
  color:var(--text);
  border-color:var(--hair-strong);
  background:rgba(178,190,222,.1);
}

.composer-hint{
  margin-top:9px;
  font-family:var(--mono);
  font-size:var(--step-micro);
  letter-spacing:.06em;
  color:var(--text-faint);
  text-align:right;
}

/* ============================================================
   右面板内容
   ============================================================ */
.panel-item{
  display:block; width:100%;
  text-align:left;
  border:0; background:transparent;
  border-top:1px solid var(--hair);
  padding:11px 0;
  transition:color var(--t) var(--ease);
}
.panel-item:first-of-type{ border-top:0; }
.panel-item:hover .panel-item-name{ color:var(--gold); }
.panel-item-name{
  display:block;
  font-family:var(--serif);
  font-size:var(--step-body);
  color:var(--text-soft);
  transition:color var(--t) var(--ease);
}
.panel-item-desc{
  display:block; margin-top:3px;
  font-size:var(--step-small);
  line-height:1.7;
  color:var(--text-quiet);
}
.panel-item-meta{
  display:block; margin-top:5px;
  font-family:var(--mono);
  font-size:var(--step-micro);
  letter-spacing:.06em;
  color:var(--text-faint);
}

/* ---------- 面板头（自选的 + / 刷新） ---------- */
.panel-head{
  display:flex; align-items:center; justify-content:space-between;
  gap:10px; margin-bottom:18px;
}
.panel-head .panel-title{ margin-bottom:0; }
.panel-head-actions{ display:flex; gap:6px; flex:none; }
.panel-act{
  border:1px solid var(--hair);
  background:transparent;
  padding:3px 8px;
  font-size:var(--step-micro);
  letter-spacing:.08em;
  color:var(--text-quiet);
  cursor:pointer;
  transition:color var(--t) var(--ease), border-color var(--t) var(--ease);
}
.panel-act:hover{ color:var(--gold); border-color:var(--gold-line); }

/* ---------- 自选一行 ----------
   询问按钮 / 行情 / 删除键是**三个兄弟**，不是嵌套。
   删除键绝不能塞进询问按钮里：嵌套交互元素是非法 HTML，
   读屏软件会把它念成外层的附属，键盘也 Tab 不进去。 */
.watch-row{
  display:grid;
  grid-template-columns:minmax(0,1fr) auto auto;
  align-items:center;
  gap:10px;
  border-top:1px solid var(--hair);
  padding:11px 0;
}
.watch-row:first-of-type{ border-top:0; }

/* 询问按钮：**长得不像按钮**。它和模板那行 .panel-item 是同一个东西的
   两个姿态 —— 唯一区别是这里还得让出行情和删除键的位置。 */
.watch-ask{
  display:block; width:100%;
  text-align:left;
  border:0; background:transparent; padding:0;
  cursor:pointer;
}
.watch-ask:hover .panel-item-name,
.watch-ask:focus-visible .panel-item-name{ color:var(--gold); }

.watch-quote{ text-align:right; }
.watch-quote .q-price,
.watch-quote .q-chg{ display:block; }
/* 涨跌色。**三元编码**（▲/▼ 字符 + 颜色 + 正负号）不靠颜色单独承载语义，
   灰度与色盲下同样读得出来 —— 这条规矩从行情卡那会儿就在，卡片没了以后
   由这里继续守着。 */
.watch-quote[data-dir="up"]   .q-chg{ color:var(--up) }
.watch-quote[data-dir="down"] .q-chg{ color:var(--down) }
.watch-quote[data-dir="flat"] .q-chg{ color:var(--text-faint) }

.watch-remove{
  width:22px; height:22px;
  display:grid; place-items:center;
  border:0; background:transparent; padding:0;
  font-size:var(--step-body); line-height:1;
  color:var(--text-faint);
  cursor:pointer;
  transition:color var(--t) var(--ease), opacity var(--t) var(--ease);
}
.watch-remove:hover{ color:var(--down); }
/* 指针设备上它平时是安静的，指到这一行才浮出来；触屏上取消不了 hover，
   所以**默认是可见的**，只在能 hover 的设备上才收起来。 */
@media (hover:hover) and (pointer:fine){
  .watch-remove{ opacity:0; }
  .watch-row:hover .watch-remove,
  .watch-remove:focus-visible{ opacity:1; }
}

.panel-empty{
  padding:14px 0;
  font-size:var(--step-small);
  line-height:1.8;
  color:var(--text-faint);
}

/* ---------- 标签云 ---------- */
.tag-cloud{ display:flex; flex-wrap:wrap; gap:7px; margin-top:14px; }
/* 标签也是按钮（点了出一组同类变式题）—— 原来是个连 hover 都没有的死 span，
   看起来跟可点的 panel-item 是两种东西，其实它们做的是同一件事。 */
.tag{
  border:1px solid var(--hair);
  background:transparent;
  padding:4px 9px;
  font-size:var(--step-micro);
  letter-spacing:.08em;
  color:var(--text-quiet);
  cursor:pointer;
  transition:color var(--t) var(--ease), border-color var(--t) var(--ease);
}
.tag:hover{ color:var(--gold); border-color:var(--gold-line); }

.field{ display:flex; flex-direction:column; gap:7px; margin-bottom:18px; }
.field label{
  font-size:var(--step-label);
  letter-spacing:var(--track-caps);
  color:var(--text-quiet);
}
.field textarea,
.field select,
.field input[type="range"]{
  width:100%;
  background:var(--space-3);
  border:1px solid var(--hair);
  padding:9px 11px;
  font-size:var(--step-small);
  line-height:1.7;
  color:var(--text-soft);
  resize:vertical;
}
.field select{ appearance:none; }
.field input[type="range"]{ padding:0; height:22px; accent-color:var(--gold); }

.panel-foot-note{
  margin-top:18px;
  font-size:var(--step-micro);
  letter-spacing:.06em;
  line-height:1.7;
  color:var(--text-faint);
}

/* ============================================================
   首次加载的加载态
   ------------------------------------------------------------
   要修的是"冷启动几十秒全黑"。三个状态，用 data-state 一个属性表达：

     idle（初值，index.html 里 hidden）→ on（显形）→ done（淡出，然后摘掉）

   **为什么是 JS 来显形，而不是起手就画着。** 热加载（贴图进了缓存）时
   星海一两秒就出来，起手就画的话会先闪一下"星海正在成形"再消失 ——
   比黑屏更烦人。所以 main.js 等 350ms，还没 ready 才把它放出来。

   代价是"脚本整个挂掉"时仍然是黑的（这一层没机会显形）。接受：那种
   情况下整个 SPA 都是死的，屏上写什么区别不大；而真正会发生的那个
   失败（~6 MB 贴图要几十秒）不受影响。

   z-index 20：在 .app(3) 之上、.gate(30) 之下 —— 盖住主界面，但盖不住
   登录门。第一次来的人可以一边等贴图一边输手机号。 */
.boot{
  position:fixed; inset:0; z-index:20;
  display:flex; flex-direction:column;
  align-items:center; justify-content:center; gap:16px;
  background:var(--ink);
  opacity:0;
  transition:opacity var(--t-slow) var(--ease);
}
.boot[data-state="on"]{ opacity:1; }
/* 淡出的半秒里它已经"不在"了 —— 别挡着底下的界面。 */
.boot[data-state="done"]{ opacity:0; pointer-events:none; }
.boot-text{
  margin:0;
  font-family:var(--serif);
  font-size:var(--step-body);
  letter-spacing:.22em;
  color:var(--text-faint);
}

/* ============================================================
   登录门
   ------------------------------------------------------------
   盖住整页但**不全遮**：星海仍在底下透出来（只是被压暗 + 虚化），
   这是"一片黑里只有一个门"与"夜还在"之间的取舍 —— 全遮会把氛围一起关掉。
   ============================================================ */
.gate{
  position:fixed; inset:0; z-index:30;
  display:grid; place-items:center;
  padding:var(--stage-inset);
  /* 数值是调出来的，不是拍的。第一版 .82 + 14px 把星海压成了一块死黑 ——
     "夜还在"是这套视觉的前提，门不该把它关掉。

     要命的是**虚化半径，不是那个透明度**：backdrop-filter 是卷积，
     一颗 1px 的星被 blur(14px) 摊到约 600px² 上，峰值亮度掉两个数量级，
     0.8 的黑膜底下就什么都剩不下了。所以半径必须小。
     现在 5px：星星化开成柔光斑，看得见，又不跟字抢注意力。 */
  background:rgba(5,6,14,.56);
  backdrop-filter:blur(5px) saturate(1.06);
  -webkit-backdrop-filter:blur(5px) saturate(1.06);
  opacity:0;
  transition:opacity var(--t-slow) var(--ease);
}
.gate[data-open="true"]{ opacity:1; }
/* 淡出的那半秒里门已经"不在"了 —— 不接指针，免得挡住身后的输入框。
   （进场时是反过来的：data-open 还没翻成 true，但那一下就该拦住点击。） */
.gate[data-open="false"]{ pointer-events:none; }

.gate-inner{
  width:min(384px, 100%);
  text-align:center;
}

/* 门上唯一的退路（见 index.html 那一段）。整行左对齐、字很小 ——
   它是一页的"返回"，不是一个行动按钮；门上的行动按钮只有底下那颗「进入」。 */
.gate-back{
  display:block;
  margin:0 0 20px;
  padding:0;
  border:0;
  background:none;
  text-align:left;
  font-size:var(--step-small);
  letter-spacing:.08em;
  color:var(--text-quiet);
  cursor:pointer;
  transition:color var(--t) var(--ease);
}
.gate-back:hover{ color:var(--gold); }

/* 门牌是唯一直接落在"半透的夜"上、没有卡片兜底的字 ——
   给它挂墨影，星光再亮也不会啃掉笔画。 */
.gate-brand{ margin-bottom:30px; text-shadow:var(--ink-shadow); }
.gate-mark{
  font-family:var(--serif);
  font-size:var(--step-read);
  letter-spacing:var(--track-label);
  color:var(--text);
}
.gate-mark i{ font-style:normal; color:var(--gold); }
.gate-sub{
  display:block; margin-top:7px;
  font-size:var(--step-micro);
  letter-spacing:var(--track-caps);
  color:var(--text-quiet);
}

.gate-card{
  text-align:left;
  background:var(--surface-raised);
  border:1px solid var(--hair);
  padding:26px 24px 22px;
  transition:border-color var(--t) var(--ease);
}
.gate[data-error="true"] .gate-card{ border-color:rgba(224,112,93,.42); }

.gate-title{
  font-family:var(--serif);
  font-size:var(--display-3);
  font-weight:400;
  letter-spacing:.22em;
  color:var(--text);
  margin-bottom:22px;
}

.gate .field input{
  background:var(--space-3);
  border:1px solid var(--hair);
  padding:10px 12px;
  font-size:var(--step-body);
  color:var(--text);
  transition:border-color var(--t) var(--ease);
}
.gate .field input:focus{
  border-color:var(--gold-line);
  box-shadow:0 0 0 1px var(--gold-dim);
}
/* 短信框在"获取验证码"之前是锁着的。disabled 要看得出来是**锁**，
   不是"坏了" —— 所以降文字对比度、去掉可点的光标，但边框留着。 */
.gate .field input:disabled{
  color:var(--text-faint);
  background:rgba(12,17,32,.5);
  cursor:not-allowed;
}

/* ---------- 输入框 + 右侧控件的两列 ----------
   auto 那一列由内容定宽（验证码图 / 发短信按钮），左边输入框吃掉剩下的。
   minmax(0,1fr) 而不是 1fr：不然输入框的固有宽度会把这一行撑破。 */
.row-split{
  display:grid;
  grid-template-columns:minmax(0,1fr) auto;
  gap:10px;
  align-items:stretch;
}

/* ---------- 图片验证码 ----------
   整张图是一个按钮。做成按钮而不是给 <img> 挂 onclick：
   键盘 Tab 能到、Enter 能按、读屏会念 aria-label，这三样都是白拿的。 */
.captcha-btn{
  padding:0;
  border:1px solid var(--hair);
  background:var(--space-3);
  line-height:0;                 /* 消掉 img 行内基线那几像素的下沉 */
  overflow:hidden;
  transition:border-color var(--t) var(--ease), opacity var(--t) var(--ease);
}
.captcha-btn:hover{ border-color:var(--gold-line); }
.captcha-btn:disabled{ opacity:.45; cursor:not-allowed; }
.captcha-btn img{ display:block; }
/* 正在换图的那一下 —— 不置空 src（会闪成裂图），只淡淡地压一下 */
.captcha-btn[data-loading="true"]{ opacity:.4; }

/* ---------- 获取验证码 ---------- */
.sms-btn{
  flex:none;
  white-space:nowrap;
  border:1px solid var(--hair-strong);
  background:transparent;
  padding:0 13px;
  font-size:var(--step-label);
  letter-spacing:.1em;
  color:var(--gold);
  font-variant-numeric:tabular-nums;   /* 倒计时逐秒变字，等宽了才不会左右抖 */
  transition:background var(--t) var(--ease), border-color var(--t) var(--ease),
             color var(--t) var(--ease), opacity var(--t) var(--ease);
}
.sms-btn:hover:not(:disabled){
  background:var(--gold-dim);
  border-color:var(--gold);
}
.sms-btn:disabled{
  color:var(--text-faint);
  border-color:var(--hair);
  cursor:not-allowed;
}
/* 倒计时的按钮**不是禁用**，是"还得等" —— 给它一点金色余温，
   跟"你还没填够东西所以点不动"那种灰区分开。 */
.sms-btn[data-cooling="true"]{
  color:var(--gold-deep);
  border-color:var(--hair);
  cursor:default;
}

.gate-error{
  margin:2px 0 14px;
  font-size:var(--step-small);
  letter-spacing:.04em;
  color:var(--up);                 /* 复用涨跌红：它已经过对比度校准 */
  min-height:1.4em;
}

.gate-submit{
  width:100%;
  border:1px solid var(--gold-line);
  background:var(--gold-dim);
  padding:12px 18px;
  font-size:var(--step-body);
  letter-spacing:var(--track-caps);
  color:var(--gold);
  transition:background var(--t) var(--ease), border-color var(--t) var(--ease),
             box-shadow var(--t) var(--ease), opacity var(--t) var(--ease);
}
.gate-submit:hover:not(:disabled){
  border-color:var(--gold);
  box-shadow:0 0 22px rgba(242,192,120,.14);
}
.gate-submit:disabled{ opacity:.45; cursor:not-allowed; }

/* 验证码框：等宽 + 疏字距，四个字母才看得清。
   text-transform 只管**显示**，input.value 仍是用户原样敲的 ——
   大小写交给服务端判定（server.mjs 里是 toUpperCase 后比对）。 */
.gate .field input#lg-captcha{
  font-family:var(--mono);
  letter-spacing:.34em;
  text-transform:uppercase;
}
.gate .field input#lg-sms{
  font-family:var(--mono);
  letter-spacing:.2em;
  font-variant-numeric:tabular-nums;
}

.gate-note{
  margin-top:16px;
  min-height:2.6em;              /* 两行位置先占住，填充时整张卡不会往下蹦 */
  font-family:var(--mono);
  font-size:var(--step-micro);
  line-height:1.7;
  letter-spacing:.06em;
  color:var(--text-faint);
  text-align:center;
}

/* ---------- 侧栏底部：当前账号 + 退出 ---------- */
.who-row{
  display:flex; align-items:center; justify-content:space-between; gap:10px;
  margin-top:10px;
}
.who-name{
  min-width:0;
  font-size:var(--step-small);
  letter-spacing:.04em;
  color:var(--text-quiet);
  overflow-wrap:anywhere;
}
.who-row .ghost-btn{ padding:4px 8px; font-size:var(--step-micro); }

/* ============================================================
   toast
   ============================================================ */
.toast{
  position:fixed; z-index:20;
  left:50%; bottom:34px; transform:translate(-50%, 10px);
  padding:9px 16px;
  background:var(--space-3);
  border:1px solid var(--gold-line);
  font-size:var(--step-small);
  letter-spacing:.08em;
  color:var(--text-soft);
  opacity:0; pointer-events:none;
  transition:opacity var(--t) var(--ease), transform var(--t) var(--ease);
}
.toast[data-open="true"]{ opacity:1; transform:translate(-50%, 0); }

/* ============================================================
   星海首页的工具星
   ------------------------------------------------------------
   容器 .galaxy 的定位在 layout.css（它属于布局），星本身在这里。
   星是 DOM 元素这件事的理由写在 ui/galaxy.js 顶上。

   一处刻意的破例：**这里的圆角是 50%**。全站直角 2px 说的是卡片和
   面板；一颗星就是个圆点，原型画布上画的也是正圆。别为了守纪律
   把它改方。

   版式（2026-10-01）整套搬自旧站：星的位置**不是 CSS 摆的**，
   每帧由相机投影算出来，写在 .star-slot 的 translate() 上。
   这边只负责那三层盒子和名字的排法，见 ui/galaxy.js。

   出处：归档 auralink/frontend/dist/assets/index-DQdH6YXc.css
         （.starfield / .star-ring / .star-orbit / .star-slot / .cluster）
   ============================================================ */

/* ---------- 舞台 ----------
   环的宽度是**尺度锚点**：galaxy3d.js 里 F = 环宽 ÷ 一个世界单位有多少
   像素，于是 u=1 就等于"离环心一个环宽" —— 整圈星多大，由这一个数说了算，
   3D 那边一个魔数都不用改。旧站是 min(1080px, 94vw, 76vh, 620px)。

   收了两个断点（旧站 94vw → 68vw，76vh → 70vh），再**减掉 56px**：
   最远那颗星投影到环心的水平距离是**环半宽的 1.03 倍**，名字又是居中挂在
   这颗星下面、四个字 18px 约 74px 宽 —— 半个名字 37px，加上按钮自己 9px，
   最坏情况要 环宽×.52 + 46px 才不越界。纯 min() 里一个 vw 系数做不到
   "桌面保持 620、窄屏让开"：桌面不缩要求系数 > .68，窄屏又要让开。
   所以是个**减法**，620 以上照旧，930 以下开始收。
   旧站那 9 颗按 94vw 算是会甩出屏幕的 —— 只是没人去量过。

   ⚠️ **那个 1.03 是"当前这几颗星"量出来的，加一颗星就得重量一次。**
   量法：`scripts/shot.mjs --serve=. --viewport=W,H --eval=…` 里，对
   `.star-ring` 取中心，再逐颗算 |星心 − 环心| ÷ 环半宽，取最大值。
   两个坑都踩过：**必须先把登录门推开**（`.galaxy` 在 `.app` 里，`.app` 是
   hidden 的，不推门量到的是一堆没被布局的元素，而且门会盖住截图），
   并且**要等两个 rAF 后断言 `.is-formed`**（贴图没到位时量到的是
   placeFlat() 的近似投影，比值是错的）。实测：

     四颗（2026-10-01 那版 GAP .68/.47）  1440×900 → 1.04   430×860 → 1.22
     五颗（同日改 GAP 后）               1440×900 → 1.03   430×860 → 1.03

   窄屏现在是拿 236.4 × 1.03 + 46 = 289 去比 430，**余量 141px**（改之前
   五颗在 .68/.47 上量出来是越界 49px，就是这次重取 GAP 的原因）。
   余量这么大，按上面的算式其实可以把 68vw 调大、让窄屏的环更宽 ——
   但那是改版式，不是修 bug，先不动。*/
.star-ring{
  --ring-w: min(620px, calc(68vw - 56px), 70vh);
  /* 名字离星多远：.cluster 的 bottom 是「这个的一半 + 15px」。
     旧站 .starfield 上的值，照抄。 */
  --cluster-icon: clamp(34px, 4.6vw, 48px);

  position:absolute; left:50%; top:50%;
  width:var(--ring-w); aspect-ratio:1;
  transform:translate(-50%,-50%);
}

/* 星环的旋转层。旧站这里挂着 transform:rotate(var(--spin))，而 --spin
   在它整个编译产物里**没有一处写过**（CSS 只给了个 0deg，JS 一次没碰）——
   是死的。真正的"转"来自相机：鼠标一动，相机平移但仍 lookAt(0,0,0)，
   等于绕原点转过一个小角度（见 galaxy3d.js 的 updateCamera）。
   所以这里不写 transform。 */
.star-orbit{ position:absolute; inset:0; }

/* 一颗星的位置盒。translate() 由 ui/galaxy.js 每帧写进来，
   原点就是它自己 left/top 的 50% —— 也就是环心。 */
.star-slot{
  position:absolute; left:50%; top:50%;
  opacity:0;
  transition:opacity .9s var(--ease);
  /* 一颗比一颗晚 .13s 显形。顺序由 --star-i 给（按 id 排序的槽位号）。 */
  transition-delay:calc(var(--star-i, 0) * .13s);
}
.star-ring.is-formed .star-slot{ opacity:1; }
/* 还没成形时别接指针：那时候它 opacity:0，但仍然能点。 */
.star-ring:not(.is-formed) .star-slot{ pointer-events:none; }
.star-slot > .star{ left:0; top:0; }

/* ---------- 星本体：照搬旧站的话题星 ----------
   这里本来是我自己配的金色星，后来跟着新接进来的星系调成冷蓝，两版都不对 ——
   用户要的是**旧站原本那套话题星**，原话："黄色工具星也删了，换成配套的工具星"、
   "将之前项目的话题星的样式搬过来"。

   **#6385f7 就是那片星系的 RING_TINT** —— galaxy3d.js 里写的是
   vec3(.39,.52,.97)，乘 255 正好是 0x63/0x85/0xf7。旧站拿同一个常量
   既给话题星上色、又给悬停时的星系染色，所以星和它照亮的那片天本来就是
   同一个颜色。"配套"在旧站不是配出来的，是同一个数。 */
.star{
  position:absolute;
  width:18px; height:18px; border-radius:50%;
  transform:translate(-50%,-50%);
  padding:0; border:0; background:transparent;
  cursor:pointer;
  pointer-events:auto;             /* .galaxy 是穿透的，只有星自己接指针 */
  -webkit-tap-highlight-color:transparent;
}

/* 星核。旧站是 .star i（<i> 在按钮里铺满），这边同样用 <i>。 */
.star i{
  position:absolute; inset:0; border-radius:inherit;
  background:radial-gradient(circle,#ffffff 0%,#a7c0ff 30%,#6385f7 55%,transparent 75%);
  box-shadow:0 0 22px 7px #6385f777;
  transition:transform var(--t) var(--ease);
}

/* 呼吸环（旧站的 .star.is-live::after）。旧站把它门在 .is-live 上 ——
   那是"这个话题有人"的意思，四个工具都是活的，所以这里不设条件。 */
.star::after{
  content:""; position:absolute; inset:-5px; border-radius:50%;
  border:1px solid #6385f7aa;
  pointer-events:none;
  animation:star-live 3.4s var(--ease) infinite;
}
@keyframes star-live{ 50%{ opacity:.3; transform:scale(1.16) } }

/* 旧站只有 :hover 这一条（键盘那半靠星系染色），这里把 :focus-visible
   并进来 —— 键盘用户看不见鼠标，但该拿到同一份放大。 */
.star:hover i,
.star:focus-visible i{ transform:scale(1.65); }

/* ---------- 名字的排法：照搬旧站的 .cluster ----------
   .star-alias-wrap 是 0×0 的一根钉子，钉在**星心上**；
   .cluster 绝对定位挂在它上面，靠 bottom 把名字抬到星的上方、居中。

   旧站还多一层 .cluster-line / .cluster-char（把长名字按字数切行、
   逐字包 span 做字距），那是给「四个字的别名 + 一句话」那种长度用的；
   这边四个工具名都是 4 个字、一行排得下，没搬。

   也没有 text-shadow —— 旧站就是纯 #fff。压在星系上够不够读，
   截图看一眼再定，够就不加。 */
.star-alias-wrap{
  position:absolute; left:50%; top:50%;
  width:0; height:0;
}
.cluster{
  position:absolute; left:0;
  bottom:calc(var(--cluster-icon, 44px) / 2 + 15px);
  transform:translateX(-50%);
  width:max-content; max-width:176px;
  text-align:center;
  color:#fff;
  pointer-events:none;
}
.cluster-title{
  display:block;
  font-size:18px;
  letter-spacing:var(--track-body);
}

/* 键盘焦点：把全站那个矩形轮廓换成**圆形**的环。
   直接用 base.css 的矩形轮廓会画在按钮的盒子上，而那颗星只有 18px、
   又整个被 translate 挪过，框跟星不会重合。
   改成让星自己的光环亮起来：位置对、形状对，而且比 1px 的框显眼。

   顺带把呼吸停掉。聚焦的那颗**不动**，其余三颗还在呼吸 ——
   静态本身就是这一屏里最强的"就是这颗"信号。 */
.star:focus-visible{ outline:none; }
.star:focus-visible::after{
  animation:none;
  opacity:1;
  transform:scale(1.1);
  border-color:#a7c0ff;
  box-shadow:0 0 0 1px #a7c0ffcc, 0 0 14px 3px #6385f7aa;
}

@media(prefers-reduced-motion:reduce){
  .star-slot{ transition:none; }
  .star::after{ animation:none; }
}

/* ============================================================
   博客（第五颗星点进来的那一屏）
   ------------------------------------------------------------
   版式规则：**正文的阅读宽度用 --measure（760px）**，跟对话线程同一个
   数 —— 这一屏存在的意义是读，那就跟读的规矩走。
   容器本身的滚动/显隐在 layout.css（它属于布局），这里只管长什么样。

   两处刻意的克制：
     · 不引新断点。列表那两栏（日期 | 标题）用 minmax(0,auto) 让它
       自己在窄屏上缩，而不是加一条 @media。
     · 不给标题加字重。全站标题都是 weight:400 + 字距，靠字号拉开层次 ——
       这里跟住，否则博客会看起来像从别的站贴过来的。
   ============================================================ */

.blog-inner{
  max-width:var(--measure);
  margin-inline:auto;
  padding:34px var(--stage-inset) 96px;
}

/* ---------- 列表 ---------- */

.blog-head{ margin-bottom:30px; }
.blog-title{
  margin:0 0 10px;
  font-family:var(--serif);
  font-size:var(--display-1);
  font-weight:400;
  letter-spacing:.14em;
  color:var(--text);
}
.blog-sub{
  margin:0;
  font-size:var(--step-small);
  letter-spacing:var(--track-caps);
  color:var(--text-faint);
}

.post-list{ list-style:none; margin:0; padding:0; }
.post-item{
  display:grid;
  /* 日期那栏 auto + minmax(0,1fr)：窄屏上自己缩，不用单开一条断点。
     minmax(0,1fr) 那个 0 是必须的 —— 写 1fr 的话长标题会撑破栅格。 */
  grid-template-columns:auto minmax(0,1fr);
  column-gap:22px; row-gap:7px;
  padding:22px 0;
  border-top:1px solid var(--hair);
  text-decoration:none;
}
.post-item:last-child{ border-bottom:1px solid var(--hair); }
.post-date{
  grid-row:1 / span 3;
  padding-top:4px;
  font-family:var(--mono);
  font-size:var(--step-small);
  color:var(--text-faint);
  white-space:nowrap;
}
.post-item .post-title{
  font-family:var(--serif);
  font-size:var(--display-3);
  font-weight:400;
  letter-spacing:.1em;
  color:var(--text);
  transition:color var(--t) var(--ease);
}
.post-item:hover .post-title,
.post-item:focus-visible .post-title{ color:var(--gold); }
.post-item:focus-visible{ outline:1px solid var(--gold-line); outline-offset:6px; }
.post-summary{
  font-size:var(--step-body);
  line-height:1.95;
  color:var(--text-quiet);
}
.post-tags{ display:flex; flex-wrap:wrap; gap:7px; }
/* 跟面板里那个 .tag 长得一样，但**不是按钮** —— 那个有 cursor:pointer 和
   hover 变色，是在说"点我"。博客的标签现在没有可点的去处，套用那个类
   就是在骗人。 */
.post-tag{
  border:1px solid var(--hair);
  padding:3px 8px;
  font-size:var(--step-micro);
  letter-spacing:.08em;
  color:var(--text-faint);
}

/* ---------- 正文 ---------- */

.post-back{
  display:inline-block;
  margin-bottom:28px;
  font-size:var(--step-small);
  letter-spacing:.06em;
  color:var(--text-quiet);
  text-decoration:none;
  transition:color var(--t) var(--ease);
}
.post-back:hover{ color:var(--gold); }

.post > .post-title{
  margin:0 0 12px;
  font-family:var(--serif);
  font-size:var(--display-1);
  font-weight:400;
  letter-spacing:.12em;
  color:var(--text);
}
/* 打开文章时焦点是脚本给的（tabindex="-1"），鼠标点击那一下不该冒出
   一个框；键盘 Tab 过来的会走 :focus-visible，那时候该有。 */
.post-title:focus-visible{ outline:1px solid var(--gold-line); outline-offset:8px; }
.post-meta{
  margin:0;
  font-family:var(--mono);
  font-size:var(--step-small);
  color:var(--text-faint);
}

/* Markdown 出来的东西。**这一段里的每个选择器都是 js/markdown.js 那份
   白名单里认得的标签** —— 两边是一套，加了新标签记得回来补样式，
   不然它会以浏览器默认样子出现在一片深色里。 */
.post-body{
  margin-top:34px;
  font-size:var(--step-read);
  line-height:2;
  color:var(--text-soft);
}
.post-body > *{ margin:0 0 1.15em; }
.post-body h2,
.post-body h3,
.post-body h4{
  margin-top:2em;
  font-family:var(--serif);
  font-weight:400;
  letter-spacing:.12em;
  color:var(--text);
}
.post-body h2{ font-size:var(--display-3); }
.post-body h3{ font-size:var(--step-read); letter-spacing:.16em; }
.post-body h4{ font-size:var(--step-body); letter-spacing:.16em; color:var(--text-quiet); }
.post-body ul,
.post-body ol{ padding-left:1.5em; }
.post-body li{ margin-bottom:.35em; }
.post-body li::marker{ color:var(--text-faint); }
.post-body a{
  color:var(--gold);
  text-decoration:none;
  border-bottom:1px solid var(--gold-line);
}
.post-body a:hover{ border-bottom-color:var(--gold); }
.post-body blockquote{
  margin:1.8em 0;
  padding:2px 0 2px 20px;
  border-left:1px solid var(--gold-line);
  color:var(--text-quiet);
}
.post-body code{
  font-family:var(--mono);
  font-size:.9em;
  background:var(--space-3);
  border:1px solid var(--hair);
  padding:1px 5px;
}
.post-body pre{
  background:var(--space-2);
  border:1px solid var(--hair);
  border-left:1px solid var(--gold-line);
  padding:16px 18px;
  overflow-x:auto;
}
/* 代码块里的 <code> 别再画一遍边框和内底色 —— 那是行内的样式，
   叠在 pre 的底上会变成两层框。 */
.post-body pre code{
  background:none; border:0; padding:0;
  font-size:var(--step-small);
  line-height:1.95;
}
.post-body hr{ border:0; border-top:1px solid var(--hair); margin:2.6em 0; }
.post-body img{ max-width:100%; height:auto; }
.post-body table{
  width:100%;
  border-collapse:collapse;
  font-size:var(--step-small);
}
.post-body th,
.post-body td{
  border:1px solid var(--hair);
  padding:9px 11px;
  text-align:left;
}
.post-body th{ font-weight:400; color:var(--text-quiet); letter-spacing:.06em; }

/* ---------- 取数中 / 出错 ---------- */

.blog-note{
  display:flex; align-items:center; gap:9px;
  margin:0 0 14px;
  font-size:var(--step-body);
  letter-spacing:.06em;
  color:var(--text-faint);
}
.blog-note-err{ color:var(--up); }   /* 复用涨跌红：它已经过对比度校准 */
.blog-code{
  font-family:var(--mono);
  font-size:var(--step-micro);
  color:var(--text-faint);
  border:1px solid var(--hair);
  padding:2px 6px;
}

/* ============================================================
   编辑器（第三态）
   ------------------------------------------------------------
   两个决定值得写下来：

   1. **编辑态把阅读列宽放开**（.is-editing）。760px 是**读**的宽度，
     左写右看分两栏之后每栏只够放歌词，预览就白做了。加宽靠一个类
     而不是 `:has(.editor)`：状态在 JS 里已经有了（setEditing），
     没必要再让 CSS 去反推一遍 DOM。

   2. **窄屏上下叠，不并排**。用的还是站里那条 899px，没引新断点 ——
     430px 上并排两栏是每栏一百多像素，那不叫预览。

   3. 正文框是等宽的（写 Markdown 要看对齐），预览那一栏是 **.post-body
     本身** —— 跟发出去的那一篇共用同一段样式。预览要是不像成品，
     它就一点用都没有。
   ============================================================ */

.blog-inner.is-editing{ max-width:calc(var(--measure) + 300px); }

/* 列表头 / 正文页的作者行。两处都留了一个 .ed-note —— 出错的去处。 */
.blog-actions,
.post-author{
  display:flex; flex-wrap:wrap; align-items:center; gap:9px;
  margin-top:18px;
}

.editor{ display:flex; flex-direction:column; gap:13px; }

.ed-head{ display:flex; align-items:baseline; gap:14px; flex-wrap:wrap; }
.ed-what{
  font-size:var(--step-small);
  letter-spacing:var(--track-caps);
  color:var(--text-quiet);
}
/* 状态行的两态：安静地说"正在存"，或者红着说"为什么没存上"。
   红复用涨跌色 —— 它已经过对比度校准，跟 .blog-note-err 同一条。 */
.ed-note{
  font-size:var(--step-small);
  letter-spacing:.06em;
  color:var(--text-faint);
}
.ed-note[data-state="err"]{ color:var(--up); }

/* 标题和标签：没有框，只有一条底线。**不引新的输入框样式** —— 全站
   的输入框（.composer）也是"没有框"的，这里是同一条。 */
.ed-input{
  width:100%;
  border:0; border-bottom:1px solid var(--hair);
  background:transparent;
  padding:7px 0;
  color:var(--text);
}
.ed-input::placeholder{ color:var(--text-faint); }
.ed-input:focus{ outline:0; border-bottom-color:var(--gold-line); }
.ed-title{ font-family:var(--serif); font-size:var(--display-3); letter-spacing:.1em; }
.ed-tags{ font-size:var(--step-small); color:var(--text-mid); }

.ed-cols{
  display:grid;
  grid-template-columns:minmax(0,1fr) minmax(0,1fr);
  gap:20px;
  margin-top:6px;
}
/* 那个 0 跟 .post-item 一样是必须的：没有它，正文里一行长 URL 就能把
   栅格撑破（minmax(auto,1fr) 的下限是内容宽度）。 */
.ed-col{ display:flex; flex-direction:column; gap:8px; min-width:0; }
.ed-label{
  font-size:var(--step-micro);
  letter-spacing:var(--track-caps);
  color:var(--text-faint);
}

.ed-body{
  min-height:360px;
  resize:vertical;
  border:1px solid var(--hair);
  background:transparent;
  color:var(--text-soft);
  padding:14px;
  font-family:var(--mono);
  font-size:var(--step-small);
  line-height:1.8;
}
.ed-body::placeholder{ color:var(--text-faint); }
.ed-body:focus{ outline:0; border-color:var(--gold-line); }

/* 预览：是 .post-body，所以只写"跟正文页不一样"的那几处。
   margin-top 那个 34px 是正文页跟标题之间的距离，在这儿是多余的。 */
.ed-preview{
  margin-top:0;
  min-height:360px;
  padding:14px;
  border:1px solid var(--hair);
  overflow-wrap:anywhere;
  font-size:var(--step-body);      /* 栏窄，阅读字号在这里会撑得慌 */
}
.ed-preview > :last-child{ margin-bottom:0; }
.ed-empty{
  font-size:var(--step-small);
  letter-spacing:.06em;
  color:var(--text-faint);
}

.ed-actions{ display:flex; flex-wrap:wrap; align-items:center; gap:9px; margin-top:4px; }
.ed-keys{
  margin-left:auto;
  font-size:var(--step-micro);
  letter-spacing:var(--track-caps);
  color:var(--text-faint);
}

/* 两步确认的第二步：这个键此刻是在问"真的要删吗"。 */
.suggest-chip[data-armed="1"]{ color:var(--up); border-color:var(--up); }
/* 存 / 删进行中按住的那些键。 */
.suggest-chip[disabled]{ opacity:.5; cursor:default; }
.suggest-chip[disabled]:hover{
  color:var(--text-quiet); border-color:var(--hair); background:transparent;
}

/* 窄屏：上下叠。用的是站里已有的 899px，**没引新断点**。 */
@media(max-width:899px){
  .ed-cols{ grid-template-columns:minmax(0,1fr); }
  .ed-body,
  .ed-preview{ min-height:240px; }
}
