/* ══════════════════════════════════════════════════════════════════
   WARNINGS — advisory, never blocking. Loud enough to notice,
   quiet enough to ignore on purpose.
   ══════════════════════════════════════════════════════════════════ */

.warnbox {
  background: var(--warn-bg);
  border: 1px solid var(--warn-ring);
  border-radius: var(--r-lg);
  padding: var(--sp-3) var(--sp-4);
  margin-bottom: var(--sp-5);
  font-size: var(--fs-md);
  color: var(--ink-2);
}
.warnbox h3 {
  font-size: var(--fs-md);
  font-weight: 700;
  color: var(--warn-strong);
  margin-bottom: var(--sp-2);
  display: flex;
  align-items: center;
  gap: var(--sp-2);
}
.warnbox ul {
  display: grid;
  gap: var(--sp-1);
}
.warnbox li {
  margin-left: var(--sp-5);
  padding-left: var(--sp-1);
}
.warnbox li::marker {
  color: var(--warn);
}
.warnbox .note {
  color: var(--warn);
  font-size: var(--fs-sm);
  margin-top: var(--sp-3);
  padding-top: var(--sp-2);
  border-top: 1px solid var(--warn-ring);
}

/* ══════════════════════════════════════════════════════════════════
   SCHEDULE GRID
   ══════════════════════════════════════════════════════════════════ */

.gridwrap {
  overflow-x: auto;
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: var(--r-lg);
  box-shadow: var(--sh-1);
}
table.sched {
  border-collapse: separate;
  border-spacing: 0;
  width: 100%;
}
table.sched th,
table.sched td {
  border-right: 1px solid var(--line);
  border-bottom: 1px solid var(--line);
  padding: var(--sp-2);
  vertical-align: top;
}
table.sched th:last-child,
table.sched td:last-child {
  border-right: none;
}
table.sched tbody tr:last-child th,
table.sched tbody tr:last-child td {
  border-bottom: none;
}
table.sched thead th {
  background: var(--surface-2);
  font-size: var(--fs-base);
  font-weight: 600;
  color: var(--muted);
  padding: var(--sp-3) var(--sp-2);
  text-align: center;
  white-space: nowrap;
}
/* Session column headers: a small "SECTION N" label above the time.
   All four look identical on purpose — colour in this app means language,
   so tinting the sessions would send a false signal. */
.section-title_1,
.section-title_2,
.section-title_3,
.section-title_4 {
  display: block;
  font-size: var(--fs-xs);
  font-weight: 700;
  letter-spacing: 0.06em;
  color: var(--muted);
}
.section-time {
  display: block;
  margin-top: 3px;
  font-size: var(--fs-md);
  font-weight: 600;
  color: var(--ink);
}

/* The room column stays put while the sessions scroll sideways. */
th.roomcol {
  position: sticky;
  left: 0;
  z-index: var(--z-sticky);
  text-align: left;
  padding-left: var(--sp-3);
  width: 168px;
  min-width: 168px;
  background: var(--surface-2);
  border-right: 1px solid var(--line-strong);
  font-size: var(--fs-base);
  font-weight: 600;
  color: var(--ink);
  white-space: nowrap;
  vertical-align: middle;
}
thead th.roomcol {
  z-index: calc(var(--z-sticky) + 1);
  color: var(--muted);
}
th.roomcol .cap {
  display: block;
  font-weight: 400;
  font-size: var(--fs-sm);
  color: var(--muted);
}
/* Chair icon before the seat count. */
.cap-icon {
  width: 13px;
  height: 13px;
  vertical-align: -2px;
  margin-right: 4px;
  color: var(--muted);
}
th.roomcol .online {
  display: block;
  color: var(--eng-meta);
  font-size: var(--fs-xs);
  font-weight: 700;
  letter-spacing: 0.02em;
}

/* Section header row that separates Onsite rooms from Online rooms. The
   label cell reuses .roomcol so it stays pinned on horizontal scroll; the
   rest of the band is filled with tinted cells. */
tr.room-group th.room-group-label {
  background: var(--surface-3);
  color: var(--ink);
  font-size: var(--fs-sm);
  font-weight: 700;
  letter-spacing: 0.03em;
  text-transform: uppercase;
  border-bottom: 1px solid var(--line-strong);
}
tr.room-group .room-group-fill {
  background: var(--surface-3);
  border-bottom: 1px solid var(--line-strong);
}
.rg-icon {
  width: 15px;
  height: 15px;
  vertical-align: -3px;
  margin-right: var(--sp-2);
}
.rg-count {
  margin-left: var(--sp-2);
  color: var(--muted);
  font-weight: 600;
}
.rg-onsite th.room-group-label .rg-icon {
  color: var(--muted);
}
.rg-online th.room-group-label .rg-icon {
  color: var(--eng-meta);
}

/* The grid is read-only (2026-07-16): no pointer cursor, no "clickable"
   hover on empty slots — booking happens on the Roster page. */
td.slot {
  min-width: 200px;
  height: 64px;
  background: var(--surface);
  transition: background-color var(--dur-1) var(--ease-out);
}
/* When the Dashboard is filtered to ONE language, a slot occupied by another
   language's class shows this red "Booked" marker instead of looking empty —
   so staff instantly see WHY they can't book here. (The "All languages"
   filter shows the real cards, so it never renders these.) */
.slot-booked {
  border: 1.5px solid var(--danger);
  background: var(--danger-bg);
  color: var(--danger);
  border-radius: var(--r-md);
  padding: var(--sp-2) var(--sp-3);
  font-size: var(--fs-base);
  line-height: 1.4;
  cursor: not-allowed;
  text-align: center;
}
.slot-booked-title {
  display: block;
  font-weight: 700;
  font-size: var(--fs-lg);
  letter-spacing: 0.06em;
  text-transform: uppercase;
}

/* A clashing slot is tinted and outlined — colour is never the only cue,
   the class cards inside also carry a ROOM CLASH tag. */
td.clash {
  background: var(--warn-bg);
  box-shadow: inset 0 0 0 2px var(--warn-solid);
}

/* ── Class card ──────────────────────────────────────────────────
   Full border + tint + a solid dot. No side stripes: a 4px left
   border reads as an accident of CSS, not a decision. */
.class-card {
  border: 1px solid;
  border-radius: var(--r-md);
  padding: var(--sp-2) var(--sp-2) var(--sp-2) var(--sp-3);
  font-size: var(--fs-base);
  line-height: 1.4;
  cursor: help; /* hover = tooltip with details; the card is not clickable */
  box-shadow: var(--sh-1);
  transition:
    transform var(--dur-1) var(--ease-out),
    box-shadow var(--dur-1) var(--ease-out);
}
.class-card + .class-card {
  margin-top: var(--sp-1);
}
.class-card:hover {
  transform: translateY(-1px);
  box-shadow: var(--sh-2);
}
.class-card:active {
  transform: translateY(0);
}

/* Card text is centered (2026-07-15, per Big) — the head row, the
   teacher/count row, and the warning tags all line up mid-cell. */
