/* =============================================================================
 * app.css —— 基础层与共享工具类
 * =============================================================================
 * 这里只放「Tailwind 工具类表达不了」的东西：字体图标的 variation settings、
 * 安全区、滚动条隐藏、以及跨屏复用的布局量。能用 Tailwind 类表达的，写在 HTML 里。
 *
 * 色值一律走 tokens.css 的变量，本文件不出现 hex。
 * ========================================================================== */

/* --- 基础 ---------------------------------------------------------------- */

html {
  -webkit-text-size-adjust: 100%;
}

body {
  background-color: rgb(var(--c-paper));
  color: rgb(var(--c-ink));
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  -webkit-tap-highlight-color: transparent;
  /* 页面永不横向滚动（PRD §8.4）。这里不是靠 overflow-x: hidden 掩盖，
     而是配合「内容列封顶」的布局，让溢出根本不发生。 */
  overflow-x: hidden;
}

/* 金额一律用等宽 tabular 数字，输入时布局不左右跳（DESIGN.md §3）。 */
.tnum {
  font-family: 'JetBrains Mono', ui-monospace, monospace;
  font-variant-numeric: tabular-nums;
  font-feature-settings: 'tnum' 1;
}

/* --- 存储不可用警示条 ---------------------------------------------------- */

/* 常驻在视口最顶上。它不是 toast —— toast 会自己消失，而「这台设备存不下数据」
   在修好之前一直成立，它会一直挂到后端变得可持久为止（由 shell.js 决定去留）。

   样式写在 css 里而不是用 Tailwind 类：这段 markup 是运行时插进去的，
   Tailwind Play CDN 是扫 DOM 现生成样式的，插进去到样式生效之间有一拍空窗，
   而 shell.js 紧接着就要量它的高度 —— 那一拍量出来的是错的高度。 */
.storage-alert {
  position: fixed;
  inset: 0 0 auto 0;
  z-index: 60;                 /* 高于底栏与侧边栏（z-50） */
  display: flex;
  align-items: flex-start;
  gap: 0.5rem;
  padding: 0.625rem 1rem;
  background-color: rgb(var(--c-cinnabar));
  color: rgb(var(--c-surface));
  font-size: 0.8125rem;
  line-height: 1.45;
}

.storage-alert .material-symbols-outlined {
  flex: none;
  font-size: 1.125rem;
}

/* 警示条是 fixed 的，会盖住三处同样贴顶的常驻元素。让它们各自让开它的高度。
   变量 --storage-alert-h 由 shell.js 按实测量出来的高度写。 */
.has-storage-alert header.sticky {          /* 明细列表 / 统计的吸顶标题栏 */
  top: var(--storage-alert-h, 0px);
}
.has-storage-alert .yibi-toast-top {        /* 顶部 toast（保存成功横幅） */
  padding-top: calc(var(--storage-alert-h, 0px) + 1.25rem);
}
.has-storage-alert aside[aria-label="主导航"] {   /* 桌面端侧边栏的品牌区 */
  padding-top: var(--storage-alert-h, 0px);
}

/* --- 字体图标 ------------------------------------------------------------ */

.material-symbols-outlined {
  font-variation-settings: 'FILL' 0, 'wght' 350, 'GRAD' 0, 'opsz' 24;
  line-height: 1;
  display: inline-block;
  white-space: nowrap;
  direction: ltr;
  text-transform: none;
  letter-spacing: normal;
  word-wrap: normal;

  /* 图标永远不许比它自己的字号宽。
     字体到位时这是个恒真的约束 —— Material Symbols 画在正方 em 盒里，字宽就是 1em，
     所以正常情况下下面两行完全不生效（改这一条前后出图逐像素相同，验过）。

     字体没到位时它才是要紧的。`font-display: block` 的 3 秒阻塞期过后，ligature 会
     退化成**字面文字**：`receipt_long` 有 99px、`assignment_turned_in` 有 211px，而
     它们所在的位置往往只有 22~40px 宽。这些 span 是 inline-block + nowrap，字会**画到
     盒子外面**；画出去的这部分会把初始包含块撑宽，于是在手机上出现横向滚动条
     —— 网速慢或字体 CDN 抽风的每一位用户，头几秒看到的就是这个。

     max-width 而不是 width：只夹住，绝不拉伸。 */
  max-width: 1em;
  overflow: hidden;
}

/* 选中态图标（底栏当前项、视图切换当前项）：实心。 */
.icon-filled {
  font-variation-settings: 'FILL' 1, 'wght' 400, 'GRAD' 0, 'opsz' 24;
}

