/* ============================================================
   布局：外壳栅格 / 侧栏 / 主区 / 输入区 / 右面板
   ============================================================ */

.app{
  position:relative; z-index:3;
  height:100vh; height:100dvh;
  display:grid;
  grid-template-columns:var(--sidebar-w) minmax(0,1fr) var(--panel-w);
}
/* minmax(0,1fr) 是必须的 —— 写 1fr 的话长内容会撑破栅格 */
.app[data-panel="closed"]{ --panel-w:0px; }

/* ============================================================
   视图：星海首页 ⇄ 工具对话页
   ------------------------------------------------------------
   一个 data-view 挂在 <body> 上，驱动全部切换。
   挂 body 而不是 .app，是因为要跟着一起退下去的东西有两个在 .app 外面：
   画布 #galaxy 和暗角 .vignette。

   首页把栅格压成一栏、收起侧栏与面板，主区因此铺满整屏。
   ============================================================ */

/* 特异性 0,2,1，压过下面两个断点里的 .app（0,1,0），所以写在哪儿都赢，
   不依赖顺序。 */
body[data-view="home"] .app{ grid-template-columns:minmax(0,1fr); }
body[data-view="home"] .sidebar,
body[data-view="home"] .panel{ display:none; }

/* 星海容器。铺满主区、盖在对话零件之上 —— 那些零件在首页是隐形的，
   但仍在流里占着位置，星海不该被它们挤成一条。
   只有星本身接指针（见 components.css 的 .star），空白处穿透到主区。
   这里必须显式写死两个方向：漏掉的话星海会跟着进工具页，
   四颗星连着名字一起浮在对话上面。 */
.galaxy{
  position:absolute; inset:0; z-index:1;
  pointer-events:none;
  opacity:0; visibility:hidden;
  transition:opacity var(--t-slow) var(--ease), visibility 0s linear var(--t-slow);
}
body[data-view="home"] .galaxy{
  opacity:1; visibility:visible;
  transition:opacity var(--t-slow) var(--ease), visibility 0s;
}

/* 主区里的对话零件在首页整体隐去。
   用 visibility + opacity 而不是 display:none：display 没有过渡，
   点星进去会「啪」地跳一下；visibility 还顺手把整棵子树移出 Tab 序和
   可访问性树 —— 首页上不该 Tab 得到一个看不见的输入框，正是要的。 */
.main-head,
.thread,
.composer-wrap{
  transition:opacity var(--t-slow) var(--ease), visibility 0s;
}
body[data-view="home"] .main-head,
body[data-view="home"] .thread,
body[data-view="home"] .composer-wrap{
  visibility:hidden; opacity:0; pointer-events:none;
  /* 隐藏时把 visibility 推迟到淡出走完 —— 提前切的话元素当场消失，
     opacity 那半秒就白等了。显形时**不**推迟（上面那条 0s），
     否则 main.js 里紧跟其后的 composer.focus() 会打在一个还不可聚焦的
     元素上，静默失败。 */
  transition:opacity var(--t-slow) var(--ease), visibility 0s linear var(--t-slow);
}

/* ============================================================
   全站一层天：原站的 WebGL 粒子星系
   ------------------------------------------------------------
   2026-10-01：首页那层换成了原站的 GPU 粒子星系（ui/galaxy3d.js），
   同一天又把工具页背后那片 2D 星空（旧的 ui/sky.js）删了 ——
   用户的原话是"黄色的那个不要了，只要刚刚接入的那个"。
   所以现在**两个视图共用这一个画布**：
     首页   → 满亮，它是主角
     工具页 → 压暗，它是阅读背景

   进工具页**压暗而不是藏起来**：全站换一层天会让点星进工具的那一下
   像跳了站，而星系本来就在那儿，只是退到后面去。压暗走的是整块画布的
   opacity：一个**均匀**的乘数，明暗比例原样保留 —— base.css 里记着别犯
   的那个错是另一回事，那是又叠了一层**空间上**的压暗，才把星海压成死黑。
   --t-slow 在 prefers-reduced-motion 下是 0s，这里白拿一个瞬切。
   ============================================================ */