.cc-head {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: var(--sp-2);
  min-width: 0;
}
.cc-dot {
  width: 7px;
  height: 7px;
  border-radius: var(--r-full);
  flex-shrink: 0;
}
.cc-head b {
  font-size: var(--fs-md);
  font-weight: 700;
  letter-spacing: -0.005em;
}
.cc-course {
  font-size: var(--fs-sm);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.class-card .meta {
  font-size: var(--fs-sm);
  display: flex;
  justify-content: center;
  gap: var(--sp-3);
  margin-top: 2px;
}
.cc-teacher {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.cc-count {
  font-weight: 600;
  flex-shrink: 0;
}
/* Dashboard head-count: a person icon + the number, no "/capacity"
   (per Big, 2026-07-29). */
.cc-count-ppl {
  display: inline-flex;
  align-items: center;
  gap: 3px;
}
.cc-count-ico {
  width: 13px;
  height: 13px;
  opacity: 0.72;
}
.cc-tags {
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  gap: var(--sp-1);
  margin-top: var(--sp-2);
}

/* Language is carried by tint + border + dot + text colour, so a
   colour-blind user still gets the border shape and the dot position. */
.lang-thai {
  background: var(--thai-bg);
  border-color: var(--thai-ring);
  color: var(--thai-meta);
}
.lang-thai .cc-head b,
.lang-thai .cc-course {
  color: var(--thai-title);
}
.lang-thai .cc-dot {
  background: var(--thai-solid);
}

.lang-eng {
  background: var(--eng-bg);
  border-color: var(--eng-ring);
  color: var(--eng-meta);
}
.lang-eng .cc-head b,
.lang-eng .cc-course {
  color: var(--eng-title);
}
.lang-eng .cc-dot {
  background: var(--eng-solid);
}

.lang-chi {
  background: var(--chi-bg);
  border-color: var(--chi-ring);
  color: var(--chi-meta);
}
.lang-chi .cc-head b,
.lang-chi .cc-course {
  color: var(--chi-title);
}
.lang-chi .cc-dot {
  background: var(--chi-solid);
}

.wtag {
  display: inline-block;
  background: var(--warn-solid);
  color: #fff;
  font-size: var(--fs-xs);
  font-weight: 700;
  letter-spacing: 0.01em;
  padding: 1px var(--sp-2);
  border-radius: var(--r-sm);
  white-space: nowrap;
}

/* ── Two-number room capacity (Class Plan / Roster only) ──────────────
   The count "9/12" plus a "Max 15" chip; both tint by how full the room is.
   Never rendered on the Dashboard — teachers must not see a room's ceiling. */
.cc-cap {
  display: inline-flex;
  align-items: center;
  gap: var(--sp-1);
  flex-shrink: 0;
}
/* count colour by zone: standard = normal, buffer = amber, past max = red */
.cc-count.cap-over-std {
  color: var(--warn-solid);
}
.cc-count.cap-over-max {
  color: var(--danger);
}
.cap-tag {
  display: inline-block;
  font-size: var(--fs-xs);
  font-weight: 700;
  letter-spacing: 0.01em;
  padding: 1px var(--sp-2);
  border-radius: var(--r-sm);
  white-space: nowrap;
  line-height: 1.5;
}
/* "Max N" chip — soft red (per Big, 2026-07-29): a pale red fill with dark-red
   text, quieter than the solid OVER CAPACITY chip below. The head-count zone is
   still carried by the count colour (grey → amber → red), so the chip stays a
   steady, low-key reminder of the room's ceiling. */
.cap-max {
  background: var(--danger-bg);
  color: var(--danger);
  box-shadow: inset 0 0 0 1px #fecaca;
}
/* red OVER CAPACITY chip, past the packed ceiling */
.cap-tag.cap-over {
  background: var(--danger);
  color: #fff;
}

/* Small grey hint under a modal input (e.g. the two room-capacity fields). */
.field-hint {
  display: block;
  margin-top: var(--sp-1);
  font-size: var(--fs-xs);
  color: var(--text-muted, #64748b);
}

/* ══════════════════════════════════════════════════════════════════
   ROOM AVAILABILITY
   ══════════════════════════════════════════════════════════════════ */

.avail td.slot {
  text-align: center;
  vertical-align: middle;
  font-size: var(--fs-base);
  font-weight: 600;
  height: 46px;
  cursor: default;
}
.avail td.free {
  background: var(--ok-bg);
  color: var(--ok);
}
.avail td.busy-thai {
  background: var(--thai-bg);
  color: var(--thai-title);
}
.avail td.busy-eng {
  background: var(--eng-bg);
  color: var(--eng-title);
}
.avail td.busy-chi {
  background: var(--chi-bg);
  color: var(--chi-title);
}

.legend {
  display: flex;
  gap: var(--sp-4);
  font-size: var(--fs-base);
  color: var(--muted);
  align-items: center;
}
.legend > span {
  display: inline-flex;
  align-items: center;
  gap: var(--sp-2);
}
.dot {
  width: 10px;
  height: 10px;
  border-radius: 3px;
  display: inline-block;
  flex-shrink: 0;
}
.dot-free {
  background: var(--ok);
}
.dot-thai {
  background: var(--thai-mark);
}
.dot-eng {
  background: var(--eng-mark);
}
.dot-chi {
  background: var(--chi-mark);
}

/* ══════════════════════════════════════════════════════════════════
   PANELS & DATA TABLES
   ══════════════════════════════════════════════════════════════════ */

.panel {
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: var(--r-lg);
  padding: var(--sp-5);
  box-shadow: var(--sh-1);
}
.panel h2 {
  font-size: var(--fs-xl);
  letter-spacing: -0.01em;
  margin-bottom: var(--sp-1);
}
.panel .sub {
  color: var(--muted);
  font-size: var(--fs-base);
  margin-bottom: var(--sp-4);
  max-width: 75ch;
}
.table-scroll {
  overflow-x: auto;
}

/* ── Roster ── */
table.roster {
  border-collapse: separate;
  border-spacing: 0;
  width: 100%;
  font-size: var(--fs-base);
}
table.roster th,
table.roster td {
  border-bottom: 1px solid var(--line);
  border-right: 1px solid var(--line);
  padding: var(--sp-2) var(--sp-3);
  text-align: center;
}
table.roster th:last-child,
table.roster td:last-child {
  border-right: none;
}
table.roster thead th {
  background: var(--surface-2);
  font-size: var(--fs-sm);
  font-weight: 600;
  color: var(--muted);
  white-space: nowrap;
  border-top: 1px solid var(--line);
}
table.roster thead th:first-child {
  border-top-left-radius: var(--r-md);
}
table.roster thead th:last-child {
  border-top-right-radius: var(--r-md);
}
table.roster tbody tr {
  transition: background-color var(--dur-1) var(--ease-out);
}
table.roster tbody tr:hover {
  background: var(--surface-2);
}
/* Student names stay pinned to the left while the attendance days scroll. */
table.roster td.name,
table.roster thead th:first-child {
  position: sticky;
  left: 0;
  z-index: var(--z-sticky);
  text-align: left;
  white-space: nowrap;
  color: var(--ink);
  background: var(--surface);
  border-right: 1px solid var(--line-strong);
}
table.roster thead th:first-child {
  background: var(--surface-2);
}
table.roster tbody tr:hover td.name {
  background: var(--surface-2);
}
table.roster tr.total td {
  background: var(--surface-2);
  font-weight: 700;
  color: var(--ink);
  border-bottom: none;
}
table.roster tr.total td.name {
  background: var(--surface-2);
}
table.roster tr.total:hover {
  background: var(--surface-2);
}

/* ── Attendance grid (js/pages/attendance.js) — reuses table.roster's
      sticky name column + borders above; these are just its own column
      types (day cells, test cells, totals, note, result). ── */
.att-pick {
  width: 100%;
  max-width: 480px;
}

/* ── Day tabs (per Xia, 2026-07-30) — a term's class days are chunked into
   fixed 10-day pages instead of one row that scrolls forever. Plain
   underline-style tabs, not pills — this sits above a dense data grid and
   shouldn't compete with it visually. ── */
.att-day-tabs {
  display: flex;
  gap: var(--sp-1);
  margin-bottom: var(--sp-2);
  border-bottom: 1px solid var(--line);
}
.att-day-tab {
  border: none;
  background: transparent;
  padding: var(--sp-2) var(--sp-3);
  font-size: var(--fs-sm);
  color: var(--muted);
  cursor: pointer;
  border-bottom: 2px solid transparent;
  margin-bottom: -1px;
}
.att-day-tab:hover {
  color: var(--ink);
}
.att-day-tab.active {
  color: var(--ink);
  font-weight: 600;
  border-bottom-color: var(--accent, #2563eb);
}

/* ── Row-number + Student sticky-left block (Attendance grid only) — per
   Xia, 2026-07-30. Adds a "#" column pinned before Student; both stay
   pinned together, mirroring the pinned summary block on the right.
   Deliberately scoped to .att-grid: table.roster's shared td.name /
   thead-th:first-child sticky rule (used by the Roster page, which has no
   "#" column) is left alone everywhere else. ── */
table.roster.att-grid td.att-col-no,
table.roster.att-grid th.att-col-no {
  position: sticky;
  left: 0;
  z-index: var(--z-sticky);
  width: 36px;
  min-width: 36px;
  text-align: center;
  color: var(--muted);
  background: var(--surface);
  border-right: 1px solid var(--line);
}
table.roster.att-grid thead th.att-col-no {
  background: var(--surface-2);
}
table.roster.att-grid tbody tr:hover td.att-col-no {
  background: var(--surface-2);
}
/* Student's own td.name already gets position:sticky/background from
   table.roster's shared rule (still matches — .name is unchanged) — this
   just shifts its left offset to sit right after the new "#" column. */
table.roster.att-grid td.name {
  left: 36px;
}
/* The Student HEADER is no longer thead's first-child (the "#" header is),
   so it no longer inherits sticky positioning from the shared rule at all —
   it needs the full treatment spelled out here, not just an offset. */
table.roster.att-grid thead th.att-col-student {
  position: sticky;
  left: 36px;
  z-index: var(--z-sticky);
  text-align: center;
  background: var(--surface-2);
  border-right: 1px solid var(--line-strong);
}

/* ── Attendance, styled to the Roster (per Big, 2026-07-29) ───────────
   This page is ONE class in ONE session, so it borrows the Roster's two
   colour cues for that: the SESSION colour (the frame the grid sits in) and
   the LEVEL colour (the class name). The grid cells themselves stay neutral —
   the same call the Roster made when it moved colour up onto the Session
   frame and left the room cards grey. The four hex pairs are the Roster's
   own .rsection-s{n} values, kept in step by hand (one palette, two pages). */
.att-frame {
  border-width: 2px;
  border-style: solid;
  /* The Roster's .rsection frame padding, not the panel's larger default — a
     frame around the grid, not a wide colour wash. */
  padding: var(--sp-4);
}
.att-frame-s1 {
  border-color: #bfdbfe;
  background: #eff6ff;
}
.att-frame-s2 {
  border-color: #60a5fa;
  background: #dbeafe;
}
.att-frame-s3 {
  border-color: #bbf7d0;
  background: #f0fdf4;
}
.att-frame-s4 {
  border-color: #4ade80;
  background: #dcfce7;
}
/* The grid keeps its own white surface inside the tinted frame — a data grid
   on a colour wash is hard to read, and the frame already carries the cue. */
.panel.att-frame .table-scroll {
  background: var(--surface);
  border-radius: var(--r-md);
  border: 1px solid var(--line);
  overflow: auto; /* clip the table's own corners to the rounded frame */
}
/* The Session tile wears its session colour as a left edge, tying the tile to
   the frame the grid sits in — same colour, said twice, so the link reads. The
   tile itself stays white (a dedicated att-tile-s{n} class, NOT att-frame-s{n},
   so it takes only the edge colour and none of the frame's background tint).
   The edge uses the darker of each session pair so 4px of it stays visible. */
/* .dash-tile's own `border` shorthand is defined LATER in this file, so these
   need the two-class specificity to keep the left edge from being reset back to
   1px var(--line) (the cascade order gotcha — see dialogs-vs-pages history). */
.dash-tile.att-tile-session {
  border-left-width: 4px;
  border-left-style: solid;
}
.dash-tile.att-tile-s1 {
  border-left-color: #60a5fa;
}
.dash-tile.att-tile-s2 {
  border-left-color: #2563eb;
}
.dash-tile.att-tile-s3 {
  border-left-color: #4ade80;
}
.dash-tile.att-tile-s4 {
  border-left-color: #16a34a;
}
/* The class name in its level colour + a matching dot — the Roster's course
   name treatment (.course-lv-* sets the colour; currentColor paints the dot). */
.att-class-name {
  display: inline-flex;
  align-items: center;
  gap: var(--sp-2);
}
.att-class-name .cc-dot {
  background: currentColor;
}
/* Wide enough for the day cell's own content at its current sizes:
   18px checkbox + 4px gap + 20px icon + the cell's 8px side padding. Below
   this the column auto-expands anyway, so keeping the two numbers in step
   just makes the layout predictable. */
.att-col-date {
  min-width: 58px;
}
.att-test-date {
  font-weight: 400;
  color: var(--muted);
  font-size: var(--fs-xs);
}
table.roster td.att-daycell,
table.roster td.att-test-col {
  padding: var(--sp-1) var(--sp-2);
  vertical-align: middle;
}
.att-mini {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 3px;
  font-size: var(--fs-xs);
  color: var(--muted);
  white-space: nowrap;
}
.att-mini + .att-mini {
  margin-top: 2px;
}
.att-mini input[type="checkbox"] {
  width: 14px;
  height: 14px;
  accent-color: var(--present, #2f9e44);
}

/* Present/Homework icon labels (per Xia, 2026-07-30) — a raise-hand /
   homework SVG sits where the old "A"/"H" letter did, inside the same
   .att-mini checkbox+label pair (icon-as-toggle-button was tried the same
   day and reverted: checkbox is back, this is just its label). The icons are
   flat multi-colour asset files loaded as <img>, so they carry their own
   colour — nothing to inherit from .att-mini's `color`.

   Sized up to fill the day cell (per Xia, 2026-07-30). Scoped to
   .att-daycell so the Test column's own .att-mini ("Taken" + a text label,
   which has to stay narrow enough for two of them side by side) keeps the
   original 14px checkbox. */
.att-mini-icon {
  display: inline-flex;
}
.att-mini-icon-img {
  width: 20px;
  height: 20px;
  display: block;
}
table.roster td.att-daycell .att-mini {
  gap: var(--sp-1);
}
table.roster td.att-daycell .att-mini input[type="checkbox"] {
  width: 18px;
  height: 18px;
}
table.roster td.att-daycell .att-mini + .att-mini {
  margin-top: var(--sp-1);
}
.att-score-input {
  width: 52px;
  margin-top: 2px;
  padding: 2px 4px;
  border: 1px solid var(--line);
  border-radius: var(--r-sm);
  font-size: var(--fs-sm);
  text-align: center;
}
/* Nickname (per Xia, 2026-07-30) — a plain text field, right after Student,
   NOT pinned like Student/# are: pinning it too would need Student to have a
   fixed width instead of its current auto/nowrap sizing, which wasn't asked
   for. Read-only rows (sub, or nothing editable) still show it as a
   <input disabled> rather than switching to plain text — unlike Result,
   a disabled text input carries no risk of a click-through save that fails
   (that was the reason Result got the plain-text treatment). */
table.roster th.att-nickname-col,
table.roster td.att-nickname-col {
  min-width: 90px;
}
.att-nickname-input {
  width: 100%;
  min-width: 80px;
  padding: 3px 6px;
  border: 1px solid var(--line);
  border-radius: var(--r-sm);
  font-size: var(--fs-sm);
}
.att-nickname-input:disabled {
  border-color: transparent;
  background: transparent;
  color: var(--muted);
}
table.roster td.att-total-col {
  font-weight: 600;
  white-space: nowrap;
  min-width: 90px;
}
/* The cell holds a single 22px Remark bubble now (per Big, 2026-07-29), not the
   old full-width text input — so the column is sized to the "Remark" heading
   plus the centred icon, not to 140px of empty space (per Big, 2026-07-29). */
table.roster th.att-note-col,
table.roster td.att-note-col {
  width: 72px;
  min-width: 72px;
  text-align: center;
}
table.roster td.att-result-col {
  width: 120px;
  min-width: 120px;
}
/* Pass/Review/Repeat colour (per Xia, 2026-07-30) — same semantic colours
   used for warn/ok/danger elsewhere (tokens.css), not new hues of their own.
   .res-pass/.res-review/.res-repeat covers BOTH the read-only plain-text
   fallback and the live cselect's shown value (toggled on by JS — see
   paintResultTrigger in attendance.js, since the closed trigger's text isn't
   reachable by an attribute selector once the user changes the pick). The
   [data-value] rules colour the OPEN dropdown's option list, which never
   changes after render so an attribute selector is safe there. */
.att-result-col .res-pass,
.att-result-col .cselect-option[data-value="Pass"] {
  color: var(--ok);
  font-weight: 600;
}
.att-result-col .res-review,
.att-result-col .cselect-option[data-value="Review"] {
  color: var(--warn);
  font-weight: 600;
}
.att-result-col .res-repeat,
.att-result-col .cselect-option[data-value="Repeat"] {
  color: var(--danger);
  font-weight: 600;
}

/* ── Pinned summary columns (Attendance → Result) — per Xia, 2026-07-30. A
   term can run 20-40 class days, so the day-by-day columns always need
   horizontal scroll; freezing this trailing block to the right edge means
   Note/Result/Total stay in view while only the day cells scroll, between
   this block and the sticky Student name on the left. Every pinned column
   below gets a FIXED width (not just min-width) — `right` offsets are hand-
   summed from these exact numbers, so an auto-expanding column would throw
   off every offset to its left and the columns would overlap. ── */
.att-pin {
  position: sticky;
  z-index: var(--z-sticky);
}
table.roster thead th.att-pin {
  background: var(--surface-2);
}
table.roster tbody td.att-pin {
  background: var(--surface);
}
table.roster tbody tr:hover td.att-pin {
  background: var(--surface-2);
}
/* Divider between the scrollable day columns and the pinned block — same
   visual idea as the sticky Student column's own border-right. */
.att-pin-start {
  border-left: 1px solid var(--line-strong);
}
/* Offsets, right to left: Result(120) 0 · Note(72) 120 · Total(70) 192 ·
   Test2(90) 262 · Test1(90) 352 · Homework(100) 442 · Attendance(100) 542. */
table.roster th.att-col-attend,
table.roster td.att-col-attend {
  width: 100px;
  min-width: 100px;
  right: 542px;
}
table.roster th.att-col-hw,
table.roster td.att-col-hw {
  width: 100px;
  min-width: 100px;
  right: 442px;
}
table.roster th.att-col-test1,
table.roster td.att-col-test1 {
  width: 90px;
  min-width: 90px;
  right: 352px;
}
table.roster th.att-col-test2,
table.roster td.att-col-test2 {
  width: 90px;
  min-width: 90px;
  right: 262px;
}
table.roster th.att-col-total,
table.roster td.att-col-total {
  width: 70px;
  min-width: 70px;
  right: 192px;
}
table.roster th.att-note-col.att-pin,
table.roster td.att-note-col.att-pin {
  right: 120px;
}
table.roster th.att-result-col.att-pin,
table.roster td.att-result-col.att-pin {
  right: 0;
}
/* The Sub row's trailing cell spans all 7 pinned columns as one <th
   colspan="7">, so it just needs to sit flush against the right edge — its
   rendered width already matches their combined total from the table's own
   auto layout. */
.att-sub-tail {
  right: 0;
}

/* The Sub strip — one row ABOVE the date headers (per Xia, 2026-07-30), one
   cell per day column (migration 0030). Sits inside <thead>, so it inherits
   the sticky/border treatment table.roster already gives header rows; these
   just style its own content. The dashed line is a border-BOTTOM now — it
   separates this row from the date row below it, not above (there's nothing
   above it any more; it's thead's first row). */
table.roster tr.att-sub-row th {
  padding: 2px var(--sp-1);
  font-size: var(--fs-xs);
  font-weight: 400;
  color: var(--muted);
  border-bottom: 1px dashed var(--line);
}
/* The date row's own generic border-top (every thead th gets one) would
   otherwise draw a second, solid line right against the sub row's dashed
   border-bottom — the sub row's dashed line is the only boundary needed
   between the two header rows now that it sits on top. */
table.roster thead tr.att-date-row th {
  border-top: none;
}
/* The corner cell shares the sticky Student column's width (sized for
   student names), so plain left-aligned text just floated in a lot of empty
   space — a centred pill badge instead (per Xia, 2026-07-30), same colour as
   the "has a sub" cells below it. */
/* Specificity note: this cell is thead th:first-child (sticky name column
   rule above sets text-align:left there) — needs the extra tr.att-sub-row
   qualifier to actually win and center the badge. */
table.roster tr.att-sub-row th.att-sub-label {
  text-align: center;
}
.att-sub-badge {
  display: inline-block;
  background: var(--sub-solid);
  color: #fff;
  font-weight: 600;
  font-size: var(--fs-xs);
  letter-spacing: 0.02em;
  padding: 1px 10px;
  border-radius: 999px;
}
.att-sub-cell {
  text-align: center;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.att-sub-cell.has-sub {
  color: var(--sub-solid);
  font-weight: 600;
}
.att-sub-btn {
  width: 100%;
  border: 1px dashed var(--line-strong);
  background: transparent;
  color: inherit;
  font: inherit;
  padding: 1px 2px;
  border-radius: var(--r-sm);
  cursor: pointer;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.att-sub-cell.has-sub .att-sub-btn {
  border-style: solid;
  border-color: var(--sub-solid);
}
.att-sub-btn:hover,
.att-sub-btn:focus-visible {
  background: var(--surface-3);
}

/* The Sub picker popover — a smaller one-field version of the Standby
   popover pattern (.sb-pop in the Class Plan section) kept local to this
   page; see the JS comment in attendance.js for why it isn't shared. */
.att-sub-pop {
  position: fixed;
  z-index: var(--z-modal);
  width: 200px;
  background: var(--surface);
  border: 1px solid var(--line-strong);
  border-radius: var(--r-lg);
  box-shadow: var(--sh-3);
  padding: var(--sp-2) var(--sp-3) var(--sp-3);
  font-size: var(--fs-base);
}
.att-sub-pop-label {
  font-size: var(--fs-sm);
  color: var(--muted);
  margin-bottom: var(--sp-1);
}

/* ── Change Log — a compact audit feed, not a big table. Fixed time rail
      on the left, who + what on the right, dense scannable rows. ── */

/* Change Log + Access Log side by side; stacks when the window is narrow. */
.log-split {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(420px, 1fr));
  gap: var(--sp-4);
  align-items: start;
}
.logfeed {
  /* Matching the mcard motif: a 3px top cap. Neutral slate for the app logs
     (Change / Access); amber for Remark, tying it to its amber entry rails and
     the roster note colour. No language colour here — these aren't teams. */
  --feed-cap: var(--line-strong);
  max-width: 880px;
  background: var(--surface);
  border: 1px solid var(--line);
  border-top: 3px solid var(--feed-cap);
  border-radius: var(--r-lg);
  box-shadow: var(--sh-1);
  overflow: hidden;
}
.logfeed-remark {
  --feed-cap: #f59e0b;
}
/* Inside the split the two feeds share the row, so drop the standalone cap. */
.log-split .logfeed {
  max-width: none;
}

/* Access Log status marks: a coloured dot before the email. */
.access-entry .log-text {
  display: flex;
  align-items: baseline;
  gap: var(--sp-2);
}
.access-dot {
  width: 8px;
  height: 8px;
  border-radius: var(--r-full);
  flex-shrink: 0;
  transform: translateY(-1px);
}
.access-entry.ev-ok .access-dot {
  background: var(--ok);
}
.access-entry.ev-deny .access-dot {
  background: var(--danger);
}
.access-entry.ev-deny .log-what {
  color: var(--danger);
}
/* Remark-edit entries live in their own feed; a faint amber rail ties them to
   the roster note bubble (same sticky-note colour). */
.remark-entry {
  box-shadow: inset 3px 0 0 rgb(245 158 11 / 0.55);
}
.logfeed-head {
  display: flex;
  align-items: baseline;
  gap: var(--sp-3);
  padding: var(--sp-3) var(--sp-4);
  background: var(--surface-2);
  border-bottom: 1px solid var(--line);
}
.logfeed-head h2 {
  font-size: var(--fs-md);
  font-weight: 700;
  display: inline-flex;
  align-items: center;
  gap: var(--sp-2);
}
/* A small leading dot echoes each panel's cap colour. */
.logfeed-head h2::before {
  content: "";
  width: 8px;
  height: 8px;
  border-radius: var(--r-full);
  background: var(--muted);
  flex-shrink: 0;
}
.logfeed-remark .logfeed-head h2::before {
  background: #f59e0b;
}
.logfeed-count {
  font-size: var(--fs-sm);
  font-weight: 600;
  color: var(--muted);
  font-variant-numeric: tabular-nums;
}
.logfeed-note {
  margin-left: auto;
  font-size: var(--fs-sm);
  color: var(--muted);
}
/* Category chips (per Big) — sit between the head and the feed, never scroll
   with it. Hidden (empty) when there's nothing worth splitting. */
.logfeed-filter {
  display: flex;
  flex-wrap: wrap;
  gap: var(--sp-2);
  padding: var(--sp-2) var(--sp-4);
  border-bottom: 1px solid var(--line);
}
.logfeed-filter:empty {
  display: none;
}
.log-chip {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  font: inherit;
  font-size: var(--fs-sm);
  font-weight: 600;
  color: var(--muted);
  background: var(--surface-2);
  border: 1px solid var(--line);
  border-radius: var(--r-full);
  padding: 3px var(--sp-3);
  cursor: pointer;
  transition:
    background-color var(--dur-1) var(--ease-out),
    border-color var(--dur-1) var(--ease-out),
    color var(--dur-1) var(--ease-out);
}
.log-chip:hover {
  border-color: var(--muted);
  color: var(--ink);
}
.log-chip.on {
  background: var(--accent);
  border-color: var(--accent);
  color: var(--accent-ink);
}
.log-chip-n {
  font-size: var(--fs-xs);
  font-weight: 700;
  color: var(--muted);
  font-variant-numeric: tabular-nums;
}
.log-chip.on .log-chip-n {
  color: var(--accent-ink);
  opacity: 0.85;
}
.logfeed-body {
  max-height: calc(100vh - 220px);
  overflow-y: auto;
  scrollbar-width: thin;
  scrollbar-color: var(--line-strong) transparent;
}
.log-entry {
  display: grid;
  grid-template-columns: 112px 1fr;
  gap: var(--sp-3);
  align-items: baseline;
  padding: 7px var(--sp-4);
  font-size: var(--fs-base);
  border-bottom: 1px solid var(--line);
  transition: background-color var(--dur-1) var(--ease-out);
}
.log-entry:last-child {
  border-bottom: none;
}
.log-entry:hover {
  background: var(--surface-2);
}
.log-when {
  color: var(--muted);
  font-size: var(--fs-sm);
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
  padding-top: 1px;
}
.log-text {
  line-height: 1.45;
  color: var(--ink-2);
}
.log-who {
  font-weight: 700;
  color: var(--ink);
}
/* Middot between who and what, so the eye separates actor from action. */
.log-who::after {
  content: "·";
  margin: 0 var(--sp-2);
  color: var(--line-strong);
  font-weight: 400;
}

/* ── Reservation table ── */
table.log {
  border-collapse: collapse;
  width: 100%;
  font-size: var(--fs-base);
}
table.log th,
table.log td {
  border-bottom: 1px solid var(--line);
  padding: var(--sp-3);
  text-align: left;
  vertical-align: middle;
}
table.log thead th {
  font-size: var(--fs-sm);
  font-weight: 600;
  color: var(--muted);
  white-space: nowrap;
  border-bottom-color: var(--line-strong);
}
table.log tbody tr {
  transition: background-color var(--dur-1) var(--ease-out);
}
table.log tbody tr:hover {
  background: var(--surface-2);
}

/* ── Sortable column headings (ClickUp-style, per Big 2026-07-24) ──────
   Click a heading to sort by it, again to reverse, a third time to go back
   to the default. The arrow only appears on the column actually sorting, so
   the head never shows two answers at once. */
.res-sortable thead th[data-sortk] {
  cursor: pointer;
  user-select: none;
  position: relative;
  padding-right: calc(var(--sp-3) + 12px);
  transition: color var(--dur-1) var(--ease-out);
}
.res-sortable thead th[data-sortk]:hover {
  color: var(--ink);
}
/* The arrow is drawn from text so it needs no icon font and inherits colour.
   A faint ↕ on hover advertises that the heading is clickable at all. */
.res-sortable thead th[data-sortk]::after {
  content: "↕";
  position: absolute;
  right: var(--sp-2);
  opacity: 0;
  font-size: 11px;
  transition: opacity var(--dur-1) var(--ease-out);
}
.res-sortable thead th[data-sortk]:hover::after {
  opacity: 0.45;
}
.res-sortable thead th.sorted {
  color: var(--accent);
  font-weight: 700;
}
.res-sortable thead th.sorted[data-dir="asc"]::after {
  content: "↑";
  opacity: 1;
}
.res-sortable thead th.sorted[data-dir="desc"]::after {
  content: "↓";
  opacity: 1;
}
/* With more than one level in play, every sorted heading gets its RANK — a
   row of identical arrows would not say which column leads. */
.res-sortable thead th.sorted[data-rank]:not([data-rank=""]) {
  padding-right: calc(var(--sp-3) + 22px);
}
.res-sortable thead th.sorted[data-rank]:not([data-rank=""])::before {
  content: attr(data-rank);
  position: absolute;
  right: calc(var(--sp-2) + 11px);
  font-size: 9px;
  font-weight: 700;
  color: var(--accent-ink);
  background: var(--accent);
  border-radius: 50%;
  width: 13px;
  height: 13px;
  line-height: 13px;
  text-align: center;
}

/* ── Group by: collapsible heading rows ───────────────────────────────
   These replaced three rows of Level chips. The count is the point — it is
   what the chips could never show without clicking each one in turn. */
.grp-row td {
  padding: 0 !important;
  background: var(--surface-2);
  border-bottom: 1px solid var(--line-strong);
}
table.log tbody tr.grp-row:hover {
  background: var(--surface-2);
}
.grp-head {
  display: flex;
  align-items: center;
  gap: var(--sp-2);
  width: 100%;
  font: inherit;
  font-size: var(--fs-sm);
  font-weight: 700;
  color: var(--ink);
  text-align: left;
  background: none;
  border: none;
  padding: var(--sp-2) var(--sp-3);
  cursor: pointer;
}
.grp-head:hover {
  background: var(--surface-3);
}
.grp-caret {
  width: 13px;
  height: 13px;
  flex-shrink: 0;
  color: var(--muted);
  transition: transform var(--dur-1) var(--ease-out);
}
.grp-caret.open {
  transform: rotate(90deg);
}
.grp-n {
  font-size: var(--fs-xs);
  font-weight: 700;
  color: var(--muted);
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: 999px;
  padding: 1px var(--sp-2);
}
table.log tbody tr:last-child td {
  border-bottom: none;
}

/* ── Inline status marks ── */
.flag {
  display: inline-block;
  font-size: var(--fs-xs);
  font-weight: 700;
  color: var(--warn-strong);
  background: var(--warn-bg);
  border: 1px solid var(--warn-ring);
  padding: 1px var(--sp-2);
  border-radius: var(--r-sm);
  margin-left: var(--sp-2);
  white-space: nowrap;
}

.pill {
  display: inline-block;
  font-size: var(--fs-xs);
  font-weight: 700;
  padding: 2px var(--sp-2);
  border-radius: var(--r-full);
  white-space: nowrap;
  border: 1px solid transparent;
}
.pill.lang-thai {
  background: var(--thai-bg);
  border-color: var(--thai-ring);
  color: var(--thai-title);
}
.pill.lang-eng {
  background: var(--eng-bg);
  border-color: var(--eng-ring);
  color: var(--eng-title);
}
.pill.lang-chi {
  background: var(--chi-bg);
  border-color: var(--chi-ring);
  color: var(--chi-title);
}
.pill.st-waiting {
  background: var(--warn-bg);
  color: var(--warn-strong);
  border-color: var(--warn-ring);
}
.pill.st-assigned {
  background: var(--ok-bg);
  color: var(--ok);
  border-color: var(--ok-ring);
}

/* ══════════════════════════════════════════════════════════════════
   MANAGE & SETTING
   ══════════════════════════════════════════════════════════════════ */

.headnote {
  font-size: var(--fs-base);
  color: var(--muted);
  margin-bottom: var(--sp-5);
  max-width: 75ch;
}
.headnote b {
  color: var(--ink-2);
}

.manage-group {
  margin-bottom: var(--sp-8);
}
/* The Terms group's academic-year picker sits at the far right of its
   heading — it switches all three language cards at once. */
.manage-group-title .mcard-yearpick {
  margin-left: auto;
  font-size: var(--fs-base);
  font-weight: 500;
}
/* A section heading, not a card. Cards inside cards are always wrong. */
.manage-group-title {
  display: flex;
  align-items: center;
  gap: var(--sp-2);
  font-size: var(--fs-lg);
  font-weight: 700;
  letter-spacing: -0.01em;
  color: var(--ink);
  padding-bottom: var(--sp-2);
  margin-bottom: var(--sp-4);
  border-bottom: 1px solid var(--line-strong);
}
/* A short rounded bar, not a dot (per Big's redesign) — reads as a section
   accent and carries the group's language colour. */
.mg-dot {
  width: 4px;
  height: 18px;
  border-radius: var(--r-full);
  flex-shrink: 0;
  background: var(--accent);
}
.manage-thai .mg-dot {
  background: var(--thai-mark);
}
.manage-eng .mg-dot {
  background: var(--eng-mark);
}
.manage-chi .mg-dot {
  background: var(--chi-mark);
}
/* The heading rule picks up the language tint too, so the whole section header
   reads as belonging to its team — colour only where it means something. */
.manage-thai .manage-group-title {
  border-bottom-color: var(--thai-ring);
}
.manage-eng .manage-group-title {
  border-bottom-color: var(--eng-ring);
}
.manage-chi .manage-group-title {
  border-bottom-color: var(--chi-ring);
}

.cards {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
  gap: var(--sp-4);
}
.mcard {
  /* A 3px top cap carries the card's colour. Neutral slate by default; a
     language group (or an .mcard-thai/eng/chi accent) overrides the variable.
     Colour only where it has meaning — never as plain decoration (tokens.css). */
  --mcard-cap: var(--line-strong);
  background: var(--surface);
  border: 1px solid var(--line);
  border-top: 3px solid var(--mcard-cap);
  border-radius: var(--r-lg);
  padding: var(--sp-4);
  box-shadow: var(--sh-1);
  transition:
    box-shadow var(--dur-2) var(--ease-out),
    border-color var(--dur-2) var(--ease-out),
    transform var(--dur-2) var(--ease-out);
}
.mcard:hover {
  border-color: var(--line-strong);
  box-shadow: var(--sh-2);
  transform: translateY(-1px);
}
.manage-thai .mcard,
.mcard-thai {
  --mcard-cap: var(--thai-mark);
}
.manage-eng .mcard,
.mcard-eng {
  --mcard-cap: var(--eng-mark);
}
.manage-chi .mcard,
.mcard-chi {
  --mcard-cap: var(--chi-mark);
}
.mcard h3 {
  font-size: var(--fs-md);
  font-weight: 600;
  margin-bottom: var(--sp-3);
  padding-bottom: var(--sp-2);
  border-bottom: 1px solid var(--line);
  display: flex;
  justify-content: space-between;
  align-items: center;
  gap: var(--sp-2);
}
.mcard ul {
  list-style: none;
  font-size: var(--fs-base);
  max-height: 264px;
  overflow-y: auto;
  overflow-x: hidden;
}
.mcard li {
  padding: var(--sp-2) var(--sp-1);
  border-radius: var(--r-sm);
  display: flex;
  justify-content: space-between;
  align-items: center;
  color: var(--ink-2);
  gap: var(--sp-2);
  min-height: 32px;
  transition: background-color var(--dur-1) var(--ease-out);
}
.mcard li + li {
  box-shadow: 0 -1px 0 var(--line);
}
.mcard li:hover {
  background: var(--surface-2);
}
.mcard li span {
  color: var(--muted);
  font-size: var(--fs-sm);
}

/* Gender badge — a soft tinted chip with a crisp Mars / Venus SVG (see
   js/gender.js). Two shapes from one component: a pill with the word (wide
   tables) or an icon-only round badge (Roster cards, Class-Plan tooltip). The
   tint is translucent so it sits well on any light surface. */
.gender-badge {
  display: inline-flex;
  align-items: center;
  gap: var(--sp-1);
  padding: 2px 9px 2px 7px;
  border-radius: 999px;
  font-size: var(--fs-sm);
  font-weight: 600;
  line-height: 1;
  vertical-align: middle;
  white-space: nowrap;
}
.gender-badge.gb-icononly {
  padding: 3px; /* equal padding → a neat round badge around the icon */
  gap: 0;
}
.gb-svg {
  width: 14px;
  height: 14px;
  display: block;
  flex: none;
}
.gb-label {
  padding-right: 1px;
}
.gb-male {
  color: #1d4ed8;
  background: rgba(37, 99, 235, 0.12);
}
.gb-female {
  color: #db2777;
  background: rgba(219, 39, 119, 0.12);
}

/* Nationality as a bordered TEXT chip right after the gender (per Big /
   execs, 2026-07-22) — flags were replaced by the country name so nobody has
   to recognise a flag or hover for a tooltip. */
.nat-chip {
  display: inline-flex;
  align-items: center;
  padding: 1px 8px;
  border: 1px solid var(--line-strong);
  border-radius: var(--r-full);
  background: var(--surface);
  color: var(--ink-2);
  font-size: var(--fs-xs);
  font-weight: 600;
  line-height: 1.6;
  white-space: nowrap;
  vertical-align: middle;
}

/* Gender + nationality kept side by side on ONE line in a table cell, so a
   long nationality never wraps under the gender chip and rows stay the same
   height whether or not the sidebar is pinned (per Big, 2026-07-29). */
.gn-cell {
  display: inline-flex;
  align-items: center;
  gap: 4px;
  white-space: nowrap;
  vertical-align: middle;
}

/* "Date Update" cell — a quiet, secondary timestamp (js/timeAgo.js). Muted so it
   reads as metadata next to the row's real data, tabular figures so the numbers
   line up down the column. */
.time-ago {
  color: var(--muted);
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
}
.time-ago.is-empty {
  opacity: 0.5;
}

/* Language flag (Thai/English/Chinese level rows) — still an IMAGE, since
   Windows has no flag-emoji glyphs. Hairline ring so pale flags keep an edge. */
.nat-flag {
  height: 14px;
  width: auto;
  border-radius: 2px;
  vertical-align: -2px;
  box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.12);
  cursor: help;
}

/* ══════════════════════════════════════════════════════════════════
   RESERVATION TOOLBAR — search + sort + filters above the table
   ══════════════════════════════════════════════════════════════════ */
.res-toolbar {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--sp-2);
  margin-bottom: var(--sp-4);
}
/* Search box with a magnifier icon inside its left edge. */
.res-search {
  position: relative;
}
.res-search svg {
  position: absolute;
  left: 12px;
  top: 50%;
  transform: translateY(-50%);
  width: 15px;
  height: 15px;
  color: var(--muted);
  pointer-events: none;
}
.res-search input {
  width: 240px;
  padding: 8px var(--sp-3) 8px 34px;
  font: inherit;
  font-size: var(--fs-base);
  color: var(--ink-2);
  background: var(--surface);
  border: 1px solid var(--line-strong);
  border-radius: var(--r-lg);
  transition:
    border-color var(--dur-1) var(--ease-out),
    box-shadow var(--dur-1) var(--ease-out);
}
.res-search input:focus {
  outline: none;
  border-color: var(--accent);
  box-shadow: 0 0 0 3px rgba(15, 23, 42, 0.09);
}
/* The dropdowns are the same cselect used in modals, just compact —
   and sized to their content instead of stretching full-width. */
.res-toolbar .cselect {
  width: auto;
}
.res-toolbar .cselect-trigger {
  width: auto;
  min-width: 130px;
  padding: 8px var(--sp-3);
  font-size: var(--fs-base);
}
.res-toolbar .hint {
  margin-left: auto;
  text-align: right;
}

/* ══════════════════════════════════════════════════════════════════
   ROSTER — room views: mini dashboard + one expandable card per class
   ══════════════════════════════════════════════════════════════════ */

/* Roster toolbar: two stacked rows (per Big, 2026-07-21) — actions +
   Morning/Afternoon on top, the sort/filter controls on their own line
   underneath. */
/* Both toolbars stack their rows: the Reservation page borrows the Roster's
   two-row layout and its Language switches (per Big, 2026-07-23). */
#roster-toolbar,
#res-toolbar {
  flex-direction: column;
  align-items: stretch;
  gap: var(--sp-3);
}
.rt-row {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--sp-2);
}
/* The controls row: each control is a label stacked ON TOP of its input, and
   they bottom-align so the dropdown and the chips sit on the same line. */
.rt-row-controls {
  align-items: flex-end;
  gap: var(--sp-5);
}
.roster-timepick,
.roster-langpick,
.roster-levelpick {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: 4px;
}
.roster-timepick .filter-label,
.roster-langpick .filter-label,
.roster-levelpick .filter-label {
  margin-right: 0;
}
/* The Level row sits under the Language row and can run long, so let it use
   the full width and wrap (per Big, 2026-07-22). */
.roster-levelpick {
  flex: 1 1 100%;
  min-width: 0;
}
.rt-chips {
  display: flex;
  flex-wrap: wrap;
  gap: var(--sp-2);
  min-height: 38px; /* match the dropdown height so the row lines up */
  align-items: center;
}
/* Each switched-on language gets its OWN Level row, labelled with its team
   and flag ("THAI LEVEL 🇹🇭"), per Big 2026-07-22. The flag is an <img> like
   every other flag in the app — Windows has no emoji flag glyphs. */
.lvl-lbl {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  white-space: nowrap;
}
.lvl-lbl .nat-flag {
  vertical-align: 0; /* the flex row already centres it */
  cursor: default;
}

/* ── Roster language switches ──────────────────────────────────────────
   Three real ON/OFF switches instead of a pick-one filter (per Big,
   2026-07-22): the Roster shows all three languages so the section totals
   count the whole school, and you flip a team off when you don't want it.
   Each switch takes its team's colour when on, and carries a count of that
   language's rooms this term. */
.rt-switches {
  display: flex;
  flex-wrap: wrap;
  gap: var(--sp-2);
  min-height: 38px; /* line up with the dropdown next to it */
  align-items: center;
}
.langsw {
  display: inline-flex;
  align-items: center;
  gap: var(--sp-2);
  padding: 5px var(--sp-3) 5px 6px;
  border: 1px solid var(--line-strong);
  border-radius: var(--r-full);
  background: var(--surface);
  color: var(--muted);
  font: inherit;
  font-size: var(--fs-base);
  font-weight: 600;
  cursor: pointer;
  transition:
    background-color var(--dur-1) var(--ease-out),
    border-color var(--dur-1) var(--ease-out),
    color var(--dur-1) var(--ease-out);
}
.langsw:hover {
  border-color: var(--muted);
  color: var(--ink);
}
.langsw-track {
  position: relative;
  width: 30px;
  height: 18px;
  flex-shrink: 0;
  border-radius: var(--r-full);
  background: var(--line-strong);
  transition: background-color var(--dur-1) var(--ease-out);
}
.langsw-knob {
  position: absolute;
  top: 2px;
  left: 2px;
  width: 14px;
  height: 14px;
  border-radius: var(--r-full);
  background: #fff;
  box-shadow: 0 1px 2px rgb(0 0 0 / 0.3);
  transition: transform var(--dur-1) var(--ease-out);
}
.langsw.on .langsw-knob {
  transform: translateX(12px);
}
/* The count of that language's rooms — dimmed, so it reads as a footnote. */
.langsw-count {
  min-width: 20px;
  padding: 0 5px;
  border-radius: var(--r-full);
  background: var(--surface-3);
  color: var(--muted);
  font-size: var(--fs-xs);
  font-weight: 700;
  text-align: center;
}
/* ON: the switch takes its team's colour, same palette as the class cards. */
.langsw.on {
  color: #fff;
  border-color: transparent;
}
.langsw.on .langsw-track {
  background: rgb(255 255 255 / 0.45);
}
.langsw.on .langsw-count {
  background: rgb(255 255 255 / 0.25);
  color: #fff;
}
.langsw.on.lsw-thai {
  background: var(--thai-solid);
}
.langsw.on.lsw-eng {
  background: var(--eng-solid);
}
.langsw.on.lsw-chi {
  background: var(--chi-solid);
}
/* OFF: greyed right down, so "off" is unmistakable at a glance. */
.langsw:not(.on) .langsw-name {
  opacity: 0.65;
}

#roster-toolbar .cselect {
  width: auto;
}
#roster-toolbar .cselect-trigger {
  width: auto;
  min-width: 200px;
  padding: 8px var(--sp-3);
  font-size: var(--fs-base);
}

