/*
 * Styles for the shared partials in backend/templates/components/ (issue #30),
 * re-skinned on the prototype in issue 132. One class prefix per partial:
 * .c-button ↔ components/button.html, and so on. Values come from tokens.css
 * only — never a literal colour, size, or radius here.
 *
 * That is a CONVENTION and not a gate. tests/test_stylesheets_use_tokens.py used
 * to check it and #216 deleted it with the rest of the appearance tests
 * (CLAUDE.md:「見た目のテストは書かない」), so a literal written here fails
 * nothing and is caught only in review. Comments across these stylesheets went on
 * naming that file for months after it stopped existing — which is worse than
 * silence, because it is read as cover.
 *
 * Contrast of every pair this skin renders, measured against the palette of
 * ADR-020 §Decision 3 (4.5:1 body text, 3:1 large text and control boundaries):
 *   table head --muted on white   4.76:1
 *   body text on a hovered row (--accent)       13.44:1
 *   hover rule --primary on --accent             4.75:1
 *   hover FILL against a row                1.09 / 1.04:1  — why the rule exists
 *   body text on a zebra row                    13.98:1
 *   secondary button label / hover label   14.63 / 5.17:1
 *   focus ring --primary on page / on white  4.94 / 5.17:1
 *   disabled label on --background          2.45:1  — exempt, see below
 */

/* Buttons — components/button.html, on the prototype's .btn-primary /
   .btn-outline. Its padding is 0.625rem 1.25rem; the 4px spacing scale reaches
   the 20px inline exactly and the 10px block only to 8px, and adding a 10px step
   for one rule buys less than it costs. */
.c-button {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: var(--space-xs);
  /* One step down from the shipped size (#216). These screens are dense tables
     and the controls around them were reading as the loudest thing on the page. */
  padding: var(--space-xs) var(--space-m);
  border: var(--border-width-thin) solid transparent;
  border-radius: var(--radius);
  font-size: var(--font-size-caption1);
  font-weight: var(--font-weight-medium);
  line-height: var(--line-height-body1);
  text-align: center;
  text-decoration: none;
  cursor: pointer;
  transition:
    background-color var(--duration-fast) ease,
    border-color var(--duration-fast) ease,
    color var(--duration-fast) ease;
}

.c-button--primary {
  background-color: var(--primary);
  color: var(--primary-foreground);
}

/* Hover and active share a step: the prototype's palette has one darker blue
   below --primary, not two. Whether the pressed state needs its own treatment is
   the button re-skin's call (issue 132), not a colour this file may invent. */
.c-button--primary:hover,
.c-button--primary:active {
  background-color: var(--primary-dark);
  color: var(--primary-foreground);
}

/* The prototype's .btn-outline: a white box on the light border, going blue on
   hover rather than grey. It took --border where a field took --border-strong,
   and that split was deliberate — WCAG 1.4.11 asks 3:1 of a boundary that
   IDENTIFIES a control, and a button is identified by its label and its padding.
   The split is gone as of 2026-08-23: the fields came down to --border too
   (see .c-field), so this rule is no longer the exception it explains. */
.c-button--secondary {
  border-color: var(--border);
  background-color: var(--card-bg);
  color: var(--foreground);
}

.c-button--secondary:hover {
  border-color: var(--primary-light);
  color: var(--primary);
}

/* The ramp has no darker danger step, so the destructive button is a tinted
   outline rather than a solid fill: --danger on --danger-bg is the
   pair tokens.css guarantees ≥5:1, and hover only strengthens the border. */
.c-button--danger {
  border-color: var(--danger-border);
  background-color: var(--danger-bg);
  color: var(--danger);
}

.c-button--danger:hover {
  border-color: var(--danger);
  color: var(--danger);
}

/* Disabled reads at 2.45:1 against --background, below the body floor and
   allowed to be: WCAG 1.4.3 exempts an inactive component from the contrast
   requirement. What it may not do is look enabled, so the fill and the label
   both move — the same treatment .c-pagination__link--disabled uses. */
.c-button[disabled],
.c-button--disabled {
  border-color: var(--border);
  background-color: var(--background);
  color: var(--foreground-disabled);
  cursor: not-allowed;
}

/* Form fields — components/form_field.html.
   The <form> element itself has no partial: its hx-* attributes belong to the
   screen that owns the action, and its layout is l-stack on all but a handful.
   A .c-form rule lived here for that job and no <form> ever wore it. */
.c-field {
  position: relative; /* anchors .c-field__tooltip */
  display: flex;
  flex-direction: column;
  gap: var(--space-xxs);
}

/* Caption-size, like the button label and the table cell (#216): a field's own
   name is the smallest thing in the control, and at body size it competed with
   the value the teacher is actually reading. */
.c-field__label {
  font-size: var(--font-size-caption1);
  line-height: var(--line-height-caption1);
  font-weight: var(--font-weight-medium);
}

/* An asterisk, not the word 必須 (PM, 2026-08-16). On a form where most fields
   are required, the word repeated down the column is louder than the labels it
   qualifies — the mark says the same thing in one character and lets the label
   stay the thing being read. The word itself stays in the accessibility tree
   (.u-visually-hidden beside it), because "*" alone is not a sentence a screen
   reader can hand over. */
.c-field__required {
  margin-inline-start: var(--space-xxs);
  color: var(--danger);
  font-size: var(--font-size-body1);
  line-height: 1;
}

/* Short-value fields (a year, a grade, a seat number — 4 characters or less):
   a full-width box reads as if long text was expected. fit-content keeps the
   FIELD narrow too, so a flex row places the next field right beside it. */
.c-field--compact {
  width: fit-content;
}

.c-field--compact input:not([type="checkbox"]):not([type="radio"]),
.c-field--compact select {
  max-width: calc(var(--space-xxxl) * 3);
}

/* The other end of the same idea: a field whose value is a NAME, not a number.
   A select sized to its content makes 「2学期期末」 and 「2026年度 第2回全統模試」
   two different widths in the same strip, so the row reflows every time the
   choice changes — and the longer name is the one that gets clipped. A floor,
   not a fixed width: a name longer than this still grows the box rather than
   being cut off. */
.c-field--wide select {
  min-width: calc(var(--space-xxxl) * 8);
}

/* One step down from the shipped padding, matched to .c-button (#216) — a field
   and the button that submits it sit in the same row and have to be the same
   height. The text stays at body size: a control's VALUE is what the teacher
   reads and checks, and shrinking that is where compact turns into cramped. */
/* The field's frame is the same hairline as everything else on the screen.

   This LOWERS a floor the project set on purpose, so it is written down rather
   than left to be discovered. WCAG 1.4.11 asks 3:1 of the boundary that
   identifies a control, and ADR-020 §Decision 3 drew these darker than the
   reference design for that reason; --border is 1.23:1 and does not meet it.

   PM's call, 2026-08-23, twice and after the alternative was put in front of
   them: 「viền mờ phải mờ giống như toggle hoặc là viền của bảng table grid」.
   The row it is about makes the case — four fields at 4.76:1 standing beside a
   segmented toggle at 1.23:1, one control louder than the rest of the screen put
   together. Softening the token to the lightest compliant grey (3.36:1) was
   tried first and read as no change at all.

   What survives of the old argument: the select keeps its arrow at
   --border-strong, so the one control whose frame was doing real identifying
   work still says "this opens" at 3:1. An <input> and a <textarea> now rest on
   their fill and their label alone. If that turns out to be too little on a
   screen full of them, the fix is a stronger FILL (--background inside the box),
   not a return to the dark edge — that was the thing being complained about. */
.c-field input:not([type="checkbox"]):not([type="radio"]),
.c-field select,
.c-field textarea {
  width: 100%;
  padding: var(--space-xs) var(--space-s);
  border: var(--border-width-thin) solid var(--border);
  border-radius: var(--radius);
  background-color: var(--card-bg);
  color: var(--foreground);
}

/* A native <select> (appearance: auto) sizes its closed box from the
   platform's own menulist metrics, not from CSS line-height — visibly
   shorter than an input or button with identical padding and border, so it
   never lines up beside them (#96). appearance: none hands sizing back to
   the box model; the arrow that comes with the native chrome has to be
   redrawn, so two small gradient triangles (no image asset, no literal
   colour/size — tokens only) stand in for it.

   取込プレビュー's column heading is a <select> outside .c-field, and it is
   listed HERE rather than given its own arrow: one ▼ in the app, stated once
   (PM, 2026-08-29). It owns its own size and colour; this owns the arrow and
   the room the arrow needs. */
.c-field select,
.c-grid__head-select {
  appearance: none;
  padding-inline-end: var(--space-xl);
  /* --border-strong, not --muted. The triangle is the same 8px on every field on
     every screen, so it is the one part of the control whose weight is fixed —
     and at --muted (4.76:1) it was reading as the darkest mark in a filter row
     whose toggle beside it is a 1.23:1 hairline (PM, 2026-08-23). It is an
     affordance rather than decoration — it is what says "this one opens" — so it
     keeps 1.4.11's 3:1: --border-strong is 3.36:1 on the field's own --card-bg.
     Same token as the box around it, which is the point: one control, one edge
     weight. */
  background-image:
    linear-gradient(45deg, transparent 50%, var(--border-strong) 50%),
    linear-gradient(135deg, var(--border-strong) 50%, transparent 50%);
  /* The two halves sit --space-s apart because that is the arrow's own width
     below; they have to abut exactly or the ▼ splits in two. */
  background-position:
    calc(100% - var(--space-m)) center,
    calc(100% - var(--space-xs)) center;
  background-size: var(--space-s) var(--space-s);
  background-repeat: no-repeat;
}

/* Sized to its value, the value ends exactly where the ▼ begins (「30分」 ran
   into the arrow). One step more end padding leaves a gap. */
.c-field--compact select {
  padding-inline-end: var(--space-xxl);
}

/* Disabled = shown, not editable (file-derived facts, locked filters); the
   arrow implies "pick one", so a disabled select drops it. */
.c-field input[disabled]:not([type="checkbox"]):not([type="radio"]),
.c-field select[disabled],
.c-field textarea[disabled] {
  border-color: var(--border);
  background-color: var(--background);
  color: var(--muted);
}

.c-field select[disabled],
.c-grid__head-select[disabled] {
  background-image: none;
}

.c-field--invalid input:not([type="checkbox"]):not([type="radio"]),
.c-field--invalid select,
.c-field--invalid textarea {
  border-color: var(--danger);
}

/* An error bubble anchored under its field — for errors that surface on an
   action (submit) rather than with the field list, e.g. the roster preview's
   学校コード check. Toggle with Alpine (x-show + x-cloak). */
.c-field__tooltip {
  position: absolute;
  z-index: 1;
  inset-block-start: 100%;
  inset-inline-start: var(--space-none);
  margin-block-start: var(--space-xs);
  padding: var(--space-s) var(--space-m);
  border: var(--border-width-thin) solid var(--danger-border);
  border-radius: var(--radius);
  background-color: var(--danger-bg);
  color: var(--danger);
  font-size: var(--font-size-caption1);
  line-height: var(--line-height-caption1);
  box-shadow: var(--shadow-overlay);
}

.c-field [aria-invalid="true"] {
  border-color: var(--danger);
}

.c-field__errors {
  display: flex;
  flex-direction: column;
  gap: var(--space-xs);
  color: var(--danger);
  font-size: var(--font-size-caption1);
  line-height: var(--line-height-caption1);
}

/* HTMX marks the element issuing a request with .htmx-request. We ship no htmx
   stylesheet (vendored JS only — ADR-006), so the indicator rule lives here. */
.c-indicator {
  visibility: hidden;
  color: var(--muted);
}

.htmx-request .c-indicator,
.htmx-request.c-indicator {
  visibility: visible;
}

/* A filter bar whose fields stand beside a .c-segmented rather than above a
   table: the control comes down to the segmented control's size, so the two read
   as one pair rather than as two unrelated boxes. Metrics only — nothing about
   the control's colour or its boundary is touched here.

   The arrow halves with the text. The shipped ▼ is two 8px triangles sized for a
   body-size box (.c-field select above); left beside 12px text it is the loudest
   thing on the row. Same recipe at half the step — the two halves still have to
   abut exactly, or the ▼ splits in two.

   Shared because two screens now pair a select with a segmented control: the
   dashboard's 志望校の集まり and 進路指導's 成績マトリクス. */
.c-filter-bar--compact .c-field select {
  padding-block: var(--space-xxs);
  padding-inline-end: var(--space-l);
  font-size: var(--font-size-caption1);
  line-height: var(--line-height-caption1);
  background-position:
    calc(100% - var(--space-s)) center,
    calc(100% - var(--space-xs)) center;
  background-size: var(--space-xs) var(--space-xs);
}

/* A scroll box that keeps its bar out of the way: nothing until the pointer or
   the keyboard is inside, then the app's own thin one. A bar standing along the
   edge of every box is ink that says nothing about the data, and one drawn over
   a chart or a sheet of coloured cells covers the thing it is next to.

   `scrollbar-gutter: stable` reserves the vertical gutter either way, so
   appearing on hover neither hides a value nor reflows the rows under the
   pointer. There is no such reserve for the horizontal bar — the standard has
   none — so a box that scrolls sideways grows by the bar's height on hover
   unless its own height is fixed.

   revert, not a length: tools/tailwind/src.css already sizes every scrollbar in
   the app, and restating the number here would leave two places to change it. */
.c-quiet-scroll {
  scrollbar-gutter: stable;
  scrollbar-width: none;
}

.c-quiet-scroll:hover,
.c-quiet-scroll:focus-within {
  scrollbar-width: thin;
}

.c-quiet-scroll::-webkit-scrollbar {
  width: 0;
  height: 0;
}

.c-quiet-scroll:hover::-webkit-scrollbar,
.c-quiet-scroll:focus-within::-webkit-scrollbar {
  width: revert;
  height: revert;
}

/* ── zoom button ─────────────────────────────────────────────────────────────
   紙の拡大の釦（`static/js/zoom_gesture.js` の釦側）。**窓の右下に留める**——紙と
   一緒に流れると、拡大したとき画面の外へ出る。置く先は `position: relative` の
   箱で、巻き取る窓そのものではない（巻き取る中身の外）。

   **釦は1つ、絵だけ**（PM 2026-09-21）。倍率の数字は出さない——読む人が決めるのは
   「大きくするか、戻すか」である。絵が状態を言う: 等倍なら expand、拡大中なら shrink。

   小論文の講評（`essays/detail.html`）で生まれ、出願書類の講評
   （`statements/review.html`）が2画面目として欲しがったのでここへ移した
   （2026-09-24、CLAUDE.md の「2画面目が欲しがったら共通化する」）。形は出願書類の側
   ——窓の角に近いほうが紙を隠さない。 */
.c-zoom {
  position: absolute;
  right: var(--space-m);
  bottom: var(--space-m);
  z-index: 1;
  display: flex;
  align-items: center;
  justify-content: center;
  inline-size: 2rem;
  block-size: 2rem;
  padding: 0;
  border: 1px solid var(--border);
  border-radius: var(--radius);
  /* 紙の字の上に乗るので、下が透けない地を敷く。 */
  background-color: var(--card-bg);
  color: var(--foreground);
  box-shadow: 0 1px 3px rgb(15 23 42 / 12%);
  cursor: pointer;
}

.c-zoom:hover {
  border-color: var(--primary-light);
  color: var(--primary);
}

/* 焦点は暈で。枠を足すと、釦の枠と合わせて1つの箱に枠が2つ出る
   （`tokens.css` の --select-halo・PM 2026-08-29）。 */
.c-zoom:focus-visible {
  outline: none;
  box-shadow:
    0 1px 3px rgb(15 23 42 / 12%),
    0 0 0 3px var(--select-halo);
}

/* A region's heading row: the title on one side, whatever leads out of the
   region on the other — a link, a set of selectors, or nothing. Shared because
   two screens draw it now: the dashboard's four regions and 進路指導's
   成績マトリクス. It lived in home.css until the second caller, which is the
   point at which a pattern moves here (CLAUDE.md promotion rule); left there it
   would have rendered the matrix's icon on a line of its own, because that
   stylesheet is only loaded on the landing screen. */
.c-region__header {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-m);
}

.c-region__title {
  display: flex;
  align-items: center;
  gap: var(--space-s);
  font-size: var(--font-size-subtitle1);
  line-height: var(--line-height-subtitle1);
  font-weight: var(--font-weight-strong);
}

/* Context for the heading, not part of it — a count, a 模試 name, the 在籍 of the
   class being read. Regular weight and muted, so the heading is still what the
   eye lands on. */
.c-region__caption {
  color: var(--muted);
  font-size: var(--font-size-body1);
  line-height: var(--line-height-body1);
  font-weight: var(--font-weight-regular);
}

/* The 偏差値 band ramp as fills — the seven steps of exams.analytics.SS_BUCKETS,
   diverging about 50 because 50 is a 偏差値's real midpoint (tokens.css records
   how each value was measured). .c-band-1 is 「40未満」 and .c-band-7 「65以上」,
   so the class number IS the bucket's position and a template can write it from
   a loop counter.

   Shared rather than owned by a screen because two now paint it: the dashboard's
   科目ごとの散らばり and 進路指導's 成績マトリクス. A colour that meant 「45〜50」 on
   one screen and something else on the other would be worse than no colour, and
   one name is what stops that.

   Fills only. Every one of them is measured against --foreground, which is the
   text that sits ON them, so a band is always safe to print a number on. */
.c-band-1 { background: var(--viz-band-1); }
.c-band-2 { background: var(--viz-band-2); }
.c-band-3 { background: var(--viz-band-3); }
.c-band-4 { background: var(--viz-band-4); }
.c-band-5 { background: var(--viz-band-5); }
.c-band-6 { background: var(--viz-band-6); }
.c-band-7 { background: var(--viz-band-7); }

/* The eighth state: no measurement. Not a colour on the ramp — a cell with no
   偏差値 in it has no position on a scale — but it still needs an opaque fill,
   because 成績マトリクス freezes some of these cells and a transparent one lets
   the rows slide visibly under it. Named here so every cell in that table wears
   exactly one band class and the template never has to branch. */
.c-band-none { background: var(--card-bg); }

/* The key for the ramp above. Wherever the bands are painted the reader needs to
   be told what 「45〜50」 looks like, so it travels with them — the dashboard's
   科目ごとの散らばり and 進路指導's 成績マトリクス both print it.

   A list and not a row of spans, because that is what it is: a screen reader
   then announces how many steps the scale has. */
.c-band-legend {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-s) var(--space-l);
  color: var(--muted);
  font-size: var(--font-size-caption1);
  line-height: var(--line-height-caption1);
  list-style: none;
  margin: var(--space-none);
  padding: var(--space-none);
}

.c-band-legend__key {
  display: inline-flex;
  align-items: center;
  gap: var(--space-xs);
}

.c-band-legend__swatch {
  display: inline-block;
  width: var(--space-xl);
  height: var(--space-m);
  border-radius: var(--radius);
}

/* ── 種別の色 ────────────────────────────────────────────────────────────────
   「これは何の日か」を7つに分ける色。tokens.css が値を持ち、ここが名前を配る。
   替えるのはこの2ファイルだけで、使う側は1つも hex を書かない。

   .c-kind--* は **色を運ぶだけ** のクラスで、見た目を持たない。札にも、点にも、
   帯にも、凡例の四角にも、表の見出しにも同じものを着せられる:

       <span class="c-kind c-kind--moshi">模試</span>      札
       <span class="c-kind-dot c-kind--moshi"></span>      印（凡例の丸）

   運ぶのは3つ。--kind-fill が地、--kind-ink がその上の字、--kind-mark が字を
   持たない印。**mark が fill と別なのは黄色1つのため**で、他の6つでは同じ値を
   指している（理由は tokens.css）。

   カレンダーの月表示は 3つ目を使わない——予定の札は地を薄めて字を載せるので
   （`calendar.css` の .cal-bar）、その薄め方はあちらが持つ。

   いまはカレンダーだけが使う。成績票が試験の種類で列を畳み、試験一覧が種類で
   絞るので、次に色が要るのはそこである——そのときこの名前を着せれば、模試の色は
   画面をまたいで1つのままになる。

   **色だけの札を作れない形にしてある。** .c-kind は中身の字を描く箱で、空にすると
   何も出ない。7色は色覚の上限を超えていて（tokens.css に測った数字がある）、
   字が無ければ種別を言えない組が残る。 */