/* --- 安全区 -------------------------------------------------------------- */

/* 底栏：56px 内容高 + 系统 Home 条。设计稿里有 4 屏引用了未定义的 `pb-safe`，
   等于安全区从未生效；这里给一个真实实现。 */
.safe-bottom {
  padding-bottom: env(safe-area-inset-bottom, 0px);
}

/* 带底栏的页面，正文底部要留出「底栏 + 安全区 + 一段呼吸」。
   设计稿里这个值在 6 屏之间是 pb-20 / pb-24 / pb-28 / pb-36 各写各的，收敛成一个。 */
.pad-for-tabbar {
  padding-bottom: calc(3.5rem + env(safe-area-inset-bottom, 0px) + 1rem);
}

/* --- 记账页的「舞台」：垂直居中 ------------------------------------------

   compose（还没解析出东西）时，标题「一句话记账」+ 输入框整块在视口里**居中** ——
   这一屏要做的事只有一件，内容吊在顶上会显得整页没写完。review（解析单子出来了）
   时回到顶部：单子有几百像素高，居中会把「确认记账」顶到视口外面去，视线也得
   从屏幕中间往下走。

   怎么做的：外框是 flex 列 + 一根撑杆在内容上下，靠 flex-grow 分剩余空间。
     compose → 两根各 grow 1（对半，内容居中）
     review  → 上方 grow 0（内容贴顶，空间全让给下面）

   为什么不用 justify-content：它是**离散值**，一切换就从中间硬跳到顶上，动画
   做不出来（这是这一屏要有动画的前提，见下）。flex-grow 是可插值的数字，
   过渡直接交给 CSS。

   为什么不用 transform 平移一段像素：transform 把盒子挪出它原本的位置，而
   **变换之后的溢出是算进可滚动区域的** —— 页面会凭空多出一截能滚动的空白
   （shot.mjs 量的 documentElement.scrollHeight 立刻会看见）。flex-grow 走的是
   真实布局，没有这个副作用。另一条路是 JS 量像素再设 translateY，代价是
   视口一变、字号一变都得重量；这里让浏览器自己算。

   min-height 减掉 --storage-alert-h：后端降级时顶上会挂一条 fixed 的警示条，
   而 body 是 min-h-screen —— 不减的话「警示条 + 舞台」合起来会比视口高出一截，
   凭空多出滚动条。那个变量是 shell.js 实测出来写在 documentElement 上的，
   本来就是这个用途（见 app.css 上面几处 sticky 标题）。 */
.record-stage {
  display: flex;
  flex-direction: column;
  min-height: calc(100vh - var(--storage-alert-h, 0px));
}
.record-spacer {
  flex-grow: 1;
  /* 340ms 与下面单子淡入同一条曲线、同一个时长：两条动画一快一慢会显得脱节。 */
  transition: flex-grow 340ms cubic-bezier(0.22, 0.61, 0.36, 1);
}
/* 解析单子在的时候，上方撑杆收掉 —— 内容贴顶。 */
[data-state='review'] .record-spacer[data-spacer='top'] {
  flex-grow: 0;
}

/* 单子「长出来」而不是「啪」地出现。
 *
 * ★ 这一层不是为了好看，是为了**上面那根撑杆的过渡能连贯**。
 * 单子有 405px 高，它一出现，撑杆能分的剩余空间立刻从 672px 掉到 267px；
 * 而 flex-grow 的过渡动画的是**比例**，不是像素 —— 于是切态那一帧里，
 * 内容会先「闪」地跳上去 202px（336 → 134，一帧之内），再花 340ms 慢慢
 * 走完剩下的 134px。实测过：逐帧记录 main 的 top，最大单帧位移 202px。
 * 那就是「突兀」的来源，光给撑杆加 transition 治不了。
 *
 * 让单子的高度也在这 340ms 里从 0 长到 405，剩余空间就是**连续**缩小的，
 * 内容的位置跟着连续变化：336 → 245 → 157 → 74 → 0，没有跳变。
 *
 * 用 grid 的 0fr → 1fr 而不是 max-height：max-height 要猜一个够大的数，
 * 猜大了前半段几乎不动、后半段猛冲；0fr → 1fr 是按内容真实高度插值的。
 * 浏览器不支持插值这条时（老 Safari），退化成「直接出现」——
 * 也就是加这层之前的样子，版面仍然是对的。
 *
 * section 上的 hidden 保持不变：它进的是无障碍树，动画不该动那个语义。 */