#galaxy{
  opacity:1;
  transition:opacity var(--t-slow) var(--ease);
}
/* .28 这个数是**唯一的调音旋钮**，而且比 2D 那层当年的 .7 小得多 ——
   星系比星空亮太多：它中间是一坨密实的核，颜色也饱和，压在正文底下
   会跟字抢。宁可偏暗：工具页的面板本身就是半透明的，星系透过面板
   还剩一点点蓝紫的底子，够了。 */
body[data-view="tool"] #galaxy{ opacity:.28; }

/* ============================================================
   视图 · 博客（第三态）
   ------------------------------------------------------------
   **这里刻意没有照抄 home 那几行**，虽然一开始是那么打算的。抄的话
   侧栏跟着没了，于是两件事变成死代码：

     1. 4.4 要求"未登录时侧栏那个位置显示「登录」"—— 侧栏都不在，
        这条判断永远走不到。
     2. 窄屏（≤899px）的侧栏是抽屉，**唯一的开抽屉按钮在 .main-head 里**。
        把 .main-head 藏掉，窄屏读者就再也打不开抽屉：看不见站名、
        没地方登录、也去不了工具页。

   所以博客视图 = 工具页那套外壳（侧栏 + 主区），只是**主区的内容换成
   博客**，右面板收掉、工具那三个按钮收掉。这也更贴合这个站的性质：
   博客是第五颗星，不是另一个站。

   ⚠️ **上面这段只对"已登录"成立（2026-10-09 补）。** 未登录的客人看到的是
   **只有博客**：侧栏整个不出现，工具列表一个字都不露。那两条死代码的顾虑
   由一个新的登录入口解决（.main-head 里那颗 head-login）。判据见下面
   「未登录的布局」那一段。 */

/* 右面板收掉。**不是直接改 grid-template-columns，而是把它那条轨道压到 0**
   （`--panel-w` 就是那条轨道，`.app[data-panel="closed"]` 用的也是这一招）。
   为什么不直接写两条轨道：那条写法是 0,2,1，比 `@media(max-width:899px)` 里
   `.app{grid-template-columns:minmax(0,1fr)}`（0,1,0）高 —— **媒体查询不影响
   优先级**，于是窄屏那一条永远被压住，430px 上博客会被挤进第二条轨道里，
   右边空出 264px 的侧栏轨道。实测过：正文只剩 264px 宽，还多出 3px 横向滚动。
   压轨道就没有这场架：窄屏那两条媒体查询各按各的生效。 */
body[data-view="blog"]{ --panel-w:0px; }
body[data-view="blog"] .panel{ display:none; }
/* 「面板」「清空」是对话页的零件，博客里没有对话可清 */
body[data-view="blog"] .main-tools{ display:none; }
/* 比工具页的 .28 更暗：那里底下是面板（半透明，透出一点蓝紫底子正好），
   这里底下是**正文**，整段整段的白字压在星系核上会花。 */
body[data-view="blog"] #galaxy{ opacity:.18; }

/* 博客容器。**跟 .thread 一样是主区里的滚动区**（flex:1 + min-height:0），
   不用绝对定位：那样会盖住 .main-head，而窄屏的「目录」按钮就在里头。
   见上面那段——正是为了保住那个按钮才没抄 home 的写法。 */
.blog{
  display:none;
  flex:1; min-height:0;
  overflow-y:auto;
  overscroll-behavior:contain;
}
body[data-view="blog"] .blog{ display:block; }

/* 对话那两块在博客视图里必须 **display:none**，不能沿用首页那套
   visibility —— visibility 隐藏的元素照样占着 flex 的位置，.thread 还是
   flex:1，会把博客挤掉一半。
   代价是这一处切换没有淡入（首页⇄工具页有）。可以接受：那两处的过渡
   为的是"两个长得差不多的大东西别啪地跳"，而这里是内容整个换掉；
   换成绝对定位 + 给 .main-head 补一层不透明底，是拿两层 z-index 换
   一次淡入，不值。

   ⚠️ 第三条选择器**不是重复**，是专门压那条空态规则的。下面那组
   `.main[data-empty="true"]` 的选择器是 0,3,0，比这里的 0,2,1 高；
   一个还没发过话的工具（`data-empty="true"`）进博客，就会让
   `.composer-wrap{display:grid}` 压住这一句 —— 于是输入区和空态标题
   照旧摊在博客上面。踩过一次，量出来 composer-wrap 还是 grid。 */