/* Mini dashboard tiles (totals for the picked section). */
.roster-dash {
  display: flex;
  flex-wrap: wrap;
  align-items: stretch;
  gap: var(--sp-3);
  margin-bottom: var(--sp-5);
}
.dash-tile {
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: var(--r-lg);
  box-shadow: var(--sh-1);
  padding: var(--sp-3) var(--sp-5);
  min-width: 130px;
}
.dash-label {
  font-size: var(--fs-sm);
  color: var(--muted);
  margin-bottom: 2px;
}
.dash-value {
  font-size: var(--fs-2xl);
  font-weight: 700;
  letter-spacing: -0.01em;
}
.dash-warn .dash-value {
  color: var(--danger);
}
.dash-hold {
  color: var(--muted);
  cursor: help; /* tooltip explains it waits for the Attendance program */
}
/* Status legend is now a lightbulb button (per Big, 2026-07-21): hover to peek
   its tooltip, click to pin it open, ✕ or outside-click to close. */
.status-help {
  align-self: center;
  margin-left: auto;
  display: inline-flex;
  align-items: center;
  gap: var(--sp-2);
  padding: 6px var(--sp-3);
  font: inherit;
  font-size: var(--fs-sm);
  font-weight: 600;
  color: var(--muted);
  background: var(--surface);
  border: 1px solid var(--line-strong);
  border-radius: var(--r-full);
  cursor: pointer;
  transition:
    color var(--dur-1) var(--ease-out),
    border-color var(--dur-1) var(--ease-out),
    background-color var(--dur-1) var(--ease-out);
}
.status-help svg {
  width: 16px;
  height: 16px;
}
.status-help:hover,
.status-help.on {
  color: var(--warn-strong);
  border-color: var(--warn-ring);
  background: var(--warn-bg);
}

/* The tooltip itself — a top-layer popover positioned in JS under the button. */
.status-tip {
  position: fixed;
  inset: auto;
  margin: 0;
  z-index: var(--z-modal);
  width: 320px;
  max-width: calc(100vw - 16px);
  background: var(--surface);
  border: 1px solid var(--line-strong);
  border-radius: var(--r-lg);
  box-shadow: var(--sh-3);
  padding: 0;
}
.status-tip-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--sp-2);
  padding: var(--sp-3) var(--sp-4);
  border-bottom: 1px solid var(--line);
  font-size: var(--fs-md);
}
.status-tip-close {
  border: none;
  background: none;
  color: var(--muted);
  font-size: var(--fs-md);
  line-height: 1;
  cursor: pointer;
  padding: 2px 6px;
  border-radius: var(--r-sm);
  transition:
    color var(--dur-1) var(--ease-out),
    background-color var(--dur-1) var(--ease-out);
}
.status-tip-close:hover {
  color: var(--ink);
  background: var(--surface-2);
}
.status-tip-list {
  list-style: none;
  margin: 0;
  padding: var(--sp-2) var(--sp-3) var(--sp-3);
  display: grid;
  gap: 6px;
  max-height: 60vh;
  overflow-y: auto;
}
.status-tip-list li {
  display: flex;
  align-items: center;
  gap: var(--sp-3);
}
.status-tip-list .st-chip {
  min-width: 34px;
  text-align: center;
}
.status-tip-mean {
  font-size: var(--fs-base);
  color: var(--ink-2);
}

/* Per-student remark note: a small, faint bubble by the status. Rows WITH a
   remark get a soft amber tint (like a sticky note) so they stand out; empty
   ones stay a quiet grey. Kept small on purpose (per Big). */
.note-btn {
  flex: none;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 22px;
  height: 22px;
  padding: 0;
  border: none;
  border-radius: 7px;
  background: rgb(100 116 139 / 0.08);
  color: #94a3b8;
  cursor: pointer;
  transition:
    background-color var(--dur-1) var(--ease-out),
    color var(--dur-1) var(--ease-out);
}
.note-btn svg {
  width: 13px;
  height: 13px;
}
.note-btn:hover {
  background: rgb(100 116 139 / 0.18);
  color: #475569;
}
.note-btn.has-note {
  background: rgb(245 158 11 / 0.16);
  color: #d97706;
}
.note-btn.has-note:hover {
  background: rgb(245 158 11 / 0.26);
}
.note-btn:focus-visible {
  outline: 2px solid var(--ring);
  outline-offset: 2px;
}

/* The remark popover — same shell as the status tooltip, with an editable
   textarea. Width must match positionNoteTip()'s W constant (300). */
.note-tip {
  position: fixed;
  inset: auto;
  margin: 0;
  z-index: var(--z-modal);
  width: 300px;
  max-width: calc(100vw - 16px);
  background: var(--surface);
  border: 1px solid var(--line-strong);
  border-radius: var(--r-lg);
  box-shadow: var(--sh-3);
  padding: 0;
}
.note-tip-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--sp-2);
  padding: var(--sp-3) var(--sp-4);
  border-bottom: 1px solid var(--line);
  font-size: var(--fs-md);
}
.note-tip-close {
  border: none;
  background: none;
  color: var(--muted);
  font-size: var(--fs-md);
  line-height: 1;
  cursor: pointer;
  padding: 2px 6px;
  border-radius: var(--r-sm);
  transition:
    color var(--dur-1) var(--ease-out),
    background-color var(--dur-1) var(--ease-out);
}
.note-tip-close:hover {
  color: var(--ink);
  background: var(--surface-2);
}
.note-tip-text {
  display: block;
  width: calc(100% - 2 * var(--sp-4));
  margin: var(--sp-3) var(--sp-4) 0;
  min-height: 84px;
  resize: vertical;
  font: inherit;
  font-size: var(--fs-base);
  color: var(--ink);
  background: var(--surface);
  border: 1px solid var(--line-strong);
  border-radius: var(--r-md);
  padding: var(--sp-2) var(--sp-3);
}
.note-tip-text:focus {
  outline: 2px solid var(--ring);
  outline-offset: 1px;
}
.note-tip-text[readonly] {
  background: var(--surface-2);
  color: var(--ink-2);
}
.note-tip-foot {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--sp-2);
  padding: var(--sp-3) var(--sp-4);
}
.note-tip-hint {
  font-size: var(--fs-xs);
  color: var(--muted);
}
.note-tip-save {
  border: none;
  border-radius: var(--r-sm);
  padding: 5px 14px;
  font: inherit;
  font-size: var(--fs-sm);
  font-weight: 700;
  color: #fff;
  background: var(--brand);
  cursor: pointer;
  transition: filter var(--dur-1) var(--ease-out);
}
.note-tip-save:hover {
  filter: brightness(1.05);
}
.note-tip-save:focus-visible {
  outline: 2px solid var(--ring);
  outline-offset: 2px;
}

/* ── Section columns: each visible time Section (1-4) is its own vertical
   COLUMN, laid out side by side (Section 1 left, Section 2 right, …) with
   its room cards stacked straight DOWN inside it — a Kanban-style board
   (redesigned 2026-07-21, per Big: the earlier version flowed cards
   sideways which read as messy). Frame is neutral slate on purpose — colour
   already means Level on the cards, so a second frame colour would be noise. */
.roster-cards {
  display: flex;
  align-items: flex-start;
  gap: var(--sp-4);
  overflow-x: auto; /* many columns (or a narrow window) scroll sideways */
  padding-bottom: var(--sp-2);
}
.rsection {
  flex: 1 1 0;
  min-width: 340px;
  border: 2px solid #94a3b8;
  background: #f8fafc;
  border-radius: var(--r-xl);
  padding: var(--sp-4);
}
/* The Session frame carries the colour (per Big, 2026-07-22) — the cards
   inside are neutral grey. Session 1 light blue · 2 blue · 3 light green ·
   4 green; a 5th+ session cycles back through the same four. */
.rsection-s1 {
  border-color: #bfdbfe;
  background: #eff6ff;
}
.rsection-s2 {
  border-color: #60a5fa;
  background: #dbeafe;
}
.rsection-s3 {
  border-color: #bbf7d0;
  background: #f0fdf4;
}
.rsection-s4 {
  border-color: #4ade80;
  background: #dcfce7;
}
/* Two columns stretch to fill the page width (no dead space on the right,
   per Big 2026-07-21). A lone column — when "Sort by time" focuses one
   section — is capped so a single card doesn't sprawl across the whole page. */
.rsection:only-child {
  max-width: 860px;
}
.rsection-title {
  display: flex;
  align-items: center;
  gap: var(--sp-2);
  font-size: var(--fs-md);
  font-weight: 700;
  color: #334155;
  margin-bottom: var(--sp-3);
}
.rsection-badge {
  background: #475569;
  color: #fff;
  font-size: var(--fs-xs);
  font-weight: 700;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  padding: 3px var(--sp-3);
  border-radius: var(--r-full);
}
.rsection-time {
  color: #64748b;
  font-weight: 600;
}
/* ── .hud — the SCOREBOARD panel (per Big, 2026-07-21) ──
   The glossy, embossed material of the Upgrade button (dark gradient face,
   top gloss, raised bottom edge, soft drop) but STATIC — it's a readout, not
   a button, so no hover-lift or press. Labels sit muted; the numbers glow
   amber like stadium/LED digits so the whole block reads as a scoreboard.

   Started life as the Roster's per-section count. The Class Plan's whole-day
   total asked for the same look (per Big, 2026-07-23), so the material lives
   here under a neutral name and each page adds only its own POSITIONING —
   copy-pasting the gradient would have left two scoreboards free to drift
   apart on the next tweak. Colours are literal, not tokens: this panel is
   deliberately its own dark world and does not follow the page's surfaces. */
