/* testing-engine.css */

/* 版本切换下拉框：默认隐藏，只有当同一分类下有多个年份版本时，
   cs1_index.js 才会加上 .show 让它显示出来 */
/* 版本切换现在住在考卷卡片里、跟标题同一行，所以尺寸要收下来——
   原来它是卡片外面独立的一行，字号 1.05rem、下面还有 20px 外边距 */
.version-selector-wrap {
    display: none;
    align-items: center;
    gap: 7px;
    font-size: 0.8rem;
    color: var(--weak-color, #6b7280);
}

.version-selector-wrap.show {
    display: flex;
}

/* 考试进行中，不管原本是否该显示，一律强制隐藏，避免中途切换年份版本 */
.version-selector-wrap.hide-during-exam {
    display: none !important;
}

.version-selector-wrap label {
    font-weight: 600;
    text-transform: uppercase;
    letter-spacing: 0.05em;
    font-size: 0.68rem;
    color: hsl(220, 8%, 58%);
}

/* 下拉框做得紧凑一点，别在标题旁边显得笨重 */
.version-selector-wrap select {
    padding: 4px 8px;
    border: 1px solid var(--divider-color, #ddd);
    border-radius: 7px;
    background: #ffffff;
    font-family: inherit;
    font-size: 0.82rem;
    font-weight: 600;
    color: hsl(220, 25%, 22%);
    cursor: pointer;
    transition: border-color 0.15s ease;
}

.version-selector-wrap select:hover,
.version-selector-wrap select:focus {
    border-color: hsl(214, 60%, 70%);
    outline: none;
}

#version-select {
    padding: 9px 16px;
    border: 1px solid var(--divider-color);
    border-radius: 8px;
    background-color: #ffffff;
    color: var(--icon-color);
    font-size: 1.05rem;
    cursor: pointer;
    transition: border-color 0.2s ease;
}

#version-select:hover {
    border-color: var(--blue);
}


/* Practice 模式的题型筛选下拉框，样式跟 version-selector 保持一致 */
/* ---------- 过长题目的折叠 ---------- */
/* 折叠高度跟 JS 里的 QUESTION_CODE_COLLAPSE_MAX 是两处，改一个要改两个。
   放在 CSS 里是因为 max-height 需要能被 class 切换，
   放在 JS 里写内联样式则展开/收起要来回改属性，更绕 */
.question-code-collapsible.code-collapsed {
    position: relative;
    max-height: 360px;
    overflow: hidden;
}

/* 底部渐隐，暗示下面还有内容。渐变的终点色要跟代码块底色一致，
   不然会看到一条边 */
.question-code-collapsible.code-collapsed::after {
    content: '';
    position: absolute;
    left: 0;
    right: 0;
    bottom: 0;
    height: 60px;
    background: linear-gradient(to bottom, transparent, var(--code-bg));
    pointer-events: none;
}

.code-collapse-toggle {
    display: inline-flex;
    align-items: center;
    /* 代码块自带 16px 下外边距，这里往回收一点，让按钮贴着它 */
    margin: -10px 0 16px;
    padding: 5px 13px;
    border: 1px solid var(--divider-color, #ddd);
    border-radius: 999px;
    background: #ffffff;
    color: var(--weak-color, #666);
    cursor: pointer;
    font-size: 0.78rem;
    font-weight: 500;
    font-family: inherit;
    transition: background-color 0.15s ease, border-color 0.15s ease, color 0.15s ease;
}

.code-collapse-toggle:hover {
    border-color: hsl(214, 55%, 74%);
    background: hsl(214, 60%, 98%);
    color: hsl(214, 84%, 42%);
}

/* 三个板块的内容统一往右挪一点。
   加在 .test-section 上而不是 .content 上，是因为 .content 还包着上面那个
   板块切换器，改 .content 会把切换器一起推走 */
.test-section {
    padding-left: 14px;
}

/* ---------- 题型筛选（Practice 和 Marked Questions 共用） ---------- */
/* 从下拉框换成一排标签。下拉框要点两下才能切换，而且不点开根本不知道
   有哪些题型可选——「Half-Program」这种类型学生压根不会知道它存在。
   6 个选项全是短词，平铺完全放得下 */
.practice-filter {
    display: flex;
    align-items: center;
    gap: 12px;
    flex-wrap: wrap;
    margin-bottom: 30px;
}

.question-filter-label {
    font-size: 0.78rem;
    font-weight: 600;
    text-transform: uppercase;
    letter-spacing: 0.05em;
    color: var(--weak-color, #888);
    white-space: nowrap;
}

.question-type-filter {
    display: flex;
    align-items: center;
    gap: 6px;
    flex-wrap: wrap;
}

.qtype-pill {
    padding: 6px 13px;
    border: 1px solid var(--divider-color, #ddd);
    border-radius: 999px;
    background: #ffffff;
    color: var(--weak-color, #666);
    cursor: pointer;
    font-size: 0.82rem;
    font-weight: 500;
    font-family: inherit;
    white-space: nowrap;
    transition: background-color 0.15s ease, border-color 0.15s ease,
                color 0.15s ease, transform 0.12s ease;
}

.qtype-pill:hover {
    border-color: hsl(216, 70%, 82%);
    color: hsl(220, 25%, 20%);
    transform: translateY(-1px);
    box-shadow: 0 2px 6px hsla(220, 30%, 25%, 0.08);
}

.qtype-pill:active {
    transform: translateY(0) scale(0.97);
    box-shadow: none;
}

/* 选中态用淡蓝底 + 蓝字，不用纯蓝填充。
   一排标签里同时只有一个选中，实心色块在这个位置太重，
   会盖过旁边的题目内容；字重加一档补足存在感 */
.qtype-pill.active {
    background: #E8F1FF;
    border-color: hsl(216, 100%, 87%);
    color: #1E6BFF;
    font-weight: 600;
}

.qtype-pill.active:hover {
    background: hsl(216, 100%, 92%);
    border-color: hsl(216, 90%, 82%);
    color: #1E6BFF;
}

#practice-type-select {
    padding: 6px 12px;
    border: 1px solid var(--divider-color);
    border-radius: 8px;
    background-color: #ffffff;
    color: var(--icon-color);
    font-size: 0.95rem;
    cursor: pointer;
    transition: border-color 0.2s ease;
}

#practice-type-select:hover {
    border-color: var(--blue);
}


/* 题目容器：跟上方的筛选框/header拉开距离，Practice和Testing/Examination共用 */
#practice-questions,
#testing-questions {
    margin-top: 36px;
    /* 只是让题目别贴着左边，不跟上面的卡片对齐——
       题目和代码块本来就需要宽一点的横向空间 */
    padding-left: 40px;
}

/* 交卷后题目跟上面拉开更多距离。
   结算时上面多出了四块分析卡片，36px 的间距会让题目
   看起来像是紧接着卡片的一部分。考试进行中不需要——
   那时候上面只有一张矮条状的考卷卡片 */
body.exam-result-mode #testing-questions {
    margin-top: 72px;
}

/* ---------- 改卷说明条 ---------- */
/* 交卷后若有可打分的自评题，先出现这一条：学生对着标准答案自己打分，
   期间不亮分，点了按钮才把所有分数一起揭示。左对齐到题目的 padding-left */
.exam-marking-hold {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 20px;
    flex-wrap: wrap;
    margin: 40px 40px 0;
    padding: 16px 22px;
    border-radius: 14px;
    background: hsl(212, 100%, 97%);
    border: 1px solid hsl(212, 60%, 88%);
    /* 进入改卷阶段不硬跳：说明条滑入。配合下面 JS 的 smooth scroll，
       整个"进入改卷"是滑进来的 */
    animation: examMarkingReveal 0.5s cubic-bezier(0.22, 0.61, 0.36, 1) both;
    scroll-margin-top: 20px;   /* smooth scroll 停下时上面留一点空 */
}

/* 改卷阶段新出现的标准答案栏 + 自评框也淡入滑上来，跟说明条一起"到位"，
   而不是瞬间铺满。稍作错峰，观感更顺 */
body.exam-marking-mode .answer-solution-box,
body.exam-marking-mode .self-assess-box {
    animation: examMarkingReveal 0.5s cubic-bezier(0.22, 0.61, 0.36, 1) both;
    animation-delay: 0.08s;
}

/* ⚠️ to 用 transform: none（关键字），不是 translateY(0)——后者算出来是
   matrix(1,0,0,1,0,0)，会给后代 position:fixed 建立包含块（见 HANDOFF 那个坑）。
   none 不会。 */
@keyframes examMarkingReveal {
    from { opacity: 0; transform: translateY(14px); }
    to   { opacity: 1; transform: none; }
}

.exam-marking-hold-text {
    margin: 0;
    flex: 1 1 340px;
    font-size: 0.9rem;
    line-height: 1.5;
    color: hsl(220, 25%, 32%);
}

.exam-marking-hold-text strong {
    display: block;
    margin-bottom: 2px;
    font-size: 0.98rem;
    color: hsl(220, 30%, 20%);
}

.exam-marking-hold-btn {
    flex-shrink: 0;
    padding: 11px 22px;
    border: none;
    border-radius: 10px;
    background: hsl(212, 90%, 52%);
    color: #fff;
    font-size: 0.95rem;
    font-weight: 700;
    cursor: pointer;
    transition: background-color 0.15s ease;
}

.exam-marking-hold-btn:hover {
    background: hsl(212, 90%, 46%);
}

/* 没全部改完时禁用：压暗 + 禁用光标，点了也不响应（JS 里也挡了一道） */
.exam-marking-hold-btn:disabled {
    background: hsl(212, 20%, 72%);
    cursor: not-allowed;
}

/* 改卷阶段藏起卷面头卡（Correction 那张）——等学生 Submit 后，
   finalizeExamResults 摘掉 exam-marking-mode，它才跟结果一起出现。
   开关加在 enterMarkingHold、摘在 finalizeExamResults / 重考重置 */
body.exam-marking-mode #testing-header {
    display: none;
}

/* ---------- 判分待机 ---------- */
/* Pyodide 首次要下 3~4MB，这段时间必须有明确反馈，
   否则学生会以为功能坏了 */

@keyframes gradeSpin {
    to { transform: rotate(360deg); }
}

/* 题目上的小转圈 */
.grade-spinner {
    display: inline-block;
    width: 13px;
    height: 13px;
    border: 2px solid hsla(220, 20%, 78%, 0.5);
    border-top-color: hsl(214, 84%, 58%);
    border-radius: 999px;
    animation: gradeSpin 0.7s linear infinite;
    vertical-align: -2px;
    margin-right: 8px;
}

.grade-panel.is-pending {
    display: flex;
    align-items: center;
}

/* 成绩卡片的"计算中"：数字换成呼吸的骨架块。
   为什么不直接显示 0 或者留空——那两种都会被读成"已经算完了，就是这个结果" */
@keyframes gradePulse {
    0%, 100% { opacity: 1; }
    50%      { opacity: 0.45; }
}

.exam-analytics-card.is-calculating .exam-stat-value,
.exam-analytics-card.is-calculating .exam-compare-num {
    /* 用 transparent 让文字隐形但仍然占位——
       换成 visibility:hidden 的话宽度会塌，卡片布局会抖 */
    color: transparent;
    background: hsl(220, 20%, 88%);
    border-radius: 6px;
    animation: gradePulse 1.3s ease-in-out infinite;
}

.exam-analytics-card.is-calculating .exam-stat-unit {
    color: transparent;
}

.exam-analytics-card.is-calculating .exam-compare-fill {
    animation: gradePulse 1.3s ease-in-out infinite;
}

/* 算完之后的说明：这个分数是几道题算出来的。
   不写的话学生不知道为什么只算了一道——没录测试用例的题不参与判分 */
.exam-score-note {
    margin: 12px 0 0;
    font-size: 0.74rem;
    color: hsl(220, 10%, 55%);
}

/* 真实分数出来之后，同一张卡上那些【仍然是假的】数字压暗 + 标 (sample)，
   免得学生以为整张卡都是自己的成绩。
   Ranking 要跨用户数据，一个人考试算不出来。

   :not([data-real-stat]) 是给「考试用时」留的口子——那一格是
   applyTimeSpentStat 换上去的真数据，不该被当成 sample */
.exam-analytics-card.has-real-score .exam-stat:not(:first-child):not([data-real-stat]) .exam-stat-value {
    opacity: 0.45;
}

.exam-analytics-card.has-real-score .exam-stat:not(:first-child):not([data-real-stat]) .exam-stat-label::after {
    content: ' (sample)';
    font-size: 0.9em;
    opacity: 0.7;
}

/* 点进来才能滚。.is-capped 由 autoGrowAnswerInput 在内容顶到上限时加上，
   两个条件缺一不可：没超出的框子不需要滚，没聚焦的框子不该抢滚轮 */
.answer-editor-input.is-capped:focus {
    overflow-y: auto;
}

/* 聚焦时的阴影效果写在 .answer-editor-input:focus 那一处（本文件靠后），
   不在这里重复——同选择器写两遍的话后面那条会整个盖掉前面的 */

/* 标准答案：同样点进来才能滚。
   靠 tabIndex=0（在 revealSolutions 里加的）让 <pre> 能被聚焦，
   不然 :focus 永远不会命中 */
.answer-solution-box .testing-answer {
    overflow: hidden;
    cursor: pointer;
    transition: box-shadow 0.15s ease;
}

.answer-solution-box .testing-answer:focus {
    outline: none;
    overflow: auto;
    cursor: auto;
    box-shadow: 0 6px 20px hsla(220, 30%, 35%, 0.16),
                0 2px 6px hsla(220, 30%, 35%, 0.1);
}

/* 内容超出时底部渐隐，暗示"下面还有"。
   .is-scrollable 由 JS 量过之后才加——内容没超出还画一道渐变的话，
   底部会凭空多一块看不出所以然的阴影。
   聚焦之后（也就是可以滚了）渐隐消失，否则它会一直挡着最后一行 */
.answer-solution-box {
    position: relative;
}

.answer-solution-box.is-scrollable::after {
    content: '';
    position: absolute;
    left: 0;
    right: 0;
    bottom: 0;
    height: 52px;
    border-radius: 0 0 10px 10px;
    background: linear-gradient(to bottom, hsla(120, 30%, 96%, 0), var(--answer-bg));
    pointer-events: none;   /* 别挡住点击，否则点渐隐区聚焦不了 */
    opacity: 1;
    transition: opacity 0.15s ease;
}

.answer-solution-box:focus-within::after {
    opacity: 0;
}

/* ---------- 题目导航条 ---------- */
/* 常规样式：右侧一块浮动面板，里面是一格一格的题号按钮。
   容器写在 cs1_index.html 的 body 直属层，按钮由 buildQuestionNav() 填。

   ⚠️ 容器【必须】在 body 直属层，不能挪进 #testing-content。
   那两层容器（#testing-content / #content-container）在切板块的
   过渡里被写过 transform，动画停下后计算值是 matrix(1,0,0,1,0,0)——
   一个"零位移的 transform"。视觉上等于什么都没做，
   但对 position: fixed 来说照样建立包含块，
   面板会变成相对那个容器定位、跟着页面内容一起滚。 */

/* ⚠️ 这条必须写。HTML 上的 hidden 属性靠的是浏览器默认样式表里的
   [hidden] { display: none }，作者样式里任何一句 display 都会盖过它——
   元素会变成"看不见但占着位置"。
   靠的是【特异性】不是顺序：.exam-question-nav[hidden] 多一个
   属性选择器，写在前面写在后面都赢 */
.exam-question-nav[hidden] {
    display: none;
}

.exam-question-nav {
    position: fixed;

    right: 28px;
    top: 50%;
    transform: translateY(-50%);
    z-index: 150;

    /* 单列竖排。面板很窄（58px），占的横向空间比四列网格少得多。
       代价是题一多就长：20 道题要 780px，撑破 70vh 之后
       面板内部滚动——那时候得先滚面板才能点到后面的题 */
    display: grid;
    grid-template-columns: 32px;
    gap: 6px;

    /* 题特别多时面板自己滚，不要长到超出屏幕 */
    max-height: 70vh;
    overflow-y: auto;

    padding: 12px;
    border-radius: 12px;
    border: 1px solid hsl(220, 12%, 88%);
    background: #fff;
    box-shadow: 0 4px 20px hsla(220, 15%, 30%, 0.1);
}

.exam-nav-item {
    /* --nav-strong = 这个按钮 hover / 当前态用的【深色调】。
       默认是中性蓝灰（不再是原来的深灰）；带颜色的状态各自覆盖成对应色调 */
    --nav-strong: hsl(220, 18%, 56%);
    width: 32px;
    height: 32px;
    padding: 0;
    border: 1px solid hsl(220, 10%, 88%);
    border-radius: 8px;
    background: #fff;
    color: hsl(220, 10%, 45%);
    font-size: 0.78rem;
    font-weight: 600;
    line-height: 1;
    cursor: pointer;
    transition: background 0.15s ease, color 0.15s ease,
                border-color 0.15s ease, box-shadow 0.15s ease;
}

/* hover：边框换成本按钮的色调，不再是统一的灰 */
.exam-nav-item:hover {
    border-color: var(--nav-strong);
    box-shadow: 0 2px 8px hsla(220, 12%, 30%, 0.16);
}

/* 已作答：浅绿（仅考试期间；只表示"写了东西"，不代表对错）。
   ⚠️ 结果页会被 clearNavExamStates() 清掉，换成得分色 */
.exam-nav-item.is-answered {
    background: hsl(145, 45%, 90%);
    border-color: hsl(145, 35%, 78%);
    color: hsl(145, 40%, 26%);
    --nav-strong: hsl(145, 46%, 42%);
}

/* 已标记：浅红（仅考试期间）。必须写在 .is-answered 后面——同特异性靠顺序，
   既答又标时该显红（"要回来改"比"写了"更要紧） */
.exam-nav-item.is-flagged {
    background: hsl(0, 70%, 93%);
    border-color: hsl(0, 55%, 82%);
    color: hsl(0, 50%, 34%);
    --nav-strong: hsl(0, 62%, 55%);
}

/* 结果页得分色带（跟题号旁的得分徽标同一套色）：红 / 橙 / 绿。
   由 setNavScoreBand() 在交卷判分后填上 */
.exam-nav-item.is-score-low {
    background: hsl(0, 75%, 94%);
    border-color: hsl(0, 58%, 84%);
    color: hsl(0, 55%, 38%);
    --nav-strong: hsl(0, 62%, 55%);
}
.exam-nav-item.is-score-mid {
    background: hsl(32, 92%, 90%);
    border-color: hsl(32, 78%, 80%);
    color: hsl(28, 75%, 36%);
    --nav-strong: hsl(30, 82%, 50%);
}
.exam-nav-item.is-score-high {
    background: hsl(145, 52%, 90%);
    border-color: hsl(145, 40%, 78%);
    color: hsl(145, 44%, 28%);
    --nav-strong: hsl(145, 46%, 42%);
}

/* 当前滚到的那道题：填成本按钮的色调（--nav-strong），不再一律深灰。
   ⚠️ 写在所有配色状态【后面】——同特异性，靠顺序盖过它们的背景 */
.exam-nav-item.is-current {
    background: var(--nav-strong);
    border-color: var(--nav-strong);
    color: #fff;
}

.exam-nav-item:focus-visible {
    outline: 2px solid var(--nav-strong);
    outline-offset: 2px;
}

/* 给面板腾地方。
   它是 position: fixed，浮在内容【上面】、自己不占位——
   不留边距的话就直接压在代码和答题框上。

   ⚠️ 条件是 body.has-question-nav，【不是】媒体查询。
   这个类由 buildQuestionNav() / destroyQuestionNav() 开关，
   所以"有没有留边距"跟"面板在不在"永远一致。
   用媒体查询判断的话，它跟面板的显示条件各判各的，
   视口一掉到断点以下就会出现"边距没了但面板还在"这种错位。

   ⚠️ 【分两处写，不要统一加在 #testing-content 上】。
   加在 #testing-content 上的话，题目区会跟着一起被推——
   而题目区本来的位置是调好的，不该因为右边多了个面板就整体挪动。

   114px = 28（面板离右边缘）+ 58（面板宽：32 按钮 + 12×2 内边距 + 1×2 边框）
   + 28（内容和面板之间的呼吸空间）。
   ⚠️ 改了 right、按钮尺寸、padding 里的任何一个，下面两个数都要跟着重算 */

/* 题目区：右边收窄，给面板让出位置。
   ⚠️ 只加右边，左边不动——加了左内边距题目会往右挪，
   跟上面的卡片就对不齐了 */
body.has-question-nav #testing-questions {
    padding-right: 114px;
}