.record-collapse {
  display: grid;
  grid-template-rows: 0fr;
  transition: grid-template-rows 340ms cubic-bezier(0.22, 0.61, 0.36, 1);
}
[data-state='review'] .record-collapse {
  grid-template-rows: 1fr;
}
/* 收起来的那 0 高度必须真的裁掉内容，否则单子会画在框外。
   min-height:0 是 grid 子项的默认最小尺寸（min-content）解药 ——
   不解的话它撑不成 0，0fr 就白写了。 */
.record-collapse > * {
  overflow: hidden;
  min-height: 0;
}

/* 解析单子出现：淡入 + 轻轻上移 10px，不硬弹出来。
   挂在 :not([hidden]) 上，所以 hidden 一去掉就播一次；单子里面的行重渲染
   （index.js 的 renderRows 换 innerHTML）不会重播 —— 播的是这一层，不是子元素。

   ★ 只有「出现」有动画，「收起」是**立刻**的。要做退场动画就得让 hidden 晚一点
   再设，而「框里空了 → 立刻收起」「落库后 → 立刻收起」是 index.js 与
   flow-walk 都在盯的行为，为动画推迟它属于拿正确性换观感。 */
[data-panel='review']:not([hidden]) {
  animation: yibi-panel-in 340ms cubic-bezier(0.22, 0.61, 0.36, 1) both;
}
@keyframes yibi-panel-in {
  from {
    opacity: 0;
    transform: translateY(10px);
  }
  to {
    opacity: 1;
    transform: none;
  }
}

/* --- 滚动 ---------------------------------------------------------------- */

.no-scrollbar {
  -ms-overflow-style: none;
  scrollbar-width: none;
}
.no-scrollbar::-webkit-scrollbar {
  display: none;
}

/* --- 输入框 -------------------------------------------------------------- */

/* 本设计的输入框是「透明底 + 一条底线」，没有边框、没有内阴影、没有投影。
   但 Tailwind 的 forms 插件会给 [type=text] 套上白底、1px 边框、内边距，
   以及一圈**蓝色** ring（#2563eb）—— 与本设计「朱砂是唯一强调色」直接冲突。
   用类而不是元素选择器：元素选择器的优先级（0,1,1）会压过 Tailwind 工具类
   （0,1,0），那样以后想给某个输入框加方框就加不上了。

   整个 reset 都带 !important，不是图省事：CDN 注入的 <style> 在文档里排在
   本文件之后，凡是同优先级（0,1,0）的声明都是它赢 —— 上一轮只给 border 和
   box-shadow 加了 !important，结果白底和内边距漏了出来，画面上是金额和备注
   各顶着一个白方块。要么全加，要么全不加；这里全加。 */
.field-bare {
  border: 0 !important;
  background-color: transparent !important;
  padding: 0 !important;
  box-shadow: none !important;
}
.field-bare:focus {
  --tw-ring-color: transparent;
  --tw-ring-shadow: 0 0 #0000;
  box-shadow: none !important;
}

/* 金额输入框的焦点提示就是它下面那条线（会转朱砂），
   再叠一圈 outline 是重复的。id 选择器保证压得过任何工具类。 */
#amount:focus,
#amount:focus-visible {
  outline: none;
}

/* 日期框：**点它任何地方都直接弹系统日历**（不只是右端那个图标）。
   日历那玩意儿是 UA 画在 shadow DOM 里的按钮，点它的「数字」那一块在桌面
   Chromium 上只是把光标放进年月日某一段，什么也不会弹 —— 而想改日期的人
   十有八九是想翻日历，不是想敲八位数字。

   为什么要把图标**变回普通像素**（pointer-events: none）：这样一来，
   点到哪儿都是同一个 click 事件、同一个 target（输入框本身），只有
   ui.js 里那一个监听器会响应。留着它可点的话，点图标会走两条路 ——
   浏览器自己弹一次、我们的监听器再弹一次，第二次是开是关取决于实现，
   而这个「关」正好会把用户点出来的日历当场收掉。一次点击只有一条路，
   这个不确定性才不存在。

   后果说清楚：**用鼠标就没法再靠点框改日期了**（改日期 = 弹日历挑一天）。
   键盘仍然是完整的 —— Tab 进框、方向键或直接敲数字都能改，因为那条路
   根本不经过 click。 */
input[type='date']::-webkit-calendar-picker-indicator {
  pointer-events: none;
}

/* --- 原生控件修正 -------------------------------------------------------- */

