/* @layer ui — 应用骨架：顶栏 + 侧栏 + 主区 + 覆盖层。
 *
 * ================================================================
 * 外壳走「地」，内容走「纸」
 * ================================================================
 * themes.css 的第一条原则是"地不是白的，纸才是白的"：--bg(#F4F5F8) 是浅色里
 * 最暗的一级，--surface(#FDFDFE) 才向纯白爬。那条原则要在骨架这一层落地，
 * 否则整套明度梯只剩遮罩后面那一次露脸。
 *
 *   .app__header  / .app__sidebar → --bg      外壳（工具、导航、元信息）
 *   .app__main                    → --surface 纸（日历本身）
 *
 * 这是 Finder / Mail / Notes 的关系：窗口的框沉下去，内容是一张被照亮的纸。
 * 之前 header=surface 90% / sidebar=surface-2 92%，渲染成 #FCFDFE / #F9F9FA，
 * 侧栏与主区只差 4/255——读不出是两种材质，只读得出"有点脏"。改到地色后差 9/255。
 * 深色下方向一致（bg L8.2% < surface L12.4%），而且顺手修掉了原先的倒置：
 * surface-2 在深色梯里比 surface **亮**，所以旧的侧栏在深色下反而浮在主区之上。
 *
 * 外壳保留半透明 + backdrop-filter：顶栏底下只有 body（同样是 --bg），所以那里
 * 等价于实色；真正吃到模糊的是 820 以下浮在日历上的抽屉侧栏。
 */

/* ---- 安全区：刘海、灵动岛、Home Indicator、横屏的圆角与摄像头 ----
   index.html 写了 viewport-fit=cover，这句的含义是"**别**替我把视口缩进安全区里"——
   于是 y=0 就真的在刘海底下，横屏时 x=0 在摄像头切口底下。cover 不是可选项：
   不写它，深色主题下屏幕上下会露出两条系统色的白边。既然选了 cover，
   把安全区加回来就是外壳这一层欠的债，只有骨架知道谁贴着屏幕边。

   四个值收成局部自定义属性，有三个理由：
     1. env() 在 calc / max 里反复出现会把每一处的意图埋掉，起了名字才读得懂；
     2. 顶部安全区要同时进 grid 行高、顶栏内边距、抽屉的 top、遮罩的 top ——
        四处必须永远相等，所以只该有一个来源（--shell-top）；
     3. **可验证**：真机以外没法让 env() 非零，但自定义属性可以在 devtools 里直接改
        （.app.style.setProperty('--safe-t','59px')），布局就按 iPhone 的值排一遍。
   env() 的第二参数给 0px：不支持 env 的浏览器（以及桌面）走这一路，行为完全不变。 */
.app {
  --safe-t: env(safe-area-inset-top, 0px);
  /* 底部安全区要**减去**键盘吃掉的高度，否则每一处底部间距都会多算一次。
     键盘从屏幕底边往上盖，Home Indicator 的 34px 是屏幕最底下那一条 ——
     键盘一上来那条就在键盘底下了，可 env(safe-area-inset-bottom) 仍然报 34。
     全仓有五处读 --safe-b（遮罩、提示条、主区、抽屉、三面栏），
     只要在这一个地方扣掉，五处就都对；写成 max(0px, …) 是因为键盘（≈300px）
     一定大于安全区（≤34px），扣完必然到 0，但公式不该依赖这个巧合。
     --kb 见 themes.css，桌面恒为 0，这一行等价于原来的 env(...)。 */
  --safe-b: max(0px, env(safe-area-inset-bottom, 0px) - var(--kb));
  --safe-l: env(safe-area-inset-left, 0px);
  --safe-r: env(safe-area-inset-right, 0px);
  /* 外壳顶部到内容之间的总高度。窄屏日期条下沉后这个数会变（见 820 段），
     所有需要"贴在顶栏下面"的东西都读它，不各算各的。 */
  --shell-top: calc(var(--header-h) + var(--safe-t));

  display: grid;
  grid-template-columns: var(--sidebar-w) minmax(0, 1fr);
  grid-template-rows: var(--shell-top) minmax(0, 1fr);
  grid-template-areas: 'header header' 'sidebar main';
  /* 三行降级，缺一不可：
     1. 100vh —— 不认 dvh 的老浏览器的兜底。在 iOS 上它算的是**地址栏收起时**的
        高度（等于 lvh），所以在 Safari 标签页里它总是比看得见的部分高一截；
        留着它只是为了"有个数"，不是为了对。
     2. 100dvh —— iOS 15.4+ / WKWebView。standalone 里没有地址栏，
        dvh == svh == lvh == 屏幕高，这才是外壳真正该占满的高度。
     3. 减 --kb —— 软键盘不属于以上任何一种视口单位管辖的东西：
        它弹出来时 dvh 一动不动（键盘不是浏览器 UI），整个 .app 会有一截
        留在键盘底下。把外壳自己收上来，比让内部每一块各自躲键盘可靠得多，
        因为顶栏、主区、抽屉的相对关系一个字都不用改。 */
  height: 100vh;
  height: 100dvh;
  height: calc(100dvh - var(--kb));
}