/* 卡片区（考卷卡片、说明、四块分析）：它们是
   max-width: 980px + margin auto 居中的，所以【不能加内边距】——
   加了会把居中轴推偏，整片卡片看起来贴着一边。

   改成压一下最大宽度：两侧各留 114px，居中不受影响。
   114 = 28（面板离右缘）+ 58（面板宽）+ 28（呼吸空间）。
   宽屏上 980px 这一档先到，等于什么都没发生；
   只有窄到卡片快碰到面板时才生效 */
body.has-question-nav .exam-layout,
body.has-question-nav .exam-version-row {
    max-width: min(980px, calc(100% - 228px));
}

/* 窄屏放不下就不显示。
   ⚠️ 断点是 760px：竖排面板只要 58px，比四列网格窄得多，
   所以断点也可以低一些 */
@media (max-width: 760px) {
    .exam-question-nav {
        display: none;
    }

    body.has-question-nav #testing-questions {
        padding-right: 0;
    }

    body.has-question-nav .exam-layout,
    body.has-question-nav .exam-version-row {
        max-width: 980px;
    }
}

/* 「标记」按钮。跟在题号后面，所以要小、要低调——
   它是备用功能，不该跟题号抢注意力 */
.exam-flag-btn {
    display: inline-flex;
    align-items: center;
    gap: 6px;
    padding: 4px 10px;
    border: 1px solid hsl(220, 12%, 88%);
    border-radius: 999px;
    background: #fff;
    color: hsl(220, 10%, 50%);
    font-size: 0.72rem;
    font-weight: 600;
    cursor: pointer;
    transition: background 0.15s ease, color 0.15s ease,
                border-color 0.15s ease;
}

.exam-flag-btn:hover {
    border-color: hsl(0, 55%, 78%);
    color: hsl(0, 50%, 45%);
}

/* 标记上了：跟导航面板里那格用同一套红，
   两处颜色对不上的话，学生要多想一步才能把它们联系起来 */
.exam-flag-btn.is-flagged {
    background: hsl(0, 70%, 95%);
    border-color: hsl(0, 55%, 80%);
    color: hsl(0, 55%, 40%);
}

/* 交卷后答题框变成只读，标记也就没意义了 */
body.exam-result-mode .exam-flag-btn {
    display: none;
}

/* 从导航跳过去时留出顶部空间，
   否则题目会被顶部的 Test 分类导航栏压住 */
#testing-questions .question-block {
    scroll-margin-top: 88px;
}

/* ---------- 学生自评 ---------- */
/* 大题（half-program / full-program）自动判不了，交卷后摊开标准答案和
   减分条件让学生自己打分。这一块放在对照区【下面】——
   先看自己写的、再看标准答案，最后才打分，顺序跟人实际改卷子一样 */
.self-assess-box {
    /* 上边距≈原来题号↔题干的间距（题干 <pre> 的默认上边距，约 16px），
       给题号留出空隙；下边距 0，让题干贴着自评框（见下面相邻规则） */
    margin: 16px 0 0;
    padding: 14px 16px;
    border-radius: 12px;
    /* 用虚线边框跟上面的判分面板（实线）区分开：
       那个是机器判的，这个是自己填的，视觉上不该长得一样 */
    border: 1px dashed hsla(220, 20%, 78%, 0.9);
    background: hsl(45, 60%, 98%);
}

/* 自评框正下方就是题干 <pre>：去掉它的默认上边距，让题干贴着自评框，
   间距只留在自评框【上方】（对着题号） */
.self-assess-box + .question-code {
    margin-top: 0;
}

.self-assess-heading {
    font-size: 0.82rem;
    font-weight: 600;
    color: hsl(220, 15%, 35%);
    margin-bottom: 8px;
}

/* 减分条件是老师录的多行文本，保留换行；
   用等宽字体之外的字体——它是说明不是代码 */
.self-assess-rubric {
    white-space: pre-wrap;
    font-size: 0.82rem;
    line-height: 1.55;
    color: hsl(220, 12%, 40%);
    margin-bottom: 12px;
}

/* 缺分值 / 缺减分条件时的说明。要看得见但不抢眼——
   它说的是"数据还没录"，是给使用者的提示，不是错误 */
.self-assess-missing {
    margin: 0 0 10px;
    font-size: 0.76rem;
    color: hsl(220, 10%, 55%);
    font-style: italic;
}

.self-assess-row {
    display: flex;
    align-items: center;
    gap: 10px;
}

.self-assess-label {
    display: inline-flex;
    align-items: center;
    gap: 8px;
    font-size: 0.82rem;
    font-weight: 600;
    color: hsl(220, 15%, 35%);
}

.self-assess-input {
    width: 72px;
    padding: 6px 10px;
    font-size: 0.9rem;
    text-align: center;
    border-radius: 8px;
    border: 1px solid hsla(220, 20%, 80%, 0.9);
    background: #fff;
    color: hsl(220, 15%, 25%);
}

.self-assess-input:focus {
    outline: none;
    border-color: hsl(210, 70%, 60%);
    box-shadow: 0 0 0 3px hsla(210, 70%, 60%, 0.15);
}

/* 提交查看成绩后，自评分锁死（readOnly）：灰底 + 不可编辑光标，表明改不了了 */
.self-assess-input.is-locked {
    background: hsl(0, 0%, 95%);
    color: hsl(220, 10%, 45%);
    cursor: not-allowed;
}

.self-assess-outof {
    font-weight: 500;
    color: hsl(220, 10%, 50%);
}

/* 自评总分。跟 .exam-score-note（自动判分的说明）并列显示，
   ⚠️ 两者【不合并】：一个是测试用例跑出来的，一个是学生自己填的，
   加在一起得到的数字没有任何一句话能准确描述它是什么 */
.exam-self-score {
    margin: 6px 0 0;
    font-size: 0.74rem;
    color: hsl(220, 10%, 55%);
}

/* 标准答案拉取失败时的提示 */
.solutions-error {
    margin: 0 0 16px;
    padding: 10px 14px;
    border-radius: 10px;
    background: hsl(0, 70%, 97%);
    color: hsl(0, 45%, 45%);
    font-size: 0.82rem;
}

/* ---------- 判分结果 ---------- */
/* 显示在学生答案和标准答案之间——先看自己错在哪，再看正确写法 */
.grade-panel {
    margin: 14px 0 18px;
    padding: 14px 16px;
    border-radius: 12px;
    border: 1px solid hsla(220, 20%, 90%, 0.9);
    background: hsl(220, 30%, 98%);
}

.grade-panel.is-pending {
    color: hsl(220, 10%, 55%);
    font-size: 0.85rem;
}

/* 满分绿、部分分橙、零分红。
   用底色而不是只改文字颜色——扫一眼就能看出哪几题需要回头看 */
.grade-panel.is-full {
    background: hsl(148, 45%, 97%);
    border-color: hsla(148, 40%, 82%, 0.9);
}

.grade-panel.is-partial {
    background: hsl(38, 70%, 97%);
    border-color: hsla(38, 60%, 82%, 0.9);
}

.grade-panel.is-zero {
    background: hsl(0, 55%, 97.5%);
    border-color: hsla(0, 50%, 88%, 0.9);
}

.grade-head {
    display: flex;
    align-items: baseline;
    gap: 12px;
    margin-bottom: 12px;
}

.grade-score {
    font-size: 1.5rem;
    font-weight: 700;
    letter-spacing: -0.02em;
    color: hsl(220, 28%, 15%);
}

.grade-score span {
    font-size: 0.9rem;
    font-weight: 600;
}

.grade-count {
    font-size: 0.82rem;
    color: hsl(220, 10%, 50%);
}

.grade-case-list {
    margin: 0;
    padding: 0;
    list-style: none;
    display: flex;
    flex-direction: column;
    gap: 7px;
}

.grade-case-list li {
    display: flex;
    align-items: center;
    flex-wrap: wrap;
    gap: 8px;
    font-size: 0.84rem;
    color: hsl(220, 15%, 32%);
}

.grade-case-list li i {
    font-size: 0.82rem;
}

.grade-case-list li.is-pass i {
    color: hsl(148, 55%, 42%);
}

.grade-case-list li.is-fail i {
    color: hsl(0, 65%, 58%);
}

/* 期望值和错误信息用等宽字体——它们是代码相关的内容 */
.grade-detail,
.grade-error {
    padding: 2px 7px;
    border-radius: 6px;
    font-family: Consolas, Monaco, 'Courier New', monospace;
    font-size: 0.76rem;
}

.grade-detail {
    background: hsla(220, 25%, 88%, 0.6);
    color: hsl(220, 20%, 35%);
}

/* 代码报错的信息对学生最有用，颜色上要能看出来是"出事了" */
.grade-error {
    background: hsla(0, 60%, 92%, 0.7);
    color: hsl(0, 55%, 38%);
    width: 100%;
}

/* ---------- 答案对照区 ---------- */
/* 学生答案和标准答案并排，能逐行对照，不用上下滚动来回看。
   
   考试期间只有左边（标准答案由 .testing-answer 藏着），
   所以这时是单列——用 grid 的 auto 列宽自动处理，不用额外写规则 */
.answer-compare {
    /* 两栏共用的高度上限。学生答案和标准答案都到这个高度为止，
       超出的部分各自内部滚动。
       
       写成变量是因为这个值必须两边【一致】：一边能长到 900px、
       另一边卡在 520px 的话，并排对照就错位了。
       想调整长度改这一个数字就够，两栏跟着走 */
    --answer-box-max-h: 620px;

    display: grid;
    grid-template-columns: 1fr;
    gap: 16px;
    margin: 18px 0 8px;
    align-items: start;
}

/* 标准答案出现后变成左右两列。

   ⚠️ 【改卷阶段(exam-marking-mode)和结果页(exam-result-mode)都要并排】——
   改卷时学生正对着标准答案打分，左右并排才好改；到结果页继续保持。
   两个 body class 都要写，漏了改卷这个，改卷时两栏会竖着堆（踩过一次）。

   为什么挂 body 不挂结构选择器：#testing-header 在 .exam-layout 里、
   #testing-questions 在外面，两者不是兄弟，~ 选择器匹配不上；
   body 上的 class 不受 DOM 层级影响，这种跨结构的状态就该挂 body。 */
body.exam-result-mode .answer-compare,
body.exam-marking-mode .answer-compare {
    grid-template-columns: 1fr 1fr;
}

/* 两栏之间的拖动条。
   
   三列布局：左栏 | 拖动条 | 右栏。
   --answer-split 是左栏占的百分比，由 JS 在拖动时改，默认 50%。
   右栏用 1fr 吃掉剩下的，所以【总宽度不变】——
   左边多一点右边就少一点，整体不会变宽也不会换行。
   
   ⚠️ 两侧都写 minmax(0, ...)：grid 子项的默认最小尺寸是 auto，
   也就是"不能小于内容"。里面是代码块，一行长代码就能顶住不让缩，
   拖动会卡住不动。写 0 才能真正拖到小 */
body.exam-result-mode .answer-compare.has-split,
body.exam-marking-mode .answer-compare.has-split {
    grid-template-columns: minmax(0, var(--answer-split, 50%))
                           auto
                           minmax(0, 1fr);
    /* gap 归零：栏间距由拖动条自己的宽度和内边距提供，
       再留 gap 的话两栏中间会空出一大块 */
    gap: 0;
}

.answer-split-handle {
    /* 只在纵向撑满自己这一格，不影响两侧高度——
       align-items: start 是父级设的，这里要单独拉满 */
    align-self: stretch;

    /* 顶上让出标签那一行，让线从【框的顶边】开始，
       而不是从 "YOUR ANSWER" / "MODEL ANSWER" 那一行开始。
       
       ⚠️ 用 margin-top 不是 padding-top。
       padding 的做法要靠 ::before 的 height: 100% 去解析父级的内容盒高度，
       在 flex 容器里这个百分比不一定按预期解析，线还是会顶到最上面。
       margin 配合 align-self: stretch 直接把盒子本身的顶边压下去，
       ::before 撑多高都跟着走。
       
       26px ≈ 标签字号 0.72rem 那一行的高度 + margin-bottom 7px。
       ⚠️ 改了 .answer-editor-label 的字号或下边距，这个数要跟着调 */
    margin-top: 26px;

    width: 18px;
    display: flex;
    align-items: center;
    justify-content: center;

    cursor: col-resize;
    /* 拖动时浏览器会去选中两侧的文字，选中之后指针一动整片就高亮了。
       touch-action 是给触屏的：不写的话按住拖会变成滚页面 */
    user-select: none;
    touch-action: none;
    background: none;
    border: none;
    padding: 0;
}

/* 中间那根线。默认很淡，鼠标靠近才明显——
   它平时不该跟代码抢注意力 */
.answer-split-handle::before {
    content: '';
    width: 2px;
    height: 100%;
    min-height: 40px;
    border-radius: 999px;
    background: hsl(220, 12%, 88%);
    transition: background 0.15s ease, width 0.15s ease;
}

.answer-split-handle:hover::before,
.answer-split-handle:focus-visible::before,
.answer-split-handle.is-dragging::before {
    width: 4px;
    background: hsl(220, 12%, 62%);
}

.answer-split-handle:focus-visible {
    outline: none;
}

/* 拖动过程中给整页加的类。
   ⚠️ 没有这个的话，指针一旦移出拖动条（拖快了必然移出），
   光标会变回箭头、而且两侧的文字会被选中，看起来像拖断了 */
body.is-splitting,
body.is-splitting * {
    cursor: col-resize !important;
    user-select: none !important;
}

/* 窄屏堆叠回上下排列。
   两边各减半之后，代码行会频繁折行，反而比上下排更难读 */
@media (max-width: 900px) {
    body.exam-result-mode .answer-compare,
    body.exam-marking-mode .answer-compare {
        grid-template-columns: 1fr;
    }

    /* 单栏堆叠时拖动条没有意义——它调的是左右比例，
       而这时候两栏是上下排的。
       ⚠️ .has-split 那条的特异性更高（多一个 class），
       所以必须在这里【显式】写一遍把它压回单列，
       只写上面那条压不住 */
    body.exam-result-mode .answer-compare.has-split,
    body.exam-marking-mode .answer-compare.has-split {
        grid-template-columns: 1fr;
        gap: 16px;
    }

    .answer-split-handle {
        display: none;
    }
}

.answer-solution-box {
    min-width: 0;   /* grid 子项的默认最小宽度是内容宽度，
                       长代码会把这一列撑破、挤掉左边那列 */
}

/* 标准答案的代码块。超过上限内部滚动，不要把整个题目块撑得很长。
   
   上限跟左边的答题框共用 --answer-box-max-h（定义在 .answer-compare 上）——
   以前这里写死 520px，而左边是无上限自动增高的，
   结果一份长答案会让左栏比右栏高出一大截，并排对照的意义就没了。
   
   ⚠️ 这里【不要】再写 overflow。滚不滚由上面那组
   "点进来才能滚" 的规则控制（默认 hidden，:focus 时 auto）。
   在这里补一句 overflow: auto 的话，因为它排在后面、
   优先级又一样，会直接把那套逻辑盖掉 */
.answer-solution-box .testing-answer {
    max-height: var(--answer-box-max-h);
    margin: 0;
}

/* ---------- 答题区 ---------- */
/* 每道题下面一个输入框，位置在题干和标准答案之间 */
.answer-editor {
    /* 外边距归 .answer-compare 管——两处都设的话间距会翻倍 */
    min-width: 0;   /* 同 .answer-solution-box：防止长代码撑破这一列 */
}

.answer-editor-label {
    display: block;
    margin-bottom: 7px;
    font-size: 0.72rem;
    font-weight: 700;
    text-transform: uppercase;
    letter-spacing: 0.05em;
    color: hsl(220, 10%, 55%);
}

/* 等宽字体 + 关掉浏览器默认的 resize 手柄（高度由 JS 自动撑）。
   tab-size: 4 让粘贴进来的制表符按 Python 惯例显示 */
/* ⚠️ min-height / max-height 这两个值 JS 也要用（autoGrowAnswerInput
   里靠 getComputedStyle 读回去），所以【只在这里写一份】。
   在 JS 里另写死一个数字的话，改了这边忘了那边，
   表现是框子停在旧高度上但滚动条按新高度算 */
.answer-editor-input {
    display: block;
    width: 100%;
    min-height: 120px;
    max-height: var(--answer-box-max-h);
    padding: 14px 16px;
    border: 1px solid hsla(220, 20%, 88%, 0.9);
    border-radius: 12px;
    background: hsl(220, 30%, 98.5%);
    font-family: Consolas, Monaco, 'Courier New', monospace;
    font-size: 0.88rem;
    line-height: 1.6;
    color: hsl(220, 28%, 18%);
    tab-size: 4;
    resize: none;
    /* 【没点进来之前不给滚】。内容超出上限时 JS 会加 .is-capped，
       但真正放出滚动条的是下面那条 :focus 规则。
       这样鼠标划过答题框时滚轮还是滚页面，
       不会出现"鼠标经过某块地方整页就不动了"的情况 */
    overflow: hidden;
    box-sizing: border-box;
    /* ⚠️ transition 只写这一条。同一个属性写两次的话，
       后面那条会把前面整条【整个】盖掉（不是合并），
       结果是先写的那些过渡全部失效 */
    transition: border-color 0.15s ease, background-color 0.15s ease,
                box-shadow 0.15s ease;
}

/* 聚焦时的效果是【阴影】，把这一栏抬起来一点。
   ⚠️ 原来这里是 border-color + 0 0 0 3px 的零模糊扩散——
   那两个看起来都是"描了一圈边"，不是阴影。
   现在换成有模糊、有垂直偏移的投影，视觉上是浮起来。
   
   ⚠️ 【聚焦的样式只写这一处】。以前下面还有一条同名规则，
   排在后面会把这条整个盖掉——CSS 同选择器同属性是后者胜，不是合并。
   标准答案那栏用的是同一套阴影值（见 .testing-answer:focus），
   两栏交卷后并排显示，长得不一样会像两种不同的控件 */
.answer-editor-input:focus {
    outline: none;
    background: #ffffff;
    box-shadow: 0 6px 20px hsla(220, 30%, 35%, 0.16),
                0 2px 6px hsla(220, 30%, 35%, 0.1);
}

.answer-editor-input::placeholder {
    color: hsl(220, 8%, 65%);
}

/* 交卷后不能再改。
   ⚠️ 用 readonly 不用 disabled：disabled 的文本没法选中复制，
   而学生交卷后往往想把自己的答案复制出来跟标准答案对照 */
/* ⚠️ 同上：只用 body.exam-result-mode。
   .testing-header.exam-finished ~ * 那条匹配不到题目区 */
body.exam-result-mode .answer-editor-input,
body.exam-marking-mode .answer-editor-input {
    background: hsl(220, 20%, 97%);
    color: hsl(220, 12%, 42%);
    border-color: hsla(220, 15%, 90%, 0.9);
}

.question-block {
    margin-bottom: 44px;
    padding-bottom: 32px;
    border-bottom: 1px solid var(--divider-color);
    scroll-margin-top: 100px; /* "Go to Question" 跳过来时，题号不会被吸顶的 nav-bar 挡住 */
}