.c-kind--apply {
  --kind-fill: var(--kind-apply);
  --kind-ink: var(--kind-apply-ink);
  --kind-mark: var(--kind-apply);
}
.c-kind--sit {
  --kind-fill: var(--kind-sit);
  --kind-ink: var(--kind-sit-ink);
  --kind-mark: var(--kind-sit);
}
.c-kind--teiki {
  --kind-fill: var(--kind-teiki);
  --kind-ink: var(--kind-teiki-ink);
  --kind-mark: var(--kind-teiki);
}
.c-kind--jitsuryoku {
  --kind-fill: var(--kind-jitsuryoku);
  --kind-ink: var(--kind-jitsuryoku-ink);
  --kind-mark: var(--kind-jitsuryoku-mark);
}
.c-kind--moshi {
  --kind-fill: var(--kind-moshi);
  --kind-ink: var(--kind-moshi-ink);
  --kind-mark: var(--kind-moshi);
}
.c-kind--school {
  --kind-fill: var(--kind-school);
  --kind-ink: var(--kind-school-ink);
  --kind-mark: var(--kind-school);
}

/* 生徒が書いたもの——自分の予定も活動記録もこの1つ（tokens.css）。 */
.c-kind--mine,
/* 学習タスクも生徒が打った字なので同じ色（PM 2026-09-18）。見分けは色ではなく、
   カレンダーの札の頭の輪が付ける（calendars/calendar.css）。 */
.c-kind--task {
  --kind-fill: var(--kind-mine);
  --kind-ink: var(--kind-mine-ink);
  --kind-mark: var(--kind-mine);
}

/* 札。**地が種別の色そのもの**で、字はその上に乗る——渡された7色は彩度が高く、
   白地の字にすると4つが 4.5:1 を切る。地にすれば7つとも floor を超えるので、
   hex を動かさずに済む（tokens.css に組ごとの実測値がある）。 */
.c-kind {
  display: inline-block;
  /* 4px と 11px は見本の実測値（Student Calendar.dc.html）。どちらもトークンの
     段の**間**にある——6px の角は 11px の字に対して丸すぎ、12px にすると札が
     題より大きくなる。段の外に出る値なので、ここに書いて理由を付けてある。 */
  border-radius: 4px;
  background: var(--kind-fill);
  padding: 1px 6px;
  color: var(--kind-ink);
  font-size: 11px;
  line-height: var(--line-height-caption1);
  font-weight: var(--font-weight-medium);
  white-space: nowrap;
}

/* 終わったもの。**色を捨てる**——過ぎた予定まで7色で塗ると、これから来るものが
   その中に埋もれる。字はそのまま残るので、何だったかは読める。 */
.c-kind--past {
  --kind-fill: var(--background);
  --kind-ink: var(--muted);
  --kind-mark: var(--muted);
}

/* 字の入らない印。凡例と、カレンダーの1マスが使う。**単独では意味を持たない**
   ——必ず、同じ種別の字が近くにある場所にだけ置く。 */
.c-kind-dot {
  display: inline-block;
  width: 5px;
  height: 5px;
  border-radius: var(--radius-circular);
  background: var(--kind-mark);
}

/* ── segmented choice ─────────────────────────────────────────────────────────
   A radio pair drawn as one control. Radios and not a two-option <select>: a
   binary choice one click away should not cost two, and both words stay
   readable instead of hiding behind the one that is current.

   The metrics are the reference layout's own (1px gap inside a 1px pad inside
   the card radius; caption text at medium weight; the chosen segment filled and
   lettered in --card-bg). The frame is the reference's --border too, since
   2026-08-23 — this file used to argue the opposite, that a form control's
   boundary owes 1.4.11 3:1 and --border reaches only 1.23:1. That is right about
   a text field and wrong about this one, and the rule below says why.

   It sat in home.css until 進路指導's 成績マトリクス asked for the same control,
   which is the second screen and so the point at which it moves here (CLAUDE.md
   promotion rule). */
/* A radio group inside a c-field: the fieldset carries no box of its own — the
   .c-segmented inside it is the control, and a border around a border is two. */
.c-field__group {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: var(--space-xxs);
  margin: var(--space-none);
  padding: var(--space-none);
  border: none;
}
/* A rendered <legend> sits outside the fieldset's flex box, so the gap above
   never reaches it. Without this the control hangs 2px higher under its legend
   than a select does under its label, and in one row the labels step. */
.c-field__group > .c-field__label {
  margin-bottom: var(--space-xxs);
}

/* The strip is as wide as its choices — stretched to the dialog it reads as an
   empty bar with three words parked at its left end. */
.c-field__group .c-segmented {
  max-width: 100%;
}

.c-segmented {
  display: flex;
  gap: var(--border-width-thin);
  padding: var(--border-width-thin);
  margin: var(--space-none);
  /* --border, not --border-strong. The strong grey is the boundary that has to
     IDENTIFY a form control on its own (WCAG 1.4.11, tokens.css) — true of a
     text field, whose frame is the only thing saying it can be typed in. This
     control says it differently and more loudly: one of its segments is a filled
     --primary block with a white label, at 5.17:1, and that is what makes it
     read as a chooser. The dark hairline was left doing a job already done, and
     it was the heaviest edge on screens whose every other line is faint (PM,
     2026-08-23). .c-tabs--segmented and the boxed .c-navtabs draw their frames
     the same way. */
  border: var(--border-width-thin) solid var(--border);
  border-radius: var(--radius);
  background: var(--background);
}

.c-segmented__option {
  /* Never narrower than its own label: the strip is a row of words, and a
     shrunk segment wraps 「要確認 30」 onto two lines inside a 20px-tall control. */
  flex: none;
  cursor: pointer;
}

/* The input stays in the accessibility tree and keeps its keyboard behaviour —
   hidden with opacity, never display:none, which would take it out of both. */
.c-segmented__option input {
  position: absolute;
  opacity: 0;
  pointer-events: none;
}

.c-segmented__option span {
  display: block;
  /* A segment is one word or two; it never wraps. Wrapped, the strip grows a
     second line and the thumb behind it sizes to a box the label no longer
     fills. */
  white-space: nowrap;
  /* ボタンと同じ背丈（PM, 2026-08-17）。切り替えはボタンと並んで置かれるもので、
     一段低いと「小さいほうは飾り」に見える。外枠の1pxぶんだけ内側を薄くして、
     .c-button と同じ高さに揃える。 */
  padding: calc(var(--space-xs) - var(--border-width-thin)) var(--space-m);
  /* The frame's radius less what the frame itself takes, so the filled segment
     follows the outer curve instead of cutting across it. */
  border-radius: calc(var(--radius) - var(--border-width-thin) * 2);
  color: var(--muted);
  font-size: var(--font-size-caption1);
  line-height: var(--line-height-body1);
  font-weight: var(--font-weight-medium);
}

/* Filled, not a white pill with a shadow: on a card that is already white the
   chosen segment has to be the strongest thing in the control, or the two
   options read as equally selected.

   --primary, not --foreground. It was the ink while the ink and the accent were
   one value; the two split on 2026-08-23 and this rule kept the wrong half, so
   the strip went on wearing the old navy while every other "this one is chosen"
   in the app — the tab, the pill, the primary button — had gone blue (PM). The
   argument above is unchanged: white on --primary is 5.17:1, and it is still
   the strongest thing in a control whose other segments are --muted on a faint
   fill. */
.c-segmented__option input:checked + span {
  background: var(--primary);
  color: var(--primary-foreground);
}

/* ── a group that GROWS in and out ───────────────────────────────────────
   A dialog whose fields depend on an answer (試験の作成's 種別: 模試 asks for two
   more) used to change height between one frame and the next, and a dialog
   centred in the viewport moves everything under the pointer when it does.

   `interpolate-size: allow-keywords` (below, on :root) is what lets `height`
   animate to and from `auto` — the browser measures, so this needs no
   ResizeObserver, no Web Animations and no magic number. A browser without it
   applies the same rules and simply arrives instantly: exactly today's
   behaviour, which is what makes this safe to use for a control that must work
   everywhere.

   The grid `0fr → 1fr` trick was tried first and collapses to nothing here: the
   wrapper is a flex item (.l-stack), so the fr track has no free space to take
   and resolves to 0.

   Alpine binds `is-open` rather than x-show, because x-show removes the group
   from the layout before it can shrink. `visibility` follows the height, so a
   closed group holds no tab stops. */
.c-collapse {
  height: 0;
  overflow: hidden;
  opacity: 0;
  visibility: hidden;
  /* A closed group is still a flex item, so the stack around it puts a gap on
     BOTH sides of something zero tall — 32px of air where 16 belongs. The
     negative margin gives one of the two back, and it travels with the height
     so the space opens as smoothly as the content does.

     --c-collapse-gap is the stack's own gap; a stack with a different rhythm
     sets it beside its gap and the two stay in step. */
  --c-collapse-gap: var(--space-l);
  margin-block-start: calc(var(--c-collapse-gap) * -1);
  /* Same drop as everything else that settles into place (tokens.css). */
  transition:
    height var(--duration-drop) var(--ease-drop),
    margin-block-start var(--duration-drop) var(--ease-drop),
    opacity var(--duration-fast) linear,
    visibility 0s linear var(--duration-drop);
}
.c-collapse.is-open {
  height: auto;
  margin-block-start: var(--space-none);
  opacity: 1;
  visibility: visible;
  transition:
    height var(--duration-drop) var(--ease-drop),
    margin-block-start var(--duration-drop) var(--ease-drop),
    opacity var(--duration-fast) linear,
    visibility 0s;
}
@media (prefers-reduced-motion: reduce) {
  .c-collapse {
    transition: none;
  }
}

/* ── the thumb: one fill that MOVES ──────────────────────────────────────
   Switching used to be two events — a fill disappearing here, another appearing
   there — and the eye has to find the new one. This is the same idea as a
   shared-layout tab (motion-primitives' Day/Week/Month/Year): a single element
   travels from the old segment to the new one, so the eye is carried to it.

   static/js/segmented.js measures the checked label and writes --seg-x / --seg-w
   / --seg-h here; it also adds `is-sliding`, which is what hands the fill over
   from the span to the thumb. Without JavaScript the class never lands and the
   plain filled segment above is what shows — the control keeps working. */
.c-segmented.is-sliding {
  position: relative;
}
.c-segmented.is-sliding .c-segmented__option input:checked + span {
  background: none;
  /* Above the thumb, and the thumb is what it is read against. */
  position: relative;
  z-index: 1;
}
.c-segmented__thumb {
  position: absolute;
  inset-block-start: var(--border-width-thin);
  inset-inline-start: 0;
  width: var(--seg-w, 0);
  height: var(--seg-h, 0);
  border-radius: calc(var(--radius) - var(--border-width-thin) * 2);
  /* The same fill the un-slid segment above wears — this element only takes over
     the job of drawing it, so the two must never be different colours. */
  background: var(--primary);
  transform: translate3d(var(--seg-x, 0), 0, 0);
  /* Fast out, settle in — the curve of something that was pushed and stopped,
     not of something fading. Width travels with it so a short label does not
     wear a wide fill on the way. */
  transition:
    transform var(--duration-drop) var(--ease-drop),
    width var(--duration-drop) var(--ease-drop);
  pointer-events: none;
}
/* First paint, a font landing, the dialog reflowing: the thumb has to BE there,
   not travel there — nobody asked for that movement. */
.c-segmented.is-jumping .c-segmented__thumb {
  transition: none;
}
@media (prefers-reduced-motion: reduce) {
  .c-segmented__thumb {
    transition: none;
  }
}

/* The focus ring has to be drawn by the label, because the input it belongs to
   is the invisible one. */
.c-segmented__option input:focus-visible + span {
  outline: var(--space-xxs) solid var(--primary);
  outline-offset: var(--space-xxs);
}


/* Tables — components/table.html. Compact rows and zebra striping: teachers
   read these tables like Excel sheets, and the alternating band keeps long
   rows traceable. */
/* The frame (border + radius) lives on the scroll wrapper: the scroll box
   clips the table's square corners against the radius, and wide tables pan
   horizontally inside it. The prototype puts its tables inside a .card, so the
   wrapper IS that card — white, --radius, the two-layer card shadow.

   .c-table stays the five-row summary inside a card and .c-grid__scroll stays
   the list SCREEN, but the two no longer draw differently: the grid took this
   frame's corner on 2026-08-23. What still separates them is the shadow — a
   summary is a card sitting on the page, a list screen IS the page. */
.c-table__scroll {
  overflow-x: auto;
  border: var(--border-width-thin) solid var(--border);
  border-radius: var(--radius);
  background-color: var(--card-bg);
  box-shadow: var(--shadow-card);
}

.c-table {
  width: 100%;
}

.c-table tbody tr:nth-child(even) {
  background-color: var(--background);
}

/* Hover after the banding on purpose: the two selectors weigh the same, so
   source order is what lets a hovered even row still light up.

   The fill alone does not carry the state. Measured, --accent is 1.09:1 against a
   white row and 1.04:1 against a banded one — the prototype never hit this because
   it dropped zebra, and zebra is what the PM kept (2026-08-04). The inset rule is
   what makes the hovered row findable on both bands: --primary against --accent is
   4.75:1, and it does not depend on telling two near-identical fills apart. */
.c-table tbody tr:hover {
  background-color: var(--accent);
  box-shadow: inset var(--border-width-thick) 0 0 0 var(--primary);
}

/* The wrapper's frame already closes the bottom edge. */
.c-table tbody tr:last-child .c-table__cell {
  border-bottom: none;
}

/* The caption stays the table's accessible name, but the screen heading and
   the pagination bar's 合計 carry the visible context — showing it a second
   time above the header read as stray text. Same recipe as .u-visually-hidden. */
.c-table__caption {
  position: absolute;
  width: var(--border-width-thin);
  height: var(--border-width-thin);
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
}

/* The prototype's cells are py-3 (12px); these stay a step tighter, because
   these screens are rosters and score tables and the PM's call on 2026-08-04 was
   density over fidelity. 8px is the scale's next step up from the 4px this
   carried before, which was tighter than either. */
.c-table__cell {
  padding: var(--space-s) var(--space-m);
  border-bottom: var(--border-width-thin) solid var(--border);
  text-align: start;
  white-space: nowrap;
}

/* A column of figures — 得点 on 試験詳細. tabular-nums so the digits sit in
   columns down the page (a proportional 1 is narrower than a 0, and a class of
   forty scores then reads as a ragged edge); start-aligned like every other
   cell, because the neighbouring 状態 and 操作 are text and a lone right-aligned
   column would pull the eye across the gap. */
.c-table__cell--numeric {
  font-variant-numeric: tabular-nums;
}

/* 得点 / 満点 — two boxes so the slash never moves. Widths are in ch, which with
   tabular-nums IS the width of one digit: 3 digits for the mark (a paper worth
   more than 999 points does not exist), plus the point and its one decimal, and
   3 for the 満点. Right-aligned then left-aligned around the slash, because what
   the eye follows down a column of marks is where the numbers END, not where
   they start. */
.c-score__value {
  display: inline-block;
  min-width: 4.5ch;
  text-align: right;
}

.c-score__total {
  display: inline-block;
  min-width: 3ch;
  text-align: start;
}

/* The head is the card's own white with muted text and a rule under it, as in
   the prototype. It cannot take the prototype's --background fill: that is the
   colour of every second body row here (zebra survives — PM, 2026-08-04), and
   the head would disappear into the banding. */
.c-table thead .c-table__cell {
  background-color: var(--card-bg);
  border-bottom-color: var(--border);
  color: var(--muted);
  font-weight: var(--font-weight-strong);
}

/* A value that MOVED — 変更前 → 変更後 in 志望校の変更履歴. The same --danger-bg
   the 取込プレビュー marks a changed 志望校 with, so a teacher learns one colour
   for "this is what is different" rather than one per screen.

   It tints the run of text rather than the cell: the row is drawn by the shared
   grid (components/grid.html), which owns its <td> and takes no per-cell class.
   Inline-block so the padding actually opens up around the arrow. */
.c-change {
  display: inline-block;
  padding: var(--space-xxs) var(--space-xs);
  border-radius: var(--radius);
  background-color: var(--danger-bg);
}

/* Colour is never the only carrier (WCAG AA, and this table gets read on paper
   at 面談): the old value stays beside the new one, struck through and muted so
   the value that HOLDS now is the one that reads first. */
.c-change__was {
  margin-inline-end: var(--space-xxs);
  color: var(--muted);
  font-weight: var(--font-weight-regular);
  text-decoration: line-through;
}

/* A button inside a table cell must not set the row height: it compresses to
   fit inside the body text line (caption line-height plus borders stays under
   it), so a row with actions is exactly as tall as a row without. */
.c-table__cell .c-button {
  padding: var(--space-none) var(--space-s);
  font-size: var(--font-size-caption1);
  line-height: var(--line-height-caption1);
}

/* Pagination — components/pagination.html: the list-control bar that sits
   directly above the table it controls (placement rule in the partial). */
.c-pagination {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  /* Left-aligned, not space-between: 合計 and 表示 belong to the pager beside
     them, and a flex edge-to-edge bar strands them at the far side of a wide
     screen where nobody reads them (PM, 2026-08-09). */
  justify-content: flex-start;
  gap: var(--space-s);
  margin-bottom: var(--space-xs);
  font-size: var(--font-size-caption1);
  color: var(--muted);
}

/* No frame, and no rules either side of the page number (PM, 2026-08-23).
   ‹ 1 / 4 › was a box with two internal rules — four lines to say what three
   glyphs already say, and above a table they landed on top of the table's own.
   Proximity groups them now: the arrows sit right beside the number and read as
   one control without anything drawn around them.

   This was #216's segmented frame, and the dashboard had already taken it off
   locally (home.css) because on a 600px card the box was the loudest thing in
   the panel. That局所的な直し is now the rule for every screen — one pager, one
   look, no per-screen override left anywhere.

   The 表示件数 select next to it KEEPS its frame: it is the one CONTROL on this
   bar, undressed it read as a caption and nobody found it could be changed
   (#216). One framed thing on the bar is exactly why the rest comes off. */
.c-pagination__pager {
  display: flex;
  align-items: stretch;
}

.c-pagination__link {
  display: flex;
  align-items: center;
  padding: var(--space-xxs) var(--space-s);
  color: var(--muted);
  text-decoration: none;
}

a.c-pagination__link:hover {
  background-color: var(--accent);
  color: var(--primary);
}

.c-pagination__link--disabled {
  color: var(--foreground-disabled);
}

/* The same pager driven by JavaScript instead of by links — 答案レビュー moves
   between the pages of one scan without a URL to move to. A <button> arrives
   with its own chrome, and this control carries no frame at all, so all of it
   comes off. */
button.c-pagination__link {
  border: none;
  background: none;
  font: inherit;
  cursor: pointer;
}
button.c-pagination__link:hover:enabled {
  background-color: var(--accent);
  color: var(--primary);
}
button.c-pagination__link:disabled {
  color: var(--foreground-disabled);
  cursor: default;
}

.c-pagination__status {
  display: flex;
  align-items: center;
  padding: var(--space-xxs) var(--space-m);
  color: var(--foreground);
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
}

.c-pagination__meta {
  display: flex;
  align-items: center;
  gap: var(--space-s);
}

.c-pagination__count {
  color: var(--foreground);
  font-weight: var(--font-weight-medium);
  font-variant-numeric: tabular-nums;
}

/* The page-size control is a real select with a visible frame: 表示：20 ⌄ read
   as a label, and nobody found that it could be changed (#216). */
.c-pagination__size {
  display: flex;
  align-items: center;
  gap: var(--space-xs);
}

.c-pagination__select {
  padding: var(--space-xxs) var(--space-xs);
  border: var(--border-width-thin) solid var(--border);
  border-radius: var(--radius);
  background-color: var(--card-bg);
  color: var(--foreground);
  font-size: var(--font-size-caption1);
  cursor: pointer;
}

/* Empty state — components/empty_state.html. The prototype renders "nothing
   here" as a plain card (`card text-center py-12`), not as a dashed grey box:
   the message is what says the list is empty, so the frame does not have to
   shout it too. */
.c-empty-state {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: var(--space-s);
  padding: var(--space-xxxl) var(--space-l);
  border: var(--border-width-thin) solid var(--border);
  border-radius: var(--radius);
  background-color: var(--card-bg);
  box-shadow: var(--shadow-card);
  text-align: center;
}

.c-empty-state__message {
  font-weight: var(--font-weight-medium);
}

/* Modal — components/modal_base.html (frame), modal_form.html, modal.html */
/* Above the shell. The header and the tab nav are sticky and carry z-index 50 and
   40 (base.html), and a fixed dialog with an auto z-index paints under both — the
   dialog would open behind the header (issue 131). */
.c-modal {
  position: fixed;
  inset: 0;
  z-index: 100;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: var(--space-l);
}

.c-modal__backdrop {
  position: absolute;
  inset: 0;
  background-color: var(--foreground);
  opacity: 0.5;
}