.app__header {
  grid-area: header;
  position: relative;
  /* 刘海那一条由顶栏自己铺底 —— 它走"地"，所以状态栏区域和顶栏是同一种材质，
     不会在刘海下面接出第二种颜色。 */
  padding-top: var(--safe-t);
  background: color-mix(in srgb, var(--bg) 80%, transparent);
  backdrop-filter: blur(24px) saturate(1.12);
  -webkit-backdrop-filter: blur(24px) saturate(1.12);
  z-index: 20;
}
/* 发丝线承担分区，不用渐变或大投影制造层级。 */
.app__header::after {
  content: '';
  position: absolute; left: 0; right: 0; bottom: 0; height: 1px;
  background: var(--border);
}

.app__sidebar {
  grid-area: sidebar;
  position: relative;
  min-width: 0;
  background: color-mix(in srgb, var(--bg) 88%, transparent);
  backdrop-filter: blur(24px) saturate(1.08);
  -webkit-backdrop-filter: blur(24px) saturate(1.08);
  overflow-y: auto;
  overscroll-behavior: contain;
}
/* 竖线降到 hairline：它和顶栏那条横线正交相交于左上角，长度又是横线的数倍，
   同权重时竖线会压过顶栏、并在交点上结出一个十字节点。差一级刚好让交点松开。 */
.app__sidebar::after {
  content: '';
  position: absolute; top: 0; bottom: 0; right: 0; width: 1px;
  background: var(--hairline);
  pointer-events: none;
}
/* base.css 的 prefers-contrast 只抬 --border / --border-strong 两级，--hairline 不在其中。
   上面那次降级如果不在这里补回来，用户明确要求"分得更开"的那一刻，这条竖线反而从
   0.30 掉到 0.055（弱 5.5 倍）。十字节点在高对比模式下是可以接受的代价，看不见的边界不是。 */
@media (prefers-contrast: more) {
  .app__sidebar::after { background: var(--border); }
}
/* 上面那条"降到 hairline"成立，有一个没写出来的前提：**线的右边紧接着是纸**。
   日历工作面的主区是 --surface（99.33 L*），侧栏是 --bg（96.54 L*），
   9/255 的大面积台阶已经把外壳和内容分开了，线只是缝，所以可以细到看不见。
   Chat 与待办不是这样：它们的工作面自己铺 --bg（chat.css / tasks.css 都是
   有意为之——书写面浮在"地"上），于是顶栏、侧栏、主区**三块一模一样的颜色**，
   这条 1px 的缝成了外壳与内容之间唯一的边界，实测只有 1.085:1。
   这两个工作面把它抬回 --border——按令牌表的定义，--border 就是
   "一个真实物体的边（卡、输入框、面板）"，侧栏正是面板。
   body.workspace-focus 由 app/ui/shell.js:209 挂上（当前视图 kind==='workspace'），
   日历工作面不带这个类，上面的十字节点结论不受影响。 */
body.workspace-focus .app__sidebar::after { background: var(--border); }

.app__main {
  grid-area: main;
  overflow: hidden;
  background: var(--surface);
  display: flex;
  flex-direction: column;
  min-width: 0;
  /* 横屏时右边也有安全区（圆角 / Home Indicator 的横向版本）。左边在 820 段补——
     宽屏下主区的左邻是侧栏，贴屏幕左边的是它不是主区。 */
  padding-right: var(--safe-r);
}

body.sidebar-hidden .app { grid-template-columns: 0 1fr; }
body.sidebar-hidden .app__sidebar { display: none; }
/* 侧栏一旦收起，贴着屏幕左边的就换成主区了 —— 左安全区必须跟着换手。
   宽屏上这一条平时是死的（env 全 0），它只在**横屏的 iPhone** 上活过来：
   844×390 的视口宽过 820，不走下面那档"侧栏变抽屉"，于是 .app__main 的
   padding-left 那条也不命中；用户只要在横屏里收掉侧栏（⌘\ 或那枚 ☰），
   日历最左边一列就压在摄像头切口底下。 */
body.sidebar-hidden .app__main { padding-left: var(--safe-l); }

/* ================================================================
   顶栏

   顶栏是全应用最容易在「窄视口 × 浏览器缩放 × 长中文标题」三者叠加时崩掉的地方，
   所以它的规则只有两条，都写死：

     1. 除标题外的一切都是 flex: none。
        分段控件一旦被压到 min-content，中文标签就会竖排成「日 / 历」——
        那不是"挤了一点"，那是坏了。装不下就整组隐藏，不半压。
     2. 标题是唯一可伸缩的元素（min-width: 0 + 省略号）。
        但省略号是最后一道防线不是常态：下面的断点按优先级先卸次要控件，把空间让给标题。

   断点是量出来的，不是拍的。以最长的中文标题「2026年8月24日 星期一」= 185px 为基准，
   各档实际需要的顶栏宽度（含内边距与间距）：

     全部：品牌字样 + 今天/翻页 + 标题 + 190px 搜索 + 三工作面 + 四图标 + 带文字的新建   1143px
     − 品牌字样、撤销/重做                                                          981px  → 断点 1200
     − 命令面板图标，搜索收到 150px                                                  907px  → 断点 1024
     − 搜索收成 40px 图标，新建只留 +                                                737px  → 断点 960
     单行再也放不下日期 → 日期连同今天/翻页整体下沉到第二行（.hdr__datebar）           → 断点 820
     手机：工作面切换器沉到底部栏                                                     → 断点 640

   820 这一档不只是顶栏的事，它是整个外壳承认"这是窄视口"的那一刻，所以侧栏也在这里
   从占位的一列改成浮起的抽屉（见下面 820 段）。侧栏原先用的是 900——一个不在这张表
   上的第六个数字。侧栏该在哪一档切，也是量出来的：侧栏占 248px，主区要放下周视图的
   时间槽 + 七列（56 + 7×74 ≈ 574px）才不算被压坏，即视口 ≥ 822px。正好落在 820。
   `app/ui/shell.js` 的 matchMedia 已经跟着改成 820，两边现在是同一个数——**必须是**，
   见下面 820 段的说明。

   每一档在自己的下界都留了 40px 以上余量。浏览器缩放（80% / 125% / 150%）只是等比
   改变视口的 CSS 像素宽度，会自动落进下一档，不需要为缩放另写规则。

   卸载顺序 = 重要性倒序：撤销/重做/命令面板（都有快捷键）→ 品牌字样（侧栏还有 logo）
   → 搜索宽度 → 新建按钮的文字 → 最后才动日期与工作面切换器，这两个永远在。
   ================================================================ */