/* 请求还没回来时显示的"加载中"提示，带一个简单的旋转小图标，避免用户以为卡住了 */
.questions-loading {
    display: flex;
    align-items: center;
    gap: 10px;
    padding: 32px 4px;
    color: var(--text-secondary, #666);
    font-size: 0.95rem;
}

.questions-loading::before {
    content: '';
    width: 16px;
    height: 16px;
    border: 2px solid var(--divider-color);
    border-top-color: hsl(207, 70%, 55%);
    border-radius: 50%;
    animation: questionsLoadingSpin 0.7s linear infinite;
}

@keyframes questionsLoadingSpin {
    to { transform: rotate(360deg); }
}

/* 请求失败时的提示，颜色跟登录/注册那边的错误提示保持一致的红色调 */
.questions-error {
    padding: 24px 16px;
    border-radius: 8px;
    background-color: hsl(0, 70%, 96%);
    color: hsl(0, 70%, 40%);
    font-size: 0.95rem;
}

.question-label-row {
    display: flex;
    align-items: center;
    gap: 10px;
    margin-bottom: 4px;
}

.question-label {
    font-weight: bold;
    margin: 0;
}

/* Practice 模式跨年份混合展示时，标注每道题来自哪一年的考卷 */
.question-year-tag {
    display: inline-block;
    padding: 2px 10px;
    border-radius: 999px;
    background-color: hsl(207, 60%, 92%);
    color: hsl(207, 60%, 35%);
    font-size: 0.8rem;
    font-weight: 600;
}

/* Practice 模式选 "All" 题型时，标注每道题具体是什么类型（浅灰色，跟年份标签的蓝色区分开） */
/* 难度徽章：跟年份、题型那两个标签同一档尺寸，摆在它们右边。
   颜色按平均难度分三档，越难越暖——扫列表的时候不用逐个看星星
   就知道哪些题硬。带个星形图标是为了让「4.5」这个数字有明确所指，
   否则孤零零一个数看不出是什么 */
.question-difficulty-tag {
    display: inline-flex;
    align-items: center;
    gap: 4px;
    padding: 2px 10px;
    border-radius: 999px;
    font-size: 0.8rem;
    font-weight: 600;
}

.question-difficulty-tag i {
    font-size: 0.68rem;
    opacity: 0.75;
}

.question-difficulty-tag.difficulty-easy {
    background-color: hsl(142, 52%, 92%);
    color: hsl(148, 55%, 27%);
}

.question-difficulty-tag.difficulty-medium {
    background-color: hsl(32, 88%, 91%);
    color: hsl(27, 72%, 33%);
}

.question-difficulty-tag.difficulty-hard {
    background-color: hsl(0, 72%, 94%);
    color: hsl(0, 58%, 41%);
}

.question-type-tag {
    display: inline-block;
    padding: 2px 10px;
    border-radius: 999px;
    background-color: hsl(0, 0%, 90%);
    color: hsl(0, 0%, 35%);
    font-size: 0.8rem;
    font-weight: 600;
}

/* 分值徽标：蓝色，跟灰色题型标签区分开，一眼看出这题值多少分 */
.question-points-tag {
    display: inline-block;
    padding: 2px 10px;
    border-radius: 999px;
    background-color: hsl(212, 100%, 93%);
    color: hsl(212, 80%, 38%);
    font-size: 0.8rem;
    font-weight: 700;
}

/* 结果页题号旁的「得分/总分」徽标，放在分值徽标左边。
   自动判分(is-auto, 云)和自评(is-self, 铅笔)配色不同，一眼区分判法 */
.question-earned-tag {
    display: inline-flex;
    align-items: center;
    gap: 5px;
    padding: 2px 10px;
    border-radius: 999px;
    font-size: 0.8rem;
    font-weight: 700;
}

.question-earned-tag i {
    font-size: 0.72rem;
}

/* 颜色按得分色带（浅色调），auto/self 的区分交给里面的云/铅笔图标。
   0–5/10 红、6–8/10 橙、9–10/10 绿 */
.question-earned-tag.is-score-low {
    background-color: hsl(0, 75%, 94%);
    color: hsl(0, 58%, 42%);
}

.question-earned-tag.is-score-mid {
    background-color: hsl(32, 92%, 90%);
    color: hsl(28, 78%, 38%);
}

.question-earned-tag.is-score-high {
    background-color: hsl(145, 52%, 90%);
    color: hsl(145, 46%, 30%);
}

/* 最终结果页把自评框藏掉——分数已经变成题号旁的 X/10 徽标了。
   ⚠️ 用 display:none 不删元素：.self-assess-input 要留在 DOM 里，
   collectSelfTotal / saveExamAttempt 还得读它的值算自评分 */
body.exam-result-mode .self-assess-box {
    display: none;
}

/* 星标按钮：紧跟在题型标签（比如 "full-program"）旁边，空心=没标，实心+黄色=已标（联动 Revision 页面） */
.question-star-btn {
    background: none;
    border: none;
    cursor: pointer;
    font-size: 1.05rem;
    color: hsl(0, 0%, 75%);
    padding: 2px 4px;
    transition: color 0.15s ease, transform 0.15s ease;
}

.question-star-btn:hover {
    color: hsl(212, 80%, 60%);
    transform: scale(1.15);
}

.question-star-btn.starred {
    color: hsl(212, 85%, 48%);
}

.question-star-btn:disabled {
    opacity: 0.5;
    cursor: not-allowed;
}

/* Revision 页面：首页 + 两个子板块，同一时间只显示一个 */
.revision-view {
    display: none;
}

.revision-view.active {
    display: block;
}

.revision-cards {
    display: grid;
    grid-template-columns: repeat(2, 1fr);
    gap: 20px;
    /* 跟 Examination 的 .exam-layout 保持同一个上边距，
       两个板块切过去内容不会一高一低。
       （原来是 28px，那是给已删除的 "Revision Center" hero 留的） */
    margin-top: 22px;
}

@media (max-width: 640px) {
    .revision-cards {
        grid-template-columns: 1fr;
    }
}

.revision-feature-card {
    background: #ffffff;
    border-radius: 16px;
    padding: 28px 24px;
    text-align: center;
    box-shadow: 0 1px 3px rgba(16, 24, 40, 0.06), 0 8px 24px rgba(16, 24, 40, 0.06);
    transition: transform 0.2s ease, box-shadow 0.2s ease;
}

.revision-feature-card:hover {
    transform: translateY(-3px);
    box-shadow: 0 2px 6px rgba(16, 24, 40, 0.08), 0 14px 32px rgba(16, 24, 40, 0.1);
}

.revision-feature-icon {
    width: 52px;
    height: 52px;
    margin: 0 auto 16px;
    border-radius: 14px;
    background: hsl(45, 90%, 94%);
    color: hsl(40, 85%, 48%);
    display: flex;
    align-items: center;
    justify-content: center;
    font-size: 1.3rem;
}

.revision-feature-card h3 {
    margin: 0 0 8px;
    font-size: 1.15rem;
}

.revision-feature-card p {
    margin: 0 0 20px;
    color: var(--weak-color, #666);
    font-size: 0.9rem;
    line-height: 1.5;
}

.revision-card-btn {
    padding: 10px 26px;
    border: none;
    border-radius: 10px;
    background: hsl(207, 70%, 50%);
    color: #ffffff;
    font-weight: 600;
    font-size: 0.9rem;
    cursor: pointer;
    transition: background-color 0.2s ease;
}

.revision-card-btn:hover {
    background: hsl(207, 70%, 42%);
}

.revision-back-btn {
    display: inline-flex;
    align-items: center;
    gap: 6px;
    background: none;
    border: none;
    color: hsl(207, 70%, 45%);
    font-size: 0.88rem;
    font-weight: 600;
    cursor: pointer;
    padding: 4px 0;
    /* 往上收一点，贴近上面那个标题（标题由板块切换器提供）。
       两个子页共用这一条，改一次两边都动 */
    margin-top: -12px;
    margin-bottom: 16px;
}

.revision-back-btn:hover {
    text-decoration: underline;
}

/* Revision 筛选行 */
/* Revision 子页的标题（Marked Questions / Cribsheet Builder）。
   原来没有规则，用的是浏览器默认外边距，跟下面的筛选行贴得太近 */
.revision-view > h2 {
    margin: 4px 0 26px;
}

/* 跟 Practice 那排完全一致，标签本身的样式复用 .qtype-pill */
.revision-filter-row {
    display: flex;
    align-items: center;
    gap: 12px;
    flex-wrap: wrap;
    margin: 0 0 30px;
}

/* "Go to Question" 按钮 */
.question-goto-btn {
    background: none;
    border: 1px solid var(--divider-color, #ddd);
    border-radius: 999px;
    padding: 3px 12px;
    font-size: 0.78rem;
    font-weight: 600;
    color: hsl(207, 70%, 45%);
    cursor: pointer;
    transition: background-color 0.15s ease;
}

.question-goto-btn:hover {
    background: hsl(207, 70%, 96%);
}

/* 备注输入框（"为什么标了它"） */
.question-note-wrap {
    display: flex;
    align-items: center;
    gap: 10px;
    margin-top: 10px;
}

.question-note-input {
    flex: 1;
    padding: 7px 12px;
    border-radius: 8px;
    border: 1px solid var(--divider-color, #ddd);
    font-size: 0.85rem;
    font-family: inherit;
}

.question-note-input:focus {
    outline: none;
    border-color: hsl(207, 70%, 55%);
}

.question-note-status {
    font-size: 0.78rem;
    color: hsl(140, 50%, 38%);
    flex-shrink: 0;
}

/* Practice 页面的难度评分组件 */
.rating-widget {
    display: flex;
    align-items: center;
    gap: 10px;
    margin-top: 14px;
    flex-wrap: wrap;
}

.rating-label {
    font-size: 0.82rem;
    color: var(--weak-color, #666);
    font-weight: 600;
}

.rating-stars {
    display: flex;
    gap: 2px;
}

.rating-star {
    background: none;
    border: none;
    cursor: pointer;
    padding: 2px;
    font-size: 1.05rem;
    color: hsl(0, 0%, 82%);
    transition: color 0.12s ease, transform 0.12s ease;
}

.rating-star:hover {
    transform: scale(1.15);
}

/* 整体评分对应一个颜色，不是每颗星各自一个颜色——比如评了 3 分，
   亮起来的 3 颗星会是同一种"3 分对应的黄色"，而不是三种不同颜色混在一起。
   颜色故意压低饱和度、拉高亮度，看起来淡一点，不刺眼 */
.rating-stars.rating-level-1 .rating-star.filled { color: hsl(145, 55%, 58%); }
.rating-stars.rating-level-2 .rating-star.filled { color: hsl(78, 45%, 54%); }
.rating-stars.rating-level-3 .rating-star.filled { color: hsl(45, 70%, 58%); }
.rating-stars.rating-level-4 .rating-star.filled { color: hsl(25, 70%, 60%); }
.rating-stars.rating-level-5 .rating-star.filled { color: hsl(5, 65%, 62%); }

.rating-avg-text {
    font-size: 0.78rem;
    color: var(--weak-color, #888);
}

/* 从 Revision 跳回 Practice 定位到某道题时，闪一下高亮，方便一眼找到 */
.question-highlight {
    animation: questionHighlightPulse 2s ease;
}

@keyframes questionHighlightPulse {
    0% { background-color: hsl(45, 90%, 88%); }
    100% { background-color: transparent; }
}

/* Revision 空状态（没标过任何题 / 没登录） */
.revision-empty {
    text-align: center;
    padding: 56px 24px;
    color: var(--weak-color, #666);
}

.revision-empty-icon {
    font-size: 2rem;
    margin-bottom: 14px;
    color: hsl(212, 75%, 60%);
}

.revision-empty-title {
    font-weight: 700;
    font-size: 1.05rem;
    color: #222;
    margin: 0 0 6px;
}

.revision-empty-subtext {
    margin: 0 auto;
    font-size: 0.9rem;
    max-width: 380px;
    line-height: 1.5;
}

/* Revision 页面两个板块之间的分隔 */
.crib-sheet-header {
    display: flex;
    align-items: center;
    justify-content: space-between;
    flex-wrap: wrap;
    gap: 10px;
}

.crib-sheet-actions {
    display: flex;
    align-items: center;
    gap: 12px;
}

.crib-sheet-status {
    font-size: 0.85rem;
    color: hsl(140, 50%, 38%);
}

.crib-sheet-save-btn {
    padding: 8px 20px;
    border: none;
    border-radius: 8px;
    background: hsl(207, 70%, 50%);
    color: #ffffff;
    font-weight: 600;
    font-size: 0.9rem;
    cursor: pointer;
    transition: background-color 0.2s ease;
}

.crib-sheet-save-btn:hover:not(:disabled) {
    background: hsl(207, 70%, 42%);
}

.crib-sheet-save-btn:disabled {
    opacity: 0.55;
    cursor: not-allowed;
}

.crib-sheet-textarea {
    width: 100%;
    min-height: 260px;
    margin-top: 14px;
    padding: 16px;
    border: 1px solid var(--divider-color, #ddd);
    border-radius: 10px;
    font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
    font-size: 0.95rem;
    line-height: 1.6;
    color: #222;
    resize: vertical;
    box-sizing: border-box;
}

.crib-sheet-textarea:focus {
    outline: none;
    border-color: hsl(207, 70%, 55%);
    box-shadow: 0 0 0 3px hsl(207, 70%, 92%);
}

.crib-sheet-textarea:disabled {
    background: hsl(0, 0%, 96%);
    color: var(--weak-color, #888);
}

/* ---------- Cribsheet Builder ---------- */
/*
   这一整块是按「纸张是主角」重做的。核心判断：原来左边笔记库有底色填充，
   跟画布是同一个视觉层级，所以在跟主体抢注意力。层级不靠给每块加个盒子来建立，
   而是靠让主体成为页面上唯一有实体感的东西，其余全部退成文字和细线。

   所以：笔记库去掉底色，只留一条竖细线；画布区域变成一块中性的「工作台」，
   纸居中浮在上面带真实投影。整个界面里只有纸和主按钮是「实的」。

   字体没有引新的（本地网络 CDN 常被墙，而且不改 HTML 加不了 link），
   层次全部由字号、字重、字距、颜色承担——系统字体在这类工具型界面里本来就够用。
*/

/* 这一层的设计变量。范围限定在 Cribsheet 视图内，不影响站内其他页面。
   弹窗也在这个容器里，所以能一起取到 */
#revision-view-cribsheet {
    --cs-ink: hsl(220, 20%, 15%);        /* 主文字 */
    --cs-ink-soft: hsl(220, 9%, 45%);    /* 次要文字 */
    --cs-ink-faint: hsl(220, 8%, 62%);   /* 图标、占位、计数 */
    --cs-line: hsl(220, 14%, 91%);       /* 细线 */
    --cs-line-strong: hsl(220, 13%, 84%);
    --cs-surface: hsl(220, 24%, 97%);    /* 纸底下那块工作台 */
    --cs-raised: #ffffff;                /* 纸、卡片 */
    --cs-accent: hsl(214, 84%, 51%);     /* 唯一的强调色 */
    --cs-accent-soft: hsl(214, 90%, 96%);
    --cs-radius: 10px;
}

.cribsheet-intro {
    max-width: 62ch;
    margin: 2px 0 12px;   /* 收紧:别让顶部空一大片,整体往上提 */
    color: var(--cs-ink-soft);
    font-size: 0.9rem;
    line-height: 1.5;
}

/* 左窄右宽。左边收窄一点、间距拉开，让视线更快落到纸上 */
.cribsheet-builder-layout {
    display: grid;
    grid-template-columns: 244px 1fr;
    gap: 32px;
    align-items: start;
}

@media (max-width: 800px) {
    .cribsheet-builder-layout {
        grid-template-columns: 1fr;
        gap: 24px;
    }
}

/* ---------- 左边：笔记库 ---------- */
/* 没有底色、没有圆角盒子。只有一条竖细线把它和画布分开——
   这是整个重做里最关键的一处改动 */
.cribsheet-library-panel {
    padding: 0 24px 0 0;
    border-right: 1px solid var(--cs-line);
    max-height: 720px;
    overflow-y: auto;
}

@media (max-width: 800px) {
    .cribsheet-library-panel {
        padding: 0 0 24px;
        border-right: none;
        border-bottom: 1px solid var(--cs-line);
        max-height: none;
    }
}

.cribsheet-library-panel h3 {
    margin: 0 0 14px;
    color: var(--cs-ink);
    font-size: 0.95rem;
    font-weight: 600;
    letter-spacing: -0.01em;
}

.cribsheet-library-search {
    width: 100%;
    padding: 9px 12px;
    margin-bottom: 6px;
    border: 1px solid var(--cs-line);
    border-radius: 8px;
    background: var(--cs-raised);
    color: var(--cs-ink);
    font-size: 0.85rem;
    font-family: inherit;
    box-sizing: border-box;
    transition: border-color 0.15s ease, box-shadow 0.15s ease;
}

.cribsheet-library-search:focus {
    outline: none;
    border-color: var(--cs-accent);
    box-shadow: 0 0 0 3px var(--cs-accent-soft);
}

.cribsheet-library-search::placeholder {
    color: var(--cs-ink-faint);
}

/* 分类手风琴，一次只展开一个 */
.cribsheet-library-category {
    border-bottom: 1px solid var(--cs-line);
}

.cribsheet-library-category:last-child {
    border-bottom: none;
}

.cribsheet-library-category-header {
    display: flex;
    align-items: center;
    gap: 8px;
    width: 100%;
    padding: 12px 2px;
    background: none;
    border: none;
    cursor: pointer;
    text-align: left;
    font-family: inherit;
}

/* 分类名是「眉标」：小字号 + 拉开字距。它是导航标签，不是内容，
   压小之后视线会直接落到下面的笔记条目上 */
.cribsheet-library-category-name {
    flex: 1;
    font-size: 0.7rem;
    font-weight: 600;
    text-transform: uppercase;
    letter-spacing: 0.07em;
    color: var(--cs-ink-faint);
    transition: color 0.15s ease;
}

/* 折起来的时候也能知道这个分类里有多少条 */
.cribsheet-library-category-count {
    min-width: 20px;
    padding: 2px 7px;
    border-radius: 999px;
    background: var(--cs-surface);
    color: var(--cs-ink-faint);
    font-size: 0.66rem;
    font-weight: 600;
    text-align: center;
}

.cribsheet-library-category-chevron {
    font-size: 0.62rem;
    color: var(--cs-ink-faint);
    transition: transform 0.2s ease;
}

.cribsheet-library-category.open .cribsheet-library-category-chevron {
    transform: rotate(180deg);
}

.cribsheet-library-category.open .cribsheet-library-category-name {
    color: var(--cs-ink);
}

/* 展开高度由 JS 按内容实测写进 max-height，这里只负责过渡和裁剪 */
.cribsheet-library-category-body {
    max-height: 0;
    overflow: hidden;
    transition: max-height 0.24s cubic-bezier(0.4, 0, 0.2, 1);
}

.cribsheet-library-category.open .cribsheet-library-category-body {
    padding-bottom: 10px;
}

/* 笔记条目做成卡片：留白更宽、圆角更大，鼠标移上去整体抬起来一点 */
.cribsheet-library-item {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 10px;
    width: 100%;
    padding: 10px 12px;
    margin-bottom: 6px;
    border: 1px solid var(--cs-line);
    border-radius: var(--cs-radius);
    background: var(--cs-raised);
    color: var(--cs-ink);
    cursor: pointer;
    font-size: 0.82rem;
    font-weight: 500;
    text-align: left;
    font-family: inherit;
    transition: border-color 0.15s ease, box-shadow 0.15s ease, transform 0.15s ease;
}

.cribsheet-library-item:hover {
    border-color: hsl(214, 55%, 78%);
    box-shadow: 0 4px 12px hsla(220, 30%, 20%, 0.09);
    transform: translateY(-2px);
}

.cribsheet-library-item:active {
    transform: translateY(0);
    box-shadow: 0 1px 3px hsla(220, 30%, 20%, 0.08);
}

/* 加号平时压得很淡，鼠标移上去才亮起来——它是操作提示，不该一直跟标题抢注意力 */
.cribsheet-library-item .fa-plus {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 20px;
    height: 20px;
    border-radius: 6px;
    color: var(--cs-accent);
    opacity: 0.22;
    transition: opacity 0.15s ease, background-color 0.15s ease;
}

.cribsheet-library-item:hover .fa-plus {
    opacity: 1;
    background: var(--cs-accent-soft);
}

.cribsheet-library-item i {
    font-size: 0.72rem;
    flex-shrink: 0;
}

.cribsheet-library-item.added {
    border-color: hsl(145, 40%, 74%);
    background: hsl(145, 50%, 97%);
    color: hsl(150, 40%, 28%);
}

.cribsheet-library-item.added i {
    color: hsl(148, 45%, 42%);
}

.cribsheet-library-item:disabled {
    opacity: 0.55;
    cursor: default;
}

.cribsheet-add-custom-btn {
    width: 100%;
    margin-top: 14px;
    padding: 10px;
    border: 1px dashed var(--cs-line-strong);
    border-radius: var(--cs-radius);
    background: none;
    color: var(--cs-ink-soft);
    cursor: pointer;
    font-size: 0.8rem;
    font-weight: 500;
    font-family: inherit;
    transition: border-color 0.15s ease, color 0.15s ease, background-color 0.15s ease, transform 0.15s ease;
}

.cribsheet-add-custom-btn:hover {
    border-color: var(--cs-accent);
    background: var(--cs-accent-soft);
    color: var(--cs-accent);
    transform: translateY(-1px);
}

.cribsheet-add-custom-btn:active {
    transform: scale(0.98);
}

/* ---------- 工具栏 ---------- */
/* 次要操作退成无边框的幽灵按钮，只有 Save as PDF 是填色的。
   整条工具栏里只有一个「实心」元素，主操作在哪一眼就看到 */
.cribsheet-sheet-toolbar {
    display: flex;
    align-items: center;
    gap: 10px;
    flex-wrap: wrap;
    margin-bottom: 14px;
}

.cribsheet-toolbar-actions {
    display: flex;
    align-items: center;
    gap: 4px;
    margin-left: auto;
}

.cribsheet-icon-btn {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 32px;
    height: 32px;
    border: none;
    border-radius: 8px;
    background: none;
    color: var(--cs-ink-soft);
    cursor: pointer;
    font-size: 0.82rem;
    transition: background-color 0.15s ease, color 0.15s ease;
}

.cribsheet-icon-btn:hover {
    background: var(--cs-surface);
    color: var(--cs-ink);
}

/* 复制和删除只有先选中方块才有意义，没选中时压暗，选中后亮起来。
   注意是压暗不是 disabled：没选中时点它仍然会弹提示告诉用户先选一个，
   直接禁用的话点了毫无反应，反而更让人困惑 */
#cribsheet-duplicate-selected-btn,
#cribsheet-delete-selected-btn {
    opacity: 0.3;
    transition: opacity 0.15s ease, background-color 0.15s ease, color 0.15s ease;
}

#cribsheet-duplicate-selected-btn.cribsheet-btn-armed,
#cribsheet-delete-selected-btn.cribsheet-btn-armed {
    opacity: 1;
}

.cribsheet-text-btn {
    padding: 7px 12px;
    border: none;
    border-radius: 8px;
    background: none;
    color: var(--cs-ink-soft);
    cursor: pointer;
    font-size: 0.8rem;
    font-weight: 500;
    font-family: inherit;
    transition: background-color 0.15s ease, color 0.15s ease;
}

.cribsheet-text-btn:hover {
    background: var(--cs-surface);
    color: var(--cs-ink);
}

/* 分段控件：底槽是中性灰，选中那格浮起来一张白片 */
.cribsheet-orientation-toggle {
    display: inline-flex;
    padding: 3px;
    border-radius: 9px;
    background: var(--cs-surface);
}

.cribsheet-orientation-toggle button {
    padding: 6px 14px;
    border: none;
    border-radius: 7px;
    background: none;
    color: var(--cs-ink-soft);
    cursor: pointer;
    font-size: 0.79rem;
    font-weight: 500;
    font-family: inherit;
    transition: background-color 0.18s ease, color 0.18s ease, box-shadow 0.18s ease;
}

.cribsheet-orientation-toggle button.active {
    background: var(--cs-raised);
    color: var(--cs-ink);
    box-shadow: 0 1px 3px hsla(220, 30%, 20%, 0.1);
}

/* 所有可点元素统一的按下反馈：按下去缩一点，松开弹回来。
   浏览器默认的按钮按下没有任何反馈，加上之后点击才有「确实按到了」的手感 */
.cribsheet-pdf-btn:active,
.cribsheet-text-btn:active,
.cribsheet-icon-btn:active,
.cribsheet-add-custom-btn:active,
.cribsheet-orientation-toggle button:active,
.cribsheet-modal-cancel-btn:active,
.cribsheet-modal-next-btn:active {
    transform: scale(0.97);
}

/* 整个界面里唯一填色的按钮 */
.cribsheet-pdf-btn {
    display: inline-flex;
    align-items: center;
    gap: 7px;
    padding: 8px 16px;
    border: none;
    border-radius: 9px;
    background: var(--cs-accent);
    color: #ffffff;
    cursor: pointer;
    font-size: 0.82rem;
    font-weight: 600;
    font-family: inherit;
    box-shadow: 0 1px 2px hsla(214, 60%, 30%, 0.2);
    transition: background-color 0.15s ease, box-shadow 0.15s ease, transform 0.1s ease;
}

.cribsheet-pdf-btn:hover {
    background: hsl(214, 84%, 45%);
    box-shadow: 0 4px 14px hsla(214, 70%, 40%, 0.3);
    transform: translateY(-1px);
}

/* :active 的 scale 要盖过上面这条 translateY，写在后面靠源码顺序取胜 */
.cribsheet-pdf-btn:active {
    transform: scale(0.97);
}

/* ---------- 画布：工作台 + 纸 ---------- */
/* 纸不是直接贴在页面背景上的，底下有一块中性工作台，四周留出大片空白。
   这是「纸放在桌上」的隐喻，也是让纸成为视觉主角最直接的办法 */
.cribsheet-page-wrap {
    padding: 36px 28px;
    border-radius: 14px;
    background: var(--cs-surface);
    overflow-x: auto;
}

@media (max-width: 640px) {
    .cribsheet-page-wrap {
        padding: 20px 14px;
    }
}

/* 纸：整个界面里投影最重的东西，因为它是主角。
   宽高比锁死 US Letter，这里的尺寸只是编辑时的等比例预览 */
.cribsheet-page {
    position: relative;
    /* 尺寸用【英寸】写死，就是一整张物理 Letter 纸。
       这样打印时 8.5in 就是 8.5in，不需要任何缩放换算，
       屏幕上看到的就是印出来的——由构造保证，不依赖倍数算得对。
       （屏幕上按 CSS 标准 1in = 96px 渲染，也就是 816 × 1056px。）
       
       padding 0.4in 是页边距：真实打印机边缘有一圈印不到的区域，
       @page 的 margin 设成了 0，所以边距得由纸张自己提供 */
    width: 8.5in;
    min-height: 11in;   /* 一整页高;内容排版引擎已按此分页,超出也不裁(见 overflow) */
    flex-shrink: 0;
    margin: 0 auto;
    /* 这两行是"纸张是固定尺寸"的最后一道保障。
       只有 aspect-ratio 是不够的——内容比它高的时候元素照样会被撑长，
       aspect-ratio 只是个建议值。加上 overflow: hidden 之后，
       纸的比例和 Letter 严格一致，多出来的部分直接裁掉。
       真正防止"放到纸外面"的是 GridStack 的 maxRow（见 getCribsheetMaxRow） */
    overflow: visible;   /* 不裁:排版靠 JS 分页,万一略超也别丢内容 */
    padding: 0.4in;
    background: var(--cs-raised);
    border-radius: 3px;
    box-sizing: border-box;
    box-shadow:
        0 1px 2px hsla(220, 30%, 20%, 0.06),
        0 8px 28px hsla(220, 30%, 20%, 0.10);
    transition: width 0.35s cubic-bezier(0.4, 0, 0.2, 1),
                height 0.35s cubic-bezier(0.4, 0, 0.2, 1);
}

/* 横版就是把两个数对调：Letter 横放是 11in × 8.5in */
.cribsheet-page.landscape {
    width: 11in;
    min-height: 8.5in;
}

/* 拖动/缩放时浮出来的网格底。平时 opacity 是 0，纸就是白纸一张。
   用伪元素而不是给 .cribsheet-page 加背景图，是因为背景图的原点会算上 padding，
   跟真正的格子差 20px；伪元素直接 inset: 20px 就跟内容区严丝合缝了。
   这里的数值必须跟 GridStack 的配置对上，画歪了比不画更误导：
   竖线 = 12 列，所以每 100%/12 一条；横线 = cellHeight 28px；inset = 画布的 padding */
.cribsheet-page::before {
    content: '';
    position: absolute;
    inset: 20px;
    pointer-events: none;
    opacity: 0;
    transition: opacity 0.15s ease;
    z-index: 0;
    background-image:
        repeating-linear-gradient(to right, hsl(214, 40%, 88%) 0 1px, transparent 1px calc(100% / 24)),
        repeating-linear-gradient(to bottom, hsl(214, 30%, 92%) 0 1px, transparent 1px 28px);
}

.cribsheet-page.cribsheet-show-grid::before {
    opacity: 1;
}

/* 对齐参考线。颜色特意用洋红，跟蓝色的网格底区分开——
   网格是「能放到哪儿」，参考线是「现在跟谁对齐了」，两件事不该看起来一样。
   不加过渡：参考线要跟着鼠标即时出现和消失，慢一拍反而干扰判断 */
.cribsheet-guide-layer {
    position: absolute;
    inset: 20px;
    pointer-events: none;
    z-index: 3;
}

.cribsheet-guide {
    position: absolute;
    background: hsl(330, 60%, 72%);
}

.cribsheet-guide-v {
    top: 0;
    bottom: 0;
    width: 1px;
    margin-left: -0.5px;
}

.cribsheet-guide-h {
    left: 0;
    right: 0;
    height: 1px;
    margin-top: -0.5px;
}

/* 中线对齐用虚线区分：边对边是硬对齐，中线对齐是次一级的信息 */
.cribsheet-guide-center {
    background: none;
    opacity: 0.75;
}

.cribsheet-guide-center.cribsheet-guide-v {
    border-left: 1px dashed hsl(330, 60%, 72%);
}

.cribsheet-guide-center.cribsheet-guide-h {
    border-top: 1px dashed hsl(330, 60%, 72%);
}

.grid-stack {
    height: 100%;
    /* 卡片要盖在网格线上面 */
    position: relative;
    z-index: 1;
}

/* ---------- 空状态 ---------- */
/* 空屏是邀请用户动手的地方，不是道歉的地方，所以文案直接说下一步做什么 */
.cribsheet-canvas-empty {
    position: absolute;
    top: 50%;
    left: 50%;
    transform: translate(-50%, -50%);
    width: 78%;
    text-align: center;
    pointer-events: none;
}

.cribsheet-canvas-empty-icon {
    margin-bottom: 12px;
    color: var(--cs-line-strong);
    font-size: 1.9rem;
}

.cribsheet-canvas-empty-title {
    margin: 0 0 6px;
    color: var(--cs-ink-soft);
    font-size: 0.92rem;
    font-weight: 600;
}

.cribsheet-canvas-empty-subtext {
    margin: 0;
    color: var(--cs-ink-faint);
    font-size: 0.8rem;
    line-height: 1.6;
}

/* 行内的空提示（搜索无结果、尺寸预设为空），跟画布那个大空状态不是一回事 */
.cribsheet-empty-hint {
    margin: 8px 0;
    color: var(--cs-ink-faint);
    font-size: 0.82rem;
    text-align: center;
}

/* ---------- 纸上的笔记卡片 ---------- */
/* 卡片按「可以拿起来的实物」做：圆角、柔和投影、悬停微微抬起、拖动时整个浮起来。
   GridStack 默认只负责把方块摆对位置，视觉反馈全部是在这一层包上去的 */
.cribsheet-note-card {
    position: relative;
    width: 100%;
    height: 100%;
    padding: 12px 14px;
    border: 1px solid hsl(214, 52%, 88%);
    border-radius: 12px;
    background: hsl(214, 45%, 98.5%);
    box-sizing: border-box;
    overflow: hidden;
    cursor: grab;
    resize: none; /* 关掉浏览器原生的缩放手柄，避免跟 GridStack 自己的缩放逻辑打架 */
    /* 卡片自己定一个字号基准，标题和正文都用 em 跟着它走。
       这样样式面板上调一次字号，整张卡片一起缩放，不用分别设两处 */
    font-size: 0.78rem;
    box-shadow: 0 1px 2px hsla(214, 40%, 25%, 0.05);
    transition:
        background-color 0.18s ease,
        border-color 0.18s ease,
        box-shadow 0.2s cubic-bezier(0.4, 0, 0.2, 1),
        transform 0.2s cubic-bezier(0.4, 0, 0.2, 1);
}

/* 悬停时微微抬起。拖动进行中要排除掉，不然两个 transform 会打架 */
/* 悬停和选中长得一样：边框略深一档，外层托一层淡蓝阴影，卡片本身不动。
   阴影必须挂在 .grid-stack-item 上——卡片自己那层被 GridStack 的
   .grid-stack-item-content 裹着，overflow 会把外阴影裁掉，散不出来。
   选中跟「只是鼠标路过」的区别，靠的是上方浮出的样式工具条 */

/* ---------- GridStack 24 列的宽度/位置规则 ---------- */
/* 为什么需要手写这一段：
   GridStack 靠 CSS 规则渲染方块的宽度和横向位置，形如
     .gs-12 > .grid-stack-item[gs-w="4"] { width: 33.333% }
   而它自带的 gridstack.min.css 只覆盖到 12 列。
   我们把列数改成 24 之后，[gs-w="4"] 找不到对应规则，宽度就塌回最小值——
   表现是 gs-w=4 明明在 DOM 上，卡片却只有一列宽。
   （gridstack-extra.css 也不行，那个文件只覆盖 2-11 列。）

   所以这里按 1/24 为单位把 24 组规则都列出来。
   ⚠️ 改 CRIBSHEET_GRID_COLS 的话这一整段都要重新生成。 */
.grid-stack.gs-24 > .grid-stack-item[gs-w="1"] { width: 4.166667%; }
.grid-stack.gs-24 > .grid-stack-item[gs-w="2"] { width: 8.333333%; }
.grid-stack.gs-24 > .grid-stack-item[gs-w="3"] { width: 12.5%; }
.grid-stack.gs-24 > .grid-stack-item[gs-w="4"] { width: 16.666667%; }
.grid-stack.gs-24 > .grid-stack-item[gs-w="5"] { width: 20.833333%; }
.grid-stack.gs-24 > .grid-stack-item[gs-w="6"] { width: 25%; }
.grid-stack.gs-24 > .grid-stack-item[gs-w="7"] { width: 29.166667%; }
.grid-stack.gs-24 > .grid-stack-item[gs-w="8"] { width: 33.333333%; }
.grid-stack.gs-24 > .grid-stack-item[gs-w="9"] { width: 37.5%; }
.grid-stack.gs-24 > .grid-stack-item[gs-w="10"] { width: 41.666667%; }
.grid-stack.gs-24 > .grid-stack-item[gs-w="11"] { width: 45.833333%; }
.grid-stack.gs-24 > .grid-stack-item[gs-w="12"] { width: 50%; }
.grid-stack.gs-24 > .grid-stack-item[gs-w="13"] { width: 54.166667%; }
.grid-stack.gs-24 > .grid-stack-item[gs-w="14"] { width: 58.333333%; }
.grid-stack.gs-24 > .grid-stack-item[gs-w="15"] { width: 62.5%; }
.grid-stack.gs-24 > .grid-stack-item[gs-w="16"] { width: 66.666667%; }
.grid-stack.gs-24 > .grid-stack-item[gs-w="17"] { width: 70.833333%; }
.grid-stack.gs-24 > .grid-stack-item[gs-w="18"] { width: 75%; }
.grid-stack.gs-24 > .grid-stack-item[gs-w="19"] { width: 79.166667%; }
.grid-stack.gs-24 > .grid-stack-item[gs-w="20"] { width: 83.333333%; }
.grid-stack.gs-24 > .grid-stack-item[gs-w="21"] { width: 87.5%; }
.grid-stack.gs-24 > .grid-stack-item[gs-w="22"] { width: 91.666667%; }
.grid-stack.gs-24 > .grid-stack-item[gs-w="23"] { width: 95.833333%; }
.grid-stack.gs-24 > .grid-stack-item[gs-w="24"] { width: 100%; }

.grid-stack.gs-24 > .grid-stack-item[gs-x="1"] { left: 4.166667%; }
.grid-stack.gs-24 > .grid-stack-item[gs-x="2"] { left: 8.333333%; }
.grid-stack.gs-24 > .grid-stack-item[gs-x="3"] { left: 12.5%; }
.grid-stack.gs-24 > .grid-stack-item[gs-x="4"] { left: 16.666667%; }
.grid-stack.gs-24 > .grid-stack-item[gs-x="5"] { left: 20.833333%; }
.grid-stack.gs-24 > .grid-stack-item[gs-x="6"] { left: 25%; }
.grid-stack.gs-24 > .grid-stack-item[gs-x="7"] { left: 29.166667%; }
.grid-stack.gs-24 > .grid-stack-item[gs-x="8"] { left: 33.333333%; }
.grid-stack.gs-24 > .grid-stack-item[gs-x="9"] { left: 37.5%; }
.grid-stack.gs-24 > .grid-stack-item[gs-x="10"] { left: 41.666667%; }
.grid-stack.gs-24 > .grid-stack-item[gs-x="11"] { left: 45.833333%; }
.grid-stack.gs-24 > .grid-stack-item[gs-x="12"] { left: 50%; }
.grid-stack.gs-24 > .grid-stack-item[gs-x="13"] { left: 54.166667%; }
.grid-stack.gs-24 > .grid-stack-item[gs-x="14"] { left: 58.333333%; }
.grid-stack.gs-24 > .grid-stack-item[gs-x="15"] { left: 62.5%; }
.grid-stack.gs-24 > .grid-stack-item[gs-x="16"] { left: 66.666667%; }
.grid-stack.gs-24 > .grid-stack-item[gs-x="17"] { left: 70.833333%; }
.grid-stack.gs-24 > .grid-stack-item[gs-x="18"] { left: 75%; }
.grid-stack.gs-24 > .grid-stack-item[gs-x="19"] { left: 79.166667%; }
.grid-stack.gs-24 > .grid-stack-item[gs-x="20"] { left: 83.333333%; }
.grid-stack.gs-24 > .grid-stack-item[gs-x="21"] { left: 87.5%; }
.grid-stack.gs-24 > .grid-stack-item[gs-x="22"] { left: 91.666667%; }
.grid-stack.gs-24 > .grid-stack-item[gs-x="23"] { left: 95.833333%; }


/* GridStack 自己生成的那层 .grid-stack-item-content 带白色底色，
   而卡片是 12px 圆角——两者形状不一致，白底就会在卡片的四个圆角外
   露出直角的一小块，看起来像"卡片外面套了个方框"。
   这里让它跟卡片同圆角，白底和浅蓝细线就贴合成一体了。
   inset: 0 是为了消掉两层之间可能的错位，让白底正好等于卡片的大小 */
/* 关于"卡片四周那圈白框"：它不是任何元素的背景，而是纸张本身透出来的。
   GridStack 的 margin: 4 让这一层内缩 4px，于是 item 四周有一圈 4px 的空隙，
   透出白色的纸；而阴影原来挂在外层 .grid-stack-item 上，就绕在这圈空隙外面——
   「卡片 → 4px 白纸 → 蓝阴影」，看起来就是个带阴影的白框。
   
   解法是把阴影挪到卡片自己身上（见下面的 .cribsheet-note-card 相关规则），
   空隙还在但外面没有阴影包着，4px 就不会读成一个边框了。
   
   这里刻意【不】设 inset：那样虽然也能消掉空隙，但会覆盖 GridStack
   按 margin 算出的定位，右下角缩放手柄就抓不到了。
   overflow 必须是 visible：卡片的外阴影要能溢出这一层，否则又被裁掉 */
#cribsheet-grid .grid-stack-item > .grid-stack-item-content {
    background: none;
    border: none;
    box-shadow: none;
    overflow: visible;
}

/* 悬停和选中只加一层淡蓝阴影，卡片本身的底色和边框都不动。
   阴影挂在【卡片】上而不是外层 .grid-stack-item 上——挂外层的话
   它会绕在那圈 4px 空隙外面，空隙里透出的白纸就被框成了一个白边。
   代价是这层的 overflow 必须是 visible（上面已经设了），否则阴影被裁 */
.grid-stack-item:not(.ui-draggable-dragging):hover .cribsheet-note-card,
.grid-stack-item.cribsheet-item-selected .cribsheet-note-card {
    box-shadow: 0 2px 12px hsla(214, 80%, 55%, 0.26);
}

/* 悬停时标题再深一档，正文不动——让层级在「被注意到」的那一刻更明确 */
.grid-stack-item:hover .cribsheet-note-title {
    color: hsl(220, 30%, 11%);
}

/* 拖动中：整张卡浮起来——放大一点、投影拉长、透明度稍降，
   让人一眼看出「这张正拿在手上」，跟躺在纸上的那些区分开 */
.grid-stack-item.ui-draggable-dragging {
    z-index: 20;
}

.grid-stack-item.ui-draggable-dragging .cribsheet-note-card {
    transform: scale(1.03);
    opacity: 0.94;
    cursor: grabbing;
    border-color: hsl(214, 60%, 72%);
    box-shadow: 0 12px 32px hsla(214, 45%, 22%, 0.22);
}

/* 新加进画布的卡片有个落下的动画。
   只在用户真的添加时播——程序化重建画布（页面加载、撤销恢复）时
   isCribsheetRestoring 是 true，那时候几十张卡一起弹会很吵 */
@keyframes cribsheetCardDropIn {
    from {
        opacity: 0;
        transform: scale(0.92) translateY(-6px);
    }
    to {
        opacity: 1;
        transform: scale(1) translateY(0);
    }
}

.cribsheet-note-card.cribsheet-just-added {
    animation: cribsheetCardDropIn 0.22s cubic-bezier(0.34, 1.3, 0.64, 1);
}

/* 删除时先缩一点、淡出，再真正从 DOM 里拿掉。
   瞬间消失会让人怀疑是不是点错了，收一下才像是「确实删掉了」 */
@keyframes cribsheetCardRemove {
    from {
        opacity: 1;
        transform: scale(1);
    }
    to {
        opacity: 0;
        transform: scale(0.95);
    }
}

.cribsheet-note-card.cribsheet-removing {
    animation: cribsheetCardRemove 0.16s ease forwards;
    pointer-events: none;
}

/* 拖动时 GridStack 会在目标位置放一个占位块，标出松手后会落在哪。
   默认长相很朴素，这里包成一个虚线框的落点提示 */
@keyframes cribsheetPlaceholderIn {
    from {
        opacity: 0;
        transform: scale(0.97);
    }
    to {
        opacity: 1;
        transform: scale(1);
    }
}

.grid-stack-placeholder > .placeholder-content,
.grid-stack-placeholder .placeholder-content {
    border: 1px dashed hsl(214, 65%, 66%);
    border-radius: 12px;
    background: hsla(214, 80%, 60%, 0.08);
    box-shadow: none;
    animation: cribsheetPlaceholderIn 0.16s ease;
}

/* 选中态：Figma 那种精确的细描边，不要粗框。
   必须用 inset——画在元素外面的 outline / 外阴影会被 GridStack 那层
   .grid-stack-item-content 的 overflow 裁掉，根本看不见 */
.grid-stack-item {
    transition: box-shadow 0.18s ease;
}

/* 缩放手柄：平时完全不显示，只有选中之后才在右下角露出一个小方块，
   跟 Figma 的选中把手一样。感应区域仍然比看到的方块大，好抓 */
.grid-stack-item > .ui-resizable-handle {
    opacity: 0;
    transition: opacity 0.15s ease;
}

/* ---------- 纸张顶部的姓名 / 考试名 ---------- */
/* 纯粹是给打印用的填空，不存到任何地方——要打印时填一下，
   打完这张纸就完成任务了。所以既没有后端字段，也没碰 localStorage */
.cribsheet-sheet-header {
    display: flex;
    /* baseline 对齐的是 Name / Exam 两个字段里的文字基线。
       水印内部是 align-items: center 的 flex，没有可参与的基线，
       所以它单独用 align-self（见 .cribsheet-watermark 后面那条）
       跟字段的文字底部对齐 */
    align-items: baseline;
    gap: 18px;
    /* 不换行：换行会把纸张顶部撑高，进而挤掉网格能用的行数。
       宁可让字段窄一点，也不能让这一行变成两行 */
    flex-wrap: nowrap;
    margin-bottom: 10px;
}

/* 左上角的水印。刻意做得很淡——它是出处标记，不该跟内容抢注意力 */
.cribsheet-watermark {
    display: inline-flex;
    /* 用 center 让 logo 和 "Code 100" 这两个高度不同的东西自己对齐，
       水印整体再通过下面的 align-self 去跟两个字段的基线对齐 */
    align-items: center;
    gap: 6px;
    flex-shrink: 0;
    opacity: 0.5;
}

.cribsheet-watermark img {
    width: 26px;
    height: 26px;
    object-fit: contain;
}

/* 水印在表头里跟两个字段水平对齐。
   父级是 align-items: baseline，但水印内部是 center 对齐的 flex 容器，
   它自己的基线是最后一行文字——直接参与 baseline 会让 logo 沉下去。
   改成 center 之后水印整体居中，视觉上跟字段那一行齐平 */
.cribsheet-watermark {
    align-self: center;
}

.cribsheet-watermark span {
    font-size: 0.92rem;
    font-weight: 700;
    letter-spacing: -0.01em;
    color: hsl(220, 15%, 40%);
    white-space: nowrap;
}

.cribsheet-sheet-field {
    display: inline-flex;
    align-items: baseline;
    gap: 6px;
    min-width: 0;
}

/* 姓名比考试名长——名字的变化范围大得多，
   而考试名基本就是 "Test 1" / "Final" 这种短词 */
/* Name 只比 Exam 略宽一点。原来是 2:1，姓名栏占掉大半行显得太空 */
.cribsheet-field-name {
    flex: 1.25 1 0;
}

.cribsheet-field-exam {
    flex: 1 1 0;
}

.cribsheet-sheet-field-label {
    flex-shrink: 0;
    font-size: 0.66rem;
    font-weight: 700;
    text-transform: uppercase;
    letter-spacing: 0.06em;
    color: hsl(220, 10%, 55%);
}

/* 输入框做成一条下划线，不是方框——它在一张"纸"上，
   方框会让整张纸看起来像网页表单 */
.cribsheet-sheet-header input[type="text"] {
    flex: 1 1 auto;
    min-width: 0;
    border: none;
    border-bottom: 1px solid hsl(214, 40%, 82%);
    border-radius: 0;
    background: none;
    padding: 1px 2px 2px;
    font-family: inherit;
    font-size: 0.78rem;
    color: hsl(220, 25%, 15%);
    outline: none;
    transition: border-color 0.15s ease;
}

.cribsheet-sheet-header input[type="text"]:focus {
    border-bottom-color: hsl(214, 84%, 55%);
}

.cribsheet-sheet-header input[type="text"]::placeholder {
    color: hsl(220, 10%, 72%);
}

/* 缩放感应区：包住卡片的右下角。
   尺寸做大到 24px 是因为它是不可见的——看不见的东西必须好抓。
   z-index 要压过卡片，否则点到的是卡片、触发的是拖动而不是缩放 */
.grid-stack-item > .ui-resizable-se {
    width: 24px;
    height: 24px;
    right: 0;
    bottom: 0;
    z-index: 20;
    cursor: se-resize;
}

/* 标题和正文拉开层级：标题更重更深，正文退成次要信息。
   原来两者字重和颜色太接近，扫一眼分不出哪个是词条 */
.cribsheet-note-title {
    margin: 0 0 5px;
    font-weight: 650;
    font-size: 1.12em;
    line-height: 1.3;
    letter-spacing: -0.005em;
    color: hsl(220, 22%, 18%);
}

.cribsheet-note-content {
    margin: 0;
    font-size: 0.96em;
    line-height: 1.5;
    color: hsl(220, 10%, 38%);
    overflow: hidden;
    /* 笔记正文放在 <p> 里，默认会把换行和连续空格全折叠掉，多行代码会挤成一行。
       这是个写代码笔记的场景，缩进和换行都是有意义的，所以保留原样 */
    white-space: pre-wrap;
    word-break: break-word;
}

/* 删除按钮平时藏起来，鼠标移到卡片上才浮出来。
   它是偶尔用一次的操作，没必要一直跟正文抢注意力。
   注意卡片的右内边距没有跟着收窄——收了的话按钮一出现就会压住文字 */
.cribsheet-note-remove {
    position: absolute;
    top: 7px;
    right: 7px;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 18px;
    height: 18px;
    background: none;
    border: none;
    border-radius: 5px;
    color: var(--cs-ink-faint, hsl(220, 8%, 62%));
    cursor: pointer;
    font-size: 0.72rem;
    padding: 0;
    line-height: 1;
    opacity: 0;
    transition: opacity 0.15s ease, background-color 0.15s ease, color 0.15s ease;
}

.grid-stack-item:hover .cribsheet-note-remove,
.cribsheet-note-remove:focus-visible {
    opacity: 1;
}

.cribsheet-note-remove:hover {
    background: hsl(0, 75%, 96%);
    color: hsl(0, 65%, 52%);
}

/* ---------- 纸下面的自由文本笔记（阶段1 就有的功能） ---------- */
.cribsheet-my-notes-section {
    margin-top: 32px;
    padding-top: 28px;
    border-top: 1px solid var(--cs-line);
}

.cribsheet-my-notes-section h4 {
    margin: 0;
    color: var(--cs-ink);
    font-size: 0.95rem;
    font-weight: 600;
}

/* ---------- 自定义笔记 / 编辑笔记 的弹窗 ---------- */
.cribsheet-modal-backdrop {
    position: fixed;
    inset: 0;
    background: hsla(220, 30%, 12%, 0.45);
    display: flex;
    align-items: center;
    justify-content: center;
    z-index: 4000;
}

.cribsheet-modal {
    background: #ffffff;
    border-radius: 16px;
    padding: 28px;
    width: 380px;
    max-width: 90vw;
    box-shadow:
        0 1px 3px hsla(220, 30%, 20%, 0.1),
        0 18px 50px hsla(220, 30%, 15%, 0.22);
}

.cribsheet-modal h3 {
    margin: 0 0 18px;
    color: var(--cs-ink, hsl(220, 20%, 15%));
    font-size: 1.02rem;
    font-weight: 600;
    letter-spacing: -0.01em;
}

.cribsheet-modal label {
    display: block;
    font-size: 0.82rem;
    font-weight: 600;
    margin: 12px 0 6px;
}

.cribsheet-modal input[type="text"],
.cribsheet-modal input[type="number"],
.cribsheet-modal select,
.cribsheet-modal textarea {
    width: 100%;
    padding: 8px 10px;
    border: 1px solid var(--divider-color, #ddd);
    border-radius: 8px;
    font-size: 0.88rem;
    font-family: inherit;
    box-sizing: border-box;
}

/* AI 生成按钮:跟"Add Custom Note"同宽同位,但用实心强调色 + ✨ 图标，
   让它一眼是"生成"而不是"再加一张空白卡" */
.cribsheet-ai-generate-btn {
    width: 100%;
    margin-top: 8px;
    padding: 10px;
    border: none;
    border-radius: var(--cs-radius);
    background: hsl(212, 90%, 52%);
    color: #fff;
    cursor: pointer;
    font-size: 0.8rem;
    font-weight: 600;
    font-family: inherit;
    transition: background-color 0.15s ease, transform 0.15s ease;
}

.cribsheet-ai-generate-btn:hover {
    background: hsl(212, 90%, 46%);
}

.cribsheet-ai-generate-btn i {
    margin-right: 6px;
}

/* 生成过程/报错的提示行 */
.cribsheet-ai-status {
    min-height: 1.1em;
    margin: 12px 0 0;
    font-size: 0.82rem;
    color: var(--cs-ink-soft, #666);
}

.cribsheet-ai-status.is-error {
    color: hsl(0, 60%, 45%);
}

/* ---------- AI Cribsheet Assistant 向导 ---------- */
.cribsheet-assistant-modal { max-width: 460px; }

.cribsheet-assistant-note {
    margin: 0 0 12px;
    font-size: 0.85rem;
    color: var(--cs-ink-soft, #667);
    line-height: 1.45;
}

.cribsheet-check {
    display: flex;
    align-items: center;
    gap: 8px;
    margin: 6px 0;
    font-size: 0.88rem;
    font-weight: 500;
    cursor: pointer;
}
.cribsheet-check input { width: auto; }
.cribsheet-check-hint { color: hsl(220, 12%, 60%); font-weight: 400; font-size: 0.8rem; }

.cribsheet-static-field {
    padding: 8px 10px;
    border: 1px dashed hsl(220, 20%, 85%);
    border-radius: 8px;
    font-size: 0.88rem;
    color: hsl(220, 15%, 45%);
    background: hsl(220, 30%, 98%);
}

/* 推荐 topic 列表 */
.cribsheet-topic-list {
    display: flex;
    flex-direction: column;
    gap: 6px;
    max-height: 300px;
    overflow-y: auto;
    margin: 6px 0 10px;
}
.cribsheet-topic-row {
    display: flex;
    align-items: center;
    gap: 10px;
    padding: 7px 10px;
    border: 1px solid hsl(220, 20%, 90%);
    border-radius: 8px;
    background: #fff;
}
.cribsheet-topic-stars {
    flex-shrink: 0;
    color: hsl(38, 92%, 50%);
    font-size: 0.82rem;
    letter-spacing: 1px;
}
.cribsheet-topic-name {
    flex: 1 1 auto;
    font-size: 0.9rem;
    font-weight: 600;
    color: hsl(220, 25%, 22%);
}
.cribsheet-topic-remove {
    flex-shrink: 0;
    width: 22px;
    height: 22px;
    padding: 0;
    border: none;
    border-radius: 6px;
    background: hsl(0, 70%, 96%);
    color: hsl(0, 55%, 50%);
    font-size: 1rem;
    line-height: 1;
    cursor: pointer;
}
.cribsheet-topic-remove:hover { background: hsl(0, 75%, 92%); }

.cribsheet-topic-add {
    display: flex;
    gap: 8px;
    margin-bottom: 6px;
}
.cribsheet-topic-add input { flex: 1 1 auto; }

/* 生成中禁用态 */
.cribsheet-modal-next-btn:disabled {
    opacity: 0.55;
    cursor: not-allowed;
}

.cribsheet-size-hint {
    margin: 0 0 4px;
    font-size: 0.78rem;
    color: var(--weak-color, #888);
}

/* ---------- 编辑弹窗：浮在卡片旁边，不居中 ---------- */
/* 遮罩透明：挡住的话就看不到正在编辑的那张卡，而"能一边看卡片一边改"
   正是不居中的理由。仍然铺满视口，用来接住点击外部关闭。
   flex 居中要取消掉，位置由 JS 写成内联的 left/top */
.cribsheet-modal-backdrop-anchored {
    display: block;
    background: none;
}

.cribsheet-modal-anchored {
    position: fixed;
    /* 比居中那版小巧：宽度收窄、内距减小 */
    width: 300px;
    padding: 18px;
    border-radius: 14px;
    box-shadow:
        0 1px 3px hsla(220, 30%, 20%, 0.12),
        0 12px 32px hsla(220, 30%, 15%, 0.26);
}

.cribsheet-modal-anchored h3 {
    margin: 0 0 12px;
    font-size: 0.95rem;
}

.cribsheet-modal-anchored label {
    font-size: 0.76rem;
    margin: 10px 0 5px;
}

.cribsheet-modal-anchored input[type="text"],
.cribsheet-modal-anchored textarea {
    padding: 7px 9px;
    font-size: 0.84rem;
}

.cribsheet-modal-anchored .cribsheet-modal-actions {
    margin-top: 14px;
}

/* 编辑弹窗里的提示条：只有编辑"引用笔记库"的方块时才显示，
   告诉学生保存之后这条会变成自己的副本，公共笔记库不受影响 */
.cribsheet-edit-warning {
    margin: 0 0 2px;
    padding: 7px 9px;
    font-size: 0.72rem;
    line-height: 1.45;
    color: hsl(32, 55%, 26%);
    background: hsl(45, 92%, 95%);
    border-left: 3px solid hsl(38, 85%, 55%);
    border-radius: 6px;
}

.cribsheet-modal-actions {
    display: flex;
    justify-content: flex-end;
    gap: 10px;
    margin-top: 18px;
}

.cribsheet-modal-cancel-btn {
    padding: 8px 16px;
    border: 1px solid var(--divider-color, #ddd);
    border-radius: 8px;
    background: #ffffff;
    cursor: pointer;
    font-size: 0.85rem;
    margin-top: 12px;
    width: 100%;
}

.cribsheet-modal-actions .cribsheet-modal-cancel-btn {
    width: auto;
    margin-top: 0;
}

.cribsheet-modal-next-btn {
    padding: 8px 16px;
    border: none;
    border-radius: 8px;
    background: hsl(207, 70%, 50%);
    color: #ffffff;
    font-weight: 600;
    cursor: pointer;
    font-size: 0.85rem;
}

.cribsheet-modal-next-btn:hover {
    background: hsl(207, 70%, 42%);
}

/* ---------- 浮动样式工具条（选中方块时浮在它上方） ---------- */
/*
   放在 .cribsheet-page 内部用 absolute 定位。上一版是 position: fixed + 视口坐标，
   在这个页面上会跑到窗口外面去（祖先链上任何一个 transform/filter 都会让 fixed 失效，
   而站内其他 CSS 文件查不到）。改成相对纸张定位之后，坐标只需要算
   「选中方块相对纸左上角的位置」，不碰视口、不用监听滚动，从结构上就不会算错。

   它是覆盖层：不占文档流，出现和消失都不会推动画布里的任何东西。
   全程只有这一个实例，切换选中的方块时是平移过去，不是销毁重建——
   所以 left/top 上挂了过渡，换一张卡它会滑过去而不是闪一下。
*/
.cribsheet-style-bar {
    position: absolute;
    z-index: 8;
    display: flex;
    align-items: center;
    gap: 2px;
    padding: 5px 7px;
    border-radius: 999px;
    background: hsl(220, 8%, 13%);
    box-shadow:
        0 2px 6px hsla(220, 40%, 5%, 0.24),
        0 10px 28px hsla(220, 40%, 5%, 0.30);

    /* 隐藏态。用 visibility 而不是 display:none —— display:none 量不到自身宽高，
       没法算居中，而且没有过渡可言。延迟切换 visibility 是为了让淡出动画播完 */
    visibility: hidden;
    opacity: 0;
    transform: translateY(6px);
    transition:
        opacity 0.17s ease,
        transform 0.17s ease,
        left 0.18s cubic-bezier(0.4, 0, 0.2, 1),
        top 0.18s cubic-bezier(0.4, 0, 0.2, 1),
        visibility 0s linear 0.17s;
}

.cribsheet-style-bar.is-visible {
    visibility: visible;
    opacity: 1;
    transform: translateY(0);
    transition:
        opacity 0.17s ease,
        transform 0.17s ease,
        left 0.18s cubic-bezier(0.4, 0, 0.2, 1),
        top 0.18s cubic-bezier(0.4, 0, 0.2, 1),
        visibility 0s linear 0s;
}

/* 摆到方块下方时（上面放不下），淡入方向跟着翻过来，从上往下落 */
.cribsheet-style-bar.is-below {
    transform: translateY(-6px);
}

.cribsheet-style-bar.is-below.is-visible {
    transform: translateY(0);
}

/* 拖动方块的时候工具条要即时跟随，位置上的过渡会拖后腿，临时关掉 */
.cribsheet-style-bar.is-tracking {
    transition:
        opacity 0.17s ease,
        transform 0.17s ease,
        visibility 0s linear 0s;
}

.cribsheet-style-divider {
    width: 1px;
    height: 16px;
    margin: 0 4px;
    background: hsla(0, 0%, 100%, 0.14);
}

/* 无边框图标按钮，悬停才出一层很淡的底 */
.cribsheet-style-toggle {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 28px;
    height: 28px;
    border: none;
    border-radius: 999px;
    background: none;
    color: hsla(0, 0%, 100%, 0.72);
    cursor: pointer;
    font-size: 0.74rem;
    font-family: inherit;
    transition: background-color 0.14s ease, color 0.14s ease, transform 0.14s ease;
}

.cribsheet-style-toggle:hover {
    background: hsla(0, 0%, 100%, 0.12);
    color: #ffffff;
    transform: scale(1.05);
}

.cribsheet-style-toggle:active {
    transform: scale(0.95);
}

/* 开关状态必须一眼看得出来——加粗斜体这种，看不出当前是开是关就没法用 */
/* 删除按钮：平时跟别的图标一样是灰的，悬停才变红。
   一上来就红会让整条工具条的重心跑到删除上——它不是主要操作。
   放最右边并单独隔一道分隔线，是因为它是不可逆操作，
   不该跟改字号改颜色那些挨在一起没有区隔 */
.cribsheet-style-danger:hover {
    background: hsla(0, 70%, 55%, 0.2);
    color: hsl(0, 85%, 74%);
}

.cribsheet-style-toggle.active {
    background: #ffffff;
    color: hsl(220, 10%, 14%);
}

/* 字号：一个图标加一个紧凑的下拉 */
.cribsheet-style-size-wrap {
    display: inline-flex;
    align-items: center;
    gap: 4px;
    height: 28px;
    padding: 0 6px 0 8px;
    border-radius: 999px;
    color: hsla(0, 0%, 100%, 0.72);
    cursor: pointer;
    font-size: 0.72rem;
    transition: background-color 0.14s ease;
}

.cribsheet-style-size-wrap:hover {
    background: hsla(0, 0%, 100%, 0.12);
}

/* 字号输入框。宽度写死是因为 number 输入框在深色条上默认很宽，
   而这里只需要放两位数 */
.cribsheet-style-size-wrap input[type="number"] {
    width: 42px;
    border: none;
    background: none;
    color: #ffffff;
    font-size: 0.74rem;
    font-family: inherit;
    padding: 0;
    outline: none;
    text-align: center;
}

.cribsheet-style-size-wrap input[type="number"]::placeholder {
    color: hsla(0, 0%, 100%, 0.5);
}

/* 自动状态：框里显示的是实测出来的字号，置灰表示"这是自动值，不是你设的"。
   双击可以恢复到这个状态 */
.cribsheet-style-size-wrap input[type="number"].is-auto {
    color: hsla(0, 0%, 100%, 0.45);
}

/* 去掉右侧那对步进箭头：深色条上很难看，而且这里更常用的是直接打字。
   两条规则分别对应 WebKit 和 Firefox */
.cribsheet-style-size-wrap input[type="number"]::-webkit-outer-spin-button,
.cribsheet-style-size-wrap input[type="number"]::-webkit-inner-spin-button {
    -webkit-appearance: none;
    margin: 0;
}

.cribsheet-style-size-wrap input[type="number"] {
    -moz-appearance: textfield;
    appearance: textfield;
}

/* 颜色选择器：原生控件在深色条上很难看，用一个圆形色块盖住它，
   真正的 input 透明地铺在上面负责点击和取色 */
.cribsheet-style-swatch {
    position: relative;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 28px;
    height: 28px;
    border-radius: 999px;
    color: hsla(0, 0%, 100%, 0.72);
    cursor: pointer;
    font-size: 0.72rem;
    transition: background-color 0.14s ease, color 0.14s ease, transform 0.14s ease;
}

.cribsheet-style-swatch:hover {
    background: hsla(0, 0%, 100%, 0.12);
    color: #ffffff;
    transform: scale(1.05);
}

.cribsheet-style-swatch input[type="color"] {
    position: absolute;
    inset: 0;
    width: 100%;
    height: 100%;
    padding: 0;
    border: none;
    border-radius: 999px;
    background: none;
    opacity: 0;
    cursor: pointer;
}

/* ---------- 降低动态效果 ---------- */
/* 系统里开了「减少动态效果」的用户（前庭功能敏感、晕动症等）需要把位移和缩放去掉。
   这一段必须跟上面的交互样式同步维护——新加一个 transform 或 animation 就要在这里补一条 */
@media (prefers-reduced-motion: reduce) {
    .cribsheet-page,
    .cribsheet-page::before,
    .cribsheet-note-card,
    .cribsheet-note-remove,
    .grid-stack-item,
    .cribsheet-library-item,
    .cribsheet-library-item .fa-plus,
    .cribsheet-library-category-body,
    .cribsheet-library-category-chevron,
    .cribsheet-library-search,
    .cribsheet-add-custom-btn,
    .cribsheet-icon-btn,
    .cribsheet-text-btn,
    .cribsheet-orientation-toggle button,
    .cribsheet-pdf-btn,
    .cribsheet-style-bar,
    .cribsheet-style-toggle,
    .cribsheet-style-swatch,
    .cribsheet-style-size-wrap {
        transition: none !important;
    }

    /* 所有位移和缩放一律取消 */
    .qtype-pill,
    .cribsheet-library-item:hover,
    .cribsheet-library-item:active,
    .grid-stack-item.ui-draggable-dragging .cribsheet-note-card,
    .cribsheet-pdf-btn:hover,
    .cribsheet-pdf-btn:active,
    .cribsheet-text-btn:active,
    .cribsheet-icon-btn:active,
    .cribsheet-add-custom-btn:hover,
    .cribsheet-add-custom-btn:active,
    .cribsheet-orientation-toggle button:active,
    .cribsheet-modal-cancel-btn:active,
    .cribsheet-modal-next-btn:active,
    .cribsheet-style-toggle:hover,
    .cribsheet-style-toggle:active,
    .cribsheet-style-swatch:hover,
    .qtype-pill:hover,
    .qtype-pill:active,
    #testing-start-btn:hover:not(:disabled),
    #testing-submit-btn:hover:not(:disabled),
    #testing-start-btn:active:not(:disabled),
    #testing-submit-btn:active:not(:disabled),
    #testing-start-btn.is-leaving {
        transform: none;
    }

    #testing-questions.exam-questions-enter,
    .testing-header,
    .exam-analytics-grid,
    .grade-spinner,
    .exam-analytics-card.is-calculating .exam-stat-value,
    .exam-analytics-card.is-calculating .exam-compare-num,
    .exam-analytics-card.is-calculating .exam-compare-fill,
    .exam-analytics-grid.is-revealing .exam-analytics-card,
    .exam-analytics-grid.is-revealing .exam-readiness-fill,
    .exam-analytics-grid.is-revealing .exam-compare-fill {
        animation: none;
    }

    /* 折叠过渡也关掉，直接切换 */
    .exam-info-panel,
    .exam-analytics-grid,
    .exam-version-row,
    .exam-card-main,
    .exam-layout {
        transition: none;
    }

    .testing-header:not(.exam-in-progress):not(.exam-finished):hover {
        transform: none;
    }

    .cribsheet-note-card.cribsheet-just-added,
    .grid-stack-placeholder .placeholder-content {
        animation: none;
    }

    /* 删除动画是个例外，不能整个关掉：JS 是等这段时长走完才把节点从 DOM 里摘掉的，
       animation: none 之后 animation 时长归零但移除逻辑仍按固定延时执行，
       中间那段时间卡片会原样杵在那儿。所以这里保留时长，只把它变成纯淡出 */
    .cribsheet-note-card.cribsheet-removing {
        animation: cribsheetCardRemove 0.16s linear forwards;
    }

    /* 工具条的淡入淡出保留（不涉及位移，只是透明度），
       但位置切换的滑动去掉，直接跳到新位置 */
    .cribsheet-style-bar,
    .cribsheet-style-bar.is-visible {
        transition: opacity 0.15s ease, visibility 0s;
        transform: translateY(0);
    }
}

/* ---------- 打印专用：只留下 Cribsheet 那一页，其他所有东西都藏起来 ---------- */
@media print {
    /* 打印只留 Cribsheet 那张纸。做法是：把纸张【上方和周围】那些块 display: none，
       纸张本身留在原位不动，于是它自然落到纸的顶部。
    
       ⚠️ 这里绝对不能对 .test-section / .revision-view 写 display: block !important。
       之前那次就是这么干的，结果 Revision 首页的两张卡片、Marked Questions 的
       题型筛选、My Notes 的文本框全印了出来——那些板块平时靠 display: none 隐藏、
       只有 .active 才显示，一条 !important 把它们全打开了。
       什么都不动的话，非当前板块本来就是隐藏的。
    
       也不用 visibility: hidden：它只让元素看不见，占的版面空间还在，
       纸的上半部分会空出一大片、内容被挤到中间甚至下一页。 */

    /* 页面外壳 */
    #header-wrapper,
    .nav-bar,
    .section-switcher,
    #footer-container,
    #auth-modal-container,
    #mini-auth-banner,
    /* Cribsheet 界面里除画布之外的部分 */
    .revision-back-btn,
    .cribsheet-intro,
    .cribsheet-library-panel,
    .cribsheet-sheet-toolbar,
    .crib-sheet-actions,
    /* 只在编辑时有意义的辅助层 */
    .cribsheet-guide-layer,
    .cribsheet-note-remove,
    .cribsheet-empty-hint,
    .cribsheet-canvas-empty,
    .cribsheet-style-bar,
    .immersive-toggle {
        display: none !important;
    }

    /* 网格底那层渐变 */
    .cribsheet-page::before {
        display: none !important;
    }

    /* 从内容区到纸张之间的每一层：只清内外边距和装饰，【不动 display】。
       这些是屏幕上的工作台样式，印在纸上只会挤占版面 */
    .content,
    #revision-content,
    #revision-view-cribsheet,
    .cribsheet-builder-layout,
    .cribsheet-sheet-panel,
    .cribsheet-page-wrap {
        padding: 0 !important;
        margin: 0 !important;
        background: none !important;
        border: none !important;
        border-radius: 0 !important;
        box-shadow: none !important;
        overflow: visible !important;
        max-height: none !important;
        animation: none !important;
    }

    /* 纸张：只去掉屏幕上的装饰。
       ⚠️ 绝对不要动它的宽高——尺寸是用英寸写死的（8.5in × 11in），
       那本来就是一张物理 Letter 纸，改成 height: auto 会让它塌成
       只有姓名那一行的高度（卡片是绝对定位的、不贡献父元素高度），
       下面的卡片全被纸边界切掉 */
    #cribsheet-page {
        margin: 0 !important;
        background: none !important;
        border: none !important;
        border-radius: 0 !important;
        box-shadow: none !important;
        transition: none !important;
    }

    /* 姓名/考试名不能看出是表单控件：占位符要消失
       （否则会印出灰色的 "Your name"），下划线加深，字号放大 */
    .cribsheet-sheet-header input[type="text"] {
        border-bottom: 1px solid #000000 !important;
        color: #000000 !important;
        font-size: 0.95rem !important;
    }

    .cribsheet-sheet-header input[type="text"]::placeholder {
        color: transparent !important;
    }

    .cribsheet-sheet-field-label {
        color: #000000 !important;
    }

    /* 选中高亮和悬停投影都是编辑时的提示，印出来不该有。
       ⚠️ 只能去掉高亮，不能把卡片本身藏掉——早先这条选择器跟上面那些
       共用一条 display: none，结果打印时只要有方块处于选中状态，
       那张笔记就整个不会出现在 PDF 里 */
    .grid-stack-item.cribsheet-item-selected,
    .grid-stack-item .cribsheet-note-card {
        outline: none !important;
        box-shadow: none !important;
    }

    .grid-stack-item.cribsheet-item-selected .cribsheet-note-card {
        border-color: hsl(214, 52%, 88%) !important;
    }
}

.question-code,
.answer-code {
    white-space: pre-wrap;
    word-break: break-word;
    font-family: Consolas, Monaco, 'Courier New', monospace;
    font-size: 14px;
    line-height: 1.6;
    padding: 12px 16px;
    border-radius: 6px;
    margin: 8px 0 16px 0;
}

.question-code {
    background-color: var(--code-bg);
    color: var(--code-text);
}

.answer-code {
    background-color: var(--answer-bg);
    color: var(--answer-text);
    margin: 8px 0 0 0;
    display: none;
}

.answer-code.show {
    display: block;
}

.show-answer-btn {
    margin-top: 16px;
    padding: 6px 14px;
    border: none;
    border-radius: 8px;
    background-color: var(--blue);
    color: white;
    font-size: 0.9rem;
    cursor: pointer;
    transition: background-color 0.2s ease;
}

.show-answer-btn:hover {
    background-color: hsl(212, 100%, 34%);
}


/* 开考时题目淡入上移。延后 140ms 起步，等 Start 按钮先淡出让开位置——
   同时发生的话两个动画在同一块地方打架，看起来很乱。
   ⚠️ 播完必须由 JS 把这个 class 去掉：动画里有 transform，
   fill-mode 会让最后一帧的 transform 永久留在元素上，而任何非 none 的
   transform 都会让后代 position: fixed 相对它定位、不再相对视口 */
@keyframes examQuestionsEnter {
    from {
        opacity: 0;
        transform: translateY(14px);
    }
    to {
        opacity: 1;
        transform: translateY(0);
    }
}

#testing-questions.exam-questions-enter {
    animation: examQuestionsEnter 0.42s cubic-bezier(0.4, 0, 0.2, 1) 0.14s both;
}

/* ---------- Examination ---------- */
/* 版本切换单独一行，在考卷卡片上方。
   min-height 是关键：版本切换有三种情况会不显示——
   只有一个年份（JS 不加 .show）、准备阶段、考试阶段。
   不固定高度的话这一行会在 0 和 28px 之间跳，下面的考卷卡片
   跟着上下移动。写死之后卡片在任何状态下都停在同一个位置 */
.exam-version-row {
    max-width: 980px;
    margin: 22px auto 12px;
    min-height: 30px;
    transition: opacity 0.28s ease;
}

/* 准备阶段：淡出但【保留位置】。
   用 visibility 而不是 display: none——后者会让下面的卡片瞬间上移，
   而且 display 根本没法过渡 */
body.exam-ready-mode .exam-version-row {
    visibility: hidden;
    opacity: 0;
}

/* 单列堆叠：考卷卡片 → 说明 → 分析卡片 */
.exam-layout {
    display: flex;
    flex-direction: column;
    gap: 16px;
    max-width: 980px;
    margin: 0 auto 32px;
    transition: padding-top 0.45s cubic-bezier(0.4, 0, 0.2, 1);
}

/* 准备阶段上面的东西（页头、切换器、导航栏）全收起了，
   卡片会一路顶到屏幕最上边。留一段上内距，让它落在舒服的位置。
   跟着收起动画一起过渡，不是瞬间跳下来 */
body.exam-ready-mode .exam-layout {
    padding-top: 48px;
}

/* 卡片进场：淡入 + 轻微上移。
   最后一帧写成 transform: none 而不是 translateY(0)——
   非 none 的 transform 会让后代 position: fixed 相对它定位而不是相对视口 */
@keyframes examCardEnter {
    from {
        opacity: 0;
        transform: translateY(10px) scale(0.995);
    }
    to {
        opacity: 1;
        transform: none;
    }
}

/* ---------- 考卷卡片 ---------- */
/* 一条矮的横向条：左边"这是什么卷子"，右边"开始" */
.testing-header {
    display: flex;
    flex-direction: row;
    align-items: center;
    justify-content: space-between;
    gap: 24px;
    flex-wrap: wrap;
    padding: 20px 26px;
    background: #ffffff;
    border-radius: 18px;
    border: 1px solid hsla(220, 20%, 91%, 0.9);
    box-shadow:
        0 1px 2px hsla(220, 30%, 25%, 0.04),
        0 6px 20px hsla(220, 30%, 25%, 0.06);
    animation: examCardEnter 0.42s cubic-bezier(0.4, 0, 0.2, 1) both;
    transition: transform 0.25s ease, box-shadow 0.25s ease;
}

/* 悬停微微抬起。考试进行中不抬——那时候它是计时器条，
   鼠标扫过去跳一下很干扰 */
.testing-header:not(.exam-in-progress):not(.exam-finished):hover {
    transform: translateY(-3px);
    box-shadow:
        0 1px 2px hsla(220, 30%, 25%, 0.05),
        0 12px 30px hsla(220, 30%, 25%, 0.11);
}

.exam-card-main {
    display: flex;
    flex-direction: column;
    gap: 8px;
    min-width: 0;
}

/* 元信息一行紧凑排布——做成大方块会把卡片撑高，
   而这张卡的定位就是"矮" */
.exam-meta {
    display: flex;
    align-items: center;
    gap: 16px;
    flex-wrap: wrap;
}

.exam-meta-item {
    display: inline-flex;
    align-items: center;
    gap: 6px;
    font-size: 0.84rem;
    color: var(--weak-color, #6b7280);
    white-space: nowrap;
}

.exam-meta-item i {
    font-size: 0.78rem;
    opacity: 0.7;
}

.exam-meta-item strong {
    font-weight: 700;
    color: hsl(220, 25%, 25%);
}

/* 上次考过：对勾用绿色。前两项是"这张卷子是什么样"，
   这一项是"你跟它的关系"，性质不同，用颜色区分开 */
.exam-meta-attempt i {
    color: hsl(148, 55%, 40%);
    opacity: 1;
}

.exam-card-actions {
    display: flex;
    align-items: center;
    gap: 16px;
    flex-wrap: wrap;
}

/* 准备阶段的退路。默认不显示——第一步时页头和导航栏都还在，
   用不着它；只有收起了才需要 */
#exam-cancel-ready-btn {
    display: none;
    padding: 11px 18px;
    border: none;
    border-radius: 12px;
    background: none;
    color: var(--weak-color, #6b7280);
    font-family: inherit;
    font-size: 0.9rem;
    font-weight: 600;
    cursor: pointer;
    transition: background-color 0.15s ease, color 0.15s ease;
}

#exam-cancel-ready-btn:hover {
    background: hsl(220, 25%, 95%);
    color: hsl(220, 25%, 25%);
}

body.exam-ready-mode #exam-cancel-ready-btn {
    display: inline-block;
}

/* 考试真正开始之后就不该再有退路了 */
body.exam-focus-mode #exam-cancel-ready-btn {
    display: none;
}

.timer-group {
    display: flex;
    align-items: center;
    gap: 17px;
}

/* 开考前整组隐藏。那时候既没有在走的计时器，也没有可隐藏的东西，
   两个控件都没意义 */
.testing-header:not(.exam-in-progress):not(.exam-finished) .timer-group {
    display: none;
}

/* ---------- 分析卡片网格 ---------- */
/* 3 列。
   主卡（Your performance）通栏占满第一行，剩下三块并排在第二行。
   
   ⚠️ minmax 的值决定列数：980px 容器里 320px 只排得下 2 列，
   300px 才够 3 列。改这个值之前先算一遍 n*min + (n-1)*gap <= 980。
   
   考试前只显示两块，auto-fit 会把空轨道收掉、剩下两块拉伸填满，
   所以不需要为那种情况单独写规则。
   align-items: start 让高矮不一的卡片顶部对齐，不被拉成一样高 */
.exam-analytics-grid {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
    gap: 14px;
    align-items: start;
    /* 折叠用的属性。max-height 的展开值只是过渡终点，实际高度仍由内容决定 */
    max-height: 1200px;
    opacity: 1;
    overflow: hidden;
    transition: max-height 0.4s cubic-bezier(0.4, 0, 0.2, 1),
                opacity 0.3s ease;
    /* backwards 而不是 both：both 会让动画的最后一帧（opacity: 1）
       一直生效，而【动画的优先级高于普通声明】，
       下面 body.exam-ready-mode 里的 opacity: 0 就永远盖不过它，淡出不会发生。
       backwards 只在动画开始前应用首帧，播完就交还给普通声明 */
    animation: examCardEnter 0.42s cubic-bezier(0.4, 0, 0.2, 1) 0.14s backwards;
}

/* ---------- 主卡：这次的表现 ---------- */
/* Your performance 是这一场的结果，Previous attempts 只是背景参照。
   两块长得一样重的话，视线会在它们之间左右比较，分不出主次。
   
   位置比样式更能表达主次：主卡通栏占满一行摆在最上面，
   剩下三块排在下面，一眼就知道谁是主角 */
.exam-analytics-card.is-primary {
    grid-column: 1 / -1;
    background: linear-gradient(135deg, hsl(214, 60%, 98%), hsl(200, 55%, 98.5%));
    border-color: hsla(214, 55%, 86%, 0.9);
    box-shadow:
        0 1px 2px hsla(214, 40%, 40%, 0.05),
        0 6px 20px hsla(214, 40%, 40%, 0.07);
}

.exam-analytics-card.is-primary h3 {
    color: hsl(214, 45%, 45%);
}

/* 通栏之后横向空间充裕，数字放大一档 */
.is-primary .exam-stat-value {
    font-size: 1.75rem;
}

.is-primary .exam-stat-unit {
    font-size: 1rem;
}

.is-primary .exam-stat-label {
    font-size: 0.72rem;
}

.is-primary .exam-compare {
    border-top-color: hsla(214, 45%, 88%, 0.9);
}

.is-primary .exam-compare-track {
    background: hsla(214, 40%, 90%, 0.7);
}

/* ---------- 仅结算后显示的分析卡 ---------- */
/* Your performance 和 Previous attempts 只在交卷之后出现。
   考试前既没有本次成绩，"历次"里也不包含还没考的这一次，
   提前摆出来会让人误以为那是当前这场的数据。
   
   开关由 body.exam-result-mode 控制，交卷时加、重考/换卷子时清。
   
   用 display: none 而不是折叠动画：这两块是"从无到有"，
   出现时有专门的揭示动画（is-revealing），不需要额外的展开过渡 */
.exam-analytics-card.is-result-only {
    display: none;
}

body.exam-result-mode .exam-analytics-card.is-result-only {
    display: block;
}

/* ---------- 交卷后的揭示动画 ---------- */
/* 整块淡入上移，里面的进度条从 0 长出来。
   由 JS 在点 All Done!! 之后加上 .is-revealing 触发 */
@keyframes examRevealCard {
    from {
        opacity: 0;
        transform: translateY(12px);
    }
    to {
        opacity: 1;
        transform: none;   /* 不写 translateY(0)：非 none 的 transform 会让
                              后代 position: fixed 相对它定位而不是相对视口 */
    }
}

/* 进度条用 scaleX 而不是 width。
   width 没法从 0 过渡到"行内样式里写的那个百分比"——
   keyframes 里引用不到行内值。scaleX 从 0 到 1 效果一样，
   而且是合成层动画，比改 width 更省性能 */
@keyframes examRevealBar {
    from { transform: scaleX(0); }
    to   { transform: none; }
}

.exam-analytics-grid.is-revealing .exam-analytics-card {
    animation: examRevealCard 0.8s cubic-bezier(0.22, 1, 0.36, 1) both;
}

/* 四张卡依次登场，间隔 130ms。
   ⚠️ 间隔和时长要一起调：只拉长时长而不拉开间隔的话，
   四张卡会几乎同时在动，反而看不出"依次"的层次。
   
   ⚠️ 用 nth-child 是安全的：揭示动画只在交卷后播，那时候
   四块都是显示的，序号跟视觉顺序一致 */
.exam-analytics-grid.is-revealing .exam-analytics-card:nth-child(1) { animation-delay: 0s; }
.exam-analytics-grid.is-revealing .exam-analytics-card:nth-child(2) { animation-delay: 0.13s; }
.exam-analytics-grid.is-revealing .exam-analytics-card:nth-child(3) { animation-delay: 0.26s; }
.exam-analytics-grid.is-revealing .exam-analytics-card:nth-child(4) { animation-delay: 0.39s; }

.exam-analytics-grid.is-revealing .exam-readiness-fill,
.exam-analytics-grid.is-revealing .exam-compare-fill {
    transform-origin: left center;
    animation: examRevealBar 1s cubic-bezier(0.22, 1, 0.36, 1) both;
}

/* 条与条之间也错开，一条条长出来比一起长更有节奏。
   延迟在卡片自身的 0.2s 之后起步，等卡片先落位 */
.exam-analytics-grid.is-revealing .exam-readiness-list li:nth-child(1) .exam-readiness-fill { animation-delay: 0.34s; }
.exam-analytics-grid.is-revealing .exam-readiness-list li:nth-child(2) .exam-readiness-fill { animation-delay: 0.47s; }
.exam-analytics-grid.is-revealing .exam-readiness-list li:nth-child(3) .exam-readiness-fill { animation-delay: 0.60s; }
.exam-analytics-grid.is-revealing .exam-readiness-list li:nth-child(4) .exam-readiness-fill { animation-delay: 0.78s; }

.exam-analytics-grid.is-revealing .exam-compare-row:nth-child(1) .exam-compare-fill { animation-delay: 0.52s; }
.exam-analytics-grid.is-revealing .exam-compare-row:nth-child(2) .exam-compare-fill { animation-delay: 0.65s; }

/* ---------- 分析卡片（全部是假数据） ---------- */
/* ⚠️ 带 data-mock 的块里所有数字都是编的，由 JS 里的 EXAM_ANALYTICS_MOCK
   开关控制显隐，默认关。这个网站不判分，分数、排名、班级平均全都无从算起 */
.exam-analytics-card {
    /* 难度卡里的标签悬停提示会向上冒出卡片边界，不能被裁掉 */
    overflow: visible;
    padding: 18px 20px;
    border-radius: 16px;
    background: #ffffff;
    border: 1px solid hsla(220, 20%, 91%, 0.9);
    box-shadow: 0 1px 2px hsla(220, 30%, 25%, 0.04);
}

.exam-analytics-card h3 {
    margin: 0 0 14px;
    font-size: 0.74rem;
    font-weight: 700;
    text-transform: uppercase;
    letter-spacing: 0.06em;
    color: hsl(220, 10%, 50%);
}

.exam-analytics-note {
    margin: 12px 0 0;
    font-size: 0.76rem;
    line-height: 1.45;
    color: hsl(220, 8%, 58%);
}

/* 就绪度：四行进度条。原来只有一个总分数字，太空——
   拆成分项之后能看出"哪一块拖后腿" */
.exam-readiness-list {
    margin: 0;
    padding: 0;
    list-style: none;
    display: flex;
    flex-direction: column;
    gap: 9px;
}

.exam-readiness-list li {
    display: flex;
    align-items: center;
    gap: 10px;
}

/* Overall 单独隔一条线并加重，它是上面三项的汇总 */
.exam-readiness-list li.is-overall {
    margin-top: 4px;
    padding-top: 10px;
    border-top: 1px solid hsla(220, 20%, 92%, 0.9);
}

.exam-readiness-label {
    flex-shrink: 0;
    width: 62px;
    font-size: 0.74rem;
    font-weight: 600;
    color: hsl(220, 8%, 55%);
}

.is-overall .exam-readiness-label {
    color: hsl(220, 25%, 28%);
}

.exam-readiness-bar {
    flex: 1;
    height: 7px;
    border-radius: 999px;
    background: hsl(220, 25%, 93%);
    overflow: hidden;
}

.exam-readiness-fill {
    height: 100%;
    border-radius: 999px;
    background: linear-gradient(90deg, hsl(214, 84%, 58%), hsl(190, 75%, 52%));
}

.exam-readiness-num {
    flex-shrink: 0;
    width: 34px;
    text-align: right;
    font-size: 0.76rem;
    font-weight: 700;
    color: hsl(220, 25%, 25%);
}

.is-overall .exam-readiness-num {
    font-size: 0.86rem;
}

/* 成绩概览：三个统计块 */
.exam-stat-grid {
    display: grid;
    grid-template-columns: repeat(3, minmax(0, 1fr));
    gap: 10px;
}

.exam-stat {
    display: flex;
    flex-direction: column;
    gap: 3px;
    min-width: 0;
}

.exam-stat-value {
    font-size: 1.15rem;
    font-weight: 700;
    letter-spacing: -0.02em;
    color: hsl(220, 28%, 15%);
    white-space: nowrap;
}

.exam-stat-unit {
    font-size: 0.78rem;
    font-weight: 600;
}

.exam-stat-accent { color: hsl(148, 55%, 32%); }
.exam-stat-muted  { color: hsl(220, 10%, 55%); }

.exam-stat-label {
    font-size: 0.68rem;
    font-weight: 600;
    text-transform: uppercase;
    letter-spacing: 0.05em;
    color: hsl(220, 8%, 60%);
}

/* 对比条 */
.exam-compare {
    display: flex;
    flex-direction: column;
    gap: 8px;
    margin-top: 16px;
    padding-top: 14px;
    border-top: 1px solid hsla(220, 20%, 92%, 0.9);
}

.exam-compare-row {
    display: flex;
    align-items: center;
    gap: 10px;
}

.exam-compare-label {
    flex-shrink: 0;
    width: 38px;
    font-size: 0.72rem;
    font-weight: 600;
    color: hsl(220, 8%, 55%);
}

.exam-compare-track {
    flex: 1;
    height: 6px;
    border-radius: 999px;
    background: hsl(220, 25%, 94%);
    overflow: hidden;
}

.exam-compare-fill {
    height: 100%;
    border-radius: 999px;
    background: hsl(220, 12%, 72%);
}

.exam-compare-fill.is-you {
    background: hsl(214, 84%, 55%);
}

.exam-compare-num {
    flex-shrink: 0;
    width: 34px;
    text-align: right;
    font-size: 0.74rem;
    font-weight: 700;
    color: hsl(220, 25%, 25%);
}

/* 历次尝试：做成时间线。左边一条竖线 + 圆点，
   比单纯两行"日期 + 分数"更能看出"这是一次次累积的" */
.exam-attempts {
    margin: 0;
    padding: 0 4px 0 18px;
    list-style: none;
    display: flex;
    flex-direction: column;
    gap: 14px;
    position: relative;
    /* 历次尝试多了不让卡片无限长高：封顶后内部滚动。
       ~4 条以内正常显示，超出就在卡内滚，外面版式不被撑乱 */
    max-height: 240px;
    overflow-y: auto;
}

/* 竖线。用 ::before 画而不是给每个 li 加边框——
   那样最后一项下面会多出一截 */
.exam-attempts::before {
    content: '';
    position: absolute;
    left: 4px;
    top: 6px;
    bottom: 6px;
    width: 1px;
    background: hsla(220, 20%, 88%, 0.9);
}

.exam-attempts li {
    position: relative;
    display: flex;
    align-items: baseline;
    flex-wrap: wrap;
    gap: 8px;
}

.exam-attempts li::before {
    content: '';
    position: absolute;
    left: -18px;
    top: 6px;
    width: 9px;
    height: 9px;
    border-radius: 999px;
    background: #ffffff;
    border: 2px solid hsl(214, 55%, 78%);
}

/* 最新一次用实心蓝点标出来 */
.exam-attempts li:last-child::before {
    background: hsl(214, 84%, 55%);
    border-color: hsl(214, 84%, 55%);
}

.exam-attempt-no {
    font-size: 0.72rem;
    font-weight: 600;
    text-transform: uppercase;
    letter-spacing: 0.04em;
    color: hsl(220, 8%, 58%);
    width: 100%;
}

/* 参照信息压暗收小，让开视觉重心——主角是上面通栏的 Your performance。
   ⚠️ 这几个值一定要写在【这一条】里：我一度在文件前面另写了一条同名规则，
   被这条盖掉了，改了等于没改 */
.exam-attempt-score {
    font-size: 0.95rem;
    font-weight: 700;
    color: hsl(220, 20%, 35%);
}

.exam-attempt-date {
    font-size: 0.78rem;
    color: hsl(220, 8%, 58%);
}

/* 提升幅度，挂在最新那次后面 */
.exam-attempt-delta {
    display: inline-flex;
    align-items: center;
    gap: 4px;
    margin-left: auto;
    padding: 3px 9px;
    border-radius: 999px;
    background: hsl(148, 30%, 96%);
    color: hsl(148, 35%, 40%);
    font-size: 0.74rem;
    font-weight: 600;
}

.exam-attempt-delta i {
    font-size: 0.62rem;
}

/* 分数比上一次低时用红。默认那套绿色是"涨"的语义，
   直接套在"跌"上会把退步显示成好事 */
.exam-attempt-delta.is-down {
    background: hsl(4, 60%, 96%);
    color: hsl(4, 55%, 46%);
}

/* Scoring detail：两层"你 vs 所有人"，每层复用 .exam-compare-* 的对比条 */
.scoring-detail {
    display: flex;
    flex-direction: column;
    gap: 16px;
}

.scoring-layer-head {
    font-size: 0.72rem;
    font-weight: 700;
    text-transform: uppercase;
    letter-spacing: 0.05em;
    color: hsl(220, 10%, 50%);
    margin-bottom: 8px;
}

.scoring-layer .exam-compare-row + .exam-compare-row {
    margin-top: 6px;
}

/* 难度分析 */
.exam-difficulty-stars {
    display: flex;
    gap: 3px;
    color: hsl(38, 92%, 52%);
    font-size: 0.88rem;
}

/* ---------- 交卷后的难度评分中间屏 ---------- */
/* 交卷 →〔评分，可跳过〕→ 结果。遮罩样式对齐 cribsheet-modal 那套 */
.paper-rating-backdrop {
    position: fixed;
    inset: 0;
    background: hsla(220, 30%, 12%, 0.45);
    display: flex;
    align-items: center;
    justify-content: center;
    z-index: 4200;
    animation: paperRatingFade 0.18s ease;
}

@keyframes paperRatingFade {
    from { opacity: 0; }
    to { opacity: 1; }
}

.paper-rating-card {
    background: #ffffff;
    border-radius: 16px;
    padding: 28px 30px;
    width: 420px;
    max-width: 90vw;
    text-align: center;
    box-shadow:
        0 1px 3px hsla(220, 30%, 20%, 0.1),
        0 18px 50px hsla(220, 30%, 15%, 0.22);
}

.paper-rating-card h3 {
    margin: 0 0 8px;
    font-size: 1.15rem;
    font-weight: 700;
    color: hsl(220, 20%, 15%);
}

.paper-rating-sub {
    margin: 0 0 20px;
    font-size: 0.82rem;
    line-height: 1.4;
    color: hsl(220, 10%, 50%);
}

.paper-rating-stars {
    display: flex;
    justify-content: center;
    gap: 8px;
    margin-bottom: 24px;
}

.paper-rating-star {
    border: none;
    background: none;
    cursor: pointer;
    padding: 4px;
    font-size: 1.9rem;
    line-height: 1;
    color: hsl(220, 16%, 82%);            /* 未点亮：灰 */
    transition: color 0.12s ease, transform 0.12s ease;
}

.paper-rating-star:hover { transform: scale(1.12); }
.paper-rating-star.filled { color: hsl(38, 92%, 52%); }   /* 点亮：金 */

.paper-rating-star:focus-visible {
    outline: 2px solid hsl(210, 90%, 55%);
    outline-offset: 2px;
    border-radius: 6px;
}

.paper-rating-actions {
    display: flex;
    justify-content: center;
    gap: 12px;
}

.paper-rating-skip,
.paper-rating-submit {
    padding: 9px 22px;
    border-radius: 999px;
    font-size: 0.86rem;
    font-weight: 600;
    cursor: pointer;
    font-family: inherit;
}

.paper-rating-skip {
    border: 1px solid hsla(220, 20%, 85%, 0.9);
    background: #ffffff;
    color: hsl(220, 12%, 42%);
}

.paper-rating-skip:hover { background: hsl(220, 25%, 97%); }

.paper-rating-submit {
    border: none;
    background: hsl(210, 90%, 52%);
    color: #ffffff;
}

.paper-rating-submit:hover:not(:disabled) { background: hsl(210, 90%, 46%); }

.paper-rating-submit:disabled {
    opacity: 0.5;
    cursor: not-allowed;
}

.exam-topic-tags {
    display: flex;
    flex-wrap: wrap;
    gap: 6px;
    margin-top: 12px;
}

/* 小标题，用在卡片内部分段 */
.exam-subhead {
    margin: 14px 0 8px;
    font-size: 0.7rem;
    font-weight: 700;
    text-transform: uppercase;
    letter-spacing: 0.05em;
    color: hsl(220, 10%, 55%);
}

.exam-topic-tags span {
    position: relative;
    padding: 4px 10px;
    border-radius: 999px;
    background: hsl(220, 30%, 96%);
    border: 1px solid hsla(220, 20%, 90%, 0.9);
    font-size: 0.72rem;
    font-weight: 600;
    color: hsl(220, 15%, 40%);
    cursor: default;
    transition: background-color 0.15s ease, border-color 0.15s ease;
}

.exam-topic-tags span:hover {
    background: hsl(220, 35%, 93%);
    border-color: hsla(220, 25%, 82%, 0.9);
}

/* 悬停提示。内容写在 data-tip 上，用伪元素画——
   浏览器原生的 title 提示要等一秒才出来，而且样式没法控制 */
.exam-topic-tags span[data-tip]::after {
    content: attr(data-tip);
    position: absolute;
    left: 50%;
    bottom: calc(100% + 8px);
    transform: translateX(-50%);
    padding: 6px 10px;
    border-radius: 8px;
    background: hsl(220, 25%, 18%);
    color: #ffffff;
    font-size: 0.7rem;
    font-weight: 500;
    white-space: nowrap;
    opacity: 0;
    pointer-events: none;
    transition: opacity 0.18s ease;
    z-index: 5;
}

.exam-topic-tags span[data-tip]:hover::after {
    opacity: 1;
}

/* ---------- 说明面板 ---------- */
/* 次要信息：没有白底和阴影，只有一层很淡的底色和细边，
   分量明显轻于左边那张卡 */
/* 说明放最后、通栏。它是"开始之前该知道的规则"，读一次就够，
   所以分量最轻：没有白底和阴影，只有一层很淡的底色 */
/* ---------- 三个状态 ---------- */
/* 1. 默认      考卷卡片 + 四块分析；按钮是「Get Ready」
   2. 准备中    分析和版本切换收起，说明出现；按钮变成「Begin Examination」
   3. 考试中    说明也收起，卡片变成计时器条（body.exam-focus-mode）
   
   说明默认不显示，是因为一直摆在那儿会被当成装饰略过去。
   点了 Get Ready 之后它是页面上唯一的新内容，才真的会被读 */
/* 说明默认收起。用 max-height + opacity 而不是 display: none——
   display 没法过渡，切换时会硬闪一下。
   max-height 的展开值取一个够大的数即可，它只是过渡的终点，
   实际高度仍由内容决定 */
.exam-info-panel {
    max-height: 0;
    opacity: 0;
    padding-top: 0;
    padding-bottom: 0;
    border-width: 0;
    overflow: hidden;
    padding-left: 24px;
    padding-right: 24px;
    border-radius: 16px;
    background: hsla(220, 40%, 98%, 0.9);
    border-style: solid;
    border-color: hsla(220, 20%, 91%, 0.8);
    /* 这里【不写延迟】，所以退回第一步时说明立刻收起。
       进入准备阶段可以慢慢来（下面那条加了 0.22s 让它最后登场），
       退出应该干脆——用户点 Back 是想马上离开，等半秒会像卡了 */
    transition: max-height 0.4s cubic-bezier(0.4, 0, 0.2, 1),
                opacity 0.3s ease,
                padding 0.4s ease,
                border-width 0.4s ease;
}

/* 说明【延后】0.22s 再展开。
   进入准备阶段时有一堆东西在同时动：页头、切换器、导航栏、页脚收起，
   分析折叠。全挤在一起看起来就是"啪"一下。
   让收起的先走，说明等版面稳定了再登场，过程才读得出层次 */
body.exam-ready-mode .exam-info-panel {
    max-height: 420px;
    opacity: 1;
    padding-top: 20px;
    padding-bottom: 20px;
    border-width: 1px;
    transition: max-height 0.4s cubic-bezier(0.4, 0, 0.2, 1) 0.22s,
                opacity 0.35s ease 0.22s,
                padding 0.4s ease 0.22s,
                border-width 0.4s ease 0.22s;
}

/* 准备中：分析收起。那些数字在准备考试的时候没有意义，反而分散注意力。
   同样用 max-height 折叠，不用 display */

body.exam-ready-mode .exam-analytics-grid {
    max-height: 0;
    opacity: 0;
}


/* 通栏之后条目排成两列，避免四条挤在一行行地拉得很长。
   ⚠️ 原来这里有两条 .exam-info-panel ul：这条写 grid，另一条写
   display: flex + column，而那条在文件里更靠后、赢了——
   两列布局从来没生效过。合并之后只剩这一条 */
.exam-info-panel ul {
    margin: 0;
    padding: 0;
    list-style: none;
    display: flex;
    flex-direction: column;
    gap: 10px;
}

.exam-info-panel h3 {
    margin: 0 0 12px;
    font-size: 0.78rem;
    font-weight: 700;
    text-transform: uppercase;
    letter-spacing: 0.06em;
    color: hsl(220, 10%, 50%);
}

.exam-info-panel li {
    position: relative;
    padding-left: 16px;
    font-size: 0.85rem;
    line-height: 1.5;
    color: hsl(220, 12%, 38%);
}

/* 还没实现的条目：整条压暗，后面挂一个 Coming soon 角标。
   压暗是为了让它跟上面那几条（当前真实行为）一眼能分开 */
.exam-info-panel li.is-upcoming {
    color: hsl(220, 8%, 60%);
}

.upcoming-tag {
    display: inline-block;
    margin-left: 8px;
    padding: 2px 8px;
    border-radius: 999px;
    background: hsl(38, 92%, 94%);
    color: hsl(30, 80%, 38%);
    font-size: 0.66rem;
    font-weight: 700;
    text-transform: uppercase;
    letter-spacing: 0.04em;
    vertical-align: 1px;
}

.exam-info-panel li.is-upcoming::before {
    background: hsl(220, 12%, 78%);
}

/* 自己画项目符号：默认的 disc 位置和大小没法精确控制 */
.exam-info-panel li::before {
    content: '';
    position: absolute;
    left: 2px;
    top: 0.6em;
    width: 4px;
    height: 4px;
    border-radius: 999px;
    background: hsl(214, 50%, 70%);
}

/* ---------- 考试进行中 ---------- */
/* 标题区和说明面板一起收起，考卷卡片变成一条居中的计时器。
   body.exam-focus-mode 由 enterExamFocusMode() 加上 */
body.exam-focus-mode .exam-analytics-grid,
body.exam-focus-mode .exam-info-panel,
body.exam-focus-mode .exam-version-row {
    display: none;
}

body.exam-focus-mode .exam-layout {
    grid-template-columns: minmax(0, 1fr);
}

.testing-header h2 {
    margin: 0;
    font-size: 1.65rem;
    font-weight: 700;
    letter-spacing: -0.02em;
    color: hsl(220, 28%, 12%);
    transition: opacity 0.3s ease;
}

/* 考试中：整个左半边（标题 + 元信息）都收掉，只留计时器。
   元信息是"开考前判断要不要考这张卷"用的，考试中已经没有意义，
   留着反而会跟计时器挤成一组，计时器就不在正中间了 */
.testing-header.exam-in-progress .exam-card-main,
.testing-header.exam-in-progress #testing-start-btn {
    display: none;
}

/* 考试中：只剩计时器，居中 */
.testing-header.exam-in-progress {
    flex-direction: row;
    align-items: center;
    justify-content: center;
    padding: 20px 28px;
    animation: none;   /* 已经在页面上了，不该再播一次进场动画 */
}

/* 批改中：标题（Correction）和 Retake 按钮都在，所以两端对齐，
   跟默认状态一致。
   ⚠️ 原来这两个状态共用一条规则、都是 justify-content: center，
   那是为"只剩计时器"设计的；批改时内容不止一个，居中会让
   Correction 缩在中间、左边空出一大块 */
.testing-header.exam-finished {
    flex-direction: row;
    align-items: center;
    justify-content: space-between;
    padding: 20px 28px;
    animation: none;
}

/* 只剩计时器和 Hide Timer 之后，让这一组占满整行再自己居中——
   否则 flex 只是把这一小组摆在中间，视觉上仍然像是靠某一边 */
.testing-header.exam-in-progress .exam-card-actions {
    flex: 1 1 auto;
    justify-content: center;
}

/* 交卷后进入批改模式：h2 显示 "Correction"，改成绿色区分一下 */
.testing-header.exam-finished h2 {
    color: black;
}

/* 计时器有两个状态：
   开考前是一句静态的时长说明（小号、灰、非等宽），
   开考后 JS 会加上 .timing-active，才变成看起来在走的倒计时。
   这样"还没开始"和"正在考"一眼能分开 */
/* 开考前不显示——元信息里已经写了「110 minutes」，这里再来一个
   「110 min limit」是重复的。
   ⚠️ 只能隐藏，不能从 HTML 里删掉：这个元素就是计时器本体，
   开考后 JS 会给它加 .timing-active 并往里写倒计时。
   用 visibility 而不是 display: none，是为了让它开考时能平滑淡入——
   display 没法过渡 */
.testing-timer {
    visibility: hidden;
    opacity: 0;
    font-size: 0.84rem;
    font-weight: 500;
    color: var(--weak-color);
    transition: font-size 0.35s ease, color 0.35s ease,
                opacity 0.3s ease, visibility 0s linear 0.3s;
}

/* 开考后出现 */
/* 开考后：出现，并变成大号等宽的倒计时。
   合并成一条——原来这里有三条同名规则（我加的两条 + 原有的一条），
   font-size 被写了三遍，最后一条才生效，改的时候极容易改错地方 */
/* 开考后：出现，变成大号等宽的蓝色倒计时 */
.testing-timer.timing-active {
    visibility: visible;
    opacity: 1;
    font-family: Consolas, Monaco, 'Courier New', monospace;
    font-variant-numeric: tabular-nums;
    font-size: 1.6rem;
    font-weight: 700;
    color: var(--blue);
    transition: font-size 0.35s ease, color 0.35s ease,
                opacity 0.3s ease, visibility 0s;
}

/* Hide Timer 按钮同理：开考前没有可隐藏的东西，它自己也不该出现。
   JS 那边本来就把它设成 disabled，这里只是让它别占位置 */
.testing-header:not(.exam-in-progress):not(.exam-finished) #timer-toggle-btn {
    display: none;
}

.testing-timer.timer-hidden {
    opacity: 0;
    pointer-events: none;
}

.testing-timer.timer-warning-10 {
    color: hsl(35, 90%, 45%);
}

.testing-timer.timer-warning-5 {
    color: hsl(0, 75%, 45%);
    animation: timerPulse 1s ease-in-out infinite;
}

.testing-timer.timer-warning-1 {
    color: hsl(0, 80%, 40%);
    animation: timerPulse 0.6s ease-in-out infinite;
}

.testing-timer.timer-overtime {
    color: hsl(0, 75%, 45%);
}

.testing-timer.timer-finished {
    color: hsl(0, 0%, 25%);
}

@keyframes timerPulse {
    0%, 100% { opacity: 1; }
    50% { opacity: 0.5; }
}

#timer-toggle-btn {
    padding: 8px 16px;
    border: none;
    border-radius: 10px;
    background-color: hsl(0, 0%, 95%);
    color: var(--icon-color);
    font-size: 0.85rem;
    font-weight: 600;
    cursor: pointer;
    transition: background-color 0.3s ease, opacity 0.3s ease;
}

#timer-toggle-btn:hover:not(:disabled) {
    background-color: hsl(0, 0%, 87%);
}

#timer-toggle-btn:disabled {
    opacity: 0.5;
    cursor: not-allowed;
}