.c-modal__panel {
  position: relative;
  display: flex;
  flex-direction: column;
  gap: var(--space-m);
  width: 100%;
  max-width: calc(var(--space-xxxxl) * 10);
  /* The panel scrolls, not the page behind it. A dialog is fixed and centered,
     so a panel taller than the viewport would hang off both edges with nothing
     to scroll — the wheel would move the document underneath and the submit
     button would be unreachable. 要項登録 is 14 fields and was a scrolling page
     before it became a dialog (#133). */
  max-height: calc(100vh - var(--space-xxxxl));
  overflow-y: auto;
  /* 棒は**動かしているあいだだけ**見せる（static/js/modal-scrollbar.js が
     `is-scrolling` を付ける）。`.c-quiet-scroll` の hover ではこの箱に効かない:
     開いているあいだ人はダイアログの中にいるので hover は常に真で、棒は出たまま
     になる。中にいる箱の合図は、指すことではなく動かすことである。

     消し方は**色**であって、太さでも `scrollbar-width: none` でもない。太さを
     none↔thin で切り替えると、棒が出た瞬間に欄が横へ寄り、消えるとまた戻る——
     読んでいる行がスクロールのたびに動く。`scrollbar-gutter: stable` は助けに
     ならない: 太さが none のあいだ、空き地は0だからである（仕様どおり）。
     macOS の重ねて描く棒では元から場所を取らないので見えないが、Windows や
     「常に表示」の macOS では跳ねる。色なら、どの流儀の棒でも場所は動かない。 */
  scrollbar-color: transparent transparent;
  /* A flick that runs past the last field stops here instead of handing the
     rest of its momentum to the page behind the dialog. */
  overscroll-behavior: contain;
  padding: var(--space-xl);
  border: var(--border-width-thin) solid var(--border);
  /* The app's one corner — a dialog matches the buttons inside it (PM,
     2026-08-10). The shadow does NOT follow suit: it stays the overlay one,
     because a dialog floats above the shell where --shadow-card is for things
     sitting in it. */
  border-radius: var(--radius);
  background-color: var(--card-bg);
  box-shadow: var(--shadow-overlay);
}

/* A wrapper that exists only to carry Alpine state a dialog needs on top of the
   frame's own. display:contents keeps its children direct children of the panel,
   so they keep the panel's own row gap instead of collapsing into one row. */
.c-modal__scope {
  display: contents;
}

/* M — width only, for a dialog that is prose rather than fields. The default
   480px is sized for a form (one label, one control per line); a paragraph in it
   breaks every 20-odd characters and the reader gets a column of fragments.
   672px is the same body text at a readable measure. XL is still for tables. */
.c-modal__panel.is-scrolling {
  scrollbar-color: var(--scrollbar-thumb) transparent;
}

/* WebKit/Blink は `scrollbar-color` より ::-webkit-scrollbar-thumb の指定を採るので、
   同じことをあちらの言葉でも言う。太さも色も tools/tailwind/src.css が1箇所で
   決めているので、ここで書くのは「いま見せるかどうか」だけ。 */
.c-modal__panel::-webkit-scrollbar-thumb {
  background: transparent;
  transition: background-color var(--duration-fast) ease-out;
}

.c-modal__panel.is-scrolling::-webkit-scrollbar-thumb {
  background: var(--scrollbar-thumb);
}

.c-modal__panel--m {
  max-width: calc(var(--space-xxxxl) * 14);
}

/* 横から出る1枚（PM 2026-09-17: カレンダーのマスを押すと開く）。

   **これは modal ではない。** 後ろは暗くならず、押せるまま・巻けるままである
   ——読み手は月を見ながら書いていて、どの日に入れているのかを隠してしまっては
   意味がない。だから地の膜を消し、枠は指を通し（`pointer-events: none`）、
   受けるのは板だけにしてある。頁の巻き取りを止める仕掛けも、この class を見て
   自分から降りる（components/modal_base.html）。

   右に立つのは PC の右の列の上である。中央に出すと、押したマスが自分の開けた箱に
   隠れる。 */
.c-modal--side {
  justify-content: flex-end;
  padding: var(--space-none);
  pointer-events: none;
}

.c-modal--side .c-modal__backdrop {
  display: none;
}

.c-modal--side .c-modal__panel {
  pointer-events: auto;
  width: min(calc(var(--space-xxxxl) * 9), 100%);
  max-width: none;
  height: 100%;
  max-height: none;
  /* **広げられる**（PM 2026-09-17）。活動の記録は欄が多く、既定の幅では
     「学んだこと」が縦に伸びる——PC には余っている横がある。掴むのは右下の角
     （ブラウザの既定）で、板は右端に貼り付いているので、広げると左へ伸びる。

     `direction: rtl` で掴む角を左下へ移す手は取らなかった: 中身まで右揃えになり、
     ラベルも釦の並びも裏返った（実測 2026-09-17）。 */
  resize: horizontal;
  min-width: calc(var(--space-xxxxl) * 7);
  border-radius: var(--radius-grid);
  /* 影だけが境目になる。膜が無いので、線が無いと板が画面に溶ける。 */
  box-shadow: -8px 0 24px rgb(15 23 42 / 12%);
  animation: c-modal-slide-in var(--duration-fast) ease-out;
}

@keyframes c-modal-slide-in {
  from {
    transform: translateX(100%);
  }
}

/* タブで中身を替えたとき（PM 指定の見本: Tabs with Transition Panel）。**板も札も
   動かない**——動くのは札の下の中身だけである。板ごと滑り直すと、替わったのが中身
   なのか板なのか読めないし、札が一緒に飛ぶと押した札がどこへ行ったか見失う。
   入ってくる中身は上から降りてきて、ぼけが晴れる。 */
/* **板そのものは動かさない。** `.c-modal--side` の横滑りはここで打ち消す——
   打ち消さないと、タブを押すたびに板が右から入り直し、中身の動きと二重になる
   （実測 2026-09-17: 一度引っ込んでから出てくるように見えた）。 */
.c-modal--tabbed .c-modal__panel {
  animation: none;
}

.c-modal--tabbed .c-modal__panel form > *:not(.cal-pick) {
  animation: c-modal-tab-in var(--duration-fast) ease-in-out;
}

@keyframes c-modal-tab-in {
  from {
    opacity: 0;
    transform: translateY(-50px);
    filter: blur(4px);
  }
}

/* 出ていく中身。htmx が差し替えを `swap:` のあいだ待ち、そのあいだ受け口に
   `htmx-swapping` を付ける——入ってくる側と逆の向き（下へ）に抜ける。 */
#modal-slot.htmx-swapping .c-modal__panel form > *:not(.cal-pick) {
  opacity: 0;
  transform: translateY(50px);
  filter: blur(4px);
  transition:
    opacity var(--duration-fast) ease-in-out,
    transform var(--duration-fast) ease-in-out,
    filter var(--duration-fast) ease-in-out;
}

@media (prefers-reduced-motion: reduce) {
  .c-modal--side .c-modal__panel,
  .c-modal--tabbed .c-modal__panel form > *:not(.cal-pick) {
    animation: none;
  }

  #modal-slot.htmx-swapping .c-modal__panel form > *:not(.cal-pick) {
    opacity: 1;
    transform: none;
    filter: none;
    transition: none;
  }
}

/* 下から出る1枚（PM 2026-09-18、見本 6・7 の bottom sheet）。**電話のときだけ**
   ——PC では付いている他の class（`.c-modal--side` なら横の板、無ければ中央）の
   ままである。カレンダーの行を押すと、横の板は電話で全画面になり、読むだけの1枚は
   中央に浮き、同じ「押す」で2つの形が出ていた。

   膜を戻し、後ろを止める（止めるかどうかは modal_base.html が膜の有無で決める）。
   閉じるのは膜を押すか、見出しを下へ引く（static/js/modal-sheet.js）。引ける
   ことは見出しの上の短い棒が言う。

   `--c-modal-sheet` は JS への合図: 引いて閉じるのは、いま下から出ているとき
   だけ。幅の線を JS に写すと、2箇所がいずれ食い違う。 */
@media (max-width: 63.99rem) {
  .c-modal--sheet {
    --c-modal-sheet: 1;
    align-items: flex-end;
    justify-content: center;
    padding: var(--space-none);
    pointer-events: auto;
  }

  .c-modal--sheet .c-modal__backdrop {
    display: block;
  }

  .c-modal--sheet .c-modal__panel {
    width: 100%;
    max-width: none;
    min-width: 0;
    height: auto;
    /* 上に膜を 48px 残す: 押せば閉じる場所が、指の届くところにいつもある。 */
    max-height: calc(100vh - var(--space-xxxxl));
    max-height: calc(100dvh - var(--space-xxxxl));
    resize: none;
    border-bottom: 0;
    border-radius: var(--radius) var(--radius) var(--radius-grid) var(--radius-grid);
    box-shadow: var(--shadow-overlay);
    animation: c-modal-sheet-in var(--duration-drop) var(--ease-drop);
    /* 引いた指を離したとき、閉じない距離なら元の位置へ戻る。 */
    transition: transform var(--duration-fast) ease-out;
  }

  /* タブで中身を替えたときは、板を下から入り直させない（上の `--tabbed` と同じ）。 */
  .c-modal--sheet.c-modal--tabbed .c-modal__panel {
    animation: none;
  }

  /* 引く場所は見出しの帯。帯は sticky なので、中身を巻いても指の下に残る。
     `touch-action: none` が無いと、下へ引く指をブラウザが頁の巻き取りとして
     持っていき、JS に来ない。 */
  .c-modal--sheet .c-modal__title {
    padding-top: var(--space-s);
    touch-action: none;
    cursor: grab;
  }

  .c-modal--sheet .c-modal__title::before {
    content: "";
    display: block;
    width: calc(var(--space-xxxl) + var(--space-xs));
    height: var(--space-xs);
    margin: var(--space-none) auto var(--space-s);
    border-radius: var(--radius);
    background: var(--scrollbar-thumb);
  }
}

@keyframes c-modal-sheet-in {
  from {
    transform: translateY(100%);
  }
}

@media (prefers-reduced-motion: reduce) {
  .c-modal--sheet .c-modal__panel {
    animation: none;
    transition: none;
  }
}

/* XL — width only, for table-carrying dialogs (import preview). The scrolling
   is the panel's own rule above; every dialog needs it, not just the wide ones. */
.c-modal__panel--xl {
  max-width: calc(var(--space-xxxxl) * 20);
}

/* 返す紙の重ね窓（components/return_preview.html）: PDF をそのまま枠に。portal の
   .paper-frame と同じ寸法。添削と点数採点の盤が同じ窓を開く（PM 2026-09-24）。 */
.rt-frame {
  display: block;
  width: 100%;
  height: calc(100vh - 240px);
  min-height: 320px;
  border: var(--border-width-thin) solid var(--border);
  border-radius: var(--radius);
}

/* The dialog header: a full-bleed band (negative margins undo the panel
   padding) so every modal opens with the same recognizable one — and it stays
   put while the body scrolls, like the footer at the other end. Between them,
   a reader who has scrolled into the middle of 要項登録 can still see which
   dialog they are in and still reach both controls that leave it.

   Flush to the panel's top edge at rest, not only when stuck: parked one step
   lower it would jump up the instant the reader scrolled a single pixel. Same
   negative offset as the footer, for the same measured reason — the panel's own
   padding is inside the scrollport, and at top:0 fields scroll through the
   strip above the band. */
.c-modal__title {
  position: sticky;
  /* Above the body, not merely before it. .c-field is position:relative (it
     anchors its tooltip), so a field and this band are both positioned with
     z-index auto and DOM order decides the winner — which hands it to the
     fields, and a text box slides over the heading. Measured, not assumed. The
     footer only wins that race by being last in the DOM; both bands say it. */
  z-index: 1;
  top: calc(var(--space-xl) * -1);
  margin-block-start: calc(var(--space-xl) * -1);
  margin-inline: calc(var(--space-xl) * -1);
  padding: var(--space-m) var(--space-xl);
  border-bottom: var(--border-width-thin) solid var(--border);
  background-color: var(--card-bg);
  font-size: var(--font-size-subtitle2);
  font-weight: var(--font-weight-strong);
  line-height: var(--line-height-subtitle2);
}

/* A dialog that carries a note beside its heading（「最大9校」）. Only that one
   becomes a row: :has() leaves every other modal's heading exactly as it was,
   and a title with no note keeps plain text wrapping. */
.c-modal__title:has(.c-modal__title-note) {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--space-m);
}

.c-modal__title-note {
  flex: none;
  color: var(--muted);
  font-size: var(--font-size-caption1);
  font-weight: var(--font-weight-regular);
}

/* The dialog footer, and it stays on screen. The panel scrolls (above), so on a
   long form — 要項登録 is 14 fields — a footer that scrolled with the content
   would hide 保存する and キャンセル until the reader hit the very bottom: the
   two controls that end the dialog would be the last thing they could reach.
   Sticky at the bottom of the scrollport instead, full-bleed like the header
   band above it, so cancelling and saving are available at any scroll position.
   Opaque background + top rule: content scrolls UNDER this, and without both it
   reads as text overlapping text.

   The NEGATIVE bottom offset is what makes "under" true at the bottom edge too:
   sticky resolves against the panel's content box, while the panel's own bottom
   padding still scrolls fields through it. At bottom:0 a text box slides into
   view in that last strip, BELOW the footer, and the dialog looks broken —
   measured in a browser, not guessed. Offsetting by exactly that padding parks
   the footer's opaque edge on the panel's; once the reader reaches the end it
   settles back above the padding, with only panel behind it either way. */
/* A dialog that ends in an action bar ends AT it: the bar is full-bleed and
   carries its own padding, so the panel's own bottom padding would only show as
   a strip of card under it. The negative-margin trick cannot do this here —
   the bar sits inside the form, and shortening the form does not move the
   panel's padding. */
.c-modal__panel:has(> * > .c-modal__actions),
.c-modal__panel:has(> .c-modal__actions) {
  padding-block-end: var(--space-none);
}

/* ダイアログの先頭に置く一文。何を書く場所かを、欄より先に言う。 */
.c-modal__lead {
  margin-block: var(--space-none);
  font-size: var(--font-size-caption1);
  color: var(--muted);
}

/* 脚のいちばん左（components/modal_form.html の actions_leading）。削除だけが
   ここに来る。auto で押しのけるので、保存する と 削除 が隣り合うことがない。 */
.c-modal__actions-lead {
  margin-inline-end: auto;
}

.c-modal__actions {
  position: sticky;
  z-index: 1; /* same reason as the header band above */
  /* Flush with the panel's own bottom edge — the panel drops its bottom padding
     for exactly this case (.c-modal__panel:has below). Sticking at the padding
     edge left a white strip UNDER the bar that is supposed to BE the end of the
     dialog. */
  bottom: 0;
  justify-content: flex-end;
  margin-inline: calc(var(--space-xl) * -1);
  padding: var(--space-m) var(--space-xl);
  border-top: var(--border-width-thin) solid var(--border);
  background-color: var(--card-bg);
}

/* Progress + polling — components/polling.html */
.c-progress {
  display: flex;
  flex-direction: column;
  gap: var(--space-xs);
  max-width: calc(var(--space-xxxxl) * 10);
}

.c-progress__label {
  font-weight: var(--font-weight-medium);
}

.c-progress__bar {
  width: 100%;
  block-size: var(--space-s);
}

.c-progress__done {
  color: var(--success);
}

.c-progress__error {
  color: var(--danger);
}

/* ── Flash messages: the floating region — components/toast.html ────────────
   The region stays in the DOM even when empty: it is the target an out-of-band
   swap replaces. It is `fixed`, so an empty one is not a gap and a full one
   does not push the screen down — which is what it used to do, moving the row
   you had just acted on out from under the pointer (PM, 2026-08-10).

   It starts below the sticky header (--layout-toast-top, tokens.css) rather
   than at the top of the viewport: the header's right end is the account menu,
   and a warning that waits to be dismissed would sit on top of it.

   pointer-events: none on the region, auto on each toast: an empty region — and
   the empty column beside a short toast — must not eat clicks meant for the
   page underneath. */
.c-toast-region {
  position: fixed;
  top: var(--layout-toast-top);
  right: var(--space-l);
  /* Over the dialog (100) as well: a save inside a modal answers with a flash,
     and a dialog left open by a REJECTED save must not hide the reason. */
  z-index: 120;
  /* **Shrink to the message, cap at 26rem** (PM, 2026-09-15). A fixed width made
     「活動を更新しました。」 sit in a box three times its own length, which reads as
     a panel that failed to fill rather than as a confirmation. Fixed-position
     elements shrink-to-fit when no width is set, so the cap is all that is left
     to state; a long message still wraps inside it. */
  max-width: min(26rem, calc(100% - var(--space-xxl)));
  pointer-events: none;
}

.c-toast-list {
  display: flex;
  flex-direction: column;
  gap: var(--space-s);
}

.c-toast {
  display: flex;
  align-items: flex-start;
  gap: var(--space-s);
  padding: var(--space-m) var(--space-l);
  border: var(--border-width-thin) solid var(--border);
  border-radius: var(--radius);
  background-color: var(--card-bg);
  color: var(--foreground);
  /* Raised, unlike the inline .message below: this one is over the page rather
     than in it, and without a shadow it reads as part of whatever it lands on. */
  box-shadow: var(--shadow-overlay);
  pointer-events: auto;
  /* The way in. It is CSS rather than x-transition because that modifier stops
     x-show from ever hiding the element on the pinned Alpine build
     (components/toast.html). There is no way OUT for the same reason — a toast
     that leaves does it by going, which is what x-show does. */
  animation: c-toast-in 0.15s ease-out;
}

@keyframes c-toast-in {
  from {
    opacity: 0;
    transform: translateY(calc(-1 * var(--space-s)));
  }
}

.c-toast__text {
  flex: 1;
}

/* The glyph is about 9px wide; the CONTROL is not. This is the only way to get
   rid of a message that waits to be dismissed, so it gets a target a hand can
   hit — the negative margin keeps that target from padding the toast out. */
.c-toast__close {
  flex: none;
  display: flex;
  align-items: center;
  justify-content: center;
  min-width: var(--space-xxl);
  min-height: var(--space-xxl);
  margin: calc(-1 * var(--space-xs)) calc(-1 * var(--space-s)) calc(-1 * var(--space-xs)) 0;
  padding: var(--space-none);
  border: none;
  border-radius: var(--radius);
  background: none;
  color: inherit;
  font-size: var(--font-size-caption1);
  line-height: var(--line-height-caption1);
  cursor: pointer;
  opacity: 0.7;
}

.c-toast__close:hover {
  opacity: 1;
}

.c-toast--success {
  border-color: var(--success-border);
  background-color: var(--success-bg);
  color: var(--success);
}

.c-toast--warning {
  border-color: var(--warning-border);
  background-color: var(--warning-bg);
  color: var(--warning);
}

.c-toast--error {
  border-color: var(--danger-border);
  background-color: var(--danger-bg);
  color: var(--danger);
}

.c-toast--info,
.c-toast--debug {
  border-color: var(--info-border);
  background-color: var(--info-bg);
  color: var(--info);
}

/* ── Inline messages — NOT the toast region ─────────────────────────────────
   .message belongs in the page flow: the import previews, 答案確認, ルーブリック
   確認 all state a condition of the screen you are on, which has to stay
   readable while you work rather than time out. Same palette, no shadow, no
   dismiss. The two were one class until the region started floating. */
.message {
  padding: var(--space-m) var(--space-l);
  border: var(--border-width-thin) solid var(--border);
  border-radius: var(--radius);
  background-color: var(--background);
  color: var(--foreground);
}

.message--success {
  border-color: var(--success-border);
  background-color: var(--success-bg);
  color: var(--success);
}

.message--warning {
  border-color: var(--warning-border);
  background-color: var(--warning-bg);
  color: var(--warning);
}

.message--error {
  border-color: var(--danger-border);
  background-color: var(--danger-bg);
  color: var(--danger);
}

.message--info {
  border-color: var(--info-border);
  background-color: var(--info-bg);
  color: var(--info);
}

/* ===========================================================================
   Motion — issue #135. The prototype's FadeIn / TypewriterText / AnalyzingLoader
   as the CSS half of static/js/motion.js (count-up needs none — it only rewrites
   text). The keyframes themselves were ported with the rest of globals.css into
   tools/tailwind/src.css (#130), so `blink` is reused below rather than restated.

   [data-motion="on"] is written on <html> by motion.js, and only when the user has
   not asked for reduced motion. Everything that hides or moves hangs off it: on a
   page with no JavaScript — and on one that asked for reduced motion — the element
   is never hidden in the first place, so what renders is the final state. The media
   query at the end is the second net, and the only one the CSS-only animation
   classes have.

   Durations are the prototype's own (FadeIn 0.6s, blink 0.8s, the loader's bar
   0.5s). tokens.css holds one duration today, --duration-fast, and it belongs to
   another PR in this wave; a duration scale lands with the sweep (#137).
   =========================================================================== */

