/* ============================================================
   拼图滑块验证码 —— 共用组件样式
   login.html（登录/注册/找回密码）与 index.html（设置页补绑邮箱）都用它。

   颜色一律直接写 --sgc-* 令牌，不在这份文件里做 :root 级别的二次派生 ——
   theme.css 把 --sgc-* 覆写在 body.light-theme 上，而 :root 里的
   `--x: var(--sgc-y)` 会在 html 那一层就把值定死成深色，浅色主题永远拿不到。
   组件自己的强调色用元素级局部变量（值是字面量，不参与继承冻结）。
   ============================================================ */

.cap {
    --cap-accent: #4470a8;          /* 与登录页商标蓝同色相；浅色下见下方覆写 */
    --cap-gap: 10px;
    /* 缓动自己带一份：登录页的 --ease-out 只声明在 login.html 的行内 <style> 里，
       index.html 上没有这个变量。借用它会变成「后台弹层不动、登录页弹层会动」。 */
    --cap-ease: cubic-bezier(.16, 1, .3, 1);
    position: fixed;
    inset: 0;
    /* 900：压在 app.css 的 .modal-root(50) 与 .dropdown-menu(60) 之上，
       但让开 .toast(1000) —— 弹层里出的提示不该被自己的遮罩盖住。 */
    z-index: 900;
    display: none;
    /* **手机与桌面都是居中**，不是"手机贴底、桌面居中"。
       贴底那版是从 app.css 的 .modal-sheet 抄的节奏，但那是底部弹层该有的样子：
       从下往上滑、可以往下甩掉。验证码不是弹层 —— 它是一次必须做完的短任务，
       贴在屏幕最下沿时，题面和轨道都挤在拇指够不太到的位置，视线也得往下压，
       观感上更像"页面底部冒出来的一块"而不是"让我先验一下"。
       留 padding 是为了卡片不贴边（窄屏下面 92% 的宽度就够，不必满宽）。 */
    align-items: center;
    justify-content: center;
    padding: 20px;
}
.cap.is-open { display: flex; }
body.light-theme .cap { --cap-accent: #2f5f94; }
/* 弹层打开期间锁住背后的滚动。加在 <html> 上是因为 body 在 index.html 里
   还有别的滚动容器；这一层关掉了整页才不会跟着滑。 */
html.cap-lock { overflow: hidden; }

/* ---------- 遮罩 ----------
   点遮罩 = 放弃这次验证。和弹窗里那个 × 同义，所以走同一个 close()。 */
.cap-veil {
    position: absolute;
    inset: 0;
    background: rgba(0, 0, 0, .72);
    opacity: 0;
    transition: opacity .25s linear;
}
.cap.is-open .cap-veil { opacity: 1; }

/* ---------- 卡片 ----------
   一套定位走两端：居中、四角同圆。组件不依赖 app.css（login.html 根本没加载它），
   所以这套定位要自带。
   入场是"从下往上 16px + 缩一点"而不是原来的 translateY(100%)：后者是贴底弹层
   往上推的手势，居中之后再从屏幕外飞进来就太远了。 */
.cap-dialog {
    position: relative;
    width: 92%;
    max-width: 360px;
    max-height: 92vh;
    overflow-y: auto;
    padding: 20px 20px 16px;
    background: var(--sgc-surface-elevated);
    border: 1px solid var(--sgc-border-active);
    border-radius: 16px;
    transform: translateY(16px) scale(.97);
    transition: transform .34s var(--cap-ease),
                border-color .3s linear, background-color .3s linear;
}
.cap.is-open .cap-dialog { transform: translateY(0) scale(1); }
/* 通过之后卡自己的边框与底色化掉：绿块收成圆之后，背后不该还留一张空卡。 */
.cap.is-solved .cap-dialog { background-color: transparent; border-color: transparent; }

/* ---------- 卡头 ---------- */
.cap-head {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 12px;
    margin-bottom: 14px;
}
.cap-title {
    font-size: 13px;
    letter-spacing: .16em;
    color: var(--sgc-text-primary);
}
.cap-x {
    flex: none;
    display: flex;
    align-items: center;
    justify-content: center;
    width: 28px; height: 28px;
    margin: -4px -6px -4px 0;    /* 视觉上贴住卡片内边距，但点区不缩到 22px */
    padding: 0;
    border: 0;
    border-radius: 6px;
    background: none;
    color: var(--sgc-text-tertiary);
    cursor: pointer;
    transition: color .18s linear, background-color .18s linear;
}
.cap-x:hover { color: var(--sgc-text-primary); background: var(--sgc-surface-highlight); }
.cap-x:focus-visible { outline: 2px solid var(--cap-accent); outline-offset: 2px; }

/* ---------- 题面 ---------- */
.cap-frame {
    position: relative;
    width: 100%;
    aspect-ratio: 300 / 150;
    overflow: hidden;
    border: 1px solid var(--sgc-border-active);
    border-radius: 2px;
    background: var(--sgc-surface);
    /* 拖拽中别让浏览器把整个题面当成可拖动的图片 */
    user-select: none;
    touch-action: none;
    /* 通过时的淡出过渡声明在下面 .cap-head/.cap-frame/.cap-track/.cap-foot 那一组里 */
}
/* 图片是 JS 拿到 data URL 之后才塞进去的，加载完之前整块题面留白但**保留高度**
   （visibility 而不是 display：轨道不能随着跳一下）。首屏与「换一张」都会经过这个状态。 */
.cap.is-loading .cap-frame { visibility: hidden; }
.cap-frame img { -webkit-user-drag: none; user-select: none; }
.cap-bg {
    display: block;
    width: 100%; height: 100%;
    object-fit: fill;
}
/* 拼块按百分比定位：题面 300x150、拼块画布 58x44，比例是死的，
   所以 58/300 与 44/150 直接写成百分数，不需要 JS 参与像素换算。
   JS 只负责把滑块的行程映射成 0..242 的 x。 */
.cap-piece {
    position: absolute;
    left: 0;
    width: 19.3333%;
    height: 29.3333%;
    object-fit: fill;
    pointer-events: none;
    filter: drop-shadow(0 1px 2px rgba(0, 0, 0, .35));
}
/* 通过之后拼块要化掉，先摘掉投影 —— 半透明的投影看着像脏边。
   原来这里写的是 .cap-frame.is-solved，但 JS 只往根上加 is-solved，
   .cap-frame 从没拿过这个类，这条规则一直是死的。 */
.cap.is-solved .cap-piece { filter: none; }

/* ---------- 成功：整个验证码「变」成一枚圆形对勾 ----------
   注意作用域是**整张卡**，不是题面那一块。绿色先铺满整张卡（.cap-done 起手就是
   inset:0，即卡片那么大），卡片自己的边框与底色同时化开，紧接着这块绿收缩成居中的
   64px 圆，最后白色对勾自己画出来。第一版只让题面折叠，视觉上就是「图里冒出个圆」，
   跟卡片其余部分没关系 —— 那不对。

   收缩走的是 left/right/top/bottom 从 0 到 calc(50% - 32px)，**不是**给 .cap-done
   写死宽高：卡的高度是内容撑的，写死就得先知道它多高，而报错文案有没有、手机版与
   桌面版都不一样。inset 四边跟着容器走，卡多高都不用管。
   两端都是 length（0 / calc(% + px)），是实测过的真插值（页面里挂 rAF 采样器采到
   几十个中间帧），不是离散跳变 —— 所以敢这么写，不必退化成 scale
   （非等比 scale 会把圆压成椭圆）。 */
.cap-done {
    position: absolute;
    left: 0; right: 0; top: 0; bottom: 0;
    border-radius: 16px;             /* 与 .cap-dialog 同形，绿面铺开时圆角对得上 */
    background-color: transparent;   /* 起手是「一张看不见的、卡片大小的方块」 */
    display: flex; align-items: center; justify-content: center;
    pointer-events: none;
    z-index: 2;                      /* 压在卡内所有内容之上 */
    transition: left .4s var(--cap-ease) .14s,
                right .4s var(--cap-ease) .14s,
                top .4s var(--cap-ease) .14s,
                bottom .4s var(--cap-ease) .14s,
                border-radius .4s var(--cap-ease) .14s,
                background-color .16s linear;
}
.cap.is-solved .cap-done {
    left: calc(50% - 32px); right: calc(50% - 32px);
    top: calc(50% - 32px);  bottom: calc(50% - 32px);
    border-radius: 50%;
    background-color: var(--sgc-success);
}
/* 卡内其余内容在绿面盖上来的时候一起化掉；卡自己的边框与底色也化掉 ——
   不然绿块收成圆之后，背后还留着一张空卡，看着像「卡片里浮着个圆」。 */
.cap-head, .cap-frame, .cap-track, .cap-foot { transition: opacity .2s linear; }
.cap.is-solved .cap-head,
.cap.is-solved .cap-frame,
.cap.is-solved .cap-track,
.cap.is-solved .cap-foot { opacity: 0; }
.cap-check { display: block; width: 30px; height: 30px; }
/* 对勾是「画」出来的：dashoffset 从整条路径长退到 0。
   21 略大于路径实测长度（7.07 + 13.45 ≈ 20.5）—— 取小了会在末尾露出一小截
   第二段 dash，看着像勾尾巴上多了一点。延迟 .5s：圆要到 .14+.4=.54s 才收完，
   让笔尖在圆定型之前先落下去。 */
.cap-check path {
    stroke-dasharray: 21;
    stroke-dashoffset: 21;
    transition: stroke-dashoffset .26s var(--cap-ease) .5s;
}
.cap.is-solved .cap-check path { stroke-dashoffset: 0; }
/* 收完再荡一圈涟漪，给「确认」一个落点。
   ::after 是 inset:0 + 50% 圆角 —— 折叠期间它是压扁的椭圆，但 `both` 让它在
   延迟结束前一直取 0% 那帧（opacity:0），所以那段时间它根本不显形。 */
.cap.is-solved .cap-done::after {
    content: '';
    position: absolute; inset: 0;
    border-radius: 50%;
    background: var(--sgc-success);
    animation: cap-ping .56s var(--cap-ease) .56s both;
}
@keyframes cap-ping {
    0%   { opacity: 0; transform: scale(1); }
    30%  { opacity: .45; }
    100% { opacity: 0; transform: scale(1.9); }
}
/* 底图、拼块、题面边框都不再单独做淡出 —— 整个 .cap-frame 已经整体 opacity:0
   （见上面那组），子元素跟着一起走。原先给 .cap-frame 单独加的
   border-color/background-color 过渡也一并去掉：那是「只有题面在变形」时的补丁，
   作用域提到整张卡之后就没有意义了。 */

/* ---------- 失败：题面底部冒出一小节红条 ----------
   开关是根上的 .is-error，由 captcha.js 的 say() 统一收口（见那边的注释）——
   CSS 这边不用 :has()，老浏览器不一定认。
   从底边外侧滑进来，没进来的部分由 .cap-frame 的 overflow:hidden 裁掉，
   所以是「从底部生出来」而不是「凭空出现」。 */
.cap-fail {
    position: absolute; left: 0; right: 0; bottom: 0;
    height: 22px;
    display: flex; align-items: center; justify-content: center;
    background: var(--sgc-danger);
    color: #fff;
    font-size: 12px;
    letter-spacing: .14em;
    pointer-events: none;
    transform: translateY(100%);
    transition: transform .26s var(--cap-ease);
}
.cap.is-error .cap-fail { transform: translateY(0); }

/* 拖拽把手：题面上不做任何**参与判定**的覆盖层，缺口本身就是被画进背景的，
   多画一层反而会盖住判定依据。进度反馈全交给下面的轨道。
   （.cap-done / .cap-fail 是纯结果展示，出现在判定结束之后，不在此列。） */

/* ---------- 轨道 ---------- */
.cap-track {
    position: relative;
    height: 40px;
    margin-top: var(--cap-gap);
    border: 1px solid var(--sgc-border-active);
    border-radius: 2px;
    overflow: hidden;
    user-select: none;
    touch-action: none;
}
.cap-fill {
    position: absolute; left: 0; top: 0; bottom: 0;
    width: 40px;
    background: var(--cap-accent);
    opacity: .13;
    pointer-events: none;
}
.cap-hint {
    position: absolute; inset: 0;
    display: flex; align-items: center; justify-content: center;
    font-size: 12px;
    letter-spacing: .18em;
    color: var(--sgc-text-secondary);
    pointer-events: none;
    transition: opacity .2s linear;
}
.cap.is-dragging .cap-hint { opacity: 0; }

.cap-handle {
    position: absolute; left: 0; top: 0;
    width: 40px; height: 38px;
    display: flex; align-items: center; justify-content: center;
    gap: 3px;
    background: var(--cap-accent);
    border-radius: 2px;
    cursor: grab;
    transition: background-color .18s linear;
}
.cap-handle:hover { background: var(--cap-accent); filter: brightness(1.12); }
.cap-handle:active { cursor: grabbing; }
.cap.is-dragging .cap-handle { transition: none; }
.cap-handle:focus-visible { outline: 2px solid var(--cap-accent); outline-offset: 2px; }
/* 抓握感用两根 1px 竖线表示，不用图标字体 */
.cap-handle::before,
.cap-handle::after {
    content: '';
    width: 1px; height: 14px;
    background: #fff;
    opacity: .85;
}
.cap.is-solved .cap-handle { cursor: default; }
.cap.is-solved .cap-handle::before,
.cap.is-solved .cap-handle::after { opacity: .4; }
.cap.is-locked .cap-handle { pointer-events: none; }

/* ---------- 脚注：组件自己的状态 + 换一张 ---------- */
.cap-foot {
    display: flex; align-items: center; justify-content: space-between;
    gap: 12px;
    margin-top: 8px;
    font-size: 12px;
    line-height: 18px;
}
.cap-msg { color: var(--sgc-text-secondary); }
.cap-msg.is-error { color: var(--sgc-danger-soft); }
body.light-theme .cap-msg.is-error { color: #b91c1c; }
.cap-msg.is-ok { color: var(--cap-accent); }
.cap-refresh {
    flex: none;
    padding: 0;
    border: 0;
    background: none;
    color: var(--sgc-text-secondary);
    font-family: inherit;
    font-size: 12px;
    letter-spacing: .08em;
    cursor: pointer;
    transition: color .18s linear;
}
.cap-refresh:hover { color: var(--cap-accent); }
.cap-refresh:focus-visible { outline: 2px solid var(--cap-accent); outline-offset: 2px; }
.cap-refresh:disabled { opacity: .45; cursor: default; }

@media (prefers-reduced-motion: reduce) {
    .cap-dialog, .cap-veil, .cap-hint, .cap-handle,
    .cap-head, .cap-frame, .cap-track, .cap-foot,
    .cap-done, .cap-check path, .cap-fail {
        transition-duration: .001ms;
        /* 只清时长不清延迟，等于「不动画、但还是慢半拍才出现」——
           对勾要等 .4s、红条要等 .16s 才现身，比有动画还难受。 */
        transition-delay: 0s;
    }
    /* 涟漪是纯装饰，没有信息量，直接不发 */
    .cap.is-solved .cap-done::after { animation: none; }
}

/* ============================================================
   设置页（index.html）的「邮箱 + 获取验证码」行
   那边的输入框是 12px 圆角的独立块，不像登录页是通栏下划线，
   所以按钮做成一个同样高、同样圆角的次级块贴在右边，而不是并进同一个框里。
   放在这里是因为它和验证码是同一条链路的东西，且本文件在 index.html 里
   排在 app.css 之后，改这几个类不必再动 app.css（省一次 ?v= 全量失效）。
   ============================================================ */
/* 行自己吃掉 .sub-config-input 那个 margin-top，两个子元素的顶边就天然对齐，
   不靠「两边都写 8px 再指望 margin-box 居中」这种巧合。 */
#settingsPage .input-code-row {
    display: flex;
    align-items: center;
    gap: 10px;
    margin-top: 8px;
}
#settingsPage .input-code-row .login-input {
    flex: 1 1 auto;
    width: auto;
    min-width: 0;
    margin-top: 0;
}
#settingsPage .code-send-btn {
    flex: none;
    height: 42px;              /* 与 #settingsPage .login-input 等高 */
    margin-top: 0;
    padding: 0 14px;
    border: 1px solid var(--sgc-border-active);
    border-radius: 12px;
    background: transparent;
    color: var(--sgc-primary);
    font-family: inherit;
    font-size: 13px;
    white-space: nowrap;
    cursor: pointer;
    transition: border-color .18s linear, color .18s linear;
}
#settingsPage .code-send-btn:hover:not(:disabled) { border-color: var(--sgc-primary); }
#settingsPage .code-send-btn:disabled { color: var(--sgc-text-tertiary); cursor: default; }