.exam-toast {
    position: fixed;
    top: 120px;
    right: 24px;
    background: #ffffff;
    border-left: 6px solid hsl(35, 90%, 45%);
    border-radius: 12px;
    padding: 18px 26px;
    max-width: 340px;
    font-size: 1.05rem;
    font-weight: 500;
    color: var(--icon-color);
    line-height: 1.6;
    box-shadow: 0 12px 32px rgba(0, 0, 0, 0.28);
    z-index: 1000;
    animation: examToastSlideIn 0.3s ease;
}

.exam-toast.exam-toast-hide {
    animation: examToastSlideOut 0.3s ease forwards;
}

@keyframes examToastSlideIn {
    from {
        opacity: 0;
        transform: translateX(24px);
    }
    to {
        opacity: 1;
        transform: translateX(0);
    }
}

@keyframes examToastSlideOut {
    from {
        opacity: 1;
        transform: translateX(0);
    }
    to {
        opacity: 0;
        transform: translateX(24px);
    }
}

/* 主操作按钮。整个页面上最重的那个动作，尺寸和反馈都要配得上：
   悬停时微微抬起并加深投影，按下时压回去 */
#testing-start-btn,
#testing-submit-btn {
    padding: 13px 26px;
    border: none;
    border-radius: 12px;
    background-color: var(--blue);
    color: white;
    font-size: 1rem;
    font-weight: 600;
    letter-spacing: -0.01em;
    cursor: pointer;
    box-shadow: 0 2px 8px hsla(212, 90%, 40%, 0.25);
    transition: background-color 0.25s ease, transform 0.18s ease,
                box-shadow 0.25s ease, padding 0.3s ease,
                font-size 0.3s ease, opacity 0.3s ease;
}