[data-motion="on"] .motion-fade-in {
  opacity: 0;
  /* FadeIn.tsx direction="up": translateY(20px). */
  transform: translateY(var(--space-xl));
  transition:
    opacity 0.6s ease-out,
    transform 0.6s ease-out;
}

[data-motion="on"] .motion-fade-in.is-revealed {
  opacity: 1;
  transform: none;
}

/* Fade, and no travel with it.

   FadeIn.tsx's 20px rise is written for a BLOCK arriving on the page, where the
   distance is a fraction of the thing moving. On an inline chip beside a number
   it is most of the element's own height, so the eye does not read "it rose in"
   — it reads the chip dropping below the line first and then climbing back to
   where it belongs (PM, 2026-08-23). The +1.1 next to 平均偏差値 is the case.

   Only the resting state needs saying: .is-revealed already ends at
   `transform: none`, so what this removes is the place it was travelling FROM.
   Same weight as the rule above and written after it, which is what makes it
   win — see .c-tabs__tab--active:hover for the full version of that trap. */
[data-motion="on"] .motion-fade-in--still {
  transform: none;
}

/* The caret exists only while the helper is typing — it adds and removes the class,
   so nothing renders it on a page where the script never ran. */
.motion-typing::after {
  content: "";
  display: inline-block;
  width: var(--border-width-thick);
  height: var(--space-l);
  margin-left: var(--space-xs);
  background-color: currentColor;
  vertical-align: middle;
  animation: blink 0.8s infinite;
}

.motion-analysing {
  display: flex;
  flex-direction: column;
  gap: var(--space-m);
}

.motion-analysing__title {
  font-weight: var(--font-weight-strong);
  color: var(--foreground);
}

/* Three states, so the list says where the work is without reading the words:
   not yet reached, done behind, current now. Every one of them is a sentence the
   user reads, not an inactive control, so none may drop below the 4.5:1 body floor
   (ADR-020) — --foreground-disabled is out here, and the quietest state is
   --muted. Reaching a step brightens it; being ON it also bolds it. */
.motion-analysing__steps {
  display: flex;
  flex-direction: column;
  gap: var(--space-xs);
  margin: 0;
  padding: 0;
  list-style: none;
  color: var(--muted);
}

.motion-analysing__steps .is-done,
.motion-analysing__steps .is-current {
  color: var(--foreground);
}

.motion-analysing__steps .is-current {
  font-weight: var(--font-weight-medium);
}

.motion-analysing__track {
  overflow: hidden;
  height: var(--space-xs);
  border-radius: var(--radius);
  background-color: var(--background);
}

/* Full unless a script says otherwise: without one, the page cannot report on the
   work the list describes, and a bar sitting at zero reads as a failure. */
.motion-analysing__bar {
  width: 100%;
  height: 100%;
  border-radius: var(--radius);
  background-color: var(--primary);
}

/* The bar animates only once it is walking FORWARD. init() drops it from the full
   no-script default to the first step, and animating THAT would make a progress bar
   running backwards the first thing the user sees; motion.js adds this modifier on
   the first step instead, which is a later frame, so the drop lands untransitioned. */
.motion-analysing--walking .motion-analysing__bar {
  transition: width 0.5s ease-out;
}

@media (prefers-reduced-motion: reduce) {
  /* Same selector as the rule it answers, or it would lose on specificity and the
     net would be decorative. */
  [data-motion="on"] .motion-fade-in {
    opacity: 1;
    transform: none;
    transition: none;
  }

  .motion-analysing--walking .motion-analysing__bar {
    transition: none;
  }

  .c-toast {
    animation: none;
  }

  /* The prototype's CSS-only animations (src.css): they run without a script, so this
     media query is all that can stop them.

     .animate-blink and .typing-dot were in this list and are not in src.css any
     more — nothing rendered either class, so both went with the other unused
     ports. .motion-typing::after stays and still animates `blink`, whose
     keyframes src.css keeps for exactly that reason. */
  .motion-typing::after,
  .animate-slide-up {
    animation: none;
    transition: none;
  }
}

/* Status badge — components/status_badge.html. ONE definition of what each
   answer-sheet state looks like, because a teacher learns "amber = my turn" once
   and then reads it on every screen. The tone names come from
   scoring/sheet_state.py TONE; nothing else may map a state to a colour. */
.c-status {
  display: inline-block;
  /* Tighter than a button: a badge is read, not pressed, so it does not need a
     button's touch target and looked oversized carrying one. */
  padding: var(--space-xxs) var(--space-xs);
  border: var(--border-width-thin) solid transparent;
  /* The app's corner, not a pill. --radius-circular on a wide chip is an oval,
     and nothing else on these screens is one (PM, 2026-08-11). */
  border-radius: var(--radius);
  font-size: var(--font-size-caption1);
  line-height: var(--line-height-caption1);
  font-weight: var(--font-weight-medium);
  white-space: nowrap;
}

/* neutral は**灰色**（PM, 2026-09-09）。--accent は #eff6ff の**青**で、しかも
   --info-bg がそれと同じ値なので、「機械が動いている」（info）と「もう動かない」
   （neutral）が同じ色の上に乗っていた。塗りだけで見分けられない2つは、色を付けた
   意味がない。--background はアプリの無彩の面で、青の隣に置いても青に見えない。 */
.c-status--neutral {
  background: var(--background);
  border-color: var(--border);
  color: var(--muted);
}

.c-status--info {
  background: var(--info-bg);
  border-color: var(--info-border);
  color: var(--info);
}

.c-status--warning {
  background: var(--warning-bg);
  border-color: var(--warning-border);
  color: var(--warning);
}

.c-status--success {
  background: var(--success-bg);
  border-color: var(--success-border);
  color: var(--success);
}

.c-status--danger {
  background: var(--danger-bg);
  border-color: var(--danger-border);
  color: var(--danger);
}

/* ── Data grid (#211) ─────────────────────────────────────────────────────────
   A reference table a teacher SCANS rather than reads: hundreds of rows compared
   column against column, the job they do in Excel today. So it is deliberately
   denser than .c-table — caption-size type, 2px cell padding, gridlines on every
   cell, a header that stays put while the body scrolls, and a header dropdown
   that carries the sort AND the value filter the way a spreadsheet's does
   (components/grid_column.html).

   .c-table is unchanged and stays the default. This is for list SCREENS whose
   point is comparison; a five-row summary inside a card still wants .c-table. */
.c-grid__scroll {
  /* Anchors the drag indicator, which is positioned against this box and
     scrolls with it (static/js/row-reorder.js). */
  position: relative;
  overflow: auto;
  max-height: 70vh;
  border: var(--border-width-thin) solid var(--border);
  /* The app's corner, like every other panel on the screen. It was square until
     2026-08-23, and the reasoning then was sound for the grid it described: a
     table whose every cell is a square box asks for a square frame. The cells
     stopped being boxes on that date — the vertical rules are gone — so the
     premise went with them, and a square frame in a rounded app is now the thing
     that reads as wrong (PM). --radius-grid still exists, for edges that have to
     meet something exactly; this is not one. */
  border-radius: var(--radius);
  background-color: var(--card-bg);
}

.c-grid {
  /* Columns sized to their content, and only stretched when the content is
     narrower than the frame. width:100% spread four count columns across half a
     wide screen, which is the "空きすぎ" a spreadsheet never shows. */
  width: max-content;
  min-width: 100%;
  border-collapse: collapse;
  font-size: var(--font-size-caption1);
  line-height: 1.35;
  /* Digits share one advance width, so a column of counts lines up as a column
     of counts rather than as ragged text. */
  font-variant-numeric: tabular-nums;
}

/* Horizontal rules only, and a stated height.

   The vertical rule on every cell was the single strongest "this is Excel" mark
   on the screen, and it was doing work nothing asked for: a column already
   separates itself by the space around it and by which way its text is aligned
   (start for words, end for figures). Notion, Linear and Airtable all draw the
   row boundary and leave the column boundary implied. Removing it takes roughly
   half the ink out of the table without taking out a single fact.

   The height comes from --grid-row-h rather than from padding, so a cell that
   happens to hold a button or a badge sits on the ROW's height instead of
   setting its own. */
.c-grid__cell {
  height: var(--grid-row-h);
  padding: var(--space-none) var(--space-m);
  border-bottom: var(--border-width-thin) solid var(--border);
  text-align: start;
  white-space: nowrap;
}

/* The frame already draws the bottom edge; a rule under the last row paints a
   second line one pixel inside it. */
.c-grid tbody tr:last-child > .c-grid__cell {
  border-bottom: none;
}

.c-grid__cell--num {
  text-align: end;
}

/* A button inside a cell needs nothing said about it any more. It used to: a
   .c-button is 30px tall and a text cell was about 20px, so one 操作 column
   stretched every row of the grid by half again, and the button had to give up
   its block padding to fit. A row is now 40px whatever it holds, so the control
   is simply a control — and the rule that squashed it is gone rather than left
   to make action columns look pressed flat in a roomy row. */

/* ── 並べ替えのつまみ（static/js/row-reorder.js） ──────────────────────────── */

/* Scoped to the reorder region and matched by position: components/grid.html
   writes one class on every cell, and a per-column class would be a new thing
   for every screen to remember. The handle is always the first column. */
[data-reorder="rows"] .c-grid__cell:first-child {
  width: 1%;
  padding-inline: var(--space-xxs);
  text-align: center;
}

/* A button, not a bare glyph: dragging is a mouse gesture and this project holds
   a WCAG AA floor, so the same handle takes ↑ / ↓ from the keyboard. */
.c-grid__handle {
  padding: var(--space-none) var(--space-xxs);
  border: none;
  background: none;
  color: var(--muted);
  cursor: grab;
  line-height: var(--line-height-caption1);
  /* The browser must not claim the gesture: without this a touch drag scrolls
     the page and pointermove never fires. */
  touch-action: none;
}

.c-grid__handle:hover {
  color: var(--foreground);
}

.c-grid__handle:focus-visible {
  outline: var(--border-width-thick) solid var(--focus-ring);
  outline-offset: var(--space-xxs);
}

.c-grid__handle:active {
  cursor: grabbing;
}

/* The row being dragged becomes the HOLE it left: invisible, but still taking up
   its height so the list never jumps. What follows the pointer is the ghost.

   Invisible and not merely faint, because the rows on either side slide over
   this slot to open the gap (static/js/row-reorder.js → shift). A faint copy
   underneath them would show through as a smear. */
.is-lifted > .c-grid__cell {
  visibility: hidden;
}

/* The rows that step aside. The transform itself is written by JavaScript; all
   the stylesheet says is how long it may take to get there. Added only for the
   length of a drag, so no row animates in from where a previous one left it. */
tr.is-sliding {
  transition: transform var(--duration-lift) var(--ease-lift);
}

/* The copy that tracks the pointer, in TWO elements on purpose.

   The outer one only positions: JavaScript rewrites its `transform` once per
   animation frame and nothing here may put a transition on that — a transition
   on the property the pointer drives is exactly what makes a drag feel like it
   is on a rubber band. The inner one is the card: it owns the radius, the
   elevation and the pick-up scale, so those CAN transition without touching
   the position. (dnd-kit layers its DragOverlay the same way, and for the same
   reason.)

   The radius has to live on a DIV rather than on the table: a table with
   `border-collapse: collapse` paints the collapsed border model and drops
   `border-radius` on the floor, which is why the first version had square
   corners while the computed style said 6px. */
.c-reorder__ghost {
  position: fixed;
  top: 0;
  left: 0;
  /* Above the sticky header (50) and the tab nav (40), below the modal (100):
     a lifted row must clear the furniture it is dragged past, and must not
     paint over a dialog. The numbers are base.html's — see .c-modal. */
  z-index: 60;
  pointer-events: none;
}

.c-reorder__lift {
  overflow: hidden;
  border-radius: var(--radius);
  background-color: var(--card-bg);
  /* Starts at rest and is raised on the next frame by .is-lifted, so the pick-up
     is a movement rather than a jump. */
  box-shadow: var(--shadow-card);
  transition:
    box-shadow var(--duration-lift) var(--ease-lift),
    transform var(--duration-lift) var(--ease-lift);
}

.c-reorder__lift > .c-grid {
  margin: var(--space-none);
  table-layout: fixed;
}

/* Held. The scale is small because the row is as wide as the screen — at 1.02 a
   1300px row grows 26px and reads as a zoom rather than as a lift. */
.c-reorder__lift.is-lifted {
  box-shadow: var(--shadow-lifted);
  transform: scale(1.008);
}

/* Let go: the ghost travels to the slot it landed in instead of vanishing, and
   sheds its elevation on the way down. Without it the row blinks from under the
   pointer to its new position and the eye loses it — the drop animation is the
   single biggest difference between this and a native-feeling list (dnd-kit
   calls it dropAnimation; react-beautiful-dnd, the drop). */
.c-reorder__ghost.is-dropping {
  transition: transform var(--duration-drop) var(--ease-drop);
}

/* There is deliberately no insertion LINE here. The gap above is what shows
   where the row lands, because a line cannot: the ghost is under the pointer and
   so is the boundary nearest it, so the ghost covers the line — measured at 19px
   inside a 23px row on the preview (2026-08-12). dnd-kit's sortable and
   react-beautiful-dnd both open a gap for the same reason. */

/* The motion is informative but it is not the information. Somebody who asked
   the system for less of it still gets the ghost, the gap and the result; they
   just arrive rather than travel (WCAG 2.3.3, and the AA floor of ADR-020). The
   JavaScript checks the same query before it adds `is-sliding` at all. */
@media (prefers-reduced-motion: reduce) {
  .c-reorder__lift,
  .c-reorder__ghost.is-dropping,
  tr.is-sliding {
    transition: none;
  }

  .c-reorder__lift.is-lifted {
    transform: none;
  }
}

/* While a drag is running: no text selection anywhere, and the grab cursor
   everywhere rather than only over the handle. */
.is-reordering {
  cursor: grabbing;
  user-select: none;
}

.is-reordering * {
  cursor: grabbing !important;
}


/* Two-layer heading (取込プレビュー). The upper row names the vendor's own 科目,
   the lower one the school's — see components/grid.html.

   Both rows are sticky, and both inherit top:0 from .c-grid__cell--head: without
   the offset below they land on top of each other and the vendor's row — the
   half a teacher checks the printout against — vanishes on the first scroll. The
   height is fixed here rather than measured, because a sticky offset cannot be
   derived from a sibling row. */
.c-grid__cell--group {
  top: 0;
  height: var(--grid-group-row-h);
  text-align: center;
  border-bottom: none;
}

.c-grid--grouped .c-grid__cell--head:not(.c-grid__cell--group) {
  top: var(--grid-group-row-h);
}

/* The seat and the name stay put while the 科目 columns scroll under them
   (取込プレビュー). An opaque ground is required, not decoration: a transparent
   sticky cell lets the columns show through as they pass beneath it.

   The widths are fixed (tokens.css) because a sticky `left` cannot be measured
   off the column beside it. Text WRAPS rather than truncating: a name is the
   thing being checked, and an ellipsis in this column would defeat the screen.

   Wrapping has to be asked for HERE. `.c-grid__cell` sets white-space:nowrap for
   every cell of every grid, so this comment described an intention the base rule
   silently overrode — a name too long for the column ran UNDER the next one
   instead of wrapping, and the two names a teacher is comparing overlapped. */
.c-grid__cell--pin {
  position: sticky;
  left: 0;
  width: var(--grid-pin-1);
  min-width: var(--grid-pin-1);
  max-width: var(--grid-pin-1);
  z-index: 2;
  background-color: var(--card-bg);
}

.c-grid__cell--pin-last {
  left: var(--grid-pin-1);
  width: var(--grid-pin-2);
  min-width: var(--grid-pin-2);
  max-width: var(--grid-pin-2);
  /* The row grows instead of the name being cut. Longer than the column is rare
     — 11rem holds the longest of a real 30-sheet batch — but a name that does
     not fit must still be readable in full. */
  white-space: normal;
  overflow-wrap: anywhere;
  /* Where the page stops and the scrolling half begins.

     Drawn as an inset shadow, NOT as border-right: the table is
     border-collapse:collapse, so a border belongs to the GRID rather than to
     the cell — it stays behind at the column's original position and the line
     vanishes the moment this column sticks. An inset shadow is painted with the
     element, so it travels with it.

     The second shadow says the columns pass UNDER this edge rather than beside
     it. Both are the same declaration because box-shadow does not cascade. */
  box-shadow:
    inset calc(-1 * var(--border-width-thin)) 0 0 0 var(--border),
    2px 0 4px -2px rgb(0 0 0 / 18%);
}

.c-grid__cell--head.c-grid__cell--pin {
  /* Above both the rows it outranks and the heading row it sits in. */
  z-index: 3;
  background-color: var(--grid-head-bg);
}

.c-grid__cell--rowlabel {
  text-align: right;
  color: var(--muted);
  font-weight: var(--font-weight-regular);
}

/* A column whose 科目 the school does not have yet — heading and figures alike:
   the point is that these numbers exist and are going nowhere.

   The SAME red as the row of a student the 生徒マスタ does not have, because it
   is the same sentence: this is not in the master yet (PM, 2026-08-29). Yellow
   is kept for the other state entirely — you changed something and have not
   saved it.

   Two classes, not one: .c-grid__cell--head sets its own background and is
   declared further down this file, so at equal specificity the heading of an
   undecided column came out the ordinary head colour and only its body was
   tinted — the column read as decided from the one row a person looks at. */
.c-grid__cell.c-grid__cell--undecided {
  background-color: var(--refused-bg);
}

.c-grid__head-select {
  width: 100%;
  /* Back to the narrow column (PM, 2026-08-29): this control sits over a column
     of 得点 and must not be wider than the numbers under it. A 科目名 longer
     than the box still runs close to the ▼ — a <select> ignores text-overflow
     in Chrome — which is the price of the narrow column, paid knowingly. */
  min-width: 4.5rem;
  /* The same ▼, drawn smaller: this one sits in a table heading, not on a
     filter row, and the app's 8px triangle read as the heaviest mark in the
     row. Half the size, and the two halves move in with it — they must stay
     exactly `background-size` apart or the triangle splits in two. */
  background-size: var(--space-xs) var(--space-xs);
  background-position:
    calc(100% - var(--space-s)) center,
    calc(100% - var(--space-xs)) center;
  /* Longhands: the END padding is the arrow's room and belongs to the rule that
     draws it. A `padding` shorthand here would win on source order and take it
     back, leaving the ▼ sitting on the text. A smaller arrow needs less. */
  padding-inline-end: var(--space-m);
  /* The same block and start padding .c-field select has, so the control is
     the height it is everywhere else in the app and its label stands off the
     left edge the same way (PM, 2026-08-29). Only the END differs, because the
     arrow here is smaller and needs less room. */
  padding-block: var(--space-xs);
  padding-inline-start: var(--space-s);
  white-space: nowrap;
  border: var(--border-width-thin) solid var(--border);
  border-radius: var(--radius);
  background-color: var(--card-bg);
  color: var(--foreground);
  font: inherit;
  font-weight: var(--font-weight-regular);
  /* The control is furniture over a column of numbers; it should not be taller
     than the row it labels. */
  line-height: 1.3;
}

/* The global focus ring is blue (app.css), and a blue box around a red or a
   yellow one makes the control two colours at once — the same reason
   --warning-halo exists for the 配点 field. Focus stays VISIBLE; it just wears
   the control's own colour. Removing it outright would leave a keyboard on this
   screen with no way to see which column it is in, and this control is the
   whole screen. */
.c-grid__head-select:focus-visible {
  /* The global ring is blue AND sits outside the rim this control already has,
     so focus read as a second frame. One shadow instead: the rim, plus a halo
     in the control's own colour. box-shadow does not cascade, so each state
     restates its rim here — a state that forgot to would lose it on focus. */
  outline: none;
  box-shadow: 0 0 0 3px var(--select-halo);
}

.c-grid__head-select.is-undecided:focus-visible {
  box-shadow:
    inset 0 0 0 1px var(--refused-mark),
    0 0 0 3px var(--refused-halo);
}

.c-grid__head-select.is-changed:focus-visible {
  box-shadow:
    inset 0 0 0 1px var(--pending-mark),
    0 0 0 3px var(--pending-halo);
}

.c-grid__head-select.is-undecided {
  border-color: var(--refused-mark);
  /* Same weight as the changed rim, and by the same trick: the inset shadow
     doubles the line without widening the box, so a column does not shift when
     its state changes. */
  box-shadow: inset 0 0 0 1px var(--refused-mark);
  background-color: var(--refused-bg);
}

/* A row this import will NOT write (取込プレビュー). It sits in the same table
   as the rest, so the ground is what separates them — and the sentence under it
   says why, because a colour cannot. */