body[data-view="blog"] .thread,
body[data-view="blog"] .composer-wrap,
body[data-view="blog"] .main[data-empty="true"] .composer-wrap{ display:none; }

/* ============================================================
   未登录的布局 —— 客人看到的就是**一个博客**
   ------------------------------------------------------------
   口径是用户定的原话："进到这个网站，先是个人博客，登录后才显示工具和
   工具的导航栏，不登录都不知道有这些东西。" 所以未登录时：

     · 侧栏整个不出现（不是"藏起来"——它连布局都不占，主区铺满整屏）
     · 窄屏那颗「目录」也收掉。它是**开抽屉的**，抽屉里就是工具列表 ——
       留着它等于在窄屏上把工具列表又露一遍
     · 博客页头那颗 head-login 反过来显示，作为唯一的登录入口

   判据挂在 <body data-authed> 上，**默认值写死在 index.html 里**
   （见那儿的注释）：等 JS 来设的话，客人第一帧会看见侧栏滑出来再消失。

   下面三条都是 0,2,1，压得过 @media 里的 `.app` / `.drawer-btn`（0,1,0）——
   跟上面 blog 那段的 `--panel-w` 是同一个理由：**媒体查询不参与优先级**。 */
body[data-authed="false"] .sidebar{ display:none; }
body[data-authed="false"] .app{ grid-template-columns:minmax(0,1fr); }
body[data-authed="false"] .drawer-btn{ display:none; }
/* 登录之后反过来：那颗键就不该再占着页头的位置了。 */
body[data-authed="true"] .head-login{ display:none; }

/* ============================================================
   侧栏
   ============================================================ */
.sidebar{
  display:flex; flex-direction:column;
  min-height:0;                     /* 让内部滚动生效 */
  background:var(--surface);
  border-right:1px solid var(--hair);
  /* 这里**不挂 backdrop-filter**：侧栏 + 正文占屏幕 80% 面积，
     底下是每帧重绘的画布，每帧重做一次模糊的代价不可接受。
     用 95% 不透明的实面替代 —— 可读性与性能在这里同向。 */
}

/* 题头同时是「回星海」的按钮。除了 Esc，这是从工具页退回首页的唯一入口，
   所以它得是个真按钮：能 Tab 到、能回车触发、有悬停反馈。
   按钮的默认外观要一条条清掉，不然会多出一个方框。 */
.brand{
  display:block; width:100%;
  text-align:left;
  padding:26px 22px 20px;
  border:0; border-bottom:1px solid var(--hair);
  background:transparent;
  transition:background var(--t) var(--ease);
}
.brand:hover{ background:var(--gold-dim); }
.brand-mark{
  display:block;                   /* 它现在是 <span>，不是 <div> */
  font-family:var(--serif);
  font-size:var(--step-read);
  letter-spacing:var(--track-label);
  color:var(--text);
  text-shadow:var(--ink-shadow);
}
.brand-mark i{ font-style:normal; color:var(--gold); }
.brand-sub{
  display:block; margin-top:7px;
  font-size:var(--step-micro);
  letter-spacing:var(--track-caps);
  color:var(--text-quiet);
}

.nav{
  flex:1; min-height:0;
  overflow-y:auto;
  padding:18px 0 12px;
}
.nav-group-label{
  padding:0 22px 10px;
  font-size:var(--step-micro);
  letter-spacing:var(--track-caps);
  color:var(--text-faint);
}

/* 类别项：原型 .nav-btn 的语汇 —— 11px、大字距，
   激活态从「下划线」改成「左侧 1px 金色竖线」，竖排列表里更省视觉。
   文案允许折行，**不许 ellipsis**：4 个中文字被截成 3 个是最糟的结果。 */