.hud {
  position: relative;
  overflow: hidden;
  display: flex;
  flex-direction: column;
  align-items: stretch;
  gap: 3px;
  padding: 7px 14px;
  border-radius: var(--r-md);
  background-color: #1e293b;
  background-image: linear-gradient(135deg, #3b4a63 0%, #1e293b 100%);
  box-shadow:
    inset 0 1px 0 rgb(255 255 255 / 0.12),
    inset 0 -1px 0 rgb(0 0 0 / 0.35),
    0 2px 0 #0f172a,
    0 5px 14px rgb(15 23 42 / 0.35);
  line-height: 1.25;
}
/* Fixed top-half gloss for the "glass" sheen (the Upgrade button's shine is a
   moving hover effect; a scoreboard just sits and shines). */
.hud::before {
  content: "";
  position: absolute;
  inset: 0 0 55% 0;
  background: linear-gradient(180deg, rgb(255 255 255 / 0.1), transparent);
  pointer-events: none;
}
.hud-line {
  display: flex;
  align-items: baseline;
  justify-content: flex-end;
  gap: var(--sp-2);
}
.hud-lbl {
  font-size: var(--fs-sm);
  font-weight: 600;
  letter-spacing: 0.02em;
  color: #cbd5e1;
}
.hud-val {
  font-size: var(--fs-xl);
  font-weight: 800;
  color: #fcd34d;
  font-variant-numeric: tabular-nums;
  min-width: 1.6em;
  text-align: right;
  text-shadow: 0 0 6px rgb(252 211 77 / 0.45);
}
/* The Roster panel's only job of its own: park at the end of the section
   heading. Everything it looks like comes from .hud. */
.rsection-count {
  margin-left: auto;
}
/* Course name on each room card, coloured by its level — same colours as
   the card's own border so the eye links them instantly. */
.course-lv {
  font-weight: 700;
}
/* The title dot follows the level colour too: the course-lv-* class sets its
   `color`, and currentColor paints the circle with it (overrides the default
   language tint from .lang-*). */
.rcard-title .cc-dot {
  background: currentColor;
}
.course-lv-J {
  color: #15803d;
}
.course-lv-E {
  color: #c2410c;
}
.course-lv-C {
  color: #1d4ed8;
}
.course-lv-other {
  color: var(--muted);
}

/* Room cards stack straight down inside their section column. */
.rsection-cards {
  display: flex;
  flex-direction: column;
  gap: var(--sp-4);
}
.rsection-empty {
  padding: var(--sp-6) var(--sp-4);
}
/* Ornamental divider between two level groups (J→E, E→C) inside a section
   column (per Big, 2026-07-21): a thin fading hairline on each side toward a
   small centred diamond + dots. Deliberately light — a soft break, not a rule. */
.rlevel-divider {
  display: flex;
  align-items: center;
  gap: var(--sp-2);
  margin: calc(-1 * var(--sp-1)) var(--sp-3);
  color: #94a3b8;
}
.rlevel-divider::before,
.rlevel-divider::after {
  content: "";
  flex: 1;
  height: 1px;
}
.rlevel-divider::before {
  background: linear-gradient(to right, transparent, currentColor);
}
.rlevel-divider::after {
  background: linear-gradient(to left, transparent, currentColor);
}
.rld-orn {
  width: 46px;
  height: 10px;
  flex-shrink: 0;
  opacity: 0.9;
}
.rcard {
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: var(--r-xl);
  box-shadow: var(--sh-1);
  overflow: hidden;
}
/* Room cards are NEUTRAL now (per Big, 2026-07-22): a plain grey frame on a
   faint grey face. Colour moved up to the Session frame that holds them, so
   the board reads by time-slot instead of a rainbow of level colours. The
   level still tints the card's TEXT (.course-lv-*), which stays as it was. */
.rcard-lv-J,
.rcard-lv-E,
.rcard-lv-C,
.rcard-lv-other {
  border: 1px solid #cbd5e1;
  background: #f8fafc;
}
/* Plain header (cards no longer collapse — every panel is always open). */
.rcard-head {
  padding: var(--sp-4) var(--sp-5);
}
.rcard-title {
  display: flex;
  align-items: center;
  gap: var(--sp-2);
  font-size: var(--fs-md);
}
.rcard-count {
  margin-left: auto;
  font-size: var(--fs-sm);
  color: var(--muted);
  font-weight: 600;
  white-space: nowrap;
}
.rcard-sub {
  margin-top: 2px;
  padding-left: calc(7px + var(--sp-2));
  font-size: var(--fs-sm);
  color: var(--muted);
}
/* Language tint on the dot only — the card stays neutral so a wall of 18
   cards doesn't turn into a colour test. */
.rcard .cc-dot {
  flex-shrink: 0;
}

/* Expanded student panel. */
.rcard-panel {
  border-top: 1px solid var(--line);
  padding: var(--sp-3) var(--sp-5) var(--sp-4);
}
/* The list shows 5 rows; longer classes scroll INSIDE the list (per Big,
   2026-07-16). overscroll-behavior keeps the leftover scroll from moving
   the page behind — same trick as the dropdown menus. */
.rstudents {
  list-style: none;
  margin: 0 0 var(--sp-3);
  padding: 0;
  /* Exactly 5 rows tall — the 6th onwards is reached by scrolling, so every
     room card keeps the same height no matter the class size (per Big,
     2026-07-21). Rows are a fixed height so "5" is exact, not approximate. */
  --rstudent-h: 44px;
  max-height: calc(var(--rstudent-h) * 5);
  overflow-y: auto;
  overscroll-behavior: contain;
}
.rstudent {
  display: flex;
  align-items: center;
  gap: var(--sp-3);
  height: var(--rstudent-h, 44px);
  box-sizing: border-box;
  border-bottom: 1px solid var(--line);
  font-size: var(--fs-md);
}
/* Tick-boxes for picking who gets sent on by Upgrade / Downgrade. */
.rstudent-check {
  width: 16px;
  height: 16px;
  flex-shrink: 0;
  accent-color: var(--brand);
  cursor: pointer;
}
.rstudent:last-child {
  border-bottom: none;
}
.rstudent-n {
  width: 22px;
  text-align: right;
  color: var(--muted);
  font-size: var(--fs-sm);
  flex-shrink: 0;
}
.rstudent-name {
  flex: 1;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap; /* keeps every row exactly one line = fixed height */
}
.rstudent-empty {
  color: var(--muted);
}

/* Per-student status (PT/VT/V/P): the app-wide cselect DROPDOWN shrunk to a
   chip — same open menu as everywhere else, tinted by the current value.
   .st-chip (plain span) is the read-only fallback for other teams. */
.st-select {
  flex-shrink: 0;
}
.st-select .cselect-trigger {
  width: auto;
  min-width: 62px;
  padding: 2px 8px 2px 10px;
  font-size: var(--fs-xs);
  font-weight: 700;
  letter-spacing: 0.03em;
  border-radius: var(--r-full);
}
.st-select .cselect-chevron {
  width: 11px;
  height: 11px;
}
.st-select .cselect-value.is-placeholder {
  font-weight: 700; /* "—" needs no extra emphasis */
}
.st-chip {
  font-size: var(--fs-xs);
  font-weight: 700;
  letter-spacing: 0.03em;
  border: 1px solid transparent;
  border-radius: var(--r-full);
  padding: 2px 10px;
  flex-shrink: 0;
}
.st-PT .cselect-trigger,
.st-chip.st-PT {
  background: #fef3c7;
  color: #92400e;
  border-color: #fcd34d;
}
.st-VT .cselect-trigger,
.st-chip.st-VT {
  background: #d1fae5;
  color: #065f46;
  border-color: #6ee7b7;
}
.st-V .cselect-trigger,
.st-chip.st-V {
  background: #dbeafe;
  color: #1e40af;
  border-color: #93c5fd;
}
.st-P .cselect-trigger,
.st-chip.st-P {
  background: #f3f4f6;
  color: #374151;
  border-color: #d1d5db;
}
/* Deposit (violet) + Free (teal) codes added 2026-07-21 (per Big). */
.st-D .cselect-trigger,
.st-chip.st-D {
  background: #ede9fe;
  color: #5b21b6;
  border-color: #c4b5fd;
}
.st-DT .cselect-trigger,
.st-chip.st-DT {
  background: #fce7f3;
  color: #9d174d;
  border-color: #f9a8d4;
}
.st-F .cselect-trigger,
.st-chip.st-F {
  background: #ccfbf1;
  color: #115e59;
  border-color: #5eead4;
}
.st-FT .cselect-trigger,
.st-chip.st-FT {
  background: #e0f2fe;
  color: #075985;
  border-color: #7dd3fc;
}
.st-none .cselect-trigger,
.st-chip.st-none {
  background: var(--surface-2);
  color: var(--muted);
  border-color: var(--line);
}

/* Panel footer: add-student picker + the destructive actions. */
.rcard-actions {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--sp-2);
  padding-top: var(--sp-3);
  border-top: 1px solid var(--line);
}
/* Button row: all four room actions sit on ONE row, never wrapping (per
   Big, 2026-07-16) — compact padding/text so they always fit the card.
   Duplicate carries an exit-arrow icon. */
.rcard-btnrow {
  display: flex;
  flex-wrap: wrap; /* the Upgrade button is wide — let it drop if it must */
  align-items: center;
  justify-content: space-between;
  gap: var(--sp-1);
  flex: 1 1 100%;
  min-width: 0;
  /* Breathing room at both ends (per Big, 2026-07-22): the leftmost hub's spoke
     reaches ~32px past the hub's own box, and the card clips its overflow — so
     without this the "Edit" spoke was sliced off by the card edge. Both sides,
     to keep the row visually centred. */
  padding-inline: 22px;
}
/* Every button in this row is a .gbtn now, so let .gbtn own the sizing. */
.rcard-btnrow button {
  white-space: nowrap;
}

/* ── Grouped actions: "Edit Room" / "Edit User" (per Big, 2026-07-22) ──
   One click and the group's actions fly out in a RADIAL wheel above the
   button — the House Flipper pie menu, in the app's own light palette. The
   spokes arc UPWARD only, so the wheel always stays inside the room card,
   which clips its overflow. */
.btn-group {
  display: inline-flex;
  align-items: center;
}
.btn-group.open {
  z-index: 30; /* the open wheel sits above the student list it covers */
}
/* The hub: a round button with its caption underneath, built exactly like a
   spoke (per Big, 2026-07-22) so the group reads as the centre of its own
   wheel. It is the wheel's positioning parent, so the spokes fly out from the
   CIRCLE's centre — anchoring to the whole button-plus-label stack instead
   would push the arc down by half the caption. */
.btn-hub {
  position: relative;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 2px;
}
.btn-radial {
  position: absolute;
  left: 50%;
  top: 21px; /* half the hub button — the circle's centre */
  width: 0;
  height: 0;
  pointer-events: none; /* closed: never swallow clicks meant for the card */
}
.btn-group.open .btn-radial {
  pointer-events: auto;
}
/* No backing plate behind the wheel (per Big, 2026-07-22) — the spokes fly
   over the student list on their own; each one carries its own shadow and its
   label its own chip, so they stay readable without a white dome. */