.c-grid__row--refused > .c-grid__cell {
  background-color: var(--refused-bg);
}

/* An empty cell draws its — in --muted, the grey of an ordinary row, which
   reads as a hole in this one. Inherit instead: the row's ink is the app's own
   (8.4:1 on this ground) and every value in the row shares it. */
.c-grid__row--refused .c-grid__muted {
  color: inherit;
}

/* A 志望校 an 上書き would change (取込プレビュー・ADR-037). Same idea as the
   refused row and a quieter ground: it is not a problem, it is the line the
   teacher's decision is about. */
.c-grid__row--changed > .c-grid__cell {
  background-color: var(--changed-bg);
}

/* セルの2行目（core.grid.Cell.sub）。1行目と同じものの別の読みかたで、
   取込プレビューは 得点（偏差値）の下に 校内順位 を置く——業者の刷る成績概況が
   そう並べているので、教員が手元の紙と突き合わせられる。

   罫線で分ける（PM, 2026-08-30）。数字が2つ縦に並ぶだけでは、下が何の数字なのか
   ——順位なのか、去年の得点なのか——が読み取れない。 */
/* **札と同じ高さの箱**（PM, 2026-09-09）。試験一覧の 状態 列は札の下にこの行が来るので、
   22px の札の下に 19px の行が座ると、揃えたはずの2つが揃って見えない。中身ではなく箱を
   合わせる: 上下に同じ余白と同じ1px、下の1pxは透明——上の1pxは2つを分ける罫線で、下は
   高さを合わせるためだけに在る。行が折り返せばそのぶん伸びる（高さは固定しない）。 */
/* モーダルの説明文は文節で折る。日本語は語の途中でも改行できてしまうので、幅が
   足りないと「公開しま／せん。」で切れる（PM, 2026-09-10）。文そのものは1行に収まる
   長さに直してあり、これは窓を狭めたときの保険である。対応していないブラウザは
   これまでどおり折り返すだけで、はみ出しはしない。 */
.c-modal .c-field__help,
.c-modal p {
  word-break: auto-phrase;
}


.c-grid__cell-sub {
  display: block;
  margin-top: var(--space-xxs);
  padding-block: var(--space-xxs);
  border-top: var(--border-width-thin) solid var(--border);
  border-bottom: var(--border-width-thin) solid transparent;
  color: var(--muted);
  font-size: var(--font-size-caption1);
  line-height: var(--line-height-caption1);
}

/* 二行目そのものが押せるとき（試験一覧の 公開日時）。<button> の既定を全部脱いで、
   札と同じ高さ・同じ位置に座る——並んだ行の二行目が、押せる行だけ1px ずれない。
   色だけが押せることを言う。 */
.c-grid__cell-sub--action {
  width: 100%;
  padding-inline: 0;
  border-inline: 0;
  background: none;
  color: var(--link);
  font-family: inherit;
  text-align: inherit;
  cursor: pointer;
}

.c-grid__cell-sub--action:hover {
  text-decoration: underline;
}


/* 「大学マスタに未登録」 beside the name it is about. Its own line, because the
   cell may also carry a 登録 button and the three read as a sentence. */
.c-grid__cell-note {
  display: block;
  font-size: var(--font-size-caption2);
  color: inherit;
}

/* A decision a person ticks above the table it is about (取込プレビュー's
   志望校上書き). Not .c-field: that one is a labelled input in a form's column,
   and this is one sentence with a box in front of it. */
.c-check {
  display: grid;
  gap: var(--space-xxs);
  padding: var(--space-s) var(--space-m);
  border: var(--border-width-thin) solid var(--border);
  border-radius: var(--radius);
  background-color: var(--card-bg);
}

.c-check > label {
  display: flex;
  align-items: center;
  gap: var(--space-xs);
  font-weight: var(--font-weight-medium);
}

/* Indented under the label so the sentence reads as belonging to the box, and
   not as a second thing to tick. */
.c-check__note {
  padding-left: calc(var(--space-xs) + 1em);
  font-size: var(--font-size-caption1);
  color: var(--muted);
}

.c-grid__row-note > td {
  padding: var(--space-xxs) var(--space-m) var(--space-s);
  background-color: var(--refused-bg);
  border-bottom: var(--border-width-thin) solid var(--border);
  font-size: var(--font-size-caption1);
}

/* The row it explains keeps its 席 and 氏名 in place while the 科目 scroll past;
   the sentence under it has to do the same, or a teacher who has scrolled to
   the twelfth column is looking at a coloured band with nothing written on it. */
.c-grid__row-note__text {
  position: sticky;
  left: 0;
  display: inline-block;
}

/* The link takes the row's ink and keeps its underline. NOT the app's link
   blue: on this ground that is 3.5:1, under the 4.5 floor for text. Underline
   is what marks it as a link then (WCAG 1.4.1 wants something besides colour,
   and this is it). */
.c-grid__row-note a {
  margin-left: var(--space-s);
  color: inherit;
  text-decoration: underline;
}

.c-grid__row-actions {
  display: inline-flex;
  align-items: center;
  gap: var(--space-xs);
}

/* Takes the line off the list — nothing is written either way. Quiet until it
   is reached for: a control that is loud about removing something invites the
   press it is asking you to confirm. */
.c-grid__row-drop {
  border: var(--border-width-thin) solid transparent;
  border-radius: var(--radius);
  padding: 0 var(--space-xs);
  background: none;
  color: var(--muted);
  font: inherit;
  line-height: 1.6;
  cursor: pointer;
}

.c-grid__row-drop:hover,
.c-grid__row-drop:focus-visible {
  border-color: var(--danger);
  color: var(--danger);
}

/* Changed, and not written yet. Nothing else on the screen moves to say so —
   the choice is not posted until 確定 — so the ring IS the feedback.

   The ink stays as it is: the thing inside is a 科目名 somebody has to read.
   The fill is the undecided column's hue taken stronger — one "needs your
   attention" colour on this table, and depth tells the two states apart: a pale
   column nobody has decided, a solid box somebody just did (PM, 2026-08-29).

   The inset shadow doubles the border's weight WITHOUT widening the box — a
   border that grows on change would nudge every column beside it. */
.c-grid__head-select.is-changed {
  border-color: var(--pending-mark);
  box-shadow: inset 0 0 0 1px var(--pending-mark);
  background-color: var(--pending-bg);
}

.c-grid__head-select:disabled {
  background-color: var(--grid-head-bg);
  color: var(--muted);
}

/* Sticky so a scrolled row still has its column names. The opaque fill is
   required, not decoration: a transparent sticky header lets the rows show
   through as they pass under it. */
.c-grid__cell--head {
  position: sticky;
  top: 0;
  z-index: 1;
  /* Zero because on a LIST grid the padding belongs to the button inside
     (.c-grid__head-toggle, components/grid_column.html): the whole cell has to
     be the sort target, so the padding is the button's or it is a dead strip
     along the edge of a control. */
  padding: var(--space-none);
  background-color: var(--grid-head-bg);
  /* Bold (PM 2026-09-07). It was medium, on the reasoning below — that ink plus
     bold would make the name compete with the figures under it. In use the
     opposite complaint arrived: on a wide table the heading row read as one more
     row of data, and a teacher scanning for a column had to find the tinted band
     first. Bold is what says "this row names the others". */
  font-weight: var(--font-weight-strong);
  /* Ink, not grey (PM, 2026-08-23). 13.10:1 on --grid-head-bg.

     A column name was --muted while the row it sits in was white, on the
     reasoning that the heading is furniture and the data is the content. The
     tinted ground changes that twice over: it already separates the row from the
     data without help from the type, and --muted on it is 4.26:1 — under the 4.5
     body floor, on the label a whole column is read through. Both point the same
     way, so the name is simply legible now and the ground does the separating.

     (The weight this paragraph argued for was medium; see the note above it.) */
  color: var(--foreground);
}

.c-grid__head {
  position: relative;
  height: 100%;
}

/* A column that neither sorts nor filters still needs the padding the toggle
   would have given it — without this its label runs straight into the next
   heading. */
.c-grid__head-static {
  display: flex;
  align-items: center;
  height: 100%;
  padding: var(--space-none) var(--space-m);
}

/* The whole heading is the control — a 2px-padded cell has no room for a label
   and a separate affordance beside it, and Excel makes the heading itself the
   button too. */
.c-grid__head-toggle {
  display: flex;
  align-items: center;
  gap: var(--space-xs);
  width: 100%;
  height: 100%;
  padding: var(--space-none) var(--space-m);
  border: none;
  background: none;
  font: inherit;
  color: inherit;
  cursor: pointer;
  white-space: nowrap;
}

.c-grid__cell--num .c-grid__head-toggle {
  justify-content: flex-end;
}

/* The heading LIGHTS UP; the ground stays put (PM, 2026-08-29). A hover that
   repainted the cell had to be loud enough to show through a row that is
   already tinted, and at that strength it read as a selected column rather than
   a pointer passing over. The label answers instead, in the same three moments
   the caret appears — and the caret follows it, because both are currentColor.

   --primary-dark and not --primary: 5.73:1 on the tinted heading against
   --primary's 4.46:1, which is under the body floor for the one word a whole
   column is read through. */
.c-grid__cell--head:hover .c-grid__head-toggle,
.c-grid__head-toggle:focus-visible,
.c-grid__head-toggle[aria-expanded="true"] {
  color: var(--primary-dark);
}

/* One heading, one mark.

   A column that was both sorted and filtered used to carry three glyphs at once
   — ↓ for the sort, ▼ for the filter, ⌄ for the menu — three shapes all pointing
   down, with three different meanings, inside a 21px cell (PM, 2026-08-23). And
   ▼ is Excel's OPEN-the-filter button, so the mark meant to say "rows are
   hidden" read as "press here" to anyone who has used a spreadsheet.

   So the filter no longer draws anything in the heading. What it narrowed to is
   spelled out in words, in the chips above the table (components/grid.html), and
   the heading only says which column carries a condition — by weight, which
   competes with nothing. Notion and Airtable both keep this state above the
   table rather than in the header; Excel does put it in the header, but as ONE
   mark that CHANGES (▼ becomes a funnel) rather than as a second one added. */
.c-grid__head-toggle.is-sorted {
  color: var(--primary);
}

/* 絞り込みが掛かっている列の印。**色**（PM 2026-09-07）。

   ここは bold だった。見出しそのものが bold になった日に、その印は消えた——絞り込んだ列と
   そうでない列が同じ字になる。印を保つには、見出しが使っていない差でなければならない。

   青は .is-sorted と同じ色で、区別は▲が付くかどうか: 並べ替え=青＋▲、絞り込み=青のみ、
   両方=青＋▲。どちらが掛かっているかは、上の絞り込みチップが名前で言っている。 */
.c-grid__head-toggle.is-filtered {
  color: var(--primary);
}

/* The sort mark is drawn, not typed. A filled triangle survives 11px in a way an
   arrow does not — it is a solid shape rather than two hairlines and a head, so
   it stays legible when the browser has half a pixel to work with (PM,
   2026-08-23) — and ▲/▼ as CHARACTERS are a lottery: the weight and the baseline
   come from whichever fallback font a school PC resolves them in, and some
   render them as colour emoji. Borders draw the same silhouette everywhere.

   The 4px/5px are the triangle's own proportions, like the scrollbar thumb's
   3px in tokens.css: a token here would make the shape follow a number that has
   nothing to do with it. */
.c-grid__arrow {
  flex: none;
  width: 0;
  height: 0;
  border-inline: 4px solid transparent;
}

.c-grid__arrow--asc {
  border-bottom: 5px solid currentColor;
}

.c-grid__arrow--desc {
  border-top: 5px solid currentColor;
}

/* The caret is an affordance, not a state, so it appears when the pointer or the
   keyboard arrives and the resting heading carries only its label. `visibility`
   rather than `display`, so the label does not shift sideways when it appears.

   It also sits BESIDE the label now instead of being pushed to the far edge by
   `margin-inline-start: auto`: on a 290px 大学名 column that put the caret 250px
   from the word it belongs to, which is two halves of one button.

   DRAWN, not typed, and the same triangle the ▼ on a <select> is — one shape in
   this app for "this one opens" (PM, 2026-08-29). It used to be a ⌄ character
   at --muted, which is the lottery the sort mark above already refuses: the
   weight and the baseline came from whichever fallback font a school PC
   resolved it in, and it read as a faint smudge beside a solid arrow.
   --border-strong is the weight every other "opens" mark in the app carries. */
.c-grid__caret {
  visibility: hidden;
  flex: none;
  width: 0;
  height: 0;
  border-inline: 4px solid transparent;
  border-top: 5px solid currentColor;
}

/* A heading that is just words gets its padding back. Ten hand-written tables
   in this app (出典, 合否の内訳, 選考, ギャップ分析 …) reuse .c-grid for its look
   without the sort machinery, and every one of them had its first column's name
   flat against the left edge of the tinted row while the cells below it sat
   12px in (PM, 2026-08-23).

   :has() and not a modifier class, so the rule follows the FACT — is there a
   control in here — rather than asking ten call sites to remember a flag. Only
   the inline padding: the row's fixed height owns the block side, and a table
   that unlocks that height (.source-table) sets its own. */
.c-grid__cell--head:not(:has(.c-grid__head-toggle)) {
  padding-inline: var(--space-m);
}

.c-grid__cell--head:hover .c-grid__caret,
.c-grid__head-toggle:focus-visible .c-grid__caret,
.c-grid__head-toggle[aria-expanded="true"] .c-grid__caret {
  visibility: visible;
}

/* Anchored to the heading and lifted above the sticky row. Start-aligned, and
   pulled back inside the scroll box at the end of the row by the modifier the
   last columns carry. */
.c-grid__menu {
  /* VIEWPORT coordinates, set by grid_column.html on open. Absolute positioning
     put the menu inside .c-grid__scroll, which is an overflow:auto box with a
     max-height — on a table scrolled near its bottom the filter list was cut off
     and its 適用 button could not be reached. z-index does not help: the clip
     comes from the scroll box, not from stacking order. */
  position: fixed;
  z-index: 3;
  /* ch, not a px token: the menu has to fit a prefecture name beside a checkbox,
     which is a measure in characters and follows the font if it ever changes. */
  min-width: 24ch;
  padding: var(--space-xs);
  border: var(--border-width-thin) solid var(--border);
  border-radius: var(--radius);
  background-color: var(--card-bg);
  box-shadow: var(--shadow-card);
  font-weight: var(--font-weight-regular);
  color: var(--foreground);
  text-align: start;
}

/* A numeric column's menu no longer needs its own side: fixed positioning
   already keeps every menu inside the window (grid_column.html clamps it). */

.c-grid__menu-sort {
  display: flex;
  align-items: center;
  gap: var(--space-s);
  padding: var(--space-xs) var(--space-s);
  border-radius: var(--radius);
  color: inherit;
  text-decoration: none;
  white-space: nowrap;
}

.c-grid__menu-sort:hover {
  background-color: var(--accent);
}

/* The direction currently in force reads as chosen, not as another option. */
.c-grid__menu-sort.is-active {
  background-color: var(--accent);
  color: var(--primary);
  font-weight: var(--font-weight-medium);
}

.c-grid__menu-rule {
  margin: var(--space-xs) var(--space-none);
  border: none;
  border-top: var(--border-width-thin) solid var(--border);
}

.c-grid__menu-needle {
  width: 100%;
  padding: var(--space-xxs) var(--space-xs);
  border: var(--border-width-thin) solid var(--border);
  border-radius: var(--radius);
  font-size: var(--font-size-caption1);
}

.c-grid__menu-bulk {
  display: flex;
  gap: var(--space-m);
  padding: var(--space-xs) var(--space-xxs);
}

.c-grid__menu-bulk button {
  border: none;
  background: none;
  padding: var(--space-none);
  color: var(--primary);
  font-size: var(--font-size-caption1);
  cursor: pointer;
}

/* Ten rows, then it scrolls — stated as ten line-heights rather than a pixel
   count so the cap tracks the type scale. */
.c-grid__menu-list {
  max-height: calc(var(--line-height-body1) * 10);
  overflow-y: auto;
  display: flex;
  flex-direction: column;
  gap: var(--space-xxs);
}

.c-grid__menu-item {
  display: flex;
  align-items: center;
  gap: var(--space-s);
  white-space: nowrap;
  cursor: pointer;
}

.c-grid__menu-actions {
  display: flex;
  align-items: center;
  gap: var(--space-m);
  margin-top: var(--space-xs);
  padding-top: var(--space-xs);
  border-top: var(--border-width-thin) solid var(--border);
}

.c-grid__menu-apply {
  padding: var(--space-xxs) var(--space-s);
  border: var(--border-width-thin) solid var(--border);
  border-radius: var(--radius);
  background-color: var(--card-bg);
  font-size: var(--font-size-caption1);
  cursor: pointer;
}

.c-grid__menu-clear,
.c-grid__note {
  font-size: var(--font-size-caption1);
}

/* A table of few, short columns: the frame keeps the width it was given, and the
   COLUMNS stop at their content.

   Dividing the row evenly is right for a wide table — 大学一覧 has nine columns and
   a name that runs — but wrong for six short values: they end up a hundred pixels
   apart and reading one row means tracking across the page (PM, 2026-08-23, ⑥採点's
   roster: 組, 出席番号, 氏名, 点, 進捗, 状態).

   Each cell asks for 1% and gets its content's width instead — the table cannot
   honour six 1% columns, and a cell that may not wrap is at least as wide as its
   text. The LAST column asks for 100% and so absorbs everything left over, which
   is what keeps the frame where the layout put it (`auto` here changes nothing:
   with every other column at 1% it is divided among them too).

   Only where the caller asks for it (`fit=1`). */
.c-grid--fit .c-grid__cell {
  width: 1%;
}

.c-grid--fit .c-grid__cell:last-child {
  width: 100%;
}

/* The other way to stop a short table from stretching: the frame comes in with the
   columns, so the right-hand border lands beside the last one instead of at the edge of
   the screen.
   
   `fit` above keeps the frame where the layout put it and gives the slack to the last
   column — right when the table has to line up with something else (⑥採点's spread sits
   under two cards of the same width). `shrink` is for a table that answers to nothing
   else: ⑦レビュー's roster, where the last column is a pair of buttons and stretching it
   leaves a hand's width of empty row between 状態 and the frame (PM, 2026-08-24).

   The PANEL shrinks too, or the frame and the zebra keep the old width and the table
   floats inside an empty box. :has() rather than a second flag at the call site — the
   panel follows the table it holds. */
.c-grid--shrink {
  width: auto;
}

.c-grid__panel:has(.c-grid--shrink) {
  width: fit-content;
  max-width: 100%;
}

/* No zebra. It was there to help the eye stay on one row across a wide table,
   which is a real job — but the horizontal rule now does it, and the two
   together left the hover invisible on every second row: the stripe was
   --background and the hover --accent, two greys a shade apart. One tint that
   follows the pointer beats a fixed pattern that fights it. */
.c-grid tbody tr:hover {
  background-color: var(--accent);
}

.c-grid__muted,
.c-grid__note {
  color: var(--muted);
}

.c-grid__note {
  margin: var(--space-none);
}

/* Metrics only — the bar itself is .c-filter-bar (app.css), the same one the
   dashboard's 学年 / 組 / 模試 row uses, so the two cannot drift apart. */
.c-grid__search {
  margin-bottom: var(--space-s);
}

/* A search box is a NAME box: wide enough to read what was typed, and the same
   width whatever is in it, so pressing 検索 does not resize the row. Sized like
   .c-field--wide's floor next door, but on the input rather than on a select. */
.c-field--search input[type="text"],
.c-field--search input[type="search"] {
  min-width: calc(var(--space-xxxl) * 8);
}

/* Faint, and rounded like every other field on the screen — which as of
   2026-08-23 means literally the same --border, not a quieter version of a
   heavier one. The square corner went with the square cells (PM). It was the
   last thing making the 検索 box look like it belonged to a different
   application than the box beside it. */
.c-grid__menu-needle,
.c-grid__menu-apply {
  border: var(--border-width-thin) solid var(--border);
  border-radius: var(--radius);
}

/* 絞り込みチップ — what this grid is narrowed to, in words, above the table.

   The condition used to live only inside the column menu, marked on the heading
   by a single ▼. That is invisible on a screen of eight columns, and it defeats
   the thing the query string was built for: a teacher sends a filtered URL to a
   colleague, and the colleague opens a list that is missing rows for no stated
   reason. Now the reason is on the screen, in the same words the menu offered,
   and each one can be taken off on its own.

   A chip is a LINK, not a form control: it goes to the same URL 絞り込み解除
   goes to, minus one column. So it works with the back button and needs no
   JavaScript. */
.c-grid__chips {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-xs) var(--space-s);
}