#testing-start-btn:hover:not(:disabled),
#testing-submit-btn:hover:not(:disabled) {
    background-color: hsl(212, 100%, 34%);
    transform: translateY(-2px);
    box-shadow: 0 6px 18px hsla(212, 90%, 40%, 0.32);
}

/* 开考时 Start 按钮淡出缩小。display 没法过渡，
   所以先加这个 class 播 180ms 的动画，播完 JS 再真正设 display: none。
   pointer-events 立刻关掉——动画期间不该还能点第二次 */
/* 开考时 Start 按钮淡出缩小。display 没法过渡，
   所以先加这个 class 播 180ms 的动画，播完 JS 再真正设 display: none。
   pointer-events 立刻关掉——动画期间不该还能点第二次 */
#testing-start-btn.is-leaving {
    opacity: 0;
    transform: scale(0.96);
    pointer-events: none;
    box-shadow: none;
}

/* 按下时回落并缩一点——反馈方向跟悬停相反，手感更实 */
#testing-start-btn:active:not(:disabled),
#testing-submit-btn:active:not(:disabled) {
    transform: translateY(0) scale(0.98);
    box-shadow: 0 2px 6px hsla(212, 90%, 40%, 0.25);
}

#testing-submit-btn {
    margin-top: 8px;
    margin-bottom: 48px;
}