.nav-item{
  position:relative;
  display:flex; align-items:center; gap:11px;
  width:100%;
  padding:11px 22px 11px 22px;
  border:0; background:transparent;
  text-align:left;
  font-size:var(--step-label);
  letter-spacing:var(--track-caps);
  color:var(--text-quiet);
  transition:color var(--t) var(--ease), background var(--t) var(--ease);
}
.nav-item::before{
  content:""; position:absolute; left:0; top:50%; transform:translateY(-50%);
  width:1px; height:0;
  background:linear-gradient(var(--gold), transparent);
  transition:height var(--t) var(--ease);
}
.nav-item:hover{ color:var(--text-soft); background:rgba(242,192,120,.03); }
.nav-item[aria-current="true"]{
  color:var(--gold);
}
.nav-item[aria-current="true"]::before{ height:62%; }

.nav-glyph{
  flex:none; width:9px; height:9px;
  border:1px solid currentColor;
  transform:rotate(45deg);
  opacity:.55;
  transition:opacity var(--t) var(--ease), box-shadow var(--t) var(--ease);
}
.nav-item[aria-current="true"] .nav-glyph{
  opacity:1;
  box-shadow:0 0 7px var(--gold-glow);
}

.sidebar-foot{
  padding:14px 22px 20px;
  border-top:1px solid var(--hair);
}

/* ============================================================
   主区
   ============================================================ */
.main{
  position:relative;               /* .galaxy 绝对定位铺满它 */
  display:flex; flex-direction:column;
  min-width:0; min-height:0;
  background:transparent;          /* 星海必须透出来 */
}

.main-head{
  flex:none;
  display:flex; align-items:center; justify-content:space-between; gap:16px;
  padding:22px var(--stage-inset) 16px;
}
.main-title{
  font-family:var(--serif);
  font-size:var(--display-3);
  font-weight:400;
  letter-spacing:.14em;
  color:var(--text);
  text-shadow:var(--ink-shadow);
}
.main-tools{ display:flex; gap:6px; }

/* 静音按钮：默认几乎隐形，悬停才浮出来 */
.ghost-btn{
  border:1px solid transparent; background:transparent;
  padding:6px 11px;
  font-size:var(--step-label);
  letter-spacing:var(--track-caps);
  color:var(--text-faint);
  transition:color var(--t) var(--ease), border-color var(--t) var(--ease),
             background var(--t) var(--ease);
}
.ghost-btn:hover{
  color:var(--gold);
  border-color:var(--hair-strong);
  background:var(--gold-dim);
}

/* ---------- 对话线程 ---------- */
.thread{
  flex:1; min-height:0;
  overflow-y:auto;
  overscroll-behavior:contain;
  padding:8px var(--stage-inset) 24px;
}
.thread-inner{
  max-width:var(--measure);
  margin-inline:auto;
}

/* ---------- 输入区 ----------
   同一个元素的两个姿态，只用 margin + max-width 切换，纯 CSS 不测高度、不会抖。 */
.composer-wrap{
  flex:none;
  padding:0 var(--stage-inset) max(20px, env(safe-area-inset-bottom));
}
.composer-inner{
  max-width:var(--measure);
  margin-inline:auto;
}

/* 空态：线程藏起来，输入区接管整块主区并居中 */
.main[data-empty="true"] .thread{ display:none; }
.main[data-empty="true"] .composer-wrap{
  flex:1;
  display:grid;
  place-items:center;
  padding-bottom:14vh;             /* 视觉重心略高于几何中心 */
}
.main[data-empty="true"] .composer-inner{ width:100%; text-align:center; }

/* 空态下顶部标题藏起来 —— 方向名已经用大号字写在正中，
   同一个词在一屏里出现两次是纯粹的噪音。
   用 visibility 而不是 display：标题仍占位，切到对话态时不会跳一下。 */
.main[data-empty="true"] .main-title{ visibility:hidden; }
/* 没有对话可清的时候，「清空」是个死按钮 */
.main[data-empty="true"] [data-bind="clear"]{ visibility:hidden; }