.c-grid__chip {
  display: inline-flex;
  align-items: center;
  gap: var(--space-xs);
  padding: var(--space-xxs) var(--space-xxs) var(--space-xxs) var(--space-s);
  border: var(--border-width-thin) solid var(--info-border);
  border-radius: var(--radius);
  background-color: var(--accent);
  font-size: var(--font-size-caption1);
  color: var(--foreground);
}

.c-grid__chip-column {
  font-weight: var(--font-weight-strong);
}

/* The × is the control; the chip around it is a label. Sized so the target is a
   real one rather than a glyph — a filter you cannot take off is worse than no
   chip at all. */
.c-grid__chip-drop {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: var(--space-l);
  height: var(--space-l);
  border-radius: var(--radius);
  color: var(--muted);
  text-decoration: none;
  line-height: 1;
}

.c-grid__chip-drop:hover {
  background-color: var(--card-bg);
  color: var(--foreground);
}

.c-grid__chip-drop:focus-visible {
  outline: var(--border-width-thick) solid var(--focus-ring);
  outline-offset: var(--space-xxs);
}

/* One block: note, search, pager and grid read as a single panel, so the gaps
   between them are the grid's own and not the page stack's. */
.c-grid__panel {
  display: flex;
  flex-direction: column;
  gap: var(--space-xs);
}

/* ── 上の入口の帯（PM, 2026-09-22 / 2026-09-24 に共通化）──────────────────────
   課題／答案の一覧 のタブが左、＋答案をアップロード が右端（段ではなく道具）。
   点数（exams/partials/board_tabs.html）と添削（correction/partials/tabs.html）が
   同じ形で、規則は `sheet_board.css` に置いていた——その CSS を読まない画面
   （correction/step1.html）ではタブとボタンが縦に積まれていた（実機 2026-09-24）。
   部品が2つのアプリのものなら、規則も共通の層に置く。 */
.sb-tabs {
  display: flex;
  flex-wrap: nowrap;
  width: 100%;
  align-items: center;
  gap: var(--space-s);
  border-bottom: var(--border-width-thin) solid var(--border);
  padding-bottom: var(--space-xs);
}

.sb-tabs .c-navtabs { margin-bottom: 0; }

.sb-tabs__ok { margin-left: 4px; color: var(--success, #15803d); font-weight: 700; }

.sb-tabs__upload { margin-left: auto; font-size: var(--font-size-caption1); padding: 4px 10px; }

/* ── A drill-down's top row (PM, 2026-08-10) ─────────────────────────────────
   Breadcrumb on the left, 戻る on the right. Every screen you reach by drilling
   INTO another one carries this row, and the button always says exactly 戻る —
   a label naming its destination makes one control read differently on every
   screen, and a return control parked under the content makes a reader scroll a
   whole table to leave. */
.c-detail-topbar {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-s);
}

/* 段の道（PM, 2026-09-24）。来た道は控えめに、いま開いている画面の名前をはっきりと。
   既定のリンク（青＋下線）のままでは、1つ上の「試験」のほうが現在地より目立っていた。
   区切りは薄い記号にして前後に息を入れる——詰まっていると名前の一部に見える。
   （`statements/statements.css` の段の道が先にこの形。ここはその共通版。） */
.c-detail-topbar > .text-caption {
  margin: 0;
  min-width: 0;
  color: var(--foreground);
  font-weight: var(--font-weight-medium);
}

.c-detail-topbar > .text-caption a {
  color: var(--muted);
  font-weight: var(--font-weight-regular);
  text-decoration: none;
}

.c-detail-topbar > .text-caption a:hover,
.c-detail-topbar > .text-caption a:focus-visible {
  color: var(--primary);
  text-decoration: underline;
}

.c-detail-topbar > .text-caption [aria-hidden="true"] {
  margin: 0 0.15em;
  color: var(--border-strong);
}

/* ── The facts band at the top of a detail screen ────────────────────────────
   What the thing IS on the left, the handful of numbers that say how far it is
   set up on the right. A card rather than loose text: it is the answer to "did
   somebody finish this?", which is the question the drill-down is opened with. */
.c-facts-band {
  display: flex;
  flex-wrap: wrap;
  align-items: flex-end;
  justify-content: space-between;
  gap: var(--space-m);
  padding: var(--space-m) var(--space-l);
  border: var(--border-width-thin) solid var(--border);
  border-radius: var(--radius);
  background-color: var(--card-bg);
}

.c-facts-band__title {
  font-size: var(--font-size-subtitle1);
  line-height: var(--line-height-subtitle1);
  font-weight: var(--font-weight-strong);
}

.c-facts-band__facts {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-l);
}

.c-facts-band__facts dt {
  color: var(--muted);
}

.c-facts-band__facts dd {
  font-weight: var(--font-weight-medium);
}

/* ── The two sides of a code-mapping grid ────────────────────────────────────
   科目コード対応表 is one comparison: what the vendor calls a subject, and what
   the school calls it. The grid draws the columns (it is components/grid.html
   like every other list) and this rule draws the seam between the two sides:
   one heavier rule before 自校の科目, and nothing else — the heading tint that
   used to help it went on 2026-08-23, for the reason recorded below.

   Positional, because a Column carries no class: the first two cells are the
   vendor's, the third is the school's. That pairing is stated in
   ingestion.provider_codes.SUBJECT_MAPPING_COLUMNS — reorder those and this
   moves with them. Nothing depends on it being right: get it wrong and the
   table is plainer, not broken. */
/* The seam. A tint would have to fight the zebra striping to be seen, so the
   two sides are told apart by one rule down the middle instead — the same
   weight a spreadsheet uses for a frozen-pane edge. */
.c-mapping-grid .c-grid__cell:nth-child(3) {
  border-left: var(--border-width-thick) solid var(--primary-light);
}

/* The tint that used to mark the vendor's own two columns is gone, and both
   halves of its reasoning went with it.

   It said the seam would otherwise read as "a stray heavy gridline". That was
   written when every column in this grid carried a vertical rule, so a heavier
   one had to be explained; the rules were removed on 2026-08-23 and the blue
   line is now the ONLY vertical mark in the table — it cannot be mistaken for a
   gridline, because there are none.

   And it painted --accent, which since the palette change is one step off
   --grid-head-bg. Two columns tinted #eff6ff beside five tinted #eef3f8 is not a
   grouping anybody can see; it is a rendering artefact. Better one honest line
   than a distinction too small to read. */

/* ── Tabs ────────────────────────────────────────────────────────────────────
   Two components, because they are two different controls that happen to look
   alike:

   .c-navtabs — links. Pressing one LOADS ANOTHER SCREEN, and which one you are
   on is `aria-current="page"`. The header's main menu and the マスタ switcher.

   .c-tabs — buttons. Pressing one shows another panel of the SAME screen, and
   which one is `aria-selected` inside a `role="tablist"`. 志望校, カルテ, 成績票,
   学年.

   Before this there were five hand-written strips with five stylesheets, and
   the one nobody looked at twice (the マスタ switcher) was the one missing its
   corner. */

.c-navtabs {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-xs);
}

/* The gap under a strip belongs to the strips that stand ALONE — the header's
   own, and the マスタ switcher, which is a direct child of <main> with nothing
   to space it. A strip inside a stack is spaced by that stack, and carrying
   both put 32px under the 学年 chips on 試験一覧 (PM, 2026-08-23). */
.c-navtabs--underline,
.site-main > .c-navtabs {
  margin-bottom: var(--space-l);
}

.c-navtabs__tab {
  padding: var(--space-xxs) var(--space-s);
  border: var(--border-width-thin) solid var(--border);
  border-radius: var(--radius);
  color: var(--muted);
  font-size: var(--font-size-caption1);
  line-height: var(--line-height-caption1);
  font-weight: var(--font-weight-medium);
  text-decoration: none;
  white-space: nowrap;
}

.c-navtabs__tab:hover {
  border-color: var(--primary-light);
  color: var(--primary);
}

/* Marked by the fill AND the border, never by colour alone (WCAG 1.4.1). */
.c-navtabs__tab[aria-current="page"] {
  border-color: var(--primary);
  background-color: var(--accent);
  color: var(--primary-dark);
}

/* How many rows the tab leads to. Same badge as .c-tabs__count next door —
   the two strips are the same control in two mechanisms (link / panel), and a
   reader should not be able to tell which one they are looking at.

   `.c-navtabs__tab` is an <a>, so the badge needs the inline-block and the gap
   that the flex `.c-tabs__tab` gets for free. */
.c-navtabs__tab {
  display: inline-flex;
  align-items: center;
  gap: var(--space-s);
}

.c-navtabs__count {
  padding: var(--space-none) var(--space-s);
  border-radius: var(--radius);
  background: var(--background);
  color: var(--muted);
  font-size: var(--font-size-caption1);
  font-variant-numeric: tabular-nums;
}

.c-navtabs:not(.c-navtabs--underline):not(.c-navtabs--chips) .c-navtabs__tab[aria-current="page"] .c-navtabs__count {
  background: var(--primary-dark);
  color: var(--primary-foreground);
}

/* ── The boxed strip: one frame around the GROUP ──────────────────────────────
   資料管理's two tabs and the マスタ switcher. They used to be a row of separate
   outlined pills, each carrying its own frame, and the chosen one was marked by
   turning its frame --primary — a hard blue rectangle in a screen whose every
   other edge is a hairline (PM, 2026-08-23).

   Now the strip itself is the box, in the same pale --border every card and
   table frame uses, and the chosen tab is a filled segment inside it. Which is
   what .c-tabs--segmented has always done for the カルテ's panel tabs: the app
   had two "pick one of these" strips that looked nothing alike, and this is the
   one that was out of step.

   Scoped with :not() rather than a new modifier so no call site has to learn a
   flag — components/nav_tabs.html has exactly one, `underline`, and the header's
   strip is the thing it names. The four rules below all sit at a higher weight
   than the plain ones above, so they win regardless of source order, and the
   two hover rules are deliberately weighted under the states they must not
   overwrite (see .c-tabs__tab--active:hover for the full version of that trap).

   1.4.1 is still satisfied without the coloured frame: the chosen tab differs by
   a filled shape and an inverted label, not by hue, and `aria-current="page"`
   carries it for anyone not reading either. */
.c-navtabs:not(.c-navtabs--underline):not(.c-navtabs--chips) {
  width: fit-content;
  gap: var(--space-xxs);
  padding: var(--space-xxs);
  border: var(--border-width-thin) solid var(--border);
  border-radius: var(--radius);
  background-color: var(--card-bg);
}

.c-navtabs:not(.c-navtabs--underline):not(.c-navtabs--chips) .c-navtabs__tab {
  padding: var(--space-xs) var(--space-m);
  border-color: transparent;
  background-color: transparent;
}

.c-navtabs:not(.c-navtabs--underline):not(.c-navtabs--chips) .c-navtabs__tab:hover {
  border-color: transparent;
  background-color: var(--background);
  color: var(--foreground);
}

.c-navtabs:not(.c-navtabs--underline):not(.c-navtabs--chips) .c-navtabs__tab[aria-current="page"] {
  border-color: var(--primary);
  background-color: var(--primary);
  color: var(--primary-foreground);
}

.c-navtabs:not(.c-navtabs--underline):not(.c-navtabs--chips) .c-navtabs__tab[aria-current="page"]:hover {
  border-color: var(--primary-dark);
  background-color: var(--primary-dark);
  color: var(--primary-foreground);
}

/* The header's own strip: one line, scrolled sideways rather than wrapped —
   a wrapped strip changes the header's height with the viewport, and that
   height is the token every sticky offset is measured against.
   `scrollbar-width: none` because the strip is short and a permanent bar under
   ten tabs reads as a broken layout; it still scrolls by wheel, trackpad and
   keyboard focus. */
.c-navtabs--underline {
  display: flex;
  flex-wrap: nowrap;
  gap: var(--space-none);
  margin-bottom: var(--space-none);
  overflow-x: auto;
  scrollbar-width: none;
  border-top: var(--border-width-thin) solid var(--border);
}

.c-navtabs--underline::-webkit-scrollbar {
  display: none;
}

/* An underline, not a box — so no border and no corner. Two boxed strips
   stacked read as one confused control, which is why only one of the two is
   boxed. */
.c-navtabs--underline .c-navtabs__tab {
  padding: var(--space-s) var(--space-m);
  border: none;
  border-bottom: var(--border-width-thick) solid transparent;
  border-radius: var(--radius-grid);
}

.c-navtabs--underline .c-navtabs__tab:hover {
  color: var(--foreground);
  background-color: var(--background);
}

/* 帯と字を別のトークンで持つ（tokens.css）。片方は面で、片方は白地の上で読まれる
   ——2px の帯は形が「ここに居る」と言っていて（隣のタブには帯が無い）、字はその
   言葉を読ませる。プロトタイプも同じ2つで、帯 #00bc7d・字 #009966 である。 */
.c-navtabs--underline .c-navtabs__tab[aria-current="page"] {
  background-color: transparent;
  color: var(--tab-active-label);
  border-bottom-color: var(--tab-active-bg);
}

.c-tabs {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-s);
}

/* The chip look, worn by two mechanisms: .c-tabs__tab is a <button> that swaps
   a panel, and .c-navtabs--chips is an <a> that reloads the screen with a
   narrower list. They must be indistinguishable on screen — a reader is picking
   one of these, and which mechanism carries it is not their problem — so the
   values live here once instead of being copied into a second block that would
   drift (PM, 2026-08-23: the 試験一覧 学年 strip is to look like /classes/). */
.c-tabs__tab,
.c-navtabs--chips .c-navtabs__tab {
  display: inline-flex;
  align-items: center;
  gap: var(--space-s);
  padding: var(--space-s) var(--space-l);
  border: var(--border-width-thin) solid var(--border);
  border-radius: var(--radius);
  background: var(--card-bg);
  font-family: inherit;
  font-size: var(--font-size-body1);
  font-weight: var(--font-weight-medium);
  color: var(--muted);
  white-space: nowrap;
  cursor: pointer;
  transition:
    background var(--duration-fast) ease,
    border-color var(--duration-fast) ease,
    color var(--duration-fast) ease;
}

.c-tabs__tab:hover,
.c-navtabs--chips .c-navtabs__tab:hover {
  border-color: var(--primary-light);
  color: var(--foreground);
}

.c-tabs__tab--active,
.c-navtabs--chips .c-navtabs__tab[aria-current="page"] {
  background: var(--primary);
  border-color: var(--primary);
  color: var(--primary-foreground);
}

/* Same trap as .c-tabs__tab--active:hover below — `:hover` outweighs a lone
   attribute selector, so without this the chosen chip kept its blue fill and
   took dark ink on it. */
.c-navtabs--chips .c-navtabs__tab[aria-current="page"]:hover {
  background: var(--primary-dark);
  border-color: var(--primary-dark);
  color: var(--primary-foreground);
}

.c-navtabs--chips {
  gap: var(--space-s);
}

.c-navtabs--chips .c-navtabs__tab[aria-current="page"] .c-navtabs__count {
  background: var(--primary-dark);
  color: var(--primary-foreground);
}

/* The same specificity trap as the segmented variant below, and this one was
   worse than ugly: `.c-tabs__tab:hover` is a class plus a pseudo-class and
   `.c-tabs__tab--active` is one class, so hovering the selected tab replaced its
   WHITE label with --foreground while leaving the blue fill in place. Dark ink
   on the primary blue is 2.83:1 — under the 4.5 floor, on the one tab the
   teacher is most likely to be pointing at. */
.c-tabs__tab--active:hover {
  background: var(--primary-dark);
  border-color: var(--primary-dark);
  color: var(--primary-foreground);
}

/* A count riding on a tab (学年 carries how many 組 it holds). */
.c-tabs__count {
  padding: var(--space-none) var(--space-s);
  border-radius: var(--radius);
  background: var(--background);
  color: var(--muted);
  font-size: var(--font-size-caption1);
  font-variant-numeric: tabular-nums;
}

.c-tabs__tab--active .c-tabs__count {
  background: var(--primary-dark);
  color: var(--primary-foreground);
}

/* Segmented: the strip itself is the box and the tabs inside it are not, so a
   row of them reads as ONE control with a chosen segment rather than as a row
   of separate chips. Scrolls sideways because カルテ carries seven tabs. */
.c-tabs--segmented {
  flex-wrap: nowrap;
  gap: var(--space-xs);
  padding: var(--space-xs);
  border: var(--border-width-thin) solid var(--border);
  border-radius: var(--radius);
  overflow-x: auto;
  /* Not decoration. The カルテ panel is a fixed-height flex column, so when a
     tab's content is taller than the panel flexbox takes the space back from
     whatever CAN shrink — and an item whose overflow is not `visible` resolves
     min-height:auto to 0, so this strip could shrink away entirely. That is
     what happened to the tab bar when ギャップ分析 was opened. */
  flex: none;
}

.c-tabs--segmented .c-tabs__tab {
  flex: none;
  border-color: transparent;
  background: transparent;
}

.c-tabs--segmented .c-tabs__tab:hover {
  border-color: transparent;
  background: var(--background);
  color: var(--foreground);
}

.c-tabs--segmented .c-tabs__tab--active {
  border-color: var(--tab-active-bg);
  background: var(--tab-active-bg);
  color: var(--tab-active-ink);
}

/* Pointing at the tab you are already on must not un-choose it.

   This is a specificity bug, not a taste one, and it is worth naming because it
   is invisible in the source order. `.c-tabs--segmented .c-tabs__tab:hover` is
   three classes; `.c-tabs--segmented .c-tabs__tab--active` is two — a
   pseudo-class weighs the same as a class — so the hover rule OUTRANKS the
   selected one no matter which is written first. The chosen segment lost its
   fill and its white label the moment the pointer touched it, and came back
   when the pointer left (PM, 2026-08-23).

   It was always broken; it became obvious when --primary went blue, because the
   state it was dropping to used to be a similar dark and is now plainly a
   different colour.

   So the selected tab gets its own hover, at the same weight as the rule that
   was beating it and after it. It DARKENS — the same step .c-button--primary
   takes when pressed — because "you are here" and "this is still pressable" are
   both true and neither may cancel the other. */
.c-tabs--segmented .c-tabs__tab--active:hover {
  border-color: var(--tab-active-bg-hover);
  background: var(--tab-active-bg-hover);
  color: var(--tab-active-ink);
}

/* ── One card on an otherwise empty page ─────────────────────────────────────
   The shape of a screen that is a single short block and nothing else: the
   login form, the パスワードの設定 screen, and the page a 403 or a 404 lands on.

   It moved here from accounts/static/accounts/login.css, which is where it was
   written for the login screen (216e73f). password_setup.html was already
   borrowing it across app boundaries — its own comment says so — and the error
   page made three, which is one past the rule (CLAUDE.md:「2画面目が欲しがったら
   共通化する」). What stayed behind in login.css is what is actually login's:
   how wide the card is, and the two tweaks its form needs.

   The card is centred in whatever the viewport has left under the sticky
   header. `min-height`, not `height`, so a card with a long error list grows the
   page instead of overflowing it; `100dvh` rather than `100vh` so a mobile URL
   bar sliding away does not shift it. */
.c-solo {
  display: grid;
  place-items: center;
  min-height: calc(100dvh - var(--layout-solo-reserve));
}

.c-solo__card {
  display: flex;
  flex-direction: column;
  gap: var(--space-xl);
  width: 100%;
  /* Less above and below than at the sides: these cards are short stacks, and
     equal padding on four sides left more air over the heading than the content
     under it was tall. */
  padding: var(--space-xxl) var(--space-xxxl);
  border: var(--border-width-thin) solid var(--border);
  border-radius: var(--radius);
  background-color: var(--card-bg);
  /* Floating on an empty page, so the overlay elevation and not --shadow-card,
     which is for a surface lying flat in the content column (tokens.css). */
  box-shadow: var(--shadow-overlay);
}

/* A phone gives the card most of its width, so the side padding that framed it
   on a desktop is just less room for the fields. */
@media (max-width: 480px) {
  .c-solo__card {
    padding: var(--space-l) var(--space-xl);
  }
}

/* ── A detail screen's head ──────────────────────────────────────────────────
   Every drill-down opened with the same question — WHAT am I looking at? — and
   the answer used to be a strip of dt/dd with no heading at all: 試験詳細 named
   the exam only in the browser tab (#229 review). One card answers it: what
   kind of thing this is, its name, and the facts that identify it, in that
   order and always in that order.

   The card ends where the actions begin. Controls belong to the screen, not to
   the identity of the thing, and mixing them is what made the old screen read
   as one undifferentiated column. */
.c-page-head {
  display: flex;
  flex-direction: column;
  gap: var(--space-s);
  padding: var(--space-m) var(--space-l);
  border: var(--border-width-thin) solid var(--border);
  border-radius: var(--radius);
  background-color: var(--card-bg);
}