.hdr {
  position: relative;
  display: flex;
  align-items: center;
  gap: 16px;
  height: var(--header-h);
  /* max() 而不是 calc()：横屏刘海一侧的 44~59px 已经远大于设计内边距，再加就成了空洞；
     竖屏与桌面上 env() 是 0，max() 原样落回 16px。下面每一档同理。 */
  padding-left: max(16px, var(--safe-l));
  padding-right: max(16px, var(--safe-r));
  flex-wrap: nowrap;
  overflow: hidden;
}
.hdr__left { flex: 1 1 auto; min-width: 0; display: flex; align-items: center; gap: 8px; }
.hdr__right { flex: 0 0 auto; display: flex; align-items: center; gap: 8px; }
/* 不变量：控件既不换行也不被压缩，只有标题让位。 */
.hdr__left > *, .hdr__right > * { flex: none; }
.hdr button, .hdr select, .hdr input { white-space: nowrap; }

.hdr__brand {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  font-size: var(--fs-lead);
  font-weight: 650;
  letter-spacing: var(--tracking-title);
}
/* 品牌字样是一个**标记**，不是一段文字：跟着 logo 走 display 字族，
   比同处顶栏的日期标题（display / 650 / -0.021em）轻一档、松一档——
   一个屏幕上只该有一个最响的字，那是日期，不是产品名。 */
.hdr__brand-text {
  --preface-size: 15px;
  white-space: nowrap;
}

/* 今天 + 前后翻页是一组，间距收到 4px 让它读起来是一块，而不是三个散件。 */
.hdr__nav { display: flex; align-items: center; gap: 4px; }