#testing-start-btn:disabled,
#testing-submit-btn:disabled {
    opacity: 0.55;
    cursor: not-allowed;
    transform: scale(0.96);
}


#testing-back-to-top-btn,
#testing-retake-btn {
    padding: 10px 22px;
    border: 1px solid var(--divider-color);
    border-radius: 10px;
    background: #ffffff;
    color: var(--icon-color);
    font-size: 0.9rem;
    font-weight: 600;
    cursor: pointer;
    transition: background-color 0.2s ease;
}

#testing-back-to-top-btn:hover,
#testing-retake-btn:hover {
    background-color: hsl(0, 0%, 96%);
}

#testing-back-to-top-btn {
    display: block;
    margin: 32px auto 48px;
}
/* ==========================================================================
   文档式 Cribsheet（v2）—— 取代 GridStack 卡片画布
   一张流式多栏文档：AI 生成 / 手动编辑，密排、天然分页、打印稳
   ========================================================================== */
.cribsheet-doc-layout {
    display: flex;
    flex-direction: column;
    gap: 14px;
}

.cribsheet-doc-toolbar {
    display: flex;
    align-items: center;
    gap: 10px;
    flex-wrap: wrap;
    /* 横幅型:整条做成一根贯通的工具条,不是几颗散按钮 */
    padding: 10px 14px;
    background: hsl(220, 32%, 97%);
    border: 1px solid hsl(220, 22%, 90%);
    border-radius: 12px;
    margin-bottom: 16px;
}