.empty-head{ margin-bottom:26px; }
.empty-title{
  font-family:var(--serif);
  font-size:var(--display-1);
  font-weight:400;
  letter-spacing:.3em;
  text-indent:.3em;                /* 补掉尾部字距造成的偏心 */
  color:var(--text);
  text-shadow:var(--ink-shadow);
}
.empty-sub{
  margin-top:14px;
  font-family:var(--serif);
  font-size:var(--step-read);
  letter-spacing:.06em;
  color:var(--text-mid);
  text-shadow:var(--ink-shadow);
}

.suggest-row{
  display:flex; flex-wrap:wrap; gap:9px; justify-content:center;
  margin-top:22px;
}

/* ============================================================
   右面板
   ============================================================ */
.panel{
  position:relative;
  min-height:0;
  background:var(--surface);
  border-left:1px solid var(--hair);
  overflow:hidden;
  transition:opacity var(--t) var(--ease);
}
.app[data-panel="closed"] .panel{ opacity:0; pointer-events:none; }

.panel-inner{
  height:100%;
  overflow-y:auto;
  padding:22px 20px 26px;
}
.panel-title{
  font-family:var(--serif);
  font-size:var(--step-read);
  font-weight:400;
  letter-spacing:var(--track-caps);
  color:var(--text-soft);
  margin-bottom:18px;
}

/* ---------- 遮罩（抽屉用） ---------- */
.scrim{
  position:fixed; inset:0; z-index:8;
  background:rgba(2,3,8,.72);
  opacity:0; pointer-events:none;
  transition:opacity var(--t) var(--ease);
}
.scrim[data-open="true"]{ opacity:1; pointer-events:auto; }

/* 窄屏才出现的抽屉开合按钮 */
.drawer-btn{ display:none; }

/* ============================================================
   断点。三个，别再多。
   ============================================================ */

/* 900–1280：右面板改成浮层盖在线程上 */
@media(max-width:1279px){
  .app{ grid-template-columns:var(--sidebar-w) minmax(0,1fr); }
  .panel{
    position:fixed; z-index:9;
    top:var(--stage-inset); right:var(--stage-inset); bottom:var(--stage-inset);
    width:min(var(--panel-w), calc(100vw - var(--stage-inset)*2));
    border:1px solid var(--hair-strong);
    /* 浮层是临时的、短暂盖住东西的 —— 这一处保留 blur */
    background:var(--surface-raised);
    backdrop-filter:blur(12px) saturate(1.1);
    -webkit-backdrop-filter:blur(12px) saturate(1.1);
    transform:translateX(12px);
    transition:opacity var(--t) var(--ease), transform var(--t) var(--ease);
  }
  .app[data-panel="closed"] .panel{ transform:translateX(12px); }
  .app[data-panel="open"] .panel{ transform:translateX(0); }
}

/* <900：侧栏改抽屉，面板改底部抽屉 */
@media(max-width:899px){
  :root{ --stage-inset:18px; }

  .app{ grid-template-columns:minmax(0,1fr); }

  .sidebar{
    position:fixed; z-index:10;
    top:0; bottom:0; left:0;
    width:min(280px, 84vw);
    transform:translateX(-100%);
    transition:transform var(--t) var(--ease);
    background:var(--ink);
  }
  .app[data-drawer="open"] .sidebar{ transform:translateX(0); }
  .drawer-btn{ display:inline-block; }

  .panel{
    top:auto; left:50%; right:auto; bottom:8px;
    transform:translateX(-50%);
    width:92vw; height:52vh;
    border:1px solid var(--hair-strong);
  }
  .app[data-panel="closed"] .panel{ transform:translateX(-50%); }
  .app[data-panel="open"] .panel{ transform:translateX(-50%); }

  /* iOS 聚焦输入框会自动放大，除非字号 ≥16px。
     这是移动端「看起来没做响应式」最常见的成因，必须显式写。 */
  .composer textarea{ font-size:16px; }
  .main-head{ padding-top:16px; }
}