/* Each spoke: parked at the centre, flung to (--x, --y) when the wheel opens. */
.btn-radial-item {
  position: absolute;
  left: 50%;
  top: 50%;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 2px;
  transform: translate(-50%, -50%) scale(0.3);
  opacity: 0;
  transition:
    transform var(--dur-3) var(--ease-out),
    opacity var(--dur-2) var(--ease-out);
}
.btn-group.open .btn-radial-item {
  transform: translate(calc(-50% + var(--x)), calc(-50% - var(--y))) scale(1);
  opacity: 1;
}
.radial-btn {
  width: 38px;
  height: 38px;
  display: grid;
  place-items: center;
  padding: 0;
  border: none;
  border-radius: 50%;
  color: #fff;
  cursor: pointer;
  /* Stronger than a normal button's: with no backing plate these float free
     over the student names and need the lift to read as "on top". */
  box-shadow:
    0 3px 10px rgb(15 23 42 / 0.32),
    0 0 0 2px var(--surface);
  transition:
    transform var(--dur-1) var(--ease-out),
    filter var(--dur-1) var(--ease-out);
}
.radial-btn .btn-ico {
  width: 17px;
  height: 17px;
  margin: 0;
  vertical-align: 0;
}
.radial-btn:hover:not(:disabled) {
  transform: scale(1.12);
  filter: brightness(1.06);
}
.radial-btn:focus-visible {
  outline: 2px solid var(--ring);
  outline-offset: 2px;
}
.radial-btn:disabled {
  background: linear-gradient(135deg, #d3d9e0 0%, #b9c1cb 100%);
  color: #f1f5f9;
  cursor: not-allowed;
  box-shadow: 0 0 0 2px var(--surface);
}
.radial-label {
  font-size: 9px;
  font-weight: 700;
  line-height: 1.5;
  color: var(--ink-2);
  background: var(--surface);
  border: 1px solid var(--line);
  box-shadow: 0 1px 4px rgb(15 23 42 / 0.18);
  padding: 0 4px;
  border-radius: var(--r-sm);
  white-space: nowrap;
  /* All captions share a floor width and centre their text (per Big,
     2026-07-22), so a short one ("Edit") and a long one ("Delete") still read
     as a matching pair instead of one tiny chip next to a wide one. */
  min-width: 46px;
  text-align: center;
}
/* A spoke that can't be used greys its caption too, so the whole thing reads
   as off — not just a grey circle with a live-looking label under it. */
.btn-radial-item:has(.radial-btn:disabled) .radial-label {
  color: var(--muted);
  background: var(--surface-2);
}
/* Spoke colours reuse the muted button palette. */
.radial-edit {
  background: linear-gradient(135deg, #8ea3ba 0%, #667e97 100%);
}
.radial-add {
  background: linear-gradient(135deg, #77bd90 0%, #4d9166 100%);
}
.radial-warn {
  background: linear-gradient(135deg, #c9b18c 0%, #a68d68 100%);
}
.radial-danger {
  background: linear-gradient(135deg, #c09292 0%, #9c6c6c 100%);
}
/* The hub button itself — a bigger sibling of .radial-btn, so the wheel looks
   like one family of circles with the parent at its centre. */
.hub-btn {
  width: 42px;
  height: 42px;
  display: grid;
  place-items: center;
  padding: 0;
  border: none;
  border-radius: 50%;
  color: #fff;
  cursor: pointer;
  box-shadow:
    0 3px 10px rgb(15 23 42 / 0.3),
    0 0 0 2px var(--surface);
  transition:
    transform var(--dur-1) var(--ease-out),
    filter var(--dur-1) var(--ease-out),
    box-shadow var(--dur-2) var(--ease-out);
}
.hub-btn .btn-ico {
  width: 19px;
  height: 19px;
  margin: 0;
  vertical-align: 0;
}
.hub-btn:hover {
  transform: scale(1.08);
  filter: brightness(1.06);
}
.hub-btn:focus-visible {
  outline: 2px solid var(--ring);
  outline-offset: 2px;
}
/* Open: the hub sinks slightly and gains a halo, so it's obvious WHICH hub the
   spokes on screen belong to when both groups sit side by side. */
.btn-group.open .hub-btn {
  transform: scale(0.94);
  box-shadow:
    0 1px 4px rgb(15 23 42 / 0.24),
    0 0 0 2px var(--surface),
    0 0 0 5px var(--hub-halo, rgb(100 116 139 / 0.3));
}
/* Two distinct colours (per Big, 2026-07-22) — the hubs used to be identical
   greys, so the only thing telling them apart was the caption. */
.hub-room {
  background: linear-gradient(135deg, #7f93d8 0%, #4c60a6 100%);
  --hub-halo: rgb(76 96 166 / 0.32);
}
.hub-user {
  background: linear-gradient(135deg, #6fb6c2 0%, #3d7d8b 100%);
  --hub-halo: rgb(61 125 139 / 0.32);
}
/* The hub's caption sits a step above its spokes' — it names the whole group. */
.hub-label {
  font-size: 10px;
}
@media (prefers-reduced-motion: reduce) {
  .btn-radial-item,
  .hub-btn,
  .radial-btn {
    transition: none;
  }
  /* Drop the hub's scaling, but NOT the spokes' — theirs is what places them
     on the wheel, not decoration. */
  .hub-btn:hover,
  .btn-group.open .hub-btn {
    transform: none;
  }
}

/* ── "Game buttons" on the room cards (per Big, 2026-07-21) ──
   One base look — gradient face, a solid edge underneath for depth, a lift on
   hover and a press on click, plus a shine that sweeps across on hover. Each
   variant only supplies its own colours via the three custom properties, so
   the behaviour stays identical across the row.
   The Upgrade button can also carry artwork: set
   `--upg-img: url("../assets/upgrade.png")` on it (or any ancestor) and the
   image layers over the gradient; leave it unset and the gradient stands alone. */
.gbtn {
  --gbtn-edge: #334155;
  --gbtn-glow: rgb(51 65 85 / 0.35);
  --gbtn-face: linear-gradient(135deg, #94a3b8 0%, #475569 100%);
  position: relative;
  overflow: hidden;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 5px;
  padding: 6px 13px;
  border: none;
  border-radius: var(--r-md);
  font: inherit;
  font-size: var(--fs-xs);
  font-weight: 800;
  white-space: nowrap;
  color: #fff;
  cursor: pointer;
  background-color: var(--gbtn-edge);
  background-image: var(--gbtn-face);
  background-size: cover;
  background-position: center;
  text-shadow: 0 1px 2px rgb(0 0 0 / 0.35);
  box-shadow:
    0 2px 0 var(--gbtn-edge),
    0 4px 12px var(--gbtn-glow);
  transition:
    transform var(--dur-1) var(--ease-out),
    box-shadow var(--dur-1) var(--ease-out);
}
.gbtn .btn-ico {
  width: 13px;
  height: 13px;
  margin-right: 0;
  vertical-align: 0;
  flex-shrink: 0;
}
/* The shine, parked off to the left until a hover sweeps it across. */
.gbtn::after {
  content: "";
  position: absolute;
  top: 0;
  bottom: 0;
  left: -60%;
  width: 40%;
  background: linear-gradient(
    90deg,
    transparent,
    rgb(255 255 255 / 0.5),
    transparent
  );
  transform: skewX(-20deg);
  pointer-events: none;
}
.gbtn:hover {
  transform: translateY(-1px);
  box-shadow:
    0 3px 0 var(--gbtn-edge),
    0 8px 18px var(--gbtn-glow);
}
.gbtn:hover::after {
  animation: gbtn-shine 0.8s var(--ease-out);
}
.gbtn:active {
  transform: translateY(1px);
  box-shadow:
    0 1px 0 var(--gbtn-edge),
    0 2px 6px var(--gbtn-glow);
}
.gbtn:focus-visible {
  outline: 2px solid var(--ring);
  outline-offset: 2px;
}
@keyframes gbtn-shine {
  from {
    left: -60%;
  }
  to {
    left: 130%;
  }
}
@media (prefers-reduced-motion: reduce) {
  .gbtn,
  .gbtn:hover,
  .gbtn:active {
    transform: none;
  }
  .gbtn:hover::after {
    animation: none;
  }
}

/* Colour variants — MUTED (per Big / execs, 2026-07-22: the old candy
   palette read as too colourful). Each keeps its own hue so the actions are
   still tellable apart, but desaturated, with a gentle shade rather than a
   bright gradient. */
.gbtn-neutral {
  --gbtn-edge: #5b6b7f;
  --gbtn-glow: rgb(100 116 139 / 0.22);
  --gbtn-face: linear-gradient(135deg, #97a6b8 0%, #74849a 100%);
}
.gbtn-edit {
  --gbtn-edge: #4d6076;
  --gbtn-glow: rgb(77 96 118 / 0.22);
  --gbtn-face: linear-gradient(135deg, #8ea3ba 0%, #667e97 100%);
}
/* A touch greener than the rest of the muted set (per Big, 2026-07-22) — still
   restrained, just enough colour to read as "this one adds something". */
.gbtn-add {
  --gbtn-edge: #3d6b4c;
  --gbtn-glow: rgb(61 107 76 / 0.26);
  --gbtn-face: linear-gradient(135deg, #77bd90 0%, #4d9166 100%);
}
.gbtn-warn {
  --gbtn-edge: #806b4c;
  --gbtn-glow: rgb(128 107 76 / 0.22);
  --gbtn-face: linear-gradient(135deg, #c9b18c 0%, #a68d68 100%);
}
.gbtn-danger {
  --gbtn-edge: #7c5353;
  --gbtn-glow: rgb(124 83 83 / 0.22);
  --gbtn-face: linear-gradient(135deg, #c09292 0%, #9c6c6c 100%);
}
/* The one genuinely GOLD button (per Big, 2026-07-22): it's the action that
   moves students between rooms, so it's allowed to stand out from the muted
   set. Warmer and more saturated than .gbtn-warn's sand. */
.upg-btn {
  --gbtn-edge: #8a6a24;
  --gbtn-glow: rgb(180 138 47 / 0.32);
  --gbtn-face:
    var(--upg-img, none), linear-gradient(135deg, #e3bf63 0%, #b8912f 100%);
}
/* Disabled (e.g. "Delete" before any student is ticked) — flat grey, no lift. */
.gbtn:disabled {
  --gbtn-edge: #9aa5b1;
  --gbtn-glow: transparent;
  --gbtn-face: linear-gradient(135deg, #d3d9e0 0%, #b9c1cb 100%);
  color: #f1f5f9;
  cursor: not-allowed;
}
.gbtn:disabled:hover {
  transform: none;
  box-shadow:
    0 2px 0 var(--gbtn-edge),
    0 4px 12px var(--gbtn-glow);
}
.gbtn:disabled:hover::after {
  animation: none;
}
.upg-btn .btn-ico {
  width: 14px;
  height: 14px;
}

/* The list of who's being sent, inside the Upgrade / Downgrade modal. */
.upg-names {
  list-style: none;
  margin: 0;
  padding: var(--sp-2) var(--sp-3);
  max-height: 132px;
  overflow-y: auto;
  overscroll-behavior: contain;
  background: var(--surface-2);
  border: 1px solid var(--line);
  border-radius: var(--r-md);
  font-size: var(--fs-base);
}
.upg-names li {
  padding: 3px 0;
}

/* ── "Add students" picker (per Big, 2026-07-22) ──
   The Add button in the Edit User wheel opens a tickable list of the pending
   students that match the room, replacing the per-card dropdown + Add row.

   This block deliberately does NOT sit inside a .modal-field. That class is
   for "caption above one input", and its rules in dialogs.css — which loads
   after this file and so wins every tie — were silently flattening the rows:
   `.modal-field label { display: block }` beat `.pick-row > label { display:
   flex }` at equal specificity, so the row was never a flex row at all and
   every alignment rule here did nothing. Own class, own rules, no tie to lose.
   Its caption is a <span>, not a <label>: it names a list, not one control. */
.pick-field {
  margin-bottom: var(--sp-4);
}
.pick-caption {
  display: block;
  margin-bottom: var(--sp-2);
  font-size: var(--fs-sm);
  font-weight: 600;
  color: var(--muted);
}
.pick-list {
  list-style: none;
  margin: 0;
  padding: var(--sp-1);
  max-height: 260px;
  overflow-y: auto;
  overscroll-behavior: contain;
  background: var(--surface-2);
  border: 1px solid var(--line);
  border-radius: var(--r-md);
}
.pick-row > label {
  display: flex;
  align-items: center;
  gap: var(--sp-2);
  padding: 6px var(--sp-2);
  border-radius: var(--r-sm);
  cursor: pointer;
}
.pick-row > label:hover {
  background: var(--surface);
}
/* Ticked rows tint, so what you're about to add reads at a glance even after
   scrolling past it. :has() on the label keeps it to CSS — no JS bookkeeping. */
.pick-row > label:has(input:checked) {
  background: var(--surface);
  box-shadow: inset 3px 0 0 var(--brand);
}
/* The tick is pinned to the far right of the row and centred against the name
   (per Big, 2026-07-22). margin-left:auto does the pinning outright rather than
   leaning on .pick-name's flex:1 to leave a gap — with a long name and a long
   meta line there is no spare room, and the tick would drift back inwards.
   Size, colour and cursor come from the global input[type="checkbox"] in
   controls.css, so it matches every other tick in the app. */
/* The Move picker (per Big, 2026-07-23) reuses these rows with RADIOS instead
   — students go to exactly one room, so a tick that allowed two would be
   lying about what the button does. Same geometry either way. */
.pick-row input[type="checkbox"],
.pick-row input[type="radio"] {
  flex: none;
  margin-left: auto;
}
.pick-row input[type="radio"] {
  width: 17px;
  height: 17px;
  accent-color: var(--accent);
  cursor: pointer;
}
.pick-name {
  flex: 1;
  min-width: 0;
  font-size: var(--fs-base);
  font-weight: 600;
  color: var(--ink);
}
.pick-meta {
  flex: none;
  font-size: var(--fs-xs);
  color: var(--muted);
  white-space: nowrap;
}
.pick-none {
  margin: var(--sp-2) 0 0;
  font-size: var(--fs-sm);
  color: var(--muted);
  text-align: center;
}
.btn-ico {
  width: 13px;
  height: 13px;
  vertical-align: -2px;
  margin-right: 5px;
}
.rcard-spacer {
  flex: 1;
}

/* ══════════════════════════════════════════════════════════════════
   CLASS PLAN TOOLTIP — hover a class card for its details
   ══════════════════════════════════════════════════════════════════ */
.sched-tooltip {
  display: none;
  position: fixed;
  z-index: var(--z-modal);
  max-width: 300px;
  background: var(--surface);
  border: 1px solid var(--line-strong);
  border-radius: var(--r-lg);
  box-shadow: var(--sh-3);
  /* No padding here on purpose: each section pads itself, so the coloured
     header can run edge to edge as one solid bar instead of a tint floating
     inside the padding (looked like a stray highlight — per Big, 2026-07-21).
     overflow:hidden clips that bar to the rounded top corners. */
  padding: 0;
  overflow: hidden;
  font-size: var(--fs-base);
  pointer-events: none; /* never steals the mouse from the card under it */
}
/* Header row: language dot + "group · room" + student count — mirrors the
   Roster card head so both read the same. */
.sched-tooltip .tt-head {
  display: flex;
  align-items: center;
  gap: var(--sp-2);
  font-size: var(--fs-md);
  /* Solid full-width header bar, tinted by the .lang-* class on it. */
  padding: var(--sp-3) var(--sp-4);
  border-bottom: 1px solid var(--line);
}
.sched-tooltip .tt-count {
  margin-left: auto;
  font-size: var(--fs-sm);
  font-weight: 600;
  color: var(--muted);
  white-space: nowrap;
}
/* Labeled facts: ครู / เวลา / คอร์ส. */
.sched-tooltip .tt-rows {
  padding: var(--sp-3) var(--sp-4);
  border-bottom: 1px solid var(--line);
}
.sched-tooltip .tt-row {
  padding: 1px 0;
}
.sched-tooltip .tt-label {
  display: inline-block;
  min-width: 48px;
  color: var(--muted);
  font-size: var(--fs-sm);
}
.sched-tooltip .tt-students {
  list-style: none;
  margin: 0;
  padding: var(--sp-2) var(--sp-4) var(--sp-3);
  max-height: 40vh;
  overflow: hidden;
}
.sched-tooltip .tt-students li {
  display: flex;
  align-items: center;
  gap: var(--sp-2);
  padding: 3px 0;
}
.sched-tooltip .tt-n {
  width: 16px;
  text-align: right;
  color: var(--muted);
  font-size: var(--fs-sm);
  flex-shrink: 0;
}
.sched-tooltip .tt-empty {
  color: var(--muted);
  padding: var(--sp-3) var(--sp-4);
}

/* Under an ASSIGNED pill: which class group · room the student landed in. */
.res-assigned-to {
  margin-top: 3px;
  font-size: var(--fs-xs);
  color: var(--muted);
  white-space: nowrap;
}

/* ══════════════════════════════════════════════════════════════════
   FIND STUDENT — a centred search, then the chosen student's history
   ══════════════════════════════════════════════════════════════════ */
/* Faint "investigator" photo behind the whole page — a subtle motif, kept very
   low-opacity so the search box and cards stay perfectly readable on top.
   Drop the image in as assets/find-student-bg.jpg (deploy copies assets/). */
#v-findstudent {
  position: relative;
  min-height: 82vh;
}
#v-findstudent::before {
  content: "";
  position: absolute;
  /* Bleed out past <main>'s padding so the photo fills the whole page area
     instead of sitting in a white frame (per Big, 2026-07-22). The values
     mirror `main { padding: var(--sp-6) var(--sp-6) var(--sp-10) }`. */
  top: calc(-1 * var(--sp-6));
  right: calc(-1 * var(--sp-6));
  bottom: calc(-1 * var(--sp-10));
  left: calc(-1 * var(--sp-6));
  background: url("../assets/find-student-bg.jpg") no-repeat center center;
  background-size: cover;
  opacity: 0.2;
  pointer-events: none;
  z-index: 0;
}
#findstudent-root {
  position: relative; /* sit above the faint background */
  z-index: 1;
}
.fs-hero {
  display: flex;
  flex-direction: column;
  align-items: center;
  text-align: center;
  padding-top: 11vh;
  gap: var(--sp-3);
}
.fs-title {
  font-size: var(--fs-2xl);
  font-weight: 800;
  letter-spacing: -0.01em;
}
.fs-sub {
  color: var(--muted);
  margin-top: -4px;
}
.fs-searchbox {
  position: relative;
  width: 100%;
  max-width: 560px;
  margin-top: var(--sp-2);
}
.fs-searchbox svg {
  position: absolute;
  left: 18px;
  top: 50%;
  transform: translateY(-50%);
  width: 20px;
  height: 20px;
  color: var(--muted);
  pointer-events: none;
}
.fs-searchbox input {
  width: 100%;
  height: 54px;
  padding: 0 20px 0 50px;
  font-size: var(--fs-lg);
  color: var(--ink);
  background: var(--surface);
  border: 1px solid var(--line-strong);
  border-radius: var(--r-full);
  box-shadow: var(--sh-1);
  transition:
    box-shadow var(--dur-1) var(--ease-out),
    border-color var(--dur-1) var(--ease-out);
}
.fs-searchbox input:focus {
  outline: none;
  border-color: var(--brand);
  box-shadow: var(--sh-3);
}

.fs-results {
  width: 100%;
  max-width: 560px;
  margin-top: var(--sp-3);
  display: flex;
  flex-direction: column;
  gap: var(--sp-2);
  text-align: left;
}
.fs-results-count {
  font-size: var(--fs-xs);
  font-weight: 600;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  color: var(--muted);
  padding: 0 var(--sp-1) var(--sp-1);
}
.fs-empty {
  color: var(--muted);
  padding: var(--sp-4);
  text-align: center;
  border: 1px dashed var(--line-strong);
  border-radius: var(--r-lg);
}
.fs-card {
  display: flex;
  align-items: center;
  gap: var(--sp-3);
  width: 100%;
  padding: var(--sp-3) var(--sp-3);
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: var(--r-lg);
  box-shadow: var(--sh-1);
  cursor: pointer;
  text-align: left;
  transition:
    transform var(--dur-1) var(--ease-out),
    box-shadow var(--dur-1) var(--ease-out),
    border-color var(--dur-1) var(--ease-out);
}
.fs-card:hover {
  transform: translateY(-1px);
  border-color: var(--brand);
  box-shadow: var(--sh-2, var(--sh-3));
}
.fs-card:focus-visible {
  outline: 2px solid var(--ring);
  outline-offset: 2px;
}
.fs-card-avatar,
.fs-avatar {
  flex: none;
  display: grid;
  place-items: center;
  border-radius: var(--r-full);
  background: linear-gradient(135deg, #60a5fa, #2563eb);
  color: #fff;
  font-weight: 800;
}
.fs-card-avatar {
  width: 38px;
  height: 38px;
  font-size: var(--fs-md);
}
.fs-card-main {
  flex: 1;
  min-width: 0;
  display: flex;
  flex-direction: column;
  gap: 2px;
}
.fs-card-name {
  font-weight: 700;
  display: inline-flex;
  align-items: center;
  gap: 6px;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.fs-card-meta {
  font-size: var(--fs-sm);
  color: var(--muted);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.fs-card-chev {
  flex: none;
  width: 18px;
  height: 18px;
  color: var(--muted);
}

/* ── "Opened today" cards (Find Student) ─────────────────────────────
   Fills the empty space under the search box with who the school has been
   looking at today (per Big, 2026-07-22). Wider than the search column —
   these are small tiles that wrap, not a list — and they hide as soon as
   someone starts typing so the results own that space instead. */
.fs-recent {
  width: 100%;
  max-width: 900px;
  margin-top: var(--sp-6);
  /* Centred like everything else in the hero — a left-aligned block under a
     centred search box reads as a mistake. */
  text-align: center;
}
.fs-recent[hidden] {
  display: none; /* an author `display` would otherwise beat the UA rule */
}
.fs-recent-head {
  font-size: var(--fs-xs);
  font-weight: 600;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--muted);
  margin-bottom: var(--sp-2);
}
/* Flex, not grid: with only two cards a grid's columns would park them at the
   left edge of the block. Wrapping flex items stay centred at any count. */
.fs-recent-grid {
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  gap: var(--sp-2);
}
.fs-recent-card {
  display: flex;
  align-items: center;
  gap: var(--sp-2);
  flex: 0 1 215px;
  min-width: 0;
  text-align: left;
  padding: var(--sp-2) var(--sp-3);
  font: inherit;
  text-align: left;
  /* Slightly translucent: it sits over the page's background image, and a
     solid tile row would blank the artwork out. */
  background: color-mix(in srgb, var(--surface) 88%, transparent);
  border: 1px solid var(--line);
  border-radius: var(--r-lg);
  cursor: pointer;
  transition:
    transform var(--dur-1) var(--ease-out),
    border-color var(--dur-1) var(--ease-out),
    background-color var(--dur-1) var(--ease-out);
}
.fs-recent-card:hover {
  transform: translateY(-1px);
  border-color: var(--brand);
  background: var(--surface);
}
.fs-recent-card:focus-visible {
  outline: 2px solid var(--ring);
  outline-offset: 2px;
}
.fs-recent-avatar {
  flex: none;
  display: grid;
  place-items: center;
  width: 30px;
  height: 30px;
  border-radius: var(--r-full);
  background: var(--surface-3);
  color: var(--ink-2);
  font-size: var(--fs-sm);
  font-weight: 800;
}
.fs-recent-main {
  min-width: 0;
  display: flex;
  flex-direction: column;
}
.fs-recent-name {
  font-weight: 600;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
/* Who opened it, and when — the reason this is shared rather than personal. */
.fs-recent-by {
  font-size: var(--fs-xs);
  color: var(--muted);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

/* ── Profile / history ── */
.fs-profile {
  max-width: 840px;
  margin: 0 auto;
  display: flex;
  flex-direction: column;
  gap: var(--sp-4);
}
/* A real button now (per Big, 2026-07-22) — it uses the shared .gbtn look, so
   this only has to place it. */
.fs-back {
  align-self: flex-start;
}

/* A panel on the profile: "Student details". */
.fs-block {
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: var(--r-lg);
  box-shadow: var(--sh-1);
  padding: var(--sp-4);
}
.fs-block-head {
  font-size: var(--fs-sm);
  font-weight: 800;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  color: var(--muted);
  margin-bottom: var(--sp-3);
}
/* Every field keeps its label even when empty, so the grid reads as a form. */
.fs-details {
  margin: 0;
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(190px, 1fr));
  gap: var(--sp-3) var(--sp-4);
}
.fs-detail {
  min-width: 0;
}
.fs-detail-lbl {
  font-size: var(--fs-sm);
  font-weight: 600;
  color: var(--muted);
  margin-bottom: 3px;
}
.fs-detail-val {
  margin: 0;
  display: flex;
  align-items: center;
  gap: 6px;
  flex-wrap: wrap;
  min-height: 24px;
  font-size: var(--fs-md);
  font-weight: 600;
  color: var(--ink);
  overflow-wrap: anywhere;
}
.fs-detail-val.is-empty {
  color: var(--muted);
  font-weight: 500;
}
.fs-phead {
  display: flex;
  align-items: center;
  gap: var(--sp-4);
  padding: var(--sp-4);
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: var(--r-lg);
  box-shadow: var(--sh-1);
}
.fs-avatar {
  width: 64px;
  height: 64px;
  font-size: var(--fs-2xl);
}
.fs-phead-main {
  min-width: 0;
  display: flex;
  flex-direction: column;
  gap: 6px;
}
.fs-pname {
  font-size: var(--fs-xl);
  font-weight: 800;
  display: inline-flex;
  align-items: center;
  gap: 8px;
  flex-wrap: wrap;
}
.fs-plangs {
  display: flex;
  gap: 6px;
  flex-wrap: wrap;
}
.fs-contacts {
  display: flex;
  flex-wrap: wrap;
  gap: var(--sp-3);
  font-size: var(--fs-sm);
  color: var(--ink-2);
}
.fs-contact b {
  color: var(--muted);
  font-weight: 600;
  margin-right: 3px;
}
.fs-pmarks {
  font-size: var(--fs-sm);
  color: var(--muted);
  display: flex;
  align-items: center;
  gap: 6px;
}

.fs-stats {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
  gap: var(--sp-3);
}
.fs-tile {
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: var(--r-lg);
  padding: var(--sp-3) var(--sp-4);
  box-shadow: var(--sh-1);
}
/* Value on the left, a soft emoji badge on the right — flex so the emoji never
   collides with a wide value (e.g. the "First applied" date). */
.fs-tile-top {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: var(--sp-2);
}
.fs-tile-emoji {
  font-size: var(--fs-xl);
  line-height: 1;
  flex-shrink: 0;
  opacity: 0.9;
}
.fs-tile-val {
  font-size: var(--fs-2xl);
  font-weight: 800;
  color: var(--ink);
  font-variant-numeric: tabular-nums;
  line-height: 1.1;
}
.fs-tile-date {
  font-size: var(--fs-lg);
}
.fs-tile-lbl {
  margin-top: 4px;
  font-size: var(--fs-sm);
  font-weight: 600;
  color: var(--ink-2);
}
.fs-tile-hint {
  margin-top: 2px;
  font-size: var(--fs-xs);
  color: var(--muted);
}

.fs-tl-head {
  display: flex;
  align-items: center;
  gap: var(--sp-2);
  font-size: var(--fs-md);
  font-weight: 700;
  margin-top: var(--sp-2);
}
.fs-tl-count {
  font-size: var(--fs-sm);
  font-weight: 600;
  color: var(--muted);
  background: var(--surface-2);
  padding: 1px 8px;
  border-radius: var(--r-full);
}
.fs-timeline {
  list-style: none;
  margin: 0;
  padding: 0;
}
.fs-tl-item {
  display: grid;
  grid-template-columns: 14px 1fr;
  gap: var(--sp-3);
  position: relative;
  padding-bottom: var(--sp-4);
}
.fs-tl-item::before {
  content: "";
  position: absolute;
  left: 6px;
  top: 8px;
  bottom: 0;
  width: 2px;
  background: var(--line);
}
.fs-tl-item:last-child::before {
  display: none;
}
.fs-tl-dot {
  width: 12px;
  height: 12px;
  border-radius: 50%;
  margin-top: 4px;
  background: var(--brand);
  box-shadow:
    0 0 0 3px var(--surface),
    0 0 0 4px var(--line-strong);
  z-index: 1;
}
.fs-tl-body {
  min-width: 0;
  display: flex;
  flex-direction: column;
  gap: 4px;
}
.fs-tl-top {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: 6px;
}
.fs-tl-date {
  font-weight: 700;
  font-variant-numeric: tabular-nums;
}
.fs-tl-type {
  font-size: var(--fs-xs);
  font-weight: 700;
  padding: 1px 8px;
  border-radius: var(--r-full);
}
.fs-tl-type.is-new {
  color: #15803d;
  background: rgb(22 163 74 / 0.12);
}
.fs-tl-type.is-cont {
  color: #b45309;
  background: rgb(217 119 6 / 0.14);
}
.fs-tl-mid {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: 6px;
  font-size: var(--fs-sm);
  color: var(--ink-2);
}
.fs-tl-course {
  font-weight: 600;
  color: var(--ink);
}
.fs-tl-sep {
  color: var(--line-strong);
}
.fs-tl-room {
  font-weight: 600;
}
.fs-tl-pending {
  color: var(--muted);
  font-style: italic;
  font-weight: 500;
}
.fs-tl-remark {
  font-size: var(--fs-sm);
  color: var(--ink-2);
  background: var(--surface-2);
  border-left: 3px solid rgb(245 158 11 / 0.55);
  border-radius: var(--r-sm);
  padding: 5px 10px;
  margin-top: 2px;
}

/* ══════════════════════════════════════════════════════════════════
   "BUY MORE LEVEL" — course line-items in the Reservation / Waiting
   List forms (js/levelRows.js). Each card = Term + Level + Session +
   Status; the ＋ button appends another course.
   ══════════════════════════════════════════════════════════════════ */
.level-rows {
  display: flex;
  flex-direction: column;
  gap: var(--sp-2);
  counter-reset: lr;
}
.level-row {
  counter-increment: lr;
  border: 1px solid var(--line);
  border-radius: var(--r-md);
  padding: var(--sp-3);
  background: var(--surface-2);
}
.level-row-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  margin-bottom: var(--sp-2);
}
.level-row-title {
  font-size: var(--fs-xs);
  font-weight: 700;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  color: var(--muted);
}
.level-row-title::before {
  content: "Course " counter(lr);
}
.lr-remove {
  border: none;
  background: none;
  color: var(--muted);
  cursor: pointer;
  font-size: var(--fs-md);
  line-height: 1;
  padding: 2px 6px;
  border-radius: var(--r-sm);
  transition:
    color var(--dur-1) var(--ease-out),
    background-color var(--dur-1) var(--ease-out);
}
.lr-remove:hover {
  color: var(--danger);
  background: var(--danger-bg);
}
.level-row-grid {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: var(--sp-2) var(--sp-3);
}
.lr-field {
  display: flex;
  flex-direction: column;
  gap: 3px;
  min-width: 0;
}
.lr-field > label {
  font-size: var(--fs-xs);
  font-weight: 600;
  color: var(--muted);
}
.lr-addmore {
  margin-top: var(--sp-1);
  width: 100%;
  border: 1px dashed var(--line-strong);
  background: var(--surface);
  color: var(--brand);
  font: inherit;
  font-weight: 700;
  padding: 9px;
  border-radius: var(--r-md);
  cursor: pointer;
  transition:
    background-color var(--dur-1) var(--ease-out),
    border-color var(--dur-1) var(--ease-out);
}
.lr-addmore:hover {
  background: var(--surface-2);
  border-color: var(--brand);
}
.lr-addmore:focus-visible {
  outline: 2px solid var(--ring);
  outline-offset: 2px;
}

/* ══════════════════════════════════════════════════════════════════
   ROOM AVAILABILITY — a Google-Calendar-style day timeline
   Rooms down the left, time across the top in 10-minute columns. One
   column = var(--ra-slot); every block is positioned with
   calc(var(--ra-slot) * n), so the whole grid stays on an exact
   10-minute scale and scrolls sideways on a narrow screen.
   ══════════════════════════════════════════════════════════════════ */
.ra-toolbar {
  flex-wrap: wrap;
  align-items: center;
  gap: var(--sp-4);
}
.ra-daynav {
  display: flex;
  align-items: center;
  gap: var(--sp-2);
}
/* One height across every control on the bar so the whole row lines up and
   reads as balanced (per Big, 2026-07-24). Today + the ‹ › arrows borrow the
   Roster's dimensional .gbtn look; the date stays a bordered field so it still
   reads as a picker, not a button. */
#avail-toolbar .gbtn,
#avail-toolbar .ra-calbtn {
  height: 34px;
}
.ra-todaybtn {
  font-size: var(--fs-sm);
  padding-inline: var(--sp-3);
}
.ra-arrow {
  width: 34px;
  padding: 0;
  font-size: var(--fs-lg);
  line-height: 1;
}
/* Weekend banner above the grid — soft, informational, not an error. */
.ra-weekend-note {
  margin: 0 0 var(--sp-3);
  padding: 8px var(--sp-3);
  border-radius: var(--r-md);
  background: var(--surface-2);
  color: var(--muted);
  font-size: var(--fs-sm);
  font-weight: 600;
}

/* ── Our own date picker: the browser's built-in calendar looked dated and
   can't be styled, so the button + popover below are ours (per Big,
   2026-07-22). Same top-layer popover trick as the cselect menus. ── */
.ra-cal {
  position: relative;
}
.ra-calbtn {
  display: inline-flex;
  align-items: center;
  gap: var(--sp-2);
  padding: 7px var(--sp-3);
  font: inherit;
  font-size: var(--fs-base);
  font-weight: 600;
  color: var(--ink);
  background: var(--surface);
  border: 1px solid var(--line-strong);
  border-radius: var(--r-md);
  cursor: pointer;
  transition: border-color var(--dur-1) var(--ease-out);
}
.ra-calbtn:hover {
  border-color: var(--muted);
}
.ra-calbtn svg {
  width: 15px;
  height: 15px;
  color: var(--muted);
  flex-shrink: 0;
}
.ra-calchev {
  width: 13px !important;
  height: 13px !important;
}
.ra-calpop {
  position: fixed;
  inset: auto;
  margin: 0;
  padding: var(--sp-3);
  width: 284px;
  z-index: var(--z-modal);
  background: var(--surface);
  border: 1px solid var(--line-strong);
  border-radius: var(--r-lg);
  box-shadow: var(--sh-3);
}
.ra-calhead {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--sp-2);
  margin-bottom: var(--sp-2);
  font-size: var(--fs-md);
}
.ra-calnav {
  border: 1px solid var(--line-strong);
  background: var(--surface);
  border-radius: var(--r-md);
  width: 28px;
  height: 28px;
  font-size: var(--fs-lg);
  line-height: 1;
  color: var(--ink);
  cursor: pointer;
}
.ra-calnav:hover {
  background: var(--surface-2);
}
.ra-caldow,
.ra-calgrid {
  display: grid;
  grid-template-columns: repeat(7, 1fr);
  gap: 2px;
}
.ra-caldow {
  margin-bottom: 2px;
}
.ra-caldow span {
  text-align: center;
  font-size: var(--fs-xs);
  font-weight: 700;
  color: var(--muted);
  padding: 2px 0;
}
.ra-cell {
  aspect-ratio: 1;
  display: flex;
  align-items: center;
  justify-content: center;
  border: 1px solid transparent;
  border-radius: var(--r-md);
  background: none;
  font: inherit;
  font-size: var(--fs-base);
  color: var(--ink);
  cursor: pointer;
}
.ra-cell.is-blank {
  cursor: default;
}
.ra-cell:not(.is-blank):hover {
  background: var(--surface-2);
}
/* Days inside the term the timeline is on — the useful ones to book. */
.ra-cell.in-term {
  background: var(--ok-bg, #f0fdf4);
  color: #15803d;
  font-weight: 700;
}
.ra-cell.is-today {
  border-color: var(--accent);
}
.ra-cell.is-sel {
  background: var(--accent);
  border-color: transparent;
  color: #fff;
  font-weight: 800;
}
.ra-calfoot {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--sp-2);
  margin-top: var(--sp-3);
  padding-top: var(--sp-2);
  border-top: 1px solid var(--line);
  font-size: var(--fs-sm);
}
.ra-caltermnote {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  color: var(--muted);
}
.ra-caltermnote i {
  width: 11px;
  height: 11px;
  border-radius: 3px;
  background: #f0fdf4;
  border: 1px solid #86efac;
}
.ra-callink {
  border: none;
  background: none;
  font: inherit;
  font-size: var(--fs-sm);
  font-weight: 700;
  color: var(--accent);
  cursor: pointer;
  padding: 2px 6px;
  border-radius: var(--r-sm);
}
.ra-callink:hover {
  background: var(--surface-2);
}
.ra-legend {
  display: flex;
  align-items: center;
  gap: var(--sp-4);
  font-size: var(--fs-sm);
  color: var(--muted);
}
.ra-legend span {
  display: inline-flex;
  align-items: center;
  gap: 6px;
}
.ra-key {
  width: 14px;
  height: 12px;
  border-radius: 3px;
  display: inline-block;
}
.ra-key-class {
  background: var(--thai-bg);
  border: 1px solid var(--thai-ring);
}
.ra-key-booking {
  background: #ede9fe;
  border: 1px solid #a78bfa;
}
.ra-key-free {
  background: var(--surface);
  border: 1px dashed var(--line-strong);
}

/* The grid + its loading cover. */
.ra-wrap {
  position: relative;
  min-height: 220px;
}
/* The grid FILLS its width: the time track flexes, and every block is placed
   in % of the day (set in rooms.js), so nothing is left blank on the right
   (per Big, 2026-07-22). --ra-slots / --ra-hours come in on the element. */
.ra-grid {
  width: 100%;
  overflow-x: auto;
  border: 1px solid var(--line);
  border-radius: var(--r-lg);
  background: var(--surface);
  box-shadow: var(--sh-1);
}
.ra-row {
  display: flex;
  align-items: stretch;
  border-bottom: 1px solid var(--line);
  min-width: max-content;
}
.ra-row:last-child {
  border-bottom: none;
}
.ra-head {
  position: sticky;
  top: 0;
  z-index: 2;
  background: var(--surface-2);
}
.ra-roomcell {
  position: sticky;
  left: 0;
  z-index: 1;
  flex: 0 0 150px;
  width: 150px;
  display: flex;
  flex-direction: column;
  justify-content: center;
  gap: 1px;
  padding: var(--sp-2) var(--sp-3);
  border-right: 1px solid var(--line-strong);
  background: var(--surface);
  font-size: var(--fs-base);
}
.ra-head .ra-roomcell {
  background: var(--surface-2);
}
.ra-roomcell-head {
  font-weight: 700;
  color: var(--muted);
  font-size: var(--fs-sm);
  text-transform: uppercase;
  letter-spacing: 0.04em;
}
.ra-seats {
  font-size: var(--fs-xs);
  color: var(--muted);
}

/* Hour ruler — flexes with the track below it so they always line up. */
.ra-ruler {
  position: relative;
  height: 34px;
  flex: 1 1 auto;
  min-width: calc(var(--ra-slots, 78) * 9px);
}
.ra-hour {
  position: absolute;
  top: 9px;
  transform: translateX(-50%);
  font-size: var(--fs-xs);
  font-weight: 600;
  color: var(--muted);
  white-space: nowrap;
}
/* The end labels would hang half outside the track if they stayed centred —
   the last one overflowing even forced a stray horizontal scrollbar. Tuck
   both inside instead. */
.ra-hour-first {
  transform: translateX(7px);
}
.ra-hour-last {
  transform: translateX(calc(-100% - 4px));
}

/* One room's strip of time. The gradients ARE the grid: a hairline every
   10 minutes and a stronger one on the hour. */
.ra-track {
  position: relative;
  flex: 1 1 auto;
  /* Never squeeze below ~9px per 10 minutes — past that the grid is unreadable,
     so the row overflows and .ra-grid scrolls sideways instead. */
  min-width: calc(var(--ra-slots, 78) * 9px);
  height: 52px;
  background-image:
    repeating-linear-gradient(
      to right,
      var(--line-strong) 0 1px,
      transparent 1px calc(100% / var(--ra-hours, 13))
    ),
    repeating-linear-gradient(
      to right,
      var(--line) 0 1px,
      transparent 1px calc(100% / var(--ra-slots, 78))
    );
  cursor: crosshair;
  touch-action: pan-y; /* let a vertical page scroll through, drag horizontally */
}

/* A class or a booking sitting on the strip. */
.ra-block {
  position: absolute;
  top: 5px;
  bottom: 5px;
  display: flex;
  flex-direction: column;
  justify-content: center;
  gap: 1px;
  overflow: hidden;
  padding: 2px var(--sp-2);
  border-radius: var(--r-md);
  border: 1px solid;
  font-size: var(--fs-xs);
  line-height: 1.2;
  white-space: nowrap;
}
.ra-block-title {
  overflow: hidden;
  text-overflow: ellipsis;
  font-weight: 700;
}
.ra-block-time {
  overflow: hidden;
  text-overflow: ellipsis;
  opacity: 0.85;
}
/* Classes take their language colour (same palette as everywhere else). */
.ra-block-class.lang-thai {
  background: var(--thai-bg);
  border-color: var(--thai-ring);
  color: var(--thai-title);
}
.ra-block-class.lang-eng {
  background: var(--eng-bg);
  border-color: var(--eng-ring);
  color: var(--eng-title);
}
.ra-block-class.lang-chi {
  background: var(--chi-bg);
  border-color: var(--chi-ring);
  color: var(--chi-title);
}
/* Bookings are violet and clickable — deliberately NOT a language colour, so
   "someone reserved this" never reads as "a class is taught here". */
.ra-block-booking {
  background: #ede9fe;
  border-color: #a78bfa;
  color: #5b21b6;
  cursor: pointer;
}
.ra-block-booking:hover {
  background: #ddd6fe;
}

/* The rectangle painted while dragging out a new booking. */
/* `hidden` has to win over the display below — an author `display` beats the
   browser's own [hidden] rule, which otherwise leaves a stray dashed sliver. */
.ra-drag[hidden] {
  display: none;
}
.ra-drag {
  position: absolute;
  top: 5px;
  bottom: 5px;
  display: flex;
  align-items: center;
  justify-content: center;
  border-radius: var(--r-md);
  background: rgb(22 163 74 / 0.18);
  border: 1.5px dashed #16a34a;
  color: #15803d;
  font-size: var(--fs-xs);
  font-weight: 700;
  pointer-events: none;
  white-space: nowrap;
}
.ra-foot {
  margin: var(--sp-3) 0 0;
  font-size: var(--fs-sm);
  color: var(--muted);
}

/* Loading cover — sits over the grid while the day is fetched. */
.ra-loading {
  position: absolute;
  inset: 0;
  z-index: 5;
  display: none;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: var(--sp-3);
  background: rgb(248 250 252 / 0.82);
  backdrop-filter: blur(2px);
  border-radius: var(--r-lg);
  color: var(--muted);
  font-weight: 600;
}
.ra-loading.is-on {
  display: flex;
}
.ra-spinner {
  width: 34px;
  height: 34px;
  border-radius: 50%;
  border: 3px solid var(--line-strong);
  border-top-color: var(--accent);
  animation: ra-spin 0.8s linear infinite;
}
@keyframes ra-spin {
  to {
    transform: rotate(360deg);
  }
}
@media (prefers-reduced-motion: reduce) {
  .ra-spinner {
    animation-duration: 2.4s;
  }
}

/* Room Availability reuses the Class Plan's tooltip card. Its class blocks
   keep the language tint from .lang-*; these two are the extra headers the
   timeline needs — a violet one for bookings (matching their block) and a
   neutral one for the room summary. Deliberately NOT a blanket `.tt-head`
   rule, which would out-specify .lang-* and grey out the Class Plan's. */
.sched-tooltip .tt-head-booking {
  background: #ede9fe;
  color: #5b21b6;
}
.sched-tooltip .tt-head-booking .cc-dot {
  background: #7c3aed;
}
.sched-tooltip .tt-head-plain {
  background: var(--surface-2);
}
.sched-tooltip .tt-head-plain .cc-dot {
  background: var(--muted);
}

/* From/To sit side by side in the booking form. */
.rb-times {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: var(--sp-3);
}

/* ── "Pull from Google Calendar" (per Big, 2026-07-22) ──
   The Setting page's Terms group can copy its dates out of the school's
   TH/EN Schedule calendars. This is the confirm-before-writing diff: a Head
   sees exactly which terms would be added or moved before anything is saved. */
/* Sits right after the year picker at the end of the group title. The picker
   already carries the `margin-left: auto` that pushes the pair over — a second
   one here would split the free space between them instead. */
.mcard-syncbtn {
  font-weight: 700;
}
.sync-loading,
.sync-intro {
  margin: 0 0 var(--sp-3);
  font-size: var(--fs-sm);
  color: var(--muted);
}
.sync-tablewrap {
  max-height: 300px;
  overflow: auto;
  overscroll-behavior: contain;
  background: var(--surface-2);
  border: 1px solid var(--line);
  border-radius: var(--r-md);
}
.sync-table {
  width: 100%;
  border-collapse: collapse;
  font-size: var(--fs-sm);
}
.sync-table td {
  padding: 6px var(--sp-2);
  border-bottom: 1px solid var(--line);
  white-space: nowrap;
}
.sync-table tr:last-child td {
  border-bottom: none;
}
/* The tag column carries the whole meaning of the row, so it gets the colour
   and the rest of the line stays quiet. */
.sync-tag {
  display: inline-block;
  min-width: 92px;
  padding: 1px 6px;
  border-radius: var(--r-sm);
  font-size: var(--fs-xs);
  font-weight: 800;
  text-align: center;
  color: #fff;
}
.sync-tag-add {
  background: #4d9166;
}
.sync-tag-change {
  background: #b8912f;
}
.sync-tag-extra {
  background: var(--muted);
}
/* An old date being replaced reads as struck-through; the new one is the one
   worth looking at. */
.sync-was {
  color: var(--muted);
  text-decoration: line-through;
}
.sync-arrow {
  color: var(--muted);
  padding: 0 4px;
}
.sync-new {
  font-weight: 700;
  color: var(--ink);
}
.sync-none {
  padding: var(--sp-4) !important;
  text-align: center;
  color: var(--muted);
}
.sync-summary {
  margin: var(--sp-2) 0 0;
  font-size: var(--fs-sm);
  color: var(--ink-2);
}
/* Calendar problems (a term with only half its events) are the reason this
   screen exists — they sit ABOVE the table so they cannot be scrolled past. */
.sync-problems ul {
  margin: var(--sp-1) 0 0;
  padding-left: var(--sp-4);
}
.sync-problems li {
  font-size: var(--fs-sm);
  line-height: 1.6;
}

/* ══ CLASS PLAN (levels × sessions) ══════════════════════════════════
   The grid itself reuses the Dashboard's table styles wholesale — same
   .sched table, same .roomcol, .slot and .class-card. Only what is genuinely
   different about a LEVEL-first, editable grid lives here. */

/* Language band above each language's levels. Reuses tr.room-group; only the
   flag needs sizing, since the label is a flag rather than a line icon. */
tr.room-group th.room-group-label .nat-flag {
  width: 18px;
  height: 13px;
  vertical-align: -1px;
  margin-right: var(--sp-2);
}

/* The level's own row header. Same sticky column as the Dashboard's room
   name. Top-aligned, not centred (per Big, 2026-07-23): rows are as tall as
   their fullest slot, so a centred label sat at a different height on every
   row and the column read as ragged. Top-aligned, every level name lines up
   with the first card of its row. */
th.roomcol.levelcol {
  vertical-align: top;
  padding-top: var(--sp-3);
  padding-left: var(--sp-2);
  padding-right: var(--sp-2);
}
/* All three lines centred in the column, Stand by included (per Big,
   2026-07-23) — they are one block about this level, so they move together. */
.lv-cell {
  display: flex;
  flex-direction: column;
  align-items: center;
  text-align: center;
  gap: 3px;
}
.lv-name {
  font-size: var(--fs-md);
  font-weight: 700;
  line-height: 1.2;
}
/* Rooms booked at this level, under the name and carrying its unit — a bare
   number beside "J1" could just as easily have been read as a level number. */
.lv-count {
  color: var(--muted);
  font-size: var(--fs-xs);
  font-weight: 600;
}
/* Classes whose class group was never set land here. Italic + muted so the
   row reads as "something to fix", not as a real level. */
.lv-nolevel {
  color: var(--muted);
  font-style: italic;
  font-weight: 500;
}

/* Cards on THIS page are clickable (the Dashboard's are not), so they say so
   on hover and take keyboard focus. */
.plan-card {
  cursor: pointer;
}
.plan-card:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
}

/* The faint ＋ in an empty slot (per Big, 2026-07-23). Barely there until the
   cell is hovered — an empty slot should read as empty at a glance, with the
   ＋ arriving only once you go looking for it.

   56px is the height of a plain room card, measured, not guessed (per Big,
   2026-07-23): every dashed ＋ frame on the page is exactly the size of the
   card it would create, so it reads as the empty seat next to the full ones
   rather than as a smaller control of its own. */
.slot-add {
  width: 100%;
  min-height: 56px;
  border: 1px dashed var(--line-strong);
  border-radius: var(--r-md);
  background: transparent;
  color: var(--muted);
  font-size: var(--fs-lg);
  line-height: 1;
  cursor: pointer;
  opacity: 0.28;
  transition:
    opacity var(--dur-1) var(--ease-out),
    border-color var(--dur-1) var(--ease-out),
    background-color var(--dur-1) var(--ease-out);
}
td.slot:hover .slot-add,
.slot-add:focus-visible {
  opacity: 1;
  border-color: var(--accent);
  background: var(--surface-2);
  color: var(--accent);
}
/* Under a card it needs the gap the cards use between themselves, and nothing
   else — same width, same height, same everything, so the empty frame sits in
   the stack as if it were already a card. */
.plan-stack .slot-add {
  margin-top: var(--sp-1);
}

/* Two or more rooms in a slot: the ＋ becomes a circle floating on the bottom
   edge of the LAST card, centred (per Big, 2026-07-23). The stack ends where
   that card ends, so "bottom of the stack" IS "bottom of the last card" — and
   the next press moves the ＋ down to the card it just created. */
.plan-stack {
  position: relative;
}
.slot-add-float {
  position: absolute;
  left: 50%;
  bottom: 0;
  transform: translate(-50%, 50%);
  z-index: 1;
  display: grid;
  place-items: center;
  width: 22px;
  height: 22px;
  padding: 0;
  border: 1px solid var(--line-strong);
  border-radius: var(--r-full);
  background: var(--surface);
  color: var(--muted);
  font-size: var(--fs-sm);
  line-height: 1;
  cursor: pointer;
  box-shadow: var(--sh-1);
  /* Hidden until the mouse is on the cell (per Big, 2026-07-23). A slot that
     is already full should read as full; the ＋ is something you go looking
     for. pointer-events stops the invisible button swallowing clicks meant
     for the card underneath — but it stays TAB-reachable, and focus brings
     it back, so the keyboard route is not lost with it. */
  opacity: 0;
  pointer-events: none;
  transition:
    opacity var(--dur-1) var(--ease-out),
    border-color var(--dur-1) var(--ease-out),
    color var(--dur-1) var(--ease-out);
}
td.slot:hover .slot-add-float,
.slot-add-float:focus-visible {
  opacity: 1;
  pointer-events: auto;
  border-color: var(--accent);
  color: var(--accent);
  box-shadow: var(--sh-2);
}

/* ── Stand by ────────────────────────────────────────────────────────
   ONE ROW PER LANGUAGE, last line of its block (per Big, 2026-07-25). It is a
   footnote to the whole language, not a level of its own, so it is quieter than
   a level row: no white card, no session grid, just names running across. */
.sb-strip > th,
.sb-strip > td {
  background: var(--surface-2);
  border-top: 1px solid var(--line-strong);
  vertical-align: middle;
}
/* The flex lives on an inner div, never on the <th>: th.roomcol is the sticky
   column and changing its display would stop it sticking. */
.sb-lblcell {
  display: flex;
  align-items: center;
  gap: var(--sp-2);
}
.sb-rowname {
  font-size: var(--fs-xs);
  font-weight: 700;
  letter-spacing: 0.02em;
  color: var(--muted);
  text-transform: uppercase;
}
.sb-rown {
  min-width: 15px;
  padding: 0 4px;
  border-radius: var(--r-full);
  background: var(--line-strong);
  color: var(--surface);
  font-size: 10px;
  line-height: 15px;
  text-align: center;
}
.sb-cell {
  padding: var(--sp-2) var(--sp-3);
}
/* Across, never down. `wrap` is the release valve for a team with more
   standbys than fit — it still fills left-to-right first, which is what was
   asked for, instead of clipping the names nobody can then scroll to. */
.sb-track {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--sp-2);
}
.sb-chip {
  display: inline-flex;
  align-items: stretch;
  border: 1px solid var(--line-strong);
  border-radius: var(--r-full);
  background: var(--surface);
  overflow: hidden;
}
.sb-chip-body {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  border: none;
  background: transparent;
  padding: 3px var(--sp-2) 3px var(--sp-3);
  font-size: var(--fs-sm);
  color: var(--ink-2);
  cursor: pointer;
  text-align: left;
}
.sb-chip-body:hover {
  background: var(--surface-2);
  color: var(--accent);
}
/* The levels this person can take. Small, so it reads as a note beside the name
   rather than competing with it — but each one carries its own level colour
   (per Big, 2026-07-25): four identical grey pills on one name is a wall of
   text, and the colours are already how the rest of the app says "Journey". */
.sb-lv {
  padding: 0 6px;
  border: 1px solid transparent;
  border-radius: var(--r-full);
  background: var(--surface-2);
  color: var(--muted);
  font-size: var(--fs-xs);
  font-weight: 600;
  line-height: 1.5;
}
/* The school's level colours — Journey green, Explore orange, Connect blue, per
   Big 2026-07-22. The soft-tint pairing of .chip.lv-*:hover in layout.css, which
   is the right weight for a static tag: the solid fill those chips use when
   ACTIVE would shout, and there can be four of these on one name.

   NOTE: this is the fourth place these six hex values are written (the others
   are .chip.lv-* in layout.css and .course-lv-* above). Worth pulling into
   tokens one day; not worth restyling three working components today. */
.sb-lv.lv-J {
  border-color: #86efac;
  background: #f0fdf4;
  color: #15803d;
}
.sb-lv.lv-E {
  border-color: #fdba74;
  background: #fff7ed;
  color: #c2410c;
}
.sb-lv.lv-C {
  border-color: #93c5fd;
  background: #eff6ff;
  color: #1d4ed8;
}
/* Levels outside J/E/C — English "Intermediate", Chinese "EX1" — keep the
   neutral tag. "No level colour" is what levelKeyOf's "other" means. */
.sb-lv.lv-other {
  border-color: var(--line);
}
/* Only the neutral tag lifts on hover; a coloured one already has its own face
   and would lose it. */
.sb-chip-body:hover .sb-lv-any {
  background: var(--surface);
}
.sb-lv-any {
  font-style: italic;
  font-weight: 500;
}
.sb-x {
  flex: none;
  border: none;
  border-left: 1px solid var(--line);
  background: transparent;
  color: var(--muted);
  font-size: var(--fs-xs);
  line-height: 1;
  padding: 0 7px;
  cursor: pointer;
  opacity: 0.55;
}
.sb-x:hover,
.sb-x:focus-visible {
  opacity: 1;
  background: var(--danger-bg);
  color: var(--danger);
}
/* Dashed like the empty-slot ＋ in the grid above: same shape of invitation. */
.sb-add-btn {
  display: inline-flex;
  align-items: center;
  gap: 4px;
  padding: 3px var(--sp-3);
  border: 1px dashed var(--line-strong);
  border-radius: var(--r-full);
  background: transparent;
  color: var(--muted);
  font-size: var(--fs-sm);
  font-weight: 600;
  cursor: pointer;
  transition:
    border-color var(--dur-1) var(--ease-out),
    color var(--dur-1) var(--ease-out);
}
.sb-add-btn:hover {
  border-color: var(--accent);
  color: var(--accent);
}
.sb-empty-inline {
  color: var(--muted);
  font-size: var(--fs-sm);
  font-style: italic;
}

/* The tooltip itself — same family as the quick-edit sheet, one size down,
   because it holds a short list of names and nothing else. */
.sb-pop {
  position: fixed;
  z-index: var(--z-modal);
  /* Wider than the old name-list tooltip: it now holds a two-column grid of
     level checkboxes, which 236px would have wrapped to one. */
  width: 320px;
  background: var(--surface);
  border: 1px solid var(--line-strong);
  border-radius: var(--r-lg);
  box-shadow: var(--sh-3);
  overflow: hidden;
  font-size: var(--fs-base);
}
.sb-head {
  display: flex;
  align-items: center;
  gap: var(--sp-2);
  padding: var(--sp-2) var(--sp-3);
  border-bottom: 1px solid var(--line);
}
.sb-sub {
  font-size: var(--fs-sm);
  color: var(--muted);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.sb-head .qe-close {
  margin-left: auto;
}
.sb-body {
  padding: var(--sp-3);
  max-height: 46vh;
  overflow-y: auto;
}
/* Who this entry is about, when editing. Shown as a heading rather than a
   disabled dropdown: the teacher is not changeable here, so a control that
   looks changeable would be a lie. */
.sb-who {
  font-weight: 600;
  color: var(--ink-2);
}
.sb-lvhead {
  margin: var(--sp-3) 0 var(--sp-2);
  font-size: var(--fs-sm);
  font-weight: 600;
  color: var(--ink-2);
}
.sb-lvhint {
  display: block;
  font-weight: 400;
  font-size: var(--fs-xs);
  color: var(--muted);
}
/* Two columns: a dozen level names read faster side by side than as one long
   column you have to scroll. */
.sb-lvgrid {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 2px var(--sp-2);
}
.sb-lvbox {
  display: flex;
  align-items: center;
  gap: 6px;
  padding: 2px 0;
  font-size: var(--fs-sm);
  cursor: pointer;
}
.sb-lvbox input {
  flex: none;
  cursor: pointer;
}
.sb-lvbox span {
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.sb-foot {
  display: flex;
  justify-content: flex-end;
  padding: var(--sp-2) var(--sp-3) var(--sp-3);
  border-top: 1px solid var(--line);
}
.sb-go {
  flex: none;
  border: 1px solid var(--ok-ring);
  background: var(--ok-bg);
  color: var(--ok);
  border-radius: var(--r-sm);
  padding: 4px var(--sp-2);
  font-size: var(--fs-sm);
  font-weight: 600;
  cursor: pointer;
}
.sb-go:hover {
  filter: brightness(0.97);
}
/* Why you cannot add anyone right now — spelled out rather than shown as a
   greyed-out box, because "not your team", "no term yet" and "everyone is
   already listed" are three different problems with three different fixes. */
.sb-note {
  padding: var(--sp-2) var(--sp-3) var(--sp-3);
  border-top: 1px solid var(--line);
  color: var(--muted);
  font-size: var(--fs-sm);
}
/* The "make this standby an Assistant" shortcut — a small pill on the row
   (neutral, not the ✕'s danger red, since this ADDS access rather than
   removing someone) that expands into a one-room picker right underneath. */
.sb-assist {
  flex: none;
  border: 1px solid var(--line-strong);
  background: transparent;
  color: var(--muted);
  font-size: var(--fs-xs);
  line-height: 1;
  padding: 3px 6px;
  border-radius: var(--r-sm);
  cursor: pointer;
  white-space: nowrap;
}
.sb-assist:hover,
.sb-assist:focus-visible {
  color: var(--ink);
  border-color: var(--accent);
}
.sb-assist-form {
  display: flex;
  flex-direction: column;
  gap: var(--sp-2);
  padding: var(--sp-2) 0 var(--sp-3);
  border-top: 1px dashed var(--line);
}
.sb-assist-label {
  font-size: var(--fs-sm);
  color: var(--muted);
}
.sb-assist-row {
  display: flex;
  align-items: center;
  gap: var(--sp-2);
}
.sb-assist-row .cselect {
  flex: 1;
  min-width: 0;
}

/* ── Quick-edit sheet ────────────────────────────────────────────────
   Floats beside the clicked card. Shaped like the Dashboard's hover tooltip
   on purpose — same page, same family — but this one takes the mouse. */
.qe-sheet {
  position: fixed;
  z-index: var(--z-modal);
  width: 300px;
  background: var(--surface);
  border: 1px solid var(--line-strong);
  border-radius: var(--r-lg);
  box-shadow: var(--sh-3);
  padding: 0; /* each section pads itself — see .sched-tooltip */
  overflow: hidden;
  font-size: var(--fs-base);
}
.qe-head {
  display: flex;
  align-items: center;
  gap: var(--sp-2);
  padding: var(--sp-3) var(--sp-4);
  border-bottom: 1px solid var(--line);
}
.qe-sub {
  font-size: var(--fs-sm);
  color: var(--muted);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.qe-close {
  margin-left: auto;
  flex: none;
  border: none;
  background: transparent;
  color: var(--muted);
  font-size: var(--fs-md);
  line-height: 1;
  padding: 2px 4px;
  border-radius: var(--r-sm);
  cursor: pointer;
}
.qe-close:hover {
  background: var(--surface-3);
  color: var(--ink);
}

.qe-field {
  display: flex;
  align-items: center;
  gap: var(--sp-3);
  padding: var(--sp-2) var(--sp-4);
}
.qe-field:first-of-type {
  padding-top: var(--sp-3);
}
.qe-label {
  flex: none;
  width: 34px;
  color: var(--muted);
  font-size: var(--fs-sm);
}
/* The cselect must take the rest of the row; on its own it is inline-sized. */
.qe-field .cselect {
  flex: 1;
  min-width: 0;
}
.qe-readonly {
  padding: var(--sp-3) var(--sp-4);
  color: var(--muted);
  font-size: var(--fs-sm);
}

.qe-students {
  border-top: 1px solid var(--line);
  margin-top: var(--sp-2);
}
.qe-students-head {
  display: flex;
  align-items: center;
  gap: var(--sp-2);
  padding: var(--sp-3) var(--sp-4) var(--sp-2);
  font-weight: 600;
}
/* Where the ＋ Add button used to sit. Deliberately not button-shaped — it is
   a signpost, not something to press. */
.qe-note {
  margin-left: auto;
  font-size: var(--fs-sm);
  font-weight: 500;
  color: var(--muted);
}
.qe-list {
  list-style: none;
  margin: 0;
  padding: 0 var(--sp-4) var(--sp-3);
  max-height: 34vh;
  overflow-y: auto;
}
.qe-list li {
  display: flex;
  align-items: center;
  gap: var(--sp-2);
  padding: 3px 0;
}
.qe-n {
  width: 16px;
  flex: none;
  text-align: right;
  color: var(--muted);
  font-size: var(--fs-sm);
}
/* Shrinks but does not GROW: the gender badge sits immediately after the name
   rather than being pushed to the far right of the sheet. A long name still
   ellipsises, because min-width:0 lets the box shrink below its text. */
.qe-name {
  flex: 0 1 auto;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.qe-list .gender-badge {
  flex: none;
}
.qe-empty {
  color: var(--muted);
  font-style: italic;
}
.qe-full {
  display: block;
  width: 100%;
  border: none;
  border-top: 1px solid var(--line);
  background: var(--surface-2);
  color: var(--ink);
  padding: var(--sp-3) var(--sp-4);
  text-align: left;
  font-size: var(--fs-sm);
  font-weight: 600;
  cursor: pointer;
}
.qe-full:hover {
  background: var(--surface-3);
}

/* ── Free Room board (per Big, 2026-07-23) ──
   The negative of the grid: which rooms nobody is using, session by session.
   A BOARD, not a tooltip — it stays open while you plan and drags out of the
   way by its header, so it can be read against the very cell it would
   otherwise be covering. --z-float keeps it over the page but UNDER a modal:
   a screenshot caught it floating on top of the Add-class form, which is the
   form it exists to help you fill in. */
.board-btn {
  align-self: center;
  margin-left: auto;
  display: inline-flex;
  align-items: center;
  gap: var(--sp-2);
  padding: 6px var(--sp-3);
  font: inherit;
  font-size: var(--fs-sm);
  font-weight: 600;
  color: var(--muted);
  background: var(--surface);
  border: 1px solid var(--line-strong);
  border-radius: var(--r-full);
  cursor: pointer;
  transition:
    color var(--dur-1) var(--ease-out),
    border-color var(--dur-1) var(--ease-out),
    background-color var(--dur-1) var(--ease-out);
}
.board-btn svg {
  width: 16px;
  height: 16px;
}
.board-btn:hover,
.board-btn.on {
  color: var(--accent);
  border-color: var(--accent);
  background: var(--surface-3);
}
.fr-board {
  position: fixed;
  z-index: var(--z-float);
  width: min(560px, calc(100vw - 16px));
  background: var(--surface);
  border: 1px solid var(--line-strong);
  border-radius: var(--r-lg);
  box-shadow: var(--sh-3);
  overflow: hidden;
  font-size: var(--fs-base);
}
.fr-head {
  display: flex;
  align-items: center;
  gap: var(--sp-2);
  padding: var(--sp-2) var(--sp-3);
  border-bottom: 1px solid var(--line);
  background: var(--surface-2);
  cursor: grab;
  /* A drag that selects the title text looks broken even when it works. */
  user-select: none;
  touch-action: none;
}
.fr-head.dragging {
  cursor: grabbing;
}
.fr-grip {
  color: var(--muted);
  font-size: var(--fs-md);
  line-height: 1;
}
.fr-sub {
  font-size: var(--fs-sm);
  color: var(--muted);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.fr-head .qe-close {
  margin-left: auto;
}
.fr-body {
  display: grid;
  grid-auto-flow: column;
  grid-auto-columns: minmax(0, 1fr);
  gap: var(--sp-2);
  padding: var(--sp-3);
  max-height: 52vh;
  overflow-y: auto;
  overscroll-behavior: contain;
}
.fr-col-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--sp-2);
  padding-bottom: 4px;
  margin-bottom: var(--sp-2);
  border-bottom: 1px solid var(--line);
  font-size: var(--fs-xs);
  font-weight: 700;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  color: var(--muted);
}
/* The count is the headline of each column — "Section 2: 7 free" is the whole
   answer on a good day, and a 0 in red is the whole answer on a bad one. */
.fr-n {
  flex: none;
  min-width: 20px;
  padding: 1px 6px;
  border-radius: var(--r-full);
  background: var(--ok-bg);
  color: var(--ok);
  font-size: var(--fs-xs);
  text-align: center;
}
.fr-n.zero {
  background: var(--danger-bg);
  color: var(--danger);
}
.fr-rooms {
  list-style: none;
  margin: 0;
  padding: 0;
  display: grid;
  gap: 4px;
}
.fr-room {
  display: flex;
  align-items: center;
  gap: var(--sp-2);
  padding: 4px var(--sp-2);
  border: 1px solid var(--line);
  border-radius: var(--r-sm);
  background: var(--surface-2);
}
.fr-room-n {
  flex: 1;
  min-width: 0;
  font-weight: 600;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
/* Seats, not a room number — right-aligned and tabular so the column of
   numbers can be compared down the list rather than read one at a time. */
.fr-cap {
  flex: none;
  font-size: var(--fs-xs);
  color: var(--muted);
  font-variant-numeric: tabular-nums;
}
.fr-online {
  border-style: dashed;
}
.fr-none {
  padding: 4px var(--sp-2);
  font-size: var(--fs-xs);
  color: var(--muted);
  font-style: italic;
}

/* ── Class Plan scoreboards (per Big, 2026-07-23) ──
   A head count in the corner of each SESSION header, and the four added up in
   a card of its own in the toolbar. Only the session headers get
   `position: relative` — `th.roomcol` is `position: sticky` and an ID-scoped
   `#plan-table thead th` would outrank it and unstick the Level column, which
   is the whole reason it can scroll sideways and keep its labels. */
#plan-table thead th:not(.roomcol) {
  position: relative;
}
/* The pill: a silhouette and a number, nothing else. The word "คน" used to sit
   here and the icon says it in less space (per Big) — the tooltip still spells
   out rooms and people for anyone who wants the sentence. */
#plan-table .ss-score {
  position: absolute;
  top: 5px;
  right: 6px;
  display: inline-flex;
  align-items: center;
  gap: 5px;
  padding: 3px 10px 3px 8px;
  border: 1px solid var(--line-strong);
  border-radius: var(--r-full);
  background: var(--surface);
  box-shadow: var(--sh-1);
  color: var(--ink);
  line-height: 1;
}
#plan-table .ss-score .sc-ico {
  width: 13px;
  height: 13px;
  flex: none;
  color: var(--muted);
}
#plan-table .ss-score .sc-n {
  font-size: var(--fs-md);
  font-weight: 800;
  font-variant-numeric: tabular-nums;
}
/* An empty session is quiet, not alarming: nobody booked yet is the normal
   state of a term being planned, not a fault. */
#plan-table .ss-score.zero {
  border-color: var(--line);
  background: transparent;
  box-shadow: none;
  color: var(--muted);
}
#plan-table .ss-score.zero .sc-ico {
  opacity: 0.55;
}

/* Which filter the numbers are under. Dimmer than the labels and without the
   amber, because it is a caption about the figures rather than one of them. */
.hud-note {
  font-size: var(--fs-xs);
  font-weight: 500;
  color: #94a3b8;
  white-space: nowrap;
}

/* ── Class Plan grid frame (per Big, 2026-07-23) ──
   The grid sits directly under the bold, dark SCOREBOARD panel, and next to
   it the default hairline frame (--line, a near-white 1px) plus the barely
   there --sh-1 made the whole table look sunken and washed out. Give THIS
   grid a crisper, lifted frame so it reads as a solid board of its own: a
   defined slate edge, a real drop shadow, and internal grid lines bumped from
   the faint --line up to --line-strong so the rules inside don't fade out
   either. Scoped with :has() to the wrapper around #plan-table — the
   Dashboard's identical table has no scoreboard above it to compete with, so
   it keeps the quieter default. */
.gridwrap:has(#plan-table) {
  border: 2px solid #9fb0c4;
  box-shadow:
    0 1px 0 #cbd5e1,
    0 6px 18px rgb(15 23 42 / 0.12);
}
#plan-table th,
#plan-table td {
  border-color: var(--line-strong);
}

/* ── Class Plan breakdown (per Big, 2026-07-23) ──
   The same .hud material, opened out: a column per language, a line per
   level, rooms and heads. The total in the toolbar is the answer; this is the
   working, so every number here is checkable against a row of the grid and
   the three subtotals add to the headline. */
.plan-breakdown {
  padding: var(--sp-2) var(--sp-4) var(--sp-3);
  margin-bottom: var(--sp-4);
  /* Drop the floating shadow this inherits from .hud (per Big, 2026-07-23):
     as a full-width bar it read as too prominent — a small Roster count pill
     can float, a 100%-wide band just looks like it is lifting off the page.
     Keep only the inset gloss + inner bottom shade so it stays the embossed
     dark scoreboard material, now sitting flat instead of hovering. */
  box-shadow:
    inset 0 1px 0 rgb(255 255 255 / 0.12),
    inset 0 -1px 0 rgb(0 0 0 / 0.35);
}
/* Folded: the header bar and nothing else, so the two headline numbers never
   leave the page (per Big, 2026-07-23) — only the working does. */
.plan-breakdown.collapsed {
  padding-bottom: var(--sp-2);
}
.plan-breakdown.collapsed .hb-body {
  display: none;
}
.plan-breakdown.collapsed .hb-caret {
  transform: rotate(-90deg);
}
/* The whole header IS the toggle: a 230px band is a big thing to open and
   close through a 16px chevron. Transparent button, so it reads as part of
   the panel until you hover it. */
.hb-bar {
  display: flex;
  align-items: center;
  gap: var(--sp-2);
  width: 100%;
  padding: var(--sp-1) 0;
  border: none;
  background: none;
  font: inherit;
  color: inherit;
  text-align: left;
  cursor: pointer;
}
.hb-bar:hover .hb-title,
.hb-bar:hover .hb-caret {
  color: #fff;
}
.hb-caret {
  flex: none;
  color: #94a3b8;
  font-size: var(--fs-md);
  line-height: 1;
  transition: transform var(--dur-1) var(--ease-out);
}
.hb-title {
  font-size: var(--fs-sm);
  font-weight: 700;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: #cbd5e1;
}
.hb-tot {
  display: flex;
  align-items: baseline;
  gap: var(--sp-2);
  margin-left: auto;
}
.hb-tot .hud-val {
  min-width: 0;
}
.hb-body {
  padding-top: var(--sp-2);
  margin-top: var(--sp-1);
  border-top: 1px solid rgb(255 255 255 / 0.14);
}
.plan-breakdown:empty {
  display: none; /* before the first render there is no panel to draw */
}
/* .hud's sheen is the top 45% of the panel, which on a two-line readout is a
   few millimetres and on this one was a hard bright band cutting straight
   across the level rows. Same shine, measured in pixels so it stays a
   highlight on the top edge however tall the panel gets. */
.plan-breakdown::before {
  inset: 0 0 auto 0;
  height: 30px;
}
/* Spread across the full width, first column flush left and last flush right,
   so the row of languages lines up with the header bar above it (per Big,
   2026-07-23 — packed left, the panel was two thirds empty on the right).
   Each column is still capped: stretching them to a third of the page each
   would leave the level name and its numbers a hand's width apart, and
   reading a row would mean tracking across empty panel. The cap is what
   space-between then has room to distribute. */
.hb-cols {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(210px, 420px));
  justify-content: space-between;
  gap: var(--sp-3) var(--sp-8);
}
.hb-head {
  display: flex;
  align-items: center;
  gap: var(--sp-2);
  padding-bottom: 4px;
  border-bottom: 1px solid rgb(255 255 255 / 0.14);
  color: #e2e8f0;
  font-size: var(--fs-sm);
  letter-spacing: 0.04em;
}
.hb-sum {
  margin-left: auto;
  font-size: var(--fs-xs);
  font-weight: 600;
  color: #94a3b8;
  font-variant-numeric: tabular-nums;
}
.hb-rows {
  list-style: none;
  margin: var(--sp-1) 0 0;
  padding: 0;
}
.hb-row {
  display: grid;
  grid-template-columns: 1fr 38px 38px;
  align-items: baseline;
  gap: var(--sp-2);
  padding: 1px 0;
}
.hb-lv {
  color: #cbd5e1;
  font-size: var(--fs-sm);
  font-weight: 600;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.hb-r,
.hb-s {
  text-align: right;
  font-size: var(--fs-sm);
  font-weight: 700;
  font-variant-numeric: tabular-nums;
}
.hb-r {
  color: #94a3b8;
}
.hb-s {
  color: #fcd34d;
}
/* Column headings, so a bare pair of numbers can't be read the wrong way
   round. Quiet — they are the same three words on every column. */
.hb-legend .hb-lv,
.hb-legend .hb-r,
.hb-legend .hb-s {
  font-size: var(--fs-xs);
  font-weight: 600;
  color: #7c8da5;
  letter-spacing: 0.04em;
}
/* A level with nothing booked stays on the list, faded. Dropping it would
   make this a list of what exists, when the hole is what you are checking
   for. */
.hb-row.empty {
  opacity: 0.45;
}
/* Classes whose level was never set: red, because unlike an empty level this
   one is a data fault — it also falls out of the auto-placer. */
.hb-row.orphan .hb-lv {
  color: #fca5a5;
}
.hb-none {
  font-size: var(--fs-xs);
  font-style: italic;
  color: #94a3b8;
}

/* ── Reservation filter bar (per Big, 2026-07-23) ──
   Replaces the Sort / Session / New-Cont dropdowns with a stack of
   conditions you build yourself. Deliberately NOT the app's chip-and-label
   toolbar look ("ไม่เอาแบบ Label แล้วมันเก่าไป") — these read as controls
   with weight: a filled button when something is on, a real shadowed panel
   when one is open. */
.fbar {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: var(--sp-2);
  min-width: 0;
}
.fbtn {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  padding: 6px var(--sp-3);
  font: inherit;
  font-size: var(--fs-sm);
  font-weight: 600;
  color: var(--ink-2);
  background: var(--surface);
  border: 1px solid var(--line-strong);
  border-radius: var(--r-full);
  cursor: pointer;
  transition:
    background-color var(--dur-1) var(--ease-out),
    border-color var(--dur-1) var(--ease-out),
    color var(--dur-1) var(--ease-out);
}
.fbtn:hover {
  border-color: var(--accent);
  background: var(--surface-3);
}
/* Filters are ON: the button stops being a suggestion and starts being a
   state, with the count of conditions riding on it. */
.fbtn.on {
  color: var(--accent-ink);
  background: var(--accent);
  border-color: var(--accent);
}
.fbtn.on:hover {
  background: var(--accent-hover);
}
.fbtn-n {
  min-width: 18px;
  padding: 0 5px;
  border-radius: var(--r-full);
  background: rgb(255 255 255 / 0.22);
  font-size: var(--fs-xs);
  font-weight: 700;
  text-align: center;
  font-variant-numeric: tabular-nums;
}
.fbtn-ghost {
  border-color: transparent;
  background: none;
  color: var(--muted);
}
.fbtn-ghost:hover {
  color: var(--danger);
  border-color: var(--danger-bg);
  background: var(--danger-bg);
}
.f-ico {
  width: 15px;
  height: 15px;
  flex: none;
}

/* One chip per condition. It prints the whole sentence — a chip that only
   said "Level" would have to be opened to find out what it is doing. */
.fchips {
  display: contents; /* chips join the bar's own flex row and wrap with it */
}
.fchip {
  display: inline-flex;
  align-items: stretch;
  border: 1px solid var(--line-strong);
  border-radius: var(--r-full);
  background: var(--surface);
  overflow: hidden;
  max-width: 100%;
}
.fchip.empty {
  border-style: dashed;
}
.fc-main {
  display: inline-flex;
  align-items: center;
  gap: 5px;
  min-width: 0;
  padding: 6px var(--sp-2) 6px var(--sp-3);
  border: none;
  background: none;
  font: inherit;
  font-size: var(--fs-sm);
  cursor: pointer;
}
.fc-main:hover {
  background: var(--surface-3);
}
.fc-ico {
  color: var(--muted);
}
.fc-k {
  font-weight: 600;
  color: var(--muted);
}
.fc-op {
  color: var(--muted);
}
/* The value is the news in the sentence, so it is the only part in full ink. */
.fc-v {
  font-weight: 700;
  color: var(--ink);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.fchip.empty .fc-v {
  font-weight: 500;
  color: var(--muted);
  font-style: italic;
}
.fc-x {
  flex: none;
  padding: 0 var(--sp-2);
  border: none;
  border-left: 1px solid var(--line);
  background: none;
  color: var(--muted);
  font-size: var(--fs-xs);
  cursor: pointer;
}
.fc-x:hover {
  background: var(--danger-bg);
  color: var(--danger);
}

/* The picker panel. Same family as the cselect menu, one size up, because it
   is a place you work in rather than a list you glance at. */
.fpop {
  position: fixed;
  z-index: var(--z-modal);
  width: 268px;
  max-height: 60vh;
  display: flex;
  flex-direction: column;
  background: var(--surface);
  border: 1px solid var(--line-strong);
  border-radius: var(--r-lg);
  box-shadow: var(--sh-3);
  overflow: hidden;
}
.fpop-head {
  display: flex;
  align-items: center;
  gap: 6px;
  padding: var(--sp-2) var(--sp-3);
  border-bottom: 1px solid var(--line);
  font-size: var(--fs-sm);
}
.fpop-head .f-ico {
  color: var(--muted);
}
/* is / is not, as a two-position switch rather than a third dropdown. */
.fop {
  display: flex;
  margin-left: auto;
  border: 1px solid var(--line-strong);
  border-radius: var(--r-full);
  overflow: hidden;
}
.fop button {
  padding: 2px var(--sp-2);
  border: none;
  background: var(--surface);
  font: inherit;
  font-size: var(--fs-xs);
  font-weight: 600;
  color: var(--muted);
  cursor: pointer;
}
.fop button.on {
  background: var(--accent);
  color: var(--accent-ink);
}
.fpop-search {
  display: flex;
  align-items: center;
  gap: 6px;
  padding: var(--sp-2) var(--sp-3);
  border-bottom: 1px solid var(--line);
  color: var(--muted);
}
.fpop-search input {
  flex: 1;
  min-width: 0;
  border: none;
  background: none;
  font: inherit;
  font-size: var(--fs-sm);
  color: var(--ink);
  outline: none;
}
.fpop-list {
  list-style: none;
  margin: 0;
  padding: var(--sp-1);
  overflow-y: auto;
  overscroll-behavior: contain;
}
.fpop-list li[hidden] {
  display: none;
}
.fpop-list button,
.fpop-list label {
  display: flex;
  align-items: center;
  gap: var(--sp-2);
  width: 100%;
  padding: 6px var(--sp-2);
  border: none;
  border-radius: var(--r-sm);
  background: none;
  font: inherit;
  font-size: var(--fs-sm);
  color: var(--ink-2);
  text-align: left;
  cursor: pointer;
}
.fpop-list button:hover,
.fpop-list label:hover {
  background: var(--surface-2);
}
.fpop-list button.on {
  font-weight: 700;
  color: var(--ink);
  background: var(--surface-3);
}
.fpop-list .f-ico {
  color: var(--muted);
}

/* ── Sort menu: one row per column, expanded to its two directions ────
   Listing every column twice made a 28-line menu; the direction only
   matters once you have chosen the column (per Big, 2026-07-24). */
.fpop-exp-name {
  flex: 1;
  min-width: 0;
}
/* The direction currently in force, shown on the collapsed row so the menu
   answers "how is this sorted right now?" without being opened up. */
.fpop-cur {
  font-weight: 700;
  color: var(--accent);
  flex: none;
}
.fpop-caret {
  width: 12px;
  height: 12px;
  flex: none;
  color: var(--muted);
  transition: transform var(--dur-1) var(--ease-out);
}
.fpop-caret.open {
  transform: rotate(90deg);
}
/* ── Sort menu, top half: the levels in force ─────────────────────────
   "Date ↓ then Student ↑". Numbered, because the order IS the meaning —
   level 2 only breaks level 1's ties. */
.sl-head {
  font-size: var(--fs-xs);
  font-weight: 700;
  letter-spacing: 0.03em;
  text-transform: uppercase;
  color: var(--muted);
  padding: var(--sp-2) var(--sp-3) var(--sp-1);
}
.sl-list {
  list-style: none;
  margin: 0 0 var(--sp-2);
  padding: 0 var(--sp-1);
  border-bottom: 1px solid var(--line);
}
.sl-row {
  display: flex;
  align-items: center;
  gap: var(--sp-2);
  padding: 3px var(--sp-2) 3px 0;
}
.sl-n {
  flex: none;
  width: 18px;
  height: 18px;
  display: grid;
  place-items: center;
  font-size: 10px;
  font-weight: 700;
  color: var(--accent-ink);
  background: var(--accent);
  border-radius: 50%;
}
.sl-name {
  flex: 1;
  min-width: 0;
  font-size: var(--fs-sm);
  font-weight: 600;
  color: var(--ink);
  white-space: nowrap;
}
/* The direction doubles as the button that flips it — the arrow you want to
   change is the thing you click. */
.sl-dir {
  flex: none;
  font: inherit;
  font-size: var(--fs-xs);
  font-weight: 600;
  color: var(--ink-2);
  background: var(--surface-2);
  border: 1px solid var(--line);
  border-radius: var(--r-sm);
  padding: 2px var(--sp-2);
  cursor: pointer;
  white-space: nowrap;
}
.sl-dir:hover {
  background: var(--surface-3);
  color: var(--ink);
}
.sl-x {
  flex: none;
  font: inherit;
  font-size: 11px;
  line-height: 1;
  color: var(--muted);
  background: none;
  border: none;
  border-radius: var(--r-sm);
  padding: 3px 5px;
  cursor: pointer;
}
.sl-x:hover {
  color: var(--danger);
  background: var(--danger-bg);
}

/* The two directions, indented under their column and marked with a rail so
   they read as belonging to it rather than as two more columns. */
.fpop-sub {
  list-style: none;
  margin: 2px 0 var(--sp-2) 0;
  padding: 0 0 0 var(--sp-3);
  border-left: 2px solid var(--line);
  margin-left: 13px;
}
.fpop-sub button {
  color: var(--ink-2);
}
/* A heading inside a value list — the Level picker groups its levels by team
   so you are not scrolling past two languages to reach the third. Sticky, so
   the team you are reading stays named while its levels scroll under it. */
.fpop-group {
  position: sticky;
  top: 0;
  z-index: 1;
  background: var(--surface);
}
/* The whole heading is the fold control, so the target is the full width
   rather than a caret you have to aim at. */
.fpop-list .fpop-group button {
  gap: var(--sp-2);
  font-size: 10px;
  font-weight: 700;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  color: var(--muted);
  padding: var(--sp-2) var(--sp-2) 4px;
}
.fpop-list .fpop-group button:hover {
  color: var(--ink);
  background: none;
}
.fpop-group-name {
  flex: 1;
  min-width: 0;
}
/* The count is what makes folding safe: a shut group still says how many are
   inside, so nothing looks lost. */
.fpop-group-n {
  flex: none;
  font-weight: 700;
  color: var(--muted);
  background: var(--surface-2);
  border-radius: 999px;
  padding: 0 6px;
  letter-spacing: 0;
}
/* No rule above the first one — it would sit under the search box. A rule
   between YEARS only; a language heading inside a year would be cut off from
   the year it belongs to. */
.fpop-group ~ .fpop-group:not(.fpop-group-2) {
  border-top: 1px solid var(--line);
}
/* The inner heading sits one step in, and drops the uppercase so the two
   levels do not read as equals. */
.fpop-list .fpop-group-2 button {
  padding-left: var(--sp-4);
  text-transform: none;
  letter-spacing: 0;
  font-size: var(--fs-xs);
}
.fpop-group-2 {
  /* Only the outer heading sticks — two stacked sticky rows would eat the
     top of a short list. */
  position: static;
}
/* Options sit inside their language, which sits inside its year. */
.fpop-checks li[data-g2] label {
  padding-left: var(--sp-5);
}

/* The plain-language result of the whole stack — "9:00 → 11:10 → the rest". */
.sl-preview {
  display: flex;
  flex-wrap: wrap;
  gap: 0 var(--sp-2);
  padding: 0 var(--sp-3) var(--sp-2);
  margin-top: -2px;
  font-size: var(--fs-xs);
  line-height: 1.5;
}
.sl-preview-k {
  font-weight: 700;
  color: var(--muted);
}
.sl-preview-v {
  flex: 1;
  min-width: 0;
  color: var(--ink-2);
}
/* A value's own two directions sit one rail deeper than the value, so the
   three levels — column, value, direction — read as a nesting rather than as
   one long list. */
.fpop-sub .fpop-sub {
  margin-left: 9px;
  padding-left: var(--sp-2);
}
.fpop-checks input {
  flex: none;
  accent-color: var(--accent);
}
.fpop-text {
  padding: var(--sp-3);
}
.fpop-text input {
  width: 100%;
  padding: 6px var(--sp-2);
  border: 1px solid var(--line-strong);
  border-radius: var(--r-sm);
  font: inherit;
  font-size: var(--fs-sm);
}
.fpop-none {
  margin: 0;
  padding: var(--sp-3);
  font-size: var(--fs-sm);
  color: var(--muted);
  text-align: center;
}
.fpop-none[hidden] {
  display: none;
}

/* ── EARLY STUDENT flag (per Big, 2026-07-23 · migration 0024) ──
   A seat held before any money arrived. It rides beside the student's name on
   the Reservation table and the Roster rows, so nobody reads a student's row
   without knowing the seat is on loan. Amber while the agreed date is still
   ahead, red once it has passed — the same two-step the rest of the app uses
   for "watch this" and "act on this". */
.eflag {
  display: inline-flex;
  align-items: center;
  gap: 3px;
  margin-left: 5px;
  padding: 1px 6px 1px 4px;
  border: 1px solid var(--warn-ring);
  border-radius: var(--r-full);
  background: var(--warn-bg);
  color: var(--warn);
  font-size: var(--fs-xs);
  font-weight: 700;
  white-space: nowrap;
  vertical-align: middle;
  font-variant-numeric: tabular-nums;
}
.eflag svg {
  width: 11px;
  height: 11px;
  flex: none;
}
.eflag.over {
  border-color: var(--danger);
  background: var(--danger-bg);
  color: var(--danger);
}
/* Roster rows are tight and already carry a gender badge, a nationality chip
   and a status dropdown — there the flag is the icon alone, with the date on
   hover. */
.eflag.sm {
  padding: 1px 4px;
  gap: 0;
}

/* The Waiting list / EARLY STUDENT tabs. */
.wl-tabs {
  gap: var(--sp-2);
}
.chip-n {
  margin-left: 6px;
  padding: 0 5px;
  border-radius: var(--r-full);
  background: rgb(15 23 42 / 0.12);
  font-size: var(--fs-xs);
  font-weight: 700;
  font-variant-numeric: tabular-nums;
}
.chip.on .chip-n {
  background: rgb(255 255 255 / 0.25);
}
/* Expire student: the only tab that means someone is out of time, so it is
   the only one allowed to shout. Red only when it actually has somebody on
   it — a permanently red tab is a light nobody looks at. */
.wl-exp-tab .fl {
  color: var(--danger);
  margin-left: 4px;
}
.wl-exp-tab.has-over {
  border-color: var(--danger);
  color: var(--danger);
}
.wl-exp-tab.has-over.on,
.wl-exp-tab.on {
  background: var(--danger);
  border-color: var(--danger);
  color: #fff;
}
.wl-exp-tab.on .fl {
  color: #fff;
}
.wl-exp-tab.on .chip-n {
  background: rgb(255 255 255 / 0.25);
}
tr.wl-over td {
  background: color-mix(in srgb, var(--danger-bg) 45%, transparent);
}
/* .wl-soon used to be dimmed, back when the in-date rows shared a list with
   the overdue ones and had to sit behind them. They have their own tab now
   (per Big, 2026-07-23), so dimming every row on it would just make the only
   thing on the page harder to read. */
/* Move with no status is a different act from Move with one, so it is a
   different button rather than the same one behaving differently. */
.wl-move-early {
  border-color: var(--warn-ring);
  background: var(--warn-bg);
  color: var(--warn-strong);
}
.quickdays {
  display: flex;
  gap: var(--sp-2);
  margin: calc(-1 * var(--sp-2)) 0 var(--sp-4);
}

/* The flag glyph itself. Sized here rather than in each caller so the pole
   stays the same weight everywhere it appears. */
.fl {
  width: 1em;
  height: 1em;
  flex: none;
  vertical-align: -0.13em;
}
/* On the Move button it is RED and nothing else on that button is (per Big) —
   the button says "Move", the flag says what kind of move. */
.wl-move-early .fl {
  color: var(--danger);
  width: 13px;
  height: 13px;
}
/* Follow up now counts flagged students, so the tile wears the flag to say
   which "follow up" it means. */
.dash-flag {
  color: var(--danger);
  font-size: var(--fs-md);
}
.dash-sub {
  display: block;
  margin-top: 2px;
  font-size: var(--fs-xs);
  font-weight: 600;
  color: var(--danger);
}

/* ── Presence: "who else is on this page" avatar stack (js/presence.js) ──
   Sits at the right end of a page's toolbar. Hidden whenever nobody else is
   here. Avatars overlap like Google Sheets; each carries a --pc colour set
   inline (one stable hue per person). */
.presence-bar {
  margin-left: auto; /* push to the trailing edge of the flex toolbar */
  display: flex;
  align-items: center;
  gap: var(--sp-1);
}
.presence-bar[hidden] {
  display: none;
}
.presence-label {
  font-size: var(--fs-sm);
  color: var(--muted);
  margin-right: 2px;
  white-space: nowrap;
}
.presence-av {
  width: 26px;
  height: 26px;
  border-radius: var(--r-full);
  background: var(--pc, var(--ink));
  color: #fff;
  font-size: var(--fs-sm);
  font-weight: 700;
  line-height: 1;
  display: flex;
  align-items: center;
  justify-content: center;
  /* A ring the colour of the surface behind it, so overlapping avatars stay
     visually separate instead of blurring into one blob. */
  border: 2px solid var(--surface);
  box-shadow: 0 1px 2px rgba(0, 0, 0, 0.18);
  cursor: default;
  user-select: none;
}
/* Overlap every avatar after the first (the ring keeps them readable). */
.presence-av + .presence-av {
  margin-left: -9px;
}
.presence-more {
  background: var(--line-strong);
  color: var(--ink);
  font-size: var(--fs-xs);
  font-weight: 700;
}