.cribsheet-doc-toolbar-spacer { flex: 1 1 auto; }

.cribsheet-doc-tool-btn {
    padding: 8px 14px;
    border: 1px solid hsl(220, 15%, 85%);
    border-radius: 8px;
    background: #fff;
    color: hsl(220, 15%, 30%);
    font-size: 0.82rem;
    font-weight: 600;
    font-family: inherit;
    cursor: pointer;
    transition: background-color 0.15s ease, border-color 0.15s ease;
}
.cribsheet-doc-tool-btn:hover { background: hsl(220, 20%, 96%); border-color: hsl(220, 15%, 72%); }

.cribsheet-doc-status {
    font-size: 0.78rem;
    color: hsl(220, 10%, 55%);
    min-width: 60px;
    text-align: right;
}

/* 纸张。屏幕上是一张 Letter 宽的白纸，内容多了就往下长（= 打印时的第 2 页）。
   多栏用 column-width，纸多宽就自动排几栏 */
.cribsheet-doc {
    width: 816px;              /* 8.5in @96dpi */
    max-width: 100%;
    margin: 0 auto;
    min-height: 1056px;        /* 11in，至少一页高 */
    box-sizing: border-box;
    padding: 40px 44px;
    background: #fff;
    border: 1px solid hsl(220, 20%, 90%);
    border-radius: 6px;
    box-shadow: 0 4px 20px hsla(220, 30%, 25%, 0.10);
    color: #1c2530;
}
.cribsheet-doc.landscape {
    width: 1056px;             /* 11in */
    min-height: 816px;         /* 8.5in */
}