.c-page-head__identity {
  display: flex;
  flex-direction: column;
  gap: var(--space-xxs);
}

/* The badge sits ABOVE the name, not beside it: 種別 is a category and the name
   is the thing, so stacking them keeps the name the largest word on the page
   however long the badge's label gets. */
.c-page-head__kind {
  display: flex;
  align-items: center;
  gap: var(--space-xs);
}

.c-page-head__title {
  font-size: var(--font-size-subtitle1);
  line-height: var(--line-height-subtitle1);
  font-weight: var(--font-weight-strong);
}

/* ── The identifying facts of one record ────────────────────────────────────
   A grid, not a wrapping cluster. Nine facts in a cluster relayout into ragged
   rows at every width and the eye has to re-find the label each time; fixed
   tracks keep 実施日 under 実施日 no matter how the window moves. auto-fit so a
   narrow window folds to fewer columns instead of scrolling sideways. */
.c-deflist {
  display: grid;
  /* Narrower tracks than the first cut: eight facts wrapped onto three rows at
     the old minimum, and a header taller than the data under it is a header
     nobody asked for. */
  grid-template-columns: repeat(auto-fit, minmax(calc(var(--space-xxxl) * 3), 1fr));
  gap: var(--space-s) var(--space-l);
  padding-top: var(--space-s);
  border-top: var(--border-width-thin) solid var(--border);
}

/* Label ABOVE value, and deliberately not beside it. Side-by-side was tried and
   is worse here: the labels are different lengths, so `模試業者 kawai` fitted on
   one line while `模試コード 266061012` wrapped onto two, and a row where some
   pairs are one line and some are two reads more cluttered than a row where all
   of them are two. Uniform beats shortest.

   The saving comes from the leading instead: a 12px label on a 16px line over
   its value, with no gap between the pair and a small one between rows. */
.c-deflist > div {
  min-width: 0;
}

/* The same grid standing on its own rather than under a card's heading. */
.c-deflist--card {
  padding: var(--space-l);
  border: var(--border-width-thin) solid var(--border);
  border-radius: var(--radius);
  background-color: var(--card-bg);
}

/* ── Facts in two deliberate rows ────────────────────────────────────────────
   `auto-fit` puts as many facts on a line as happen to fit, which is right when
   the facts are peers. On the import preview they are not: the first four
   answer "what is about to be written" — the numbers a teacher presses 確定 on
   — and the last four answer "where did this file come from". A wrap point
   decided by the window width would split that pairing somewhere different at
   every size, so this variant fixes the columns and the grouping holds.

   Four columns: 実施日 and 年度 are two facts and read as two, which puts four
   in each row exactly. Eight tracks across would have every date and filename
   wrapping, which is what the fixed count is here to prevent. */
.c-deflist--two-rows {
  /* `auto`, not `1fr`: equal tracks stretched 3年 and 3名 across a third of the
     card each and left the row mostly empty. Content-sized tracks packed to the
     start put the slack at the right edge instead, where it is margin rather
     than a hole between every pair. The two rows still line up, because a grid
     column is as wide as the widest item in it — whichever row that is. */
  grid-template-columns: repeat(4, auto);
  justify-content: start;
  column-gap: var(--space-xxxl);
}

/* Same 40rem the header's user chip uses (app.css). Two columns keeps the pairs
   adjacent — one column would turn eight facts into eight rows. */
@media (max-width: 40rem) {
  .c-deflist--two-rows {
    grid-template-columns: repeat(2, auto);
  }

}

.c-deflist dt {
  flex: none;
  color: var(--muted);
  font-size: var(--font-size-caption1);
  line-height: var(--line-height-caption1);
}

.c-deflist dd {
  /* A grid track does not shrink below its content by default, so one long
     value — a vendor's filename — pushed itself over the next column instead of
     wrapping inside its own. */
  overflow-wrap: anywhere;
  font-weight: var(--font-weight-medium);
}

/* One LINE of facts instead of a grid of cells — for the identity card of a
   screen whose facts are read as a sentence ("化学 · 5年 · 2026年度 · 2026-11-25")
   rather than looked up one at a time (PM, 2026-08-16). Same markup, so a screen
   changes its mind by adding one class.

   The dot is a separator, not a bullet: it is drawn on the WRAPPER's start edge
   and the first one is dropped, which is what keeps it out of the reading order
   of a screen reader as well. */
.c-deflist--inline {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-xs) var(--space-m);
  align-items: baseline;
}
.c-deflist--inline > div {
  display: inline-flex;
  align-items: baseline;
  gap: var(--space-xs);
}
.c-deflist--inline > div + div::before {
  content: "";
  align-self: center;
  width: var(--space-xxs);
  height: var(--space-xxs);
  margin-inline-end: var(--space-xs);
  border-radius: var(--radius-circular);
  background-color: var(--border-strong);
}


/* ── A section heading inside a data screen ─────────────────────────────────
   Not h2's 20px: these sit directly over grids whose cells are caption-size, and
   at subtitle1 the label shouts over the numbers it introduces. 大学詳細 settled
   on this size first (university-detail__subheading); the import preview is the
   second screen to want it, so it moved here. */
.c-section-heading {
  font-size: var(--font-size-body1-strong);
  line-height: var(--line-height-body1-strong);
  font-weight: var(--font-weight-strong);
}


/* ── file drop: click or drag onto one zone (components/file_drop.html) ─────
   手順①の資料スロットが持っていた形。答案アップロードが2画面目に欲しがったので
   ここへ移した（PM, 2026-08-16 / リポジトリの規約どおり）。 */
.c-drop {
  display: flex;
  flex-direction: column;
  gap: var(--space-xs);
  padding: var(--space-m);
  border: var(--border-width-thin) solid var(--border);
  border-radius: var(--radius);
}

/* もう使えない枠。消さずに残すのは、この画面が「答案を足すところ」であることは
   変わらないから——枠ごと消すと、機能が閉じたのか画面が壊れたのかを言わなくなる。
   受け口としては死んでいるので、光ることも、押されることもない。 */
.c-drop--blocked {
  background-color: var(--background);
  color: var(--muted);
}

/* ファイルを運んでいる指の下だけ光る。空手のときの破線は、ただの模様。 */
.c-drop--over {
  border-color: var(--blue);
  box-shadow: 0 0 0 calc(var(--border-width-thin) * 3) var(--accent);
}

/* ボタンは自分の幅で。枠は答案アップロードでは画面いっぱいの1枚になるので、
   伸ばすと「ファイルを追加」が1200pxの帯になる。 */
.c-drop > .c-button {
  align-self: flex-start;
}

/* ── 落とし口 ────────────────────────────────────────────────────────────
   まだ空で、物を受けるために空いている枠。実線は「ここに何かが入っている」箱の描き方
   なので、破線でそれを字より先に言う。中央に寄せるのは、絵とボタンが縦に並ぶから
   ——左揃えのまま絵を足すと、絵・字・ボタンが別々の左端を持つ列になる。

   破線は 2px（--border-width-thick）。1px の破線は刻みが 3px しかなく、遠目には点線に
   見えて、枠なのか影なのか読めなかった（PM 2026-09-07）。太らせると刻みも伸びる
   ——Chrome の破線は線の太さから刻みを決めるので、長さは太さでしか動かせない。

   ③答案の取り込み（答案を落とす1枚）と ①資料（問題／模範解答の2枚）が同じ形を使う。

   ボタンの中央寄せは `.c-drop > .c-button` と同じ強さなので、**あとに**書く。 */
.c-drop--zone {
  align-items: center;
  border-style: dashed;
  border-width: var(--border-width-thick);
  gap: var(--space-xs);
  text-align: center;
}

.c-drop--zone > .c-button {
  align-self: center;
}

/* 絵は飾りなので、字より弱い色で。大きさは呼び手が決める（既定 20px）。 */
.c-drop--zone > svg {
  color: var(--muted);
}

/* 補足の中の、割ってはいけない一語（components/file_drop.html の note_nowrap）。
   幅がいくつでも、字がいくつでも、この一語は1行に収まる。字を縮めて「いまの幅なら
   たまたま割れない」ようにするのは直したことにならない——列が狭くなればまた割れる。 */
.c-drop__nowrap {
  white-space: nowrap;
}

/* 枠の名前。見出しとして読む——「どちらのファイルか」が最初の一語。 */
.c-drop__label {
  display: flex;
  align-items: center;
  gap: var(--space-s);
}

.c-drop__name {
  font-size: var(--font-size-subtitle2);
  line-height: var(--line-height-subtitle2);
  font-weight: var(--font-weight-strong);
  color: var(--foreground);
}

/* 任意 / 必須 — 枠の契約。ここでは言葉のまま（フォーム項目の必須印は赤い * で、
   あちらは列に何度も出るもの、こちらは1画面に1度読むもの）。 */
.c-drop__tag {
  padding: var(--space-xxs) var(--space-s);
  border: var(--border-width-thin) solid var(--border);
  border-radius: var(--radius);
  color: var(--muted);
  font-size: var(--font-size-caption1);
  line-height: var(--line-height-caption1);
  white-space: nowrap;
}

.c-drop__tag--need {
  border-color: var(--danger-border);
  color: var(--danger);
}

.c-drop__hint,
.c-drop__note {
  color: var(--muted);
  font-size: var(--font-size-caption1);
  line-height: var(--line-height-caption1);
}

.c-drop__status {
  font-size: var(--font-size-caption1);
  color: var(--muted);
}

.c-drop__status--ok {
  color: var(--success);
}


/* ── 資料管理 の カード ────────────────────────────────────────────────────
   The screen's own head used to be here — a title beside the アップロード /
   アップロード履歴 strip. Both are gone: the title said the screen's name a
   second time (the menu above is already lit), and with it gone the strip is
   one more item in the page's stack. */
/* One card per kind of material. auto-fit rather than a fixed three: the five
   kinds are a list that will grow, and a hand-set column count turns every
   addition into a layout edit. */
.c-doccards {
  display: grid;
  /* 320px, not 280: at 280 a wide window fits four columns and the longest
     label (カリキュラム・シラバス) wraps in every one of them. */
  grid-template-columns: repeat(auto-fit, minmax(320px, 1fr));
  gap: var(--space-m);
  margin: var(--space-none);
  padding: var(--space-none);
  list-style: none;
}

/* The cards arrive one after another, top-left to bottom-right.

   The reveal itself is .animate-slide-up, the design system's own (src.css:
   0.3s, fade and 12px up) — the same one the home screen's cards use, and it is
   defined once there rather than restated here. All this adds is the offset, so
   the group reads as one movement with a direction instead of five things
   appearing at the same instant.

   `backwards` is not optional: without it a delayed card paints at full opacity
   first and then jumps back to the start of its own animation.

   Reduced motion is already handled — the block near .c-toast sets
   `animation: none` on .animate-slide-up, which leaves every card at its final
   state and makes these delays inert (a delay cannot start an animation that
   has no name). Only longhands are set here for exactly that reason: an
   `animation:` shorthand this far down the file would re-enable what that block
   turned off.

   Six selectors for five kinds (ingestion.catalog.DOCUMENT_KINDS). A sixth card
   would arrive with the fifth rather than a beat later, which is a smaller
   mistake than a rule per card forever. */
.c-doccards > .animate-slide-up {
  animation-fill-mode: backwards;
}

.c-doccards > .animate-slide-up:nth-child(2) {
  animation-delay: var(--stagger-step);
}

.c-doccards > .animate-slide-up:nth-child(3) {
  animation-delay: calc(var(--stagger-step) * 2);
}

.c-doccards > .animate-slide-up:nth-child(4) {
  animation-delay: calc(var(--stagger-step) * 3);
}

.c-doccards > .animate-slide-up:nth-child(5) {
  animation-delay: calc(var(--stagger-step) * 4);
}

.c-doccards > .animate-slide-up:nth-child(n + 6) {
  animation-delay: calc(var(--stagger-step) * 5);
}

.c-doccard {
  display: flex;
  align-items: flex-start;
  gap: var(--space-m);
  padding: var(--space-l);
  border: var(--border-width-thin) solid var(--border);
  border-radius: var(--radius);
  background-color: var(--card-bg);
  box-shadow: var(--shadow-card);
  /* Named on the RESTING state, not inside :hover, so the card eases back down
     when the pointer leaves instead of snapping. .card-hover in src.css puts it
     in the hover rule and travels one way only; the values here are the same
     tokens, so the two cannot drift. */
  transition:
    box-shadow var(--duration-fast) ease,
    border-color var(--duration-fast) ease,
    transform var(--duration-fast) ease;
}

/* Ready to take a file, and it says so by answering the pointer: a 1px lift and
   the blue-tinted shadow the whole design system uses for "this is pressable"
   (--shadow-card-hover, the demo's own values). A 準備中 card deliberately does
   not move — that difference is the screen telling you which cards are alive
   before you have read a single word. */
.c-doccard:not(.c-doccard--soon):hover {
  /* The lift glows in the CARD's hue, not in the app's blue (PM, 2026-08-23).
     --shadow-card-hover is the same two layers with rgb(37 99 235) baked in
     (tokens.css), which left a purple card leaning toward the cursor in blue —
     the one moment the card is being reached for is the worst moment to change
     its colour.

     Mixed here rather than named in tokens.css: that file holds VALUES, and
     five more hover shadows would be five more values to keep in step with five
     hues that already exist. A blue card comes out at exactly the token it used
     to read, so nothing moved for 模試成績表. */
  box-shadow:
    /* The ring: a second 1px of the hue OUTSIDE the border, so the outline
       reads as thickening under the pointer. A spread shadow and not a wider
       border-width, which would resize the card and nudge everything in the
       row by a pixel. */
    0 0 0 var(--border-width-thin) color-mix(in srgb, var(--doccard-ink, var(--primary)) 45%, transparent),
    0 4px 12px color-mix(in srgb, var(--doccard-ink, var(--primary)) 16%, transparent),
    0 2px 4px color-mix(in srgb, var(--doccard-ink, var(--primary)) 8%, transparent);
  transform: translateY(calc(var(--border-width-thin) * -1));
}

/* A card that can take a file is outlined in its OWN hue — the same colour as
   its tile, its 対応形式 and its button (PM, 2026-08-23). That is the whole card
   saying one thing: this is the purple kind, and here is where its purple
   control is.

   Only the cards that WORK. A 準備中 card keeps the neutral --border, so the
   outline is also the answer to 「どれが使えるのか」 from across the room, before
   any label has been read — the same job the missing shadow does.

   The hover rule above no longer names a border colour for that reason: it would
   have turned a purple card blue on the way to being clicked. */
.c-doccard:not(.c-doccard--soon) {
  border-color: var(--doccard-ink, var(--border));
}

/* No importer yet. The card stays — a screen that lists only what works cannot
   say whether a file has nowhere to go or whether the button is just hard to
   find — and it says so by going quiet: no elevation, no answer to the pointer,
   no control on it at all, and the word 準備中 for anyone who cannot see those
   (.c-drop--blocked makes the same argument about a dead drop zone).

   What it no longer does is drain the colour out. The card used to take a grey
   fill AND grey out its icon tile, and since four of the five kinds have no
   importer yet, that left one coloured tile on the whole screen — a 資料管理
   that reads as switched off, for a room full of teenagers (PM, 2026-08-23).
   Quiet is now carried by the type and by the missing shadow, and the hue stays
   to tell one kind from another. */
.c-doccard--soon {
  box-shadow: none;
  color: var(--muted);
}

/* One hue per kind, declared on the CARD and read by everything on it.

   The tints used to be the STATUS colours (--success-bg for カリキュラム and so
   on), which was the only way to colour a card while the palette held nothing
   decorative — and it meant a card wore a sentence it was not saying. They are
   the decorative five now (tokens.css --deco-*), approved 2026-08-23.

   The modifier sits on the card rather than on the tile because the tile is not
   the only thing that wants the hue: 対応形式 takes it too. Two custom
   properties, set once here, and any part of the card can read either. */
.c-doccard--blue {
  --doccard-ink: var(--deco-blue);
  --doccard-tint: var(--deco-blue-bg);
}

.c-doccard--purple {
  --doccard-ink: var(--deco-purple);
  --doccard-tint: var(--deco-purple-bg);
}

.c-doccard--emerald {
  --doccard-ink: var(--deco-emerald);
  --doccard-tint: var(--deco-emerald-bg);
}

.c-doccard--amber {
  --doccard-ink: var(--deco-amber);
  --doccard-tint: var(--deco-amber-bg);
}

.c-doccard--pink {
  --doccard-ink: var(--deco-pink);
  --doccard-tint: var(--deco-pink-bg);
}

.c-doccard__icon {
  display: flex;
  align-items: center;
  justify-content: center;
  flex-shrink: 0;
  width: var(--space-xxxl);
  height: var(--space-xxxl);
  border-radius: var(--radius);
  /* The fallbacks are what a card with no hue gets, and they are the app's own
     accent — a kind added without a modifier renders quietly rather than
     unstyled. */
  background-color: var(--doccard-tint, var(--accent));
  color: var(--doccard-ink, var(--primary));
}

/* A 準備中 card keeps its tint — see .c-doccard--soon above for why the override
   that used to grey it out is gone. */

.c-doccard__body {
  display: flex;
  flex-direction: column;
  gap: var(--space-xs);
  min-width: 0;
  /* Fill the card's height so the action row can sit at the BOTTOM of it. The
     cards in a row are already the same height (grid stretch); without this the
     body is only as tall as its own text, and a card with a one-line
     description parks its button halfway up while its neighbour's sits lower
     (PM, 2026-08-23). The icon keeps `align-items: flex-start` from the card. */
  align-self: stretch;
  flex: 1;
}

/* The action row, pushed to the bottom edge of the body — so every card on the
   row offers its controls on the same line, whatever the description above them
   ran to. `auto` and not a fixed spacer: the gap above stays the body's own. */
.c-doccard__body > .l-cluster {
  margin-top: auto;
}

.c-doccard__title {
  display: flex;
  align-items: center;
  gap: var(--space-s);
  font-size: var(--font-size-subtitle2);
  line-height: var(--line-height-subtitle2);
  font-weight: var(--font-weight-strong);
  color: var(--foreground);
}

.c-doccard--soon .c-doccard__title {
  color: var(--muted);
}

/* 準備中. A word, not a colour — the card is already dim, and a reader who
   cannot see the dimming still has to be told. */
.c-doccard__badge {
  padding: var(--space-xxs) var(--space-s);
  border: var(--border-width-thin) solid var(--border);
  border-radius: var(--radius);
  color: var(--muted);
  font-size: var(--font-size-caption1);
  line-height: var(--line-height-caption1);
  font-weight: var(--font-weight-regular);
  white-space: nowrap;
}

.c-doccard__description {
  color: var(--muted);
  font-size: var(--font-size-caption1);
  line-height: var(--line-height-caption1);
}

/* 対応形式, in the card's own hue.

   It was --muted, on the reasoning that this line is read only after the
   description has said what the card is for, so it should be the quietest thing
   on the card. That is true of the READING order and wrong about the SCANNING
   one: a teacher arrives at this screen holding a file, and 「PDF は使えるのか」
   is the question they came with. The reference design colours it for the same
   reason.

   All five --deco-* hues clear 4.5:1 on their own tile fill and more than that
   on the card's white, so this is a letter and is allowed to be one. */
.c-doccard__formats {
  color: var(--doccard-ink, var(--muted));
  font-size: var(--font-size-caption1);
  line-height: var(--line-height-caption1);
}

/* The card's control wears the card's hue, like the tile and 対応形式 above it
   (PM, 2026-08-23): 答案をアップロード is purple because 解答用紙・答案 is the
   purple card, and a teacher who comes back for it is looking for that colour
   rather than reading five labels.

   Nothing moves for 模試成績表 — --deco-blue IS --primary — so this is a rule
   about the cards that are NOT blue, written once for all of them rather than
   as a special case for one card.

   Every --deco-* hue clears 4.5:1 against white (tokens.css records each), so
   the label stays a label. The fallback is --primary, which is what a kind
   added without a tone modifier already gets everywhere else on the card. */
.c-doccard .c-button--primary {
  background-color: var(--doccard-ink, var(--primary));
}

.c-doccard .c-button--primary:hover,
.c-doccard .c-button--primary:active {
  /* One step darker, derived from the hue itself — the palette holds a --dark
     for the app's blue and for no other colour, and inventing five more tokens
     for one hover state is five more things to keep in step. */
  background-color: color-mix(in srgb, var(--doccard-ink, var(--primary)) 85%, black);
}

/* ── 対応ファイル形式 — the same answer, indexed by file rather than by 資料種別.
   A teacher holding a file asks "can I send THIS?", which the cards answer only
   after they have picked a kind. */