.hdr__title {
  flex: 0 1 auto;
  min-width: 0;
  margin-left: 4px;
  font-family: var(--font-display);
  font-size: var(--fs-title);
  font-weight: 650;
  letter-spacing: var(--tracking-title);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

.hdr__search { position: relative; display: flex; align-items: center; }
/* 图标在 DOM 里排在输入框之后，这样才能用 ~ 跟随输入框的展开/收起状态。 */
.hdr__search-icon {
  position: absolute;
  left: 11px;
  /* 顶栏里 ☰ / ⚙ / ⌕ 是同一个角色（一枚图标按钮），所以是同一个墨色 --text-muted
     （.icon-btn 就是这个值）。原先只有这一枚走 faint —— 比邻居淡一档，
     而它又是窄屏下搜索唯一的入口。理由是"同一角色一种长相"，不是对比度：
     两级墨色在地色上都过 AA（复核当天实测 faint 5.72:1 / muted 8.09:1，
     令牌归 themes.css 管，数值随它走，别把这两个数当常量引用）。 */
  color: var(--text-muted);
  font-size: var(--fs-body);
  pointer-events: none;
  transition: left var(--dur) var(--ease);
}
.hdr__search-input {
  width: 190px;
  height: 34px;
  padding: 0 28px;
  border: 1px solid var(--hairline);
  border-radius: var(--radius-pill);
  background: var(--surface-sunken);
  color: var(--text);
  font-size: var(--fs-body);
  transition: width var(--dur) var(--ease), border-color var(--dur-fast) var(--ease),
              box-shadow var(--dur-fast) var(--ease), background var(--dur-fast) var(--ease);
}
.hdr__search-input:focus {
  outline: none;
  width: 240px;
  background: var(--surface);
  border-color: var(--accent);
  box-shadow: 0 0 0 3.5px var(--accent-soft);
}
.hdr__search-input::-webkit-search-cancel-button { display: none; }
.hdr__search-clear {
  position: absolute;
  right: 7px;
  padding: 2px 4px;
  border-radius: var(--radius-xs);
  color: var(--text-faint);
  font-size: 16px;
  line-height: 1;
}
.hdr__search-clear:hover { color: var(--text); background: var(--surface-hover); }
.hdr__search-input:placeholder-shown ~ .hdr__search-clear { opacity: 0; pointer-events: none; }

.workspace-switch {
  display: flex;
  align-items: center;
  gap: 2px;
  padding: 3px;
  border: 1px solid var(--hairline);
  border-radius: var(--radius-sm);
  background: var(--surface-sunken);
}
.workspace-switch__item {
  flex: none;
  white-space: nowrap;          /* 少了这一条，「日历」就会被压成竖排两行 */
  height: 28px;
  padding: 0 12px;
  border: 0;
  border-radius: calc(var(--radius-sm) - 2px);
  background: transparent;
  color: var(--text-muted);
  font-size: var(--fs-small);
  font-weight: 520;
}
.workspace-switch__item:hover { color: var(--text); background: var(--surface-hover); }
.workspace-switch__item.is-active {
  background: var(--surface);
  color: var(--text);
  font-weight: 650;
  box-shadow: var(--shadow-sm);
}

/* 次要图标合成一组：组内 2px、组外 8px，读起来是一块而不是四个散件，也少占 18px。 */
.hdr__tools { display: flex; align-items: center; gap: 2px; }

.hdr__new { padding: 0 13px; gap: 5px; }
.hdr__new-icon { font-size: var(--fs-lead); line-height: 1; }

/* 日期条：只在窄屏出现（见 820 断点），把日期从横向争抢里整体摘出去。 */
.hdr__datebar { display: none; }

/* ---- 侧栏 ---- */
/* 侧栏永远贴着屏幕左边（宽屏是第一列、窄屏是 left:0 的抽屉），所以横屏的左安全区
   由它自己吃掉。吃在 .side 上而不是 .app__sidebar 上：后者是滚动容器，
   给滚动容器加内边距会连带影响滚动条与那条 right:0 的发丝线的落点。 */
.side {
  min-height: 100%;
  width: 100%;
  min-width: 0;
  overflow-x: hidden;
  padding: 16px 14px;
  padding-left: max(14px, var(--safe-l));
  display: flex; flex-direction: column; gap: 20px;
}
.side__new { min-height: 38px; }
.side__section { display: flex; flex-direction: column; gap: 6px; min-width: 0; }
/* 「我的日历」「接下来」这种分区小标题，在这个产品里出现了十几处：
   .palette__group / .field__label / .deskcard__head / .chat-pulse__section h3 /
   .tasks-rail__block h3 / .event-detail__row dt …… 全部是
   --fs-micro(11) + 620 + --tracking-caps(0.085em) + --text-faint。
   只有侧栏这一处写死成 11.5 / 680 / 0.07em——三个数字都差一点点，
   谁也说不出为什么，正是"两个人各排了一次版"的样子。归队。 */
/* 墨色是 --text-muted，不是 --text-faint。docs/UI_PATTERNS.md §5 给这个角色定的就是
   muted；这里走 faint 是一处漏网。这行字是"我的日历""接下来"，是侧栏里唯一说明
   下面那一堆是什么的东西，11px 又已经在字阶最小端，没有再淡一档的余地。
   （令牌本身归 themes.css 管，不在这里动；这里改的是"侧栏文字不该用 faint 那一级"。
   复核当天在浅色地色上实测 faint 5.72:1 / muted 8.09:1，深色 6.78 / 10.34 ——
   两级都过 AA，所以这一处的判据是角色，不是对比度；数值跟着 themes.css 走。） */
.side__header {
  display: flex; align-items: center; justify-content: space-between;
  font-size: var(--fs-micro); font-weight: 620; letter-spacing: var(--tracking-caps);
  color: var(--text-muted); text-transform: uppercase;
}
.side__empty { font-size: var(--fs-small); color: var(--text-muted); padding: 4px 2px; }
.side__add-cal { align-self: flex-start; margin-top: 2px; }
.side__panel-body { font-size: var(--fs-body); }

/* ---- 覆盖层 ---- */
.app__overlay { position: fixed; inset: 0; pointer-events: none; z-index: 100; }
.app__overlay:empty { display: none; }
.scrim {
  /* 上面那条 .app { height: calc(100dvh - --kb) } 管不到这里：弹层是 fixed 的，
     它量的是**布局视口**，而布局视口在 iOS 上不因键盘变化。所以底边自己扣。
     事件编辑器里有标题/地点/备注三个输入框，弹层是第四个"输入区"——
     不扣的话，点标题弹出键盘，保存/删除那一排按钮正好落在键盘底下。 */
  position: fixed; inset: 0 0 var(--kb) 0;
  display: flex; align-items: flex-start; justify-content: center;
  /* 弹层是 inset: 0 的，它和外壳一样贴着屏幕边，所以四边同样要吃安全区。
     竖屏原先是"刚好躲过"：7vh 在 844 高的机器上是 59.1px，与刘海的 59px 只差 0.1
     ——换一台比例不同的机器就不成立了。横屏更直接：底边 16px 落在 21px 的
     Home Indicator 里。这里的 max() 在桌面（env 全 0）原样落回 40/16，零改动。

     7vh → 7svh：这个上边距是一段**静态装饰性留白**，不该跟着浏览器 UI 动。
       · vh（== lvh）量的是地址栏收起时的高度，在 Safari 标签页里比实际可见区高
         一截 —— 空间最紧的时候留白反而最大，正好反了。
       · dvh 会跟着地址栏收放**实时变**：手指一滚，整个弹层就跟着上下爬。
       · svh 取的是"地址栏在场"的那一档，是三者里唯一恒定且一定放得下的。
     standalone 里没有地址栏，三者相等，这一改只影响 Safari 标签页那一路。 */
  padding: max(7svh, 40px, var(--safe-t)) max(16px, var(--safe-r))
           max(16px, var(--safe-b)) max(16px, var(--safe-l));
  /* 苹果式的材质遮罩：模糊 + 轻微降饱和，而不是压一层黑 */
  background: color-mix(in srgb, var(--bg) 55%, transparent);
  backdrop-filter: blur(20px) saturate(1.4);
  -webkit-backdrop-filter: blur(20px) saturate(1.4);
  pointer-events: auto;
  animation: scrim-in var(--dur) var(--ease);
}
@keyframes scrim-in { from { opacity: 0; backdrop-filter: blur(0); } to { opacity: 1; } }

.app__toast {
  position: fixed;
  left: 50%;
  /* 与 .scrim 同理：fixed 元素量的是布局视口，键盘那一段要自己加回来。
     --safe-b 已经在定义处扣过 --kb 了，所以这里是 22 + 0 + 键盘，不会重复计。 */
  bottom: calc(22px + var(--safe-b) + var(--kb));
  transform: translateX(-50%);
  display: flex;
  flex-direction: column-reverse;
  gap: 8px;
  z-index: 200;
  pointer-events: none;
  width: min(480px, calc(100vw - 24px - var(--safe-l) - var(--safe-r)));
}
.app__toast > * { pointer-events: auto; }

/* ================================================================
   响应式：按上面那张表逐级卸载
   ================================================================ */

/* 1200 —— 品牌字样与撤销/重做先走：logo 侧栏还有，撤销是 ⌘Z。 */
@media (max-width: 1200px) {
  .hdr__brand-text { display: none; }
  .hdr__tool--undo, .hdr__tool--redo { display: none; }
}

/* 1024 —— 命令面板图标也走（⌘K 仍在），搜索收窄。 */
@media (max-width: 1024px) {
  .hdr__tool--palette { display: none; }
  .hdr__search-input { width: 150px; }
  .hdr__search-input:focus { width: 200px; }
}

/* 960 —— 搜索收成一枚图标，聚焦或已有关键词时才展开；
   展开时挤掉的是标题（唯一可伸缩者），不会顶坏任何控件。新建按钮只留 +。 */
@media (max-width: 960px) {
  /* 收起时把文字整个推出可视框，只剩那枚放大镜——半截字比没有字更难看。
     并且**收起的搜索不是一个输入框，是一颗按钮**，所以它就该长成按钮：
     底和描边都撤掉，只留一枚 ⌕，和左右的 ☰ / ⚙ 完全同一副长相。
     原先这里是一枚沉下去的灰胶囊夹在两枚裸图标中间——一个"域"打扮成一个"键"，
     是顶栏上最扎眼的一处不齐（docs/UI_PATTERNS.md 的"同一角色一种长相"）。
     它真的变回输入框是在被点开之后：那一刻底、边、圆角一起长回来。
     圆角始终留在 --radius-pill：收起时底是透明的，看不见圆角，
     所以没必要为这个再切一次值、也就不会在展开动画里"啪"地换形。 */
  .hdr__search-input {
    width: 34px;
    padding: 0 0 0 34px;
    border-color: transparent;
    background: transparent;
  }
  /* 撤掉了底，悬停反馈就交给字重之外唯一还剩的通道：墨色。
     （几何一点不动 —— 一枚会长出灰底的图标按钮和它旁边不会的两枚，又是一处不齐。） */
  .hdr__search-input:hover ~ .hdr__search-icon { color: var(--text); }
  /* 站到中间就不再是"域里的一枚提示符"，而是这颗按钮的**全部内容**，
     所以字号要跟 .icon-btn 走（--fs-lead 16px），不是域内附饰的 13.5——
     ☰ ⌕ ⚙ 并排时，小两号的那一枚一眼就看得出来。展开后它退回域里，字号也退回去。 */
  .hdr__search-icon { left: 50%; transform: translateX(-50%); font-size: var(--fs-lead); }
  .hdr__search-input:focus,
  .hdr__search-input:not(:placeholder-shown) { width: 200px; padding: 0 28px; }
  /* 有关键词但没聚焦：里面装着字，它就不再是一颗按钮了，底和边长回来。 */
  .hdr__search-input:not(:placeholder-shown) { border-color: var(--hairline); background: var(--surface-sunken); }
  /* 聚焦压在上面一条之后，否则"既聚焦又有字"时会退回发丝线边、丢掉强调色的聚焦态。 */
  .hdr__search-input:focus { border-color: var(--accent); background: var(--surface); }
  .hdr__search-input:focus ~ .hdr__search-icon,
  .hdr__search-input:not(:placeholder-shown) ~ .hdr__search-icon {
    left: 11px; transform: none; font-size: var(--fs-body);
  }
  .hdr__new { width: 34px; padding: 0; }
  .hdr__new-label { display: none; }
}

/* 820 —— 窄视口从这里开始，两件事一起发生：
   1. 单行已经放不下「日期 + 工具」。日期连同今天/翻页整体下沉到第二行：
      两行各自宽松，标题从此不再需要省略号。这比继续截断标题诚实得多。
   2. 侧栏从占位的一列改成浮在日历上的抽屉，主区拿回整个宽度。
      抽屉收起时必须 visibility: hidden——只把它推到视口左侧之外，它仍然在
      Tab 序与无障碍树里，从顶栏按 Tab 会走进十几个看不见的控件。
      visibility 走 transition 的特例（visible→hidden 在动画结束时才生效），
      所以滑出动画完整保留，滑入则立刻可见。

   抽屉「默认收起」这件事 CSS 一个人做不到：body.sidebar-hidden 对应 state.ui.sidebar，
   默认是 true，CSS 分不清"默认开着"和"用户刚点开"。真正把它按下去的是
   app/ui/shell.js 里的 matchMedia，**那个数必须和这里的 820 一模一样**：
   shell.js 还拿同一个 matchMedia 判断"抽屉是不是盖在内容上"，进而决定要不要把
   落在主区的第一次点击吞掉当作"关抽屉"。两边错位过一次（CSS 820 / JS 900），
   结果是 821–900 这一段侧栏明明是一列什么都没盖住的栏，点日历格子却毫无反应、
   侧栏自己收了。改这里请连 shell.js 一起改。 */
@media (max-width: 820px) {
  /* 日期条下沉，外壳顶部就高了 48px。只改 --shell-top 这一个数，
     grid 行高、抽屉的 top、遮罩的 top 三处自动跟上（见文件开头）。 */
  body:not(.workspace-focus) .app {
    --shell-top: calc(var(--header-h) + 48px + var(--safe-t));
  }

  .app { grid-template-columns: 0 minmax(0, 1fr); }
  /* 主区这时是整幅的，左边也贴着屏幕，所以左安全区归它。 */
  .app__main { padding-left: var(--safe-l); }
  .app__sidebar {
    position: fixed;
    top: var(--shell-top);
    /* 抽屉也是 fixed，同样够不到 .app 那次收缩。侧栏里有"新建日程"和同步卡，
       抽屉开着的时候键盘一般不在，但主界面搜索框聚焦时抽屉可以同时开着 ——
       那时抽屉最后一屏内容会落在键盘底下，怎么滚都滚不出来。 */
    bottom: var(--kb);
    left: 0;
    width: min(var(--sidebar-w), calc(100vw - max(48px, var(--safe-r))));
    z-index: 30;
    box-shadow: var(--shadow-lg);
    transform: translateX(0);
    transition: transform var(--dur) var(--ease), visibility var(--dur);
  }
  body.sidebar-hidden .app__sidebar {
    display: block;
    visibility: hidden;
    transform: translateX(-100%);
  }

  /* ---- 抽屉遮罩 ----
     抽屉盖在日历上时，主区那一片**已经**是遮罩了：shell.js 的 onMainTapWhenDrawerOpen
     把落在那里的第一次点击吞掉当作"关抽屉"。缺的从来不是行为，是**告知**——
     界面上没有任何东西说"这一片现在只负责关我"，用户只能靠试。

     所以这一层只做告知，不做拦截：pointer-events: none，那条吞点击的逻辑一个字不改。

     材质按 .scrim 的同一套语言，但剂量降一档，而且**不加模糊**：
     模糊是"这后面的事你现在管不着"（弹层），抽屉不是那个意思——日历还在那儿，
     你随时能点回去。何况在手机上给整幅日历网格上实时高斯模糊也太贵了。
     这里用的是"降饱和 + 一层地色的薄纱"：颜色退半步、明度掉一级（实测把纸从
     #FDFDFE 压到 #F8F9FB，正好是明度梯上的一格），日历还读得清，但一眼知道它凉了。
     压一层黑是 Material 的做法，themes.css 第五条明确不要。

     只从外壳底下开始盖：顶栏不参与。抽屉是从顶栏那枚 ☰ 里拉出来的，
     把关它的那颗按钮一起蒙灰只会让人更不知道该点哪。 */
  .app::after {
    content: '';
    position: fixed;
    top: var(--shell-top);
    right: 0;
    bottom: 0;
    left: 0;
    z-index: 25;                 /* 顶栏 20 < 这里 < 抽屉 30 */
    pointer-events: none;
    background: color-mix(in srgb, var(--bg) 55%, transparent);
    backdrop-filter: saturate(0.82);
    -webkit-backdrop-filter: saturate(0.82);
    transition: opacity var(--dur) var(--ease);
  }
  body.sidebar-hidden .app::after { opacity: 0; }
  /* 不用在这里补 prefers-reduced-motion：base.css 的全局那条已经覆盖 *::before / *::after。 */

  .hdr { overflow: visible; }
  .hdr--calendar .hdr__title, .hdr__nav { display: none; }

  .hdr__datebar {
    position: absolute;
    left: 0;
    right: 0;
    top: 100%;
    height: 48px;
    display: flex;
    align-items: center;
    gap: 8px;
    padding: 6px max(16px, var(--safe-r)) 6px max(16px, var(--safe-l));
  }
  .hdr__datebar-title {
    flex: 1 1 auto;
    min-width: 0;
    font-family: var(--font-display);
    font-size: var(--fs-title);
    font-weight: 680;
    letter-spacing: var(--tracking-title);
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
  }
  /* 2px → 12px。‹ › 画出来是 32 宽，命中区是 44（见下面那段），
     各自向外多出 6px；间距还留 2px 的话两个命中区会**重叠 10px**，
     重叠处按绘制顺序归后画的那个——点 ‹ 的右半边会翻到下一页。
     12px 让两块命中区正好首尾相接、一格不叠。视觉上这一组是松了一点，
     但它本来就是"上一页/下一页"两个相反的动作，不是一块。 */
  .hdr__datebar-nav { display: flex; align-items: center; gap: 12px; }
  .hdr__datebar-nav .icon-btn,
  .hdr__datebar-today { flex: none; }
  .hdr__datebar-today { min-height: 32px; padding: 0 11px; font-size: var(--fs-small); }

  /* ================================================================
     触摸命中区：一律 ≥ 44×44（Apple HIG 下限，docs/UI_PATTERNS.md §9）

     **画出来的尺寸一个都不改。** ☰ 36、⚙ 36、+ 36、‹ › 32、今天 50×32 ——
     把它们真画成 44 会让 56px 高的顶栏被塞满，那是另一种廉价感（"按钮好大好圆"）。
     所以扩的是一块透明的 ::before：它是伪元素，事件照样算在按钮头上，
     shell.js 的 [data-act] 委托不受影响。

     width/height 都写 max(100%, 44px)：本来就 ≥44 的（「今天」宽 50）保持原样不被截窄，
     不够的才补到 44，一条规则覆盖所有情况，不用逐个控件列尺寸。

     命中区互相不许重叠 —— 重叠区归绘制顺序里靠后的那个，用户点 A 却触发 B，
     比命中区小还难查。所以每一档的 gap 都要 ≥ 两侧各自的外扩之和：
       36 宽的控件外扩 4 → 相邻两个需要 gap ≥ 8（.hdr__right / .hdr__tools 都满足）
       32 宽的控件外扩 6 → 相邻两个需要 gap ≥ 12（.hdr__datebar-nav 上面刚改）
     ================================================================ */
  .hdr .icon-btn,
  .hdr .btn,
  .hdr__search-clear,
  .workspace-switch__item { position: relative; }

  .hdr .icon-btn::before,
  .hdr .btn::before,
  .hdr__search-clear::before,
  .workspace-switch__item::before {
    content: '';
    position: absolute;
    top: 50%;
    left: 50%;
    width: max(100%, 44px);
    height: max(100%, 44px);
    transform: translate(-50%, -50%);
  }

  /* 搜索框是这里唯一的**替换元素**，<input> 没有伪元素可挂，上面那一招用不了。
     它也不需要：960 那一档已经把收起态的底和描边撤成透明了（见上面），
     所以这里直接把盒子做成 44×44 —— 画面上一点变化都没有（透明的盒子撑大不显形），
     命中区却是货真价实的 44。左内边距跟着走 42：42 + 两条 1px 的边正好 44，
     多一点点就会把盒子顶宽、让 ⌕ 偏出中心。

     试过在外层包一个 <label> 再拿伪元素扩 —— 走不通，而且值得记下来省得下一个人再试：
     那块伪元素要么盖在输入框上（点已输入文字的中段只会重新聚焦，光标跳到末尾），
     要么 z-index: -1 沉下去，结果被 .hdr 自己的盒子挡掉（.hdr 是 position: relative，
     在 .app__header 这个层叠上下文里排在负层之上），实测命中区纹丝不动还是 36×34。 */
  .hdr__search-input { width: 44px; height: 44px; padding-left: 42px; }

  /* 顶行的控件在这一档统一成 36（原来 ☰ ⚙ 是 32、+ 是 34）。
     不是为了"大一点好点"——是为了让**外扩量全档一致**：
     36 的控件外扩 4，那么 .hdr__right 现有的 gap: 8 正好首尾相接；
     混着 32（外扩 6）和 34（外扩 5）时，相邻两块要 11px 才不叠，8px 就会叠 3px。
     实测 760 宽下 ⚙ 的横向命中只剩 41 —— 右边 3px 被「+」抢走了。
     统一到 36 之后不用为这一档另调 gap，也少一个只在某一档成立的数字。
     （36 本来就是 640 以下用的值，这里只是把它提前到窄屏开始的那一刻。） */
  .hdr__left .icon-btn,
  .hdr__tools .icon-btn { flex: 0 0 36px; width: 36px; height: 36px; }
  .hdr__new { width: 36px; min-height: 36px; }
  /* 组内 2px 是"读起来是一块"的间距，但那是给鼠标的。这一档里 .hdr__tools 只剩 ⚙ 一枚，
     gap 用不上；写成 8 是把"命中区不许重叠"变成结构上成立的事，
     日后谁把撤销/重做放回来也不会撞车。 */
  .hdr__tools { gap: 8px; }
}

/* 640 —— 手机：工作面切换器沉到底部栏，拇指够得着。 */
@media (max-width: 640px) {
  .app__main { padding-bottom: calc(64px + var(--safe-b)); }
  /* 底部三面栏是 .app__header 的 fixed 子元素，而 .app__header 是
     position: relative + z-index: 20 —— 一个**层叠上下文**。于是底栏自己写的
     z-index: 90 只在顶栏内部管用，对外它整个人排在 20：低于抽屉的 30，也低于
     抽屉遮罩的 25。后果不是"看上去暗了一点"——抽屉打开时，底栏左边两枚
     （对话 / 日历）压在抽屉底下，点下去命中的是抽屉里的控件
     （实测 390 宽点「对话」，elementFromPoint 给的是侧栏同步卡里的一行）。
     顶栏与底栏是同一个角色的两半：三个工作面的入口。上面那条"遮罩不盖顶栏"
     的理由（别把出口一起蒙灰）对底栏一字不差地成立。
     抬到 40：高于抽屉 30 与遮罩 25，低于弹层 100 与提示条 200。顶栏与抽屉在
     几何上从不重叠（抽屉从 --shell-top 起算），所以抬它盖不住任何东西，
     唯一的变化是抽屉那道 shadow-lg 不再糊到顶栏的发丝线上。

     第二条（backdrop-filter: none）是老规矩：backdrop-filter 会把 fixed 子元素
     变成相对顶栏定位，移动端必须关掉，底栏才真的贴视口底部。关掉模糊就得给一个
     实色，这个实色仍然是**地**——否则顶栏在手机上会突然变成纸，和主区同色，
     只剩一条发丝线撑着分界。 */
  .app__header {
    z-index: 40;
    overflow: visible;
    background: var(--bg);
    backdrop-filter: none;
    -webkit-backdrop-filter: none;
  }
  /* 底栏浮在抽屉之上（上一条），抽屉自己最后一段内容就会被它盖住 ——
     主区早为这件事留了 64px，抽屉是同一个道理，留同一个数。
     不留的话，侧栏滚到底时最后一张卡有 94px 落在底栏与 Home Indicator 底下，
     怎么滚都露不出来。 */
  .side { padding-bottom: calc(64px + var(--safe-b)); }
  .hdr {
    gap: 10px;
    padding-left: max(10px, var(--safe-l));
    padding-right: max(10px, var(--safe-r));
  }
  /* 顶行本来就没有可让位的标题了，搜索一展开就让次要控件先退场。 */
  .hdr__search:has(:focus) ~ .hdr__tools,
  .hdr__search:has(:focus) ~ .hdr__new,
  .hdr__search:has(.hdr__search-input:not(:placeholder-shown)) ~ .hdr__tools,
  .hdr__search:has(.hdr__search-input:not(:placeholder-shown)) ~ .hdr__new { display: none; }

  .workspace-switch {
    position: fixed;
    left: max(12px, var(--safe-l));
    right: max(12px, var(--safe-r));
    /* max() 而不是 calc()：Home Indicator 的 34px 安全区里，指示条本身只占中间 5px，
       浮起的胶囊底边落在安全区上沿就已经离它很远了，再叠 8px 只是把栏往上顶。
       没有指示条的设备（安卓、桌面窄窗）env 为 0，落回 8px。 */
    bottom: max(8px, var(--safe-b));
    z-index: 90;
    height: 52px;
    padding: 4px;
    gap: 4px;
    border-color: var(--border-strong);
    border-radius: var(--radius-lg);
    background: color-mix(in srgb, var(--surface) 92%, transparent);
    box-shadow: var(--shadow-md);
    backdrop-filter: blur(24px) saturate(1.12);
    -webkit-backdrop-filter: blur(24px) saturate(1.12);
  }
  .workspace-switch__item {
    flex: 1 1 0;
    /* 42 → 44。这一档是白拿的：轨道 52 高、内边距 4，本来就留着 44 的净空，
       之前只填了 42，两边各空 1px。现在填满，底栏一点没长高，命中区到线。 */
    height: 44px;
    padding: 0 8px;
    border-radius: var(--radius);
    font-size: var(--fs-body);
    font-weight: 600;
  }

  .hdr__datebar { padding-left: max(10px, var(--safe-l)); padding-right: max(10px, var(--safe-r)); }

  /* 底栏浮在主区之上，提示条不能再按"离视口底 22px"算，否则正好压在底栏上。
     22（原来的间距）+ 52（底栏）+ 8（底栏离底）+ 安全区 + 键盘。
     键盘弹起时底栏整个收走（见 ios.css 的 body.kb-open），52 这一段就不该再留 ——
     那一条在 ios.css 里，和"收走底栏"的决定写在一起，不在这里分成两半。 */
  .app__toast { bottom: calc(22px + 52px + max(8px, var(--safe-b)) + var(--kb)); }
  /* 键盘打开时三面栏先退场，把高度让给正在写的内容；点消息区或空白处会由
     viewport.js 统一 blur，键盘与三面栏一起恢复。 */
  .app.kb-open .workspace-switch { display: none; }
  .app.kb-open .app__main { padding-bottom: 0; }
  .app.kb-open .app__toast { bottom: calc(12px + var(--kb)); }
}

@media (max-width: 400px) {
  .hdr {
    gap: 6px;
    padding-left: max(7px, var(--safe-l));
    padding-right: max(7px, var(--safe-r));
  }
  /* 6 → 8：右侧三枚控件（⌕ ⚙ +）画出来都是 36，命中区 44，各自外扩 4px。
     间距 6 时相邻两块命中区叠 2px，点 ⚙ 的右边缘会新建日程。8 是"正好不叠"。
     顶栏在 390 宽下左右两组之间还空着 194px，这 4px 是白给的。 */
  .hdr__right { gap: 8px; }
  .workspace-switch { left: max(7px, var(--safe-l)); right: max(7px, var(--safe-r)); }
  /* 同理：4 → 8，「今天」的命中区（宽 50，不外扩）与 ‹ 的命中区（外扩 6）之间
     至少要 6px 才不叠，取 8 留一格余量。 */
  .hdr__datebar {
    padding-left: max(7px, var(--safe-l));
    padding-right: max(7px, var(--safe-r));
    gap: 8px;
  }
  .hdr__datebar-title { font-size: var(--fs-lead); }
}