.cribsheet-doc-header {
    display: flex;
    align-items: center;   /* 品牌区和 Name/Exam 同一条中线,别再 flex-end 让大 logo 跟小字底对底 */
    gap: 24px;
    padding-bottom: 12px;
    margin-bottom: 14px;
    border-bottom: 2px solid hsl(220, 30%, 20%);
}
/* 顶部品牌区:做大、不淡化——这是要印在每个学生带进考场的小抄上的，顺带宣传。
   opacity 显式设 1，盖过 .cribsheet-watermark 基类的 0.5 */
.cribsheet-doc-header .cribsheet-watermark {
    display: flex;
    align-items: center;
    gap: 12px;
    opacity: 1;
    font-size: 2rem;          /* 放大到跟 logo 体量相称,原来 1.5rem 太小、跟 46px logo 不成比例 */
    font-weight: 800;
    line-height: 1;           /* 去掉行高留白,文字才能跟 logo 真正居中对齐 */
    letter-spacing: -0.01em;
    color: hsl(214, 84%, 44%);
    white-space: nowrap;
}
.cribsheet-doc-header .cribsheet-watermark img { width: 48px; height: 48px; }

/* Name / Exam:改成「Label: ____」——冒号 + 下划线，没有方框。
   屏幕上可编辑，打印时就是一条干净的横线，不带表单框感。 */
.cribsheet-doc-header .cribsheet-sheet-field {
    display: inline-flex;
    align-items: baseline;
    gap: 6px;
}
.cribsheet-doc-header .cribsheet-sheet-field-label {
    flex-shrink: 0;
    font-size: 0.82rem;
    font-weight: 700;
    text-transform: none;
    letter-spacing: 0;
    color: hsl(220, 20%, 30%);
}
.cribsheet-doc-header .cribsheet-sheet-field-label::after { content: ':'; }
.cribsheet-doc-header .cribsheet-sheet-field input[type="text"] {
    flex: 1 1 auto;
    min-width: 100px;
    border: none;
    border-bottom: 1px solid hsl(220, 25%, 55%);
    border-radius: 0;
    background: none;
    padding: 1px 3px 2px;
    font-family: inherit;
    font-size: 0.85rem;
    color: hsl(220, 25%, 15%);
    outline: none;
    transition: border-color 0.15s ease;
}
.cribsheet-doc-header .cribsheet-sheet-field input[type="text"]:focus {
    border-bottom-color: hsl(214, 84%, 55%);
}
.cribsheet-doc-header .cribsheet-sheet-field input[type="text"]::placeholder {
    color: hsl(220, 12%, 72%);
}

/* 多栏流式正文。column-width 让纸越宽栏越多；块不跨栏断开 */
.cribsheet-doc-body {
    column-width: 232px;
    column-gap: 22px;
    column-rule: 1px solid hsl(220, 20%, 92%);
}

.cribsheet-doc-block {
    break-inside: avoid;
    position: relative;
    margin: 0 0 12px;
    padding: 11px 13px;   /* 多点留白,看着舒服 */
    border: 1px solid hsl(220, 20%, 92%);
    border-radius: 8px;
    background: hsl(210, 40%, 99%);
}

/* 标题 / 头部也带上中文字体回退,理由同正文(打印时中文才不会空白) */
.cribsheet-doc-block-title,
.cribsheet-doc-header {
    font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto,
        "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei", "Noto Sans CJK SC", sans-serif;
}

/* 字号用 em:相对于栏容器的 font-size(= 排版引擎按密度设的 baseFont),
   这样密度滑块一拖,标题正文一起缩放,测量和渲染也一致 */
.cribsheet-doc-block-title {
    font-size: 1.08em;
    font-weight: 700;
    color: hsl(212, 45%, 32%);
    margin-bottom: 5px;
    letter-spacing: 0.01em;
    outline: none;
}
.cribsheet-doc-block-content {
    font-size: 0.95em;
    line-height: 1.55;   /* 行距松一点,代码分行后不挤 */
    white-space: pre-wrap;      /* 保留代码换行/缩进 */
    word-break: break-word;
    /* ⚠️ 等宽栈后面【必须】跟上中文字体回退。纯 monospace 通用族在打印/导出 PDF 时
       不一定回退到系统中文字体,中文会印成空白。显式列出 CJK 字体,屏幕和打印都稳。 */
    font-family: "SF Mono", Menlo, Consolas,
        "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei", "Noto Sans CJK SC", monospace;
    outline: none;
}

/* 空 contenteditable 的占位提示 */
.cribsheet-doc-block-title:empty::before,
.cribsheet-doc-block-content:empty::before {
    content: attr(data-placeholder);
    color: hsl(220, 12%, 68%);
}

/* 编辑时高亮当前块 */
.cribsheet-doc-block:focus-within {
    border-color: hsl(212, 70%, 70%);
    background: #fff;
}

/* 删除按钮：悬停块时才显；打印隐藏 */
.cribsheet-doc-block-del {
    position: absolute;
    top: 3px;
    right: 3px;
    width: 18px;
    height: 18px;
    padding: 0;
    border: none;
    border-radius: 4px;
    background: transparent;
    color: hsl(0, 45%, 55%);
    font-size: 0.7rem;
    cursor: pointer;
    opacity: 0;
    transition: opacity 0.12s ease, background-color 0.12s ease;
}
.cribsheet-doc-block:hover .cribsheet-doc-block-del { opacity: 1; }
.cribsheet-doc-block-del:hover { background: hsl(0, 70%, 95%); }

.cribsheet-doc-empty {
    margin: 40px 0;
    text-align: center;
    color: hsl(220, 12%, 60%);
    font-size: 0.9rem;
}

/* ---------- 多页排版引擎:容器 / 页 / 栏 / 控件 ---------- */
.cribsheet-doc-pages {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 28px;          /* 页与页之间的间距 */
}

.cribsheet-page-cols {
    display: flex;
    align-items: flex-start;
}
.cribsheet-page-col {
    flex: 1 1 0;
    min-width: 0;
}

/* 页码,纸右下角 */
.cribsheet-page-no {
    position: absolute;
    bottom: 0.18in;
    right: 0.4in;
    font-size: 0.62rem;
    color: hsl(220, 12%, 62%);
}

/* 每块的悬浮控制(上移/下移/删),打印隐藏 */
.cribsheet-doc-block-tools {
    position: absolute;
    top: 2px;
    right: 2px;
    display: flex;
    gap: 2px;
    opacity: 0;
    transition: opacity 0.12s ease;
}
.cribsheet-doc-block:hover .cribsheet-doc-block-tools,
.cribsheet-doc-block:focus-within .cribsheet-doc-block-tools { opacity: 1; }
.cribsheet-doc-block-tools button {
    width: 18px; height: 18px; padding: 0;
    border: none; border-radius: 4px;
    background: hsl(220, 30%, 94%);
    color: hsl(220, 20%, 40%);
    font-size: 0.6rem; cursor: pointer;
}
.cribsheet-doc-block-tools button:hover { background: hsl(220, 40%, 88%); }
.cribsheet-doc-block-tools button[data-act="del"] { color: hsl(0, 55%, 50%); }

/* 每块右下角的拖拽调大小手柄:悬停块时才显,打印隐藏 */
.cribsheet-doc-block-resize {
    position: absolute;
    right: 1px;
    bottom: 1px;
    width: 14px;
    height: 14px;
    cursor: nwse-resize;
    opacity: 0;
    transition: opacity 0.12s ease;
    /* 右下角两道斜线,像常见的 resize 抓手 */
    background:
        linear-gradient(135deg, transparent 0 45%, hsl(220, 20%, 60%) 45% 55%, transparent 55% 100%),
        linear-gradient(135deg, transparent 0 70%, hsl(220, 20%, 60%) 70% 80%, transparent 80% 100%);
    touch-action: none;
}
.cribsheet-doc-block:hover .cribsheet-doc-block-resize,
.cribsheet-doc-block.is-resizing .cribsheet-doc-block-resize { opacity: 0.8; }
.cribsheet-doc-block.is-resizing {
    outline: 2px solid hsl(212, 84%, 60%);
    outline-offset: -1px;
}

/* 分页器 */
.cribsheet-pager {
    display: flex;
    align-items: center;
    flex-wrap: wrap;
    gap: 6px;
    margin-bottom: 12px;
}
.cribsheet-pager-label { font-size: 0.78rem; color: var(--cs-ink-soft); margin-right: 4px; }
.cribsheet-pager-btn {
    min-width: 28px; height: 28px; padding: 0 8px;
    border: 1px solid hsl(220, 20%, 85%);
    border-radius: 7px; background: #fff;
    font-size: 0.8rem; font-weight: 600; cursor: pointer;
    color: hsl(220, 20%, 35%);
}
.cribsheet-pager-btn:hover { border-color: hsl(212, 70%, 60%); color: hsl(212, 70%, 45%); }

/* 密度滑块 */
.cribsheet-density-wrap {
    display: inline-flex;
    align-items: center;
    gap: 6px;
    color: hsl(220, 15%, 45%);
}
.cribsheet-density-wrap input[type="range"] { width: 90px; }

/* ---------- 打印:每张 .cribsheet-page 印成一张真纸 ---------- */
@media print {
    /* ⚠️ 空白页根因:祖先层带 transform / overflow / 高度上限,会把打印子树整页吞掉。
       这里清掉祖先链上这三样。【只碰 transform/overflow/height,不动 display】——
       动 display 会把隐藏的其它板块也印出来(见下面 3088 那段告诫)。
       JS 在 beforeprint 里还会再强清一遍 inline(见 stripCribsheetPrintClippers)。 */
    #content-container,
    #testing-content,
    .content,
    #revision-content,
    #revision-view-cribsheet,
    .cribsheet-doc-layout,
    .cribsheet-page-wrap,
    .cribsheet-doc-pages {
        transform: none !important;
        overflow: visible !important;
        height: auto !important;
        max-height: none !important;
    }

    /* 编辑用的控件不打印 */
    .cribsheet-doc-toolbar,
    .cribsheet-pager,
    .cribsheet-doc-block-tools,
    .cribsheet-doc-block-resize,
    .cribsheet-page-no,
    .cribsheet-doc-empty {
        display: none !important;
    }

    .cribsheet-doc-layout,
    .cribsheet-doc-layout .cribsheet-page-wrap,
    .cribsheet-doc-pages {
        display: block !important;
        padding: 0 !important;
        margin: 0 !important;
        gap: 0 !important;
        background: none !important;
    }

    /* 保留 .cribsheet-page 的 8.5in 宽 + 0.4in padding —— 屏幕上按这个排的,
       打印也按这个,所见即所得。@page margin 设 0,让纸的 padding 就是页边距(不叠加)。
       去掉投影/圆角,每页之后强制分页。 */
    .cribsheet-page {
        margin: 0 auto !important;
        box-shadow: none !important;
        border-radius: 0 !important;
        break-after: page;
        page-break-after: always;
    }
    .cribsheet-page:last-child {
        break-after: auto;
        page-break-after: auto;
    }
    .cribsheet-doc-block {
        break-inside: avoid;
        background: none !important;
    }

    @page { size: letter; margin: 0; }
}