.c-filetypes {
  display: flex;
  flex-direction: column;
  gap: var(--space-m);
  padding: var(--space-l);
  border: var(--border-width-thin) solid var(--border);
  border-radius: var(--radius);
  background-color: var(--card-bg);
}

.c-filetypes__list {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
  gap: var(--space-s);
  margin: var(--space-none);
  padding: var(--space-none);
  list-style: none;
}

.c-filetypes__item {
  display: flex;
  align-items: center;
  gap: var(--space-m);
  padding: var(--space-m);
  border-radius: var(--radius);
  background-color: var(--background);
  color: var(--primary);
}

.c-filetypes__item--soon {
  color: var(--muted);
}

.c-filetypes__text {
  display: flex;
  flex-direction: column;
  gap: var(--space-xxs);
  min-width: 0;
}

.c-filetypes__label {
  color: var(--foreground);
  font-size: var(--font-size-caption1);
  line-height: var(--line-height-caption1);
  font-weight: var(--font-weight-strong);
}

.c-filetypes__item--soon .c-filetypes__label {
  color: var(--muted);
}


/* ── Standing notices, floating in the top-right ───────────────────────────
   They cost the page NO height (PM, 2026-08-23): the screens they sit on are
   about the table underneath, and a notice in the flow pushed that table down
   whether it was a band or a row.

   Below the drill-down topbar, not level with it — every one of these screens
   puts 戻る in that corner, and a notice that covers a control is worse than
   the band it replaced. That is the whole reason for the offset, and why it is
   measured like --layout-toast-top rather than guessed.

   A column, because a screen can have more than one thing to say: the 取込
   プレビュー stacks 「全員そろっています」 under 「対応表にない科目コード」. Right
   aligned, so each is only as wide as its own words. */
.c-float-stack {
  position: fixed;
  top: var(--layout-float-notice-top);
  right: var(--space-l);
  /* Under the toasts (z 120): a flash confirming the very save that changed a
     notice may sit over it for its five seconds, after which the notice is
     there and updated. */
  z-index: 110;
  display: flex;
  flex-direction: column;
  align-items: flex-end;
  gap: var(--space-s);
  max-width: calc(100% - var(--space-xxl));
  pointer-events: none;
}

.c-float-stack > * {
  pointer-events: auto;
  box-shadow: var(--shadow-overlay);
}

/* A state that is true until somebody fixes it, said in one line.
   Not a toast — a toast is a one-shot flash about something that just happened,
   goes away on a timer, and carries no controls. Not a callout either: this
   screen is about the table underneath, and the callout this replaced spent
   ~100px of the first screenful on a state that is usually empty.
   Floating it was tried and dropped: fixed to the viewport it covered 戻る,
   anchored to the panel it covered the first rows of the table (the partial
   records both). */
.c-inline-notice {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-s);
  /* As wide as its own words. It floats over the page, so every extra
     millimetre is content it hides for no reason. */
  width: fit-content;
  padding: var(--space-xs) var(--space-s) var(--space-xs) var(--space-m);
  border: var(--border-width-thin) solid var(--warning-border);
  border-radius: var(--radius);
  background-color: var(--warning-bg);
}

.c-inline-notice__title {
  color: var(--warning);
  font-size: var(--font-size-caption1);
  line-height: var(--line-height-caption1);
  font-weight: var(--font-weight-strong);
  white-space: nowrap;
}

/* The quiet half of the same component: 「全員そろっています」 is reassurance, not
   a caution, and it appears on the same row as the cautions. A modifier rather
   than a second component — one shape, two tones. */
.c-inline-notice--success {
  border-color: var(--success-border);
  background-color: var(--success-bg);
}

.c-inline-notice--success .c-inline-notice__title {
  color: var(--success);
}

/* components/not_yet.html — a screen the prototype has but no table backs YET:
   an empty slot WITH A SHAPE, the same principle as 判定不可. It names what is
   missing instead of inventing numbers to fill itself.

   Lived in guidance.css until 2026-09-09, where only the カルテ could reach it.
   生徒の 進捗確認 wants the same block for 週別学習達成率 and 学習遅れの検知
   (neither has a table), so the partial moved to components/ and its rules
   moved with it — CLAUDE.md's promotion rule is a MOVE, not a copy. */
.not-yet {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: var(--space-s);
  padding: var(--space-xxl);
  border: var(--border-width-thick) dashed var(--border-strong);
  border-radius: var(--radius);
  background: var(--background);
}

.not-yet__message {
  margin: 0;
  font-size: var(--font-size-subtitle2);
  line-height: var(--line-height-subtitle2);
  font-weight: var(--font-weight-medium);
}

.not-yet__description,
.not-yet__blocker {
  margin: 0;
}

/* ── Stat cards ────────────────────────────────────────────────────────────
   A label over a big figure. Moved here from home.css on 2026-09-09 when
   試験一覧 became the second caller (CLAUDE.md: 2画面目が欲しがったら共通化する).
   Cards: components/stat_card.html, and the クラスの成績 偏差値 card
   (partials/class_performance_card.html), which adds a 前回比 beside the figure.
   The surface itself is .stat-card from tools/tailwind/src.css; this is the
   arrangement inside it. */

/* One row, equal columns however many cards there are; the phone breakpoint
   stacks them instead of shaving each to a quarter of a narrow screen. */
.c-stat-row {
  display: grid;
  gap: var(--space-l);
  grid-auto-flow: column;
  /* minmax(0, …), not 1fr: a bare 1fr never shrinks below its content, so four
     cards with a 前回比 pushed the row past its card between 641px and 900px. */
  grid-auto-columns: minmax(0, 1fr);
}

@media (max-width: 640px) {
  .c-stat-row {
    grid-auto-flow: row;
  }
}

.c-stat {
  display: flex;
  flex-direction: column;
  gap: var(--space-xs);
  /* A card that is a link keeps the card's ink: the figure and the label are
     the words, and blue underlined text across a whole card is not a card. */
  color: inherit;
  text-decoration: none;
}

a.c-stat:hover,
a.c-stat:focus-visible {
  box-shadow: var(--shadow-card-hover);
}

/* The leading rule marks a number that is WORK waiting, as against a count of
   stored things. */
.c-stat--act {
  border-left: var(--space-xxs) solid var(--primary);
}

.c-stat__label {
  color: var(--muted);
  font-size: var(--font-size-body1);
  line-height: var(--line-height-body1);
}

/* baseline, not center: the unit is body text next to a 24px figure and
   centering floats it off the number's feet. */
.c-stat__figure {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: var(--space-s);
}

.c-stat__value {
  font-size: var(--font-size-title3);
  line-height: var(--line-height-title3);
  font-weight: var(--font-weight-strong);
}

/* The figure's colour says what kind of number it is, the same four tones the
   status badge uses: warning = waiting for YOU, danger = out of time. */
.c-stat__value--info { color: var(--info); }
.c-stat__value--warning { color: var(--warning); }
.c-stat__value--success { color: var(--success); }
.c-stat__value--danger { color: var(--danger); }

.c-stat__unit {
  color: var(--muted);
  font-size: var(--font-size-body1);
  line-height: var(--line-height-body1);
}

.c-stat__sub {
  color: var(--muted);
  font-size: var(--font-size-caption1);
  line-height: var(--line-height-caption1);
}

.c-stat__sub--danger {
  color: var(--danger);
  font-weight: var(--font-weight-medium);
}

.c-stat__sub--success {
  color: var(--success);
  font-weight: var(--font-weight-medium);
}

/* A stat card whose label leads with a tile（生徒ホーム: ✓ 校 締 評）. */
.c-stat__label:has(.c-tile) {
  display: flex;
  align-items: center;
  gap: var(--space-s);
}

/* ── Tile ──────────────────────────────────────────────────────────────────
   One character in a small tinted square that names what a block is about
   （✓ 今日のタスク・校 志望校・書 出願書類）. Two callers on 生徒ホーム: the stat
   card's label and a card's heading. The tone is the same vocabulary as the
   figure's, plus purple for a number that is none of the four（評定平均）. */
.c-tile {
  display: inline-grid;
  place-items: center;
  flex: none;
  width: 26px;
  height: 26px;
  border-radius: var(--radius);
  font-size: var(--font-size-caption1);
  font-weight: var(--font-weight-strong);
}

.c-tile--success { background: var(--success-bg); color: var(--success); }
.c-tile--info { background: var(--info-bg); color: var(--info); }
.c-tile--warning { background: var(--warning-bg); color: var(--warning); }
.c-tile--danger { background: var(--danger-bg); color: var(--danger); }
.c-tile--purple { background: var(--deco-purple-bg); color: var(--deco-purple); }

/* The delta under a figure (#191, クラスの成績). The colour is the design
   system's up/down pair; only the arrow-next-to-number arrangement is here.
   Marks and text take the INK variant: the reference hues reach only 2.58:1 on
   --card-bg, fine under a bar with a legend and not fine for a number. */
.c-stat__delta {
  display: inline-flex;
  align-items: center;
  gap: var(--space-xs);
  font-size: var(--font-size-body1);
  line-height: var(--line-height-body1);
  font-weight: var(--font-weight-strong);
}

.c-stat__delta--up { color: var(--viz-above-ink); }
.c-stat__delta--down { color: var(--viz-below-ink); }

/* ── on / off を1行で ─────────────────────────────────────────────────────
   真偽の1つを、札と摘みで言う。`components/form_field.html` を通すと見出しが上の
   行に立ち、四角だけが下に残る——「終日」のように**読んで即決める**ものは、字と
   摘みが同じ行にいないと目が往復する（PM 2026-09-17）。

   本物の checkbox は残したまま隠す（`.u-visually-hidden` と同じやり方）。消すと
   キーボードで届かなくなり、form も値を送らない。摘みは兄弟の <span> が描く。 */
.c-toggle {
  display: inline-flex;
  align-items: center;
  gap: var(--space-s);
  cursor: pointer;
}

.c-toggle > input {
  position: absolute;
  width: 1px;
  height: 1px;
  margin: -1px;
  padding: var(--space-none);
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
}

.c-toggle__track {
  position: relative;
  flex: none;
  width: 36px;
  height: 20px;
  border-radius: var(--radius-pill);
  background: var(--border-strong);
  transition: background-color var(--duration-fast) ease;
}

.c-toggle__thumb {
  position: absolute;
  top: 2px;
  left: 2px;
  width: 16px;
  height: 16px;
  border-radius: var(--radius-circular);
  background: var(--card-bg);
  transition: transform var(--duration-fast) ease;
}

.c-toggle > input:checked + .c-toggle__track {
  background: var(--primary);
}

.c-toggle > input:checked + .c-toggle__track .c-toggle__thumb {
  transform: translateX(16px);
}

/* 輪は札が描く。入力そのものは隠してあり、隠れた箱の周りの輪は誰にも見えない
   （`.c-segmented` と同じ理由）。 */
.c-toggle > input:focus-visible + .c-toggle__track {
  outline: var(--border-width-thick) solid var(--focus-ring);
  outline-offset: var(--border-width-thin);
}

@media (prefers-reduced-motion: reduce) {
  .c-toggle__track,
  .c-toggle__thumb {
    transition: none;
  }
}

/* 通知の一覧（notifications/partials/panel.html）。鐘の下の札に入る。
   行そのものが押しどころなので、カードを並べずに1本の線で区切る。 */
.c-notifications__head {
  padding: var(--space-m) var(--space-l);
  border-bottom: var(--border-width-thin) solid var(--border);
}

.c-notifications__title {
  margin: 0;
  font-size: var(--font-size-subtitle2);
  font-weight: var(--font-weight-strong);
}

.c-notifications {
  flex: 1 1 auto;
  min-height: 0;
  margin: 0;
  padding: 0;
  list-style: none;
  overflow-y: auto;
  overscroll-behavior: contain;
}

.c-notification + .c-notification {
  border-top: var(--border-width-thin) solid var(--border);
}

/* 既読は薄くするのではなく、未読に印を足す。薄い文字は「重要でない」ではなく
   「読みにくい」になる（WCAG）。 */
.c-notification__open {
  display: flex;
  flex-direction: column;
  gap: var(--space-xxs);
  width: 100%;
  padding: var(--space-m) var(--space-l);
  border: none;
  border-left: var(--space-xxs) solid transparent;
  background: none;
  color: inherit;
  font: inherit;
  text-align: start;
  cursor: pointer;
}

.c-notification.is-unread .c-notification__open {
  border-left-color: var(--warning);
  background-color: var(--accent);
}

.c-notification.is-unread[data-severity="danger"] .c-notification__open {
  border-left-color: var(--danger);
}

.c-notification__open:hover {
  background-color: var(--accent);
}

.c-notification__head {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--space-s);
}

.c-notification__kind {
  font-weight: var(--font-weight-strong);
}

.c-notification__who {
  font-weight: var(--font-weight-medium);
}

.c-notification__class {
  margin-inline-start: var(--space-xs);
  color: var(--muted);
  font-size: var(--font-size-caption1);
  font-weight: var(--font-weight-regular);
}

.c-notification__when {
  color: var(--muted);
  font-size: var(--font-size-caption1);
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
}

.c-notification__body {
  color: var(--foreground);
  font-size: var(--font-size-caption1);
}

/* 札そのものが枠なので、空の札に枠をもう1つ重ねない。 */
.c-notifications__empty .c-empty-state {
  padding-block: var(--space-xl);
  border: none;
  box-shadow: none;
}

/* ── 判定・状態の札（`.g-chip`）─────────────────────────────────────────
 * guidance.css から移した（PM 2026-09-20）。先生の カルテ と生徒の 志望校 が
 * 同じ語・同じ色で判定を出すので、札の語彙は片方の app には置けない。
 *
 * **札は必ず語を連れている。** 色は上塗りにすぎない（WCAG AA、そして白黒で
 * 印刷される）。
 */

.g-chip {
  display: inline-flex;
  align-items: center;
  gap: var(--space-xs);
  padding: var(--space-xs) var(--space-s);
  border: var(--border-width-thin) solid var(--border);
  border-radius: var(--radius);
  font-size: var(--font-size-caption1);
  line-height: var(--line-height-caption1);
  font-weight: var(--font-weight-medium);
  white-space: nowrap;
  background: var(--card-bg);
  color: var(--foreground);
}

.g-chip--ok {
  color: var(--success);
  background: var(--success-bg);
  border-color: var(--success-border);
}

.g-chip--borderline,
.g-chip--deadline,
.g-chip--unpublished,
.g-chip--weakness {
  color: var(--warning);
  background: var(--warning-bg);
  border-color: var(--warning-border);
}

.g-chip--short {
  color: var(--danger);
  background: var(--danger-bg);
  border-color: var(--danger-border);
}

.g-chip--unknown,
.g-chip--unconfirmed {
  color: var(--muted);
  background: var(--background);
  border-color: var(--border);
}

.g-chip--published {
  color: var(--success);
  background: var(--success-bg);
  border-color: var(--success-border);
}

/* ── 出願条件との照合の並び（`careers/partials/gap_findings.html`）────────
 * guidance.css から移した（PM 2026-09-20）。同じ partial を先生の カルテ と
 * 生徒の 志望校 が読むので、様式も片方の app には置けない。
 */
.gap-findings,
.gap-evidence,
.gap-certifications__list {
  list-style: none;
  margin: 0;
  padding: 0;
}

.gap-finding {
  padding-block: var(--space-s);
  border-top: var(--border-width-thin) solid var(--border);
}

.gap-finding__rule {
  font-weight: var(--font-weight-strong);
  margin-inline-end: var(--space-s);
}

/* Evidence and caveat are indented to the same place: both qualify the sentence
   above them rather than standing beside it. */
.gap-evidence__item,
.gap-finding__caveat {
  padding-inline-start: var(--space-l);
}

/* The 参考値 caveat qualifies the number right above it, so it reads as part of
   the finding — warning colour, not the muted grey the evidence list uses. */
.gap-finding__caveat {
  color: var(--warning);
}

/* ── Chat bubbles ─────────────────────────────────────────────────────── */

/* Two screens speak in turns: the student's AIチャット (tutor/) and the 模擬面接
   transcript (interviews/). These came from tutor.css when the second one
   arrived — moved rather than copied, so the two cannot drift apart.

   **Whoever is reading sits on the right.** Their own question in the chat,
   their own answer in the interview; the other voice is on the left. Stack both
   on one side and you cannot tell, reading back, who said what.

   Spacing steps in 8s (PM 2026-09-20). The bubble was 12/16, which is the one
   pair on this screen that did not land on the grid. */
.chat-log {
  gap: var(--space-l);
}

.chat-turn {
  display: flex;
  flex-direction: column;
  gap: var(--space-s);
}

.chat-bubble {
  max-width: min(46rem, 100%);
  margin: 0;
  padding: var(--space-s) var(--space-l);
  border-radius: var(--radius);
  line-height: 1.7;
  overflow-wrap: anywhere;
}

/* Two vocabularies, one rule. `--asked`/`--answer` are what the chat's markup
   has always said; `--me`/`--them` is what a transcript needs, where the AI is
   the one asking. Renaming the chat's would be a diff across a working screen
   for no gain. */
.chat-bubble--me,
.chat-bubble--asked {
  align-self: flex-end;
  background: var(--accent);
  color: var(--foreground);
}

.chat-bubble--them,
.chat-bubble--answer,
.chat-bubble--thinking,
.chat-bubble--failed {
  align-self: flex-start;
  background: var(--card-bg);
  border: var(--border-width-thin) solid var(--border);
}

/* Who is speaking, above their bubble and on the same side. The chat does not
   need it (two voices, one of them always yours); a transcript read weeks later
   does. */
.chat-who {
  color: var(--muted);
  font-size: var(--font-size-caption2);
  font-weight: var(--font-weight-medium);
}

.chat-turn--me .chat-who {
  align-self: flex-end;
}

/* 待っているあいだの輪。言葉と並べて出す——**動く物が1つも無い画面は、待っているのか
   壊れているのか区別が付かない。**

   色は持たない（`currentColor`）。周りの字の色を着るので、置いた場所に合う。

   採点の2画面が `sa-spin` として持っていたものを、3枚目（小論文の添削）が要ったので
   ここへ移した（CLAUDE.md の UI ルール: 2画面目が欲しがったら共通化する）。 */
.c-spin {
  display: inline-block;
  inline-size: 11px;
  block-size: 11px;
  margin-inline-end: 6px;
  vertical-align: -1px;
  border: 2px solid currentColor;
  border-block-start-color: transparent;
  border-radius: 50%;
  animation: c-spin 0.8s linear infinite;
}

@keyframes c-spin {
  to {
    transform: rotate(360deg);
  }
}

/* 動きを減らす設定の人には回さない。輪は残るので「待っている」ことは伝わる。 */
@media (prefers-reduced-motion: reduce) {
  .c-spin {
    animation: none;
  }
}

/* ── AIが走っているあいだ、画面ぜんぶをふさぐ1枚（components/ai_busy.html） ──────
 *
 * ダイアログ（.c-modal, z 100）より上に出す。AIを呼ぶのはダイアログの中からでもある
 * （ルーブリック生成）ので、下に敷くと、ふさいだはずの窓が幕の上に残る。
 *
 * 押せなくするのは `hidden` と <main> の `inert`（static/js/ai_busy.js）。この幕は
 * 覆うだけで、覆いの下を押せないことは幕の役目ではない——幕を透かした一瞬に
 * クリックが通る隙間を作らないため。 */
.c-aibusy {
  position: fixed;
  inset: 0;
  z-index: 200;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: var(--space-l);
  background-color: rgba(30, 41, 59, 0.45);
}

.c-aibusy[hidden] {
  display: none;
}

.c-aibusy__card {
  background: var(--card-bg);
  border-radius: 10px;
  padding: var(--space-xxl) var(--space-xxxl);
  width: min(420px, 92vw);
  text-align: center;
  box-shadow: 0 18px 48px rgba(15, 23, 42, 0.28);
}

.c-aibusy__spin {
  width: 34px;
  height: 34px;
  margin: 0 auto var(--space-m);
  border-radius: var(--radius-circular);
  border: 3px solid var(--accent);
  border-top-color: var(--primary);
  animation: c-aibusy-spin 0.9s linear infinite;
}

/* 目が回るのが苦手な人には回さない。待っていることは文が言っている。 */
@media (prefers-reduced-motion: reduce) {
  .c-aibusy__spin {
    animation: none;
    border-top-color: var(--border);
  }
}

@keyframes c-aibusy-spin {
  to {
    transform: rotate(360deg);
  }
}

.c-aibusy__title {
  margin: 0 0 var(--space-xs);
  font-size: var(--font-size-subtitle2);
  font-weight: var(--font-weight-strong);
}

.c-aibusy__sub {
  margin: 0;
  font-size: var(--font-size-caption1);
  color: var(--foreground-secondary);
}