/* 金额输入用系统键盘，不要数字微调箭头。 */
input[type='number']::-webkit-outer-spin-button,
input[type='number']::-webkit-inner-spin-button {
  -webkit-appearance: none;
  margin: 0;
}
input[type='number'] {
  -moz-appearance: textfield;
  appearance: textfield;
}

/* 焦点环统一用朱砂，且只在键盘操作时出现（鼠标点击不显示，避免干扰视觉）。 */
:focus-visible {
  outline: 2px solid rgb(var(--c-cinnabar));
  outline-offset: 2px;
}
:focus:not(:focus-visible) {
  outline: none;
}

/* --- 动效 ---------------------------------------------------------------- */

/* 一句话输入框的假光标删了（连同 @keyframes yibi-blink）—— placeholder 接管了
   「空框时告诉用户这里能说话」这件事，两个一起出现会叠字。见 index.html。 */

/* 尊重系统的「减少动态效果」设置。 */
@media (prefers-reduced-motion: reduce) {
  *,
  *::before,
  *::after {
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.01ms !important;
  }
}

/* --- 解析单「重新解析中」 -------------------------------------------------- */

/**
 * 停笔自动识别的那半秒里，屏幕上那张单子记的是**上一句**的解析。
 * 变淡是告诉用户「这张先别信」，配合同期置灰的「确认记账」一起用
 * （见 index.js 的 setPending）。只置灰不变淡的话，用户会以为按钮坏了。
 *
 * 写在这里而不是用 Tailwind 的 opacity-50：这个类是运行时 toggle 的，
 * 交给 Play CDN 的 JIT 会有一帧没样式。理由同 .storage-alert。
 */
[data-panel="review"].is-pending {
  opacity: 0.45;
}

/* --- 解析单每一行的「依据」占位 ------------------------------------------
 *
 * 四行（金额 / 分类 / 账户 / 日期）左边都是「字段名 + 依据」两行。依据会因为
 * 用户改了这一行而消失 —— 改过的行不再引用原文（见 index.js 的 evidence()），
 * 因为那句话已经不是用户改后的意思了。
 *
 * 问题在这儿：依据一没，那一行就矮 18px（= label-sm 的 16px 行高 + 2px 上边距），
 * 整张单子跟着矮 18px。而**用户正低头核对的东西会往上跳一格**，尤其改日期时
 * 最明显 —— 那一行就在手指底下，一改整张单子就动。
 *
 * 所以这一格永远占着：没有依据时元素仍在、只是空的，高度由这里钉住。
 * 1lh 就是这一格自己的行高（label-sm = 16px），跟着字号令牌走；前面那条 16px
 * 是给不认 lh 单位的浏览器兜底的 —— 两条同时存在时后面那条赢，认 lh 的浏览器
 * 永远用的是 1lh。 */
.why-slot {
  min-height: 16px;
  min-height: 1lh;
}

/* --- 账户开关（accounts.html 的「记账时选择账户」）------------------------
 *
 * 【为什么状态样式挂在 aria-checked 上，而不是再养一个布尔类】
 * 状态本来就要写进 aria-checked（role="switch" 的全部意义所在）。再养一个
 * `.is-on` 类的话，两处状态迟早有一天只剩一处被改到 —— 而**读屏读的是 aria、
 * 眼睛看的是类**，那种不一致谁都发现不了。挂属性上就只有一处状态。
 * 这也是 app.css 里 [data-state='review'] / [data-panel='review'] 那一套的老做法。
 *
 * 【为什么写在这里而不是用 Tailwind 的 translate-x-[24px]】
 * 与 .is-pending 同一个理由：这些类**只在运行时出现**，交给 Play CDN 的 JIT
 * 会有一帧没样式（更糟的情况是它压根没扫到这个 arbitrary value，圆点永远不动）。
 * 色值仍走令牌 —— rgb(var(--c-*)) 是 app.css 的既有写法，hex 只许住 tokens.css。
 *
 * 尺寸与 accounts.html 上的标记是一对（轨道 52×28、圆点 24、内边距 2）：
 * 24 = 52 − 24 − 2×2。改一处必须改另一处。 */
[data-accounts-switch] [data-track] {
  background-color: rgb(var(--c-hairline));
  transition: background-color 0.15s linear;
}
[data-accounts-switch] [data-knob] {
  transform: translateX(0);
  transition: transform 0.15s linear;
}
[data-accounts-switch][aria-checked="true"] [data-track] {
  background-color: rgb(var(--c-cinnabar));
}
[data-accounts-switch][aria-checked="true"] [data-knob] {
  transform: translateX(24px);
}
