/*
 * 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 takes --border where a field takes
   --border-strong, and that split is 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. On a field the border is the only thing saying where the box is. */
.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);
}

/* 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. */
.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-strong);
  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. */
.c-field select {
  appearance: none;
  padding-inline-end: var(--space-xl);
  background-image:
    linear-gradient(45deg, transparent 50%, var(--muted) 50%),
    linear-gradient(135deg, var(--muted) 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;
}

/* 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] {
  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;
}

/* 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);
}

/* ── 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 with
   --foreground and lettered in --card-bg). One deliberate difference: the
   reference draws the frame in --border, and the boundary that identifies a form
   control needs 3:1 (WCAG 1.4.11) — --border reaches 1.23:1. --border-strong is
   the step over the line, and it is the trade ADR-020 §Decision 3 already made
   for every input on the screen; this control does not get to reopen it.

   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;
}
/* 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: var(--border-width-thin) solid var(--border-strong);
  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 darkest thing in the control, or the two options
   read as equally selected. --card-bg on --foreground is 14.4:1. */
.c-segmented__option input:checked + span {
  background: var(--foreground);
  color: var(--card-bg);
}

/* ── 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);
  background: var(--foreground);
  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. It is not
   the grid's frame and does not take --radius-grid: .c-table is the five-row
   summary inside a card, while .c-grid__scroll is a list SCREEN whose whole
   surface is cells (PM, 2026-08-09). */
.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;
}

/* 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);
}

/* One segmented control rather than three loose glyphs: the arrows and the
   position belong to each other, and a shared frame says so (#216). */
.c-pagination__pager {
  display: flex;
  align-items: stretch;
  border: var(--border-width-thin) solid var(--border);
  /* --radius-grid, not --radius: this is the table's furniture, and it stays
     square with the cells while the rest of the app rounds (PM). */
  border-radius: var(--radius-grid);
  overflow: hidden;
}

.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);
}

.c-pagination__status {
  display: flex;
  align-items: center;
  padding: var(--space-xxs) var(--space-m);
  border-inline: var(--border-width-thin) solid var(--border);
  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-grid);
  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;
  /* 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--m {
  max-width: calc(var(--space-xxxxl) * 14);
}

/* 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);
}

/* 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);
}

/* 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);
}

.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;
  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;
}

/* 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;
}

.c-status--neutral {
  background: var(--accent);
  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);
  /* Square. Rounding the frame of a table whose every cell is a square box is
     the one place the card radius reads as wrong (PM, 2026-08-09). Named now
     rather than written as 0, so the grid's furniture below reads the same
     token this frame does. */
  border-radius: var(--radius-grid);
  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;
}

.c-grid__cell {
  padding: var(--space-xxs) var(--space-xs);
  border-right: var(--border-width-thin) solid var(--border);
  border-bottom: var(--border-width-thin) solid var(--border);
  text-align: start;
  white-space: nowrap;
}

.c-grid__cell:last-child {
  border-right: none;
}

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

/* A control inside a cell sits on the ROW's height, not on its own.
   .c-button is sized for a form: 20px of line plus 4px of padding top and
   bottom plus its border is 30px, against a text cell's ~20px — so one 操作
   column stretched every row of the grid by half again. Here the button gives
   up its block padding and takes the caption line-height the cells use, which
   is the same control at the row's own scale.

   Scoped to the cell rather than offered as a --compact modifier: an action
   column is the only place a button lands inside a grid, and a modifier is a
   thing to remember. Inline padding stays — a button still has to read as one
   next to the text beside it. */
.c-grid__cell .c-button {
  padding-block: var(--space-none);
  line-height: var(--line-height-caption1);
}

/* ── 並べ替えのつまみ（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;
}

/* 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;
  padding: var(--space-none);
  background-color: var(--background);
  font-weight: var(--font-weight-medium);
  color: var(--muted);
}

.c-grid__head {
  position: relative;
}

/* 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: block;
  padding: var(--space-xxs) var(--space-xs);
}

/* 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-xxs);
  width: 100%;
  padding: var(--space-xxs) var(--space-xs);
  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;
}

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

/* Sorted and filtered are different facts and get different marks: the arrow
   says which way this column is ordered, the funnel says rows are hidden. A
   column can be both, and a person needs to know which one to undo. */
.c-grid__head-toggle.is-sorted,
.c-grid__head-toggle.is-filtered {
  color: var(--primary);
}

.c-grid__arrow,
.c-grid__funnel {
  color: var(--primary);
}

.c-grid__caret {
  margin-inline-start: auto;
  /* No colour of its own: it inherits --muted from the header cell. Neither
     `opacity` nor --foreground-disabled is available here — the first reads as
     "hides content" to the fade-in guard, the second as unreadable text — and
     the caret needs neither, being aria-hidden decoration beside a real label. */
}

.c-grid__cell--num .c-grid__caret {
  margin-inline-start: var(--space-xxs);
}

/* 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-grid);
  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-grid);
  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-grid);
  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-grid);
  background-color: var(--card-bg);
  font-size: var(--font-size-caption1);
  cursor: pointer;
}

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

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

.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);
}

.c-grid__search {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-xs);
}

/* Square and faint, like a spreadsheet's own boxes: --border-strong and a
   radius belong to a form a person fills in, while this is a filter above a
   grid of square cells. */
.c-grid__search input[type="text"],
.c-grid__search input[type="search"],
.c-grid__menu-needle,
.c-grid__menu-apply {
  border: var(--border-width-thin) solid var(--border);
  /* --radius-grid, and no longer a literal 0. It used to be one because
     --radius-none had been retired in #137 and "no radius" is not a value a
     palette can restate usefully. That reasoning held only while every corner
     in the app was 0: now that the controls round, this square is a CHOICE
     about the grid and has to be named, or the next person reads it as a rule
     nobody updated. */
  border-radius: var(--radius-grid);
}

.c-grid__search input[type="text"] {
  padding: var(--space-xxs) var(--space-xs);
  font-size: var(--font-size-caption1);
  height: auto;
}

/* 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);
}

/* ── 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);
}

/* ── 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);
}

/* ── A callout that needs doing ──────────────────────────────────────────────
   Not a toast: a toast is what just happened, and this is a standing condition
   that stays until somebody clears it. The tone tokens are the message ones, so
   a warning here is the same amber as a warning anywhere. */
.c-callout {
  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);
  background-color: var(--card-bg);
}

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

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

/* ── 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 —
   a heavier rule before 自校の科目, and the vendor's own columns tinted so the
   eye can tell which half it is in without reading the headings.

   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 headings above the vendor's own columns, tinted so the seam reads as
   "these belong together" rather than as a stray heavy gridline. The header is
   sticky and needs an opaque fill anyway, so this replaces one rather than
   layering over it. */
.c-mapping-grid .c-grid__cell--head:nth-child(-n + 2) {
  background-color: var(--accent);
}

/* ── 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);
  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);
}

/* 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);
}

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

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

.c-tabs__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 {
  border-color: var(--primary-light);
  color: var(--foreground);
}

.c-tabs__tab--active {
  background: var(--primary);
  border-color: var(--primary);
  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(--primary);
  background: var(--primary);
  color: var(--primary-foreground);
}

/* ── 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--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;
}

/* 枠の名前。見出しとして読む——「どちらのファイルか」が最初の一語。 */
.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);
}
