/*
  Dashboard shell + data-visualisation chrome — 26 Aug 2026
  =========================================================
  Split into its own file rather than piled onto components.css because
  it is a coherent subsystem (the admin shell and the figures that live
  inside it) and because components.css was already 600 lines. Loaded
  globally in base.html, since the stat cards and figures are used on
  the public homepage too.

  Everything here consumes tokens.css. No hardcoded brand colour appears
  below — that rule is what makes the dark theme a re-declaration of
  variables rather than a second stylesheet.
*/

/* ===========================================================================
   0. Icons — a default size, so a new usage can never blow up
   ======================================================================== */
/* templates/partials/icon.html emits a <svg> with a viewBox and NO width
   or height, so it inherits `currentColor` and scales with its context —
   which also means that without a size it expands to fill whatever box
   it is in. It did exactly that in the mobile "Dashboard menu" button:
   a 200px grid icon.

   Every deliberate placement below re-declares its own size. This is the
   floor under the ones nobody remembered to. */
.icon {
  width: 18px;
  height: 18px;
  flex: none;
}

/* ===========================================================================
   1. Panels — the surface every dashboard block sits on
   ======================================================================== */
.panel {
  background: var(--color-surface);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-lg);
  box-shadow: var(--shadow-sm);
  padding: var(--space-5);
}
.panel + .panel { margin-top: var(--space-5); }
.panel__head {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--space-3);
  flex-wrap: wrap;
  margin-bottom: var(--space-4);
}
.panel__title {
  font-family: var(--font-display);
  font-size: var(--text-lg);
  font-weight: 600;
  margin: 0;
}
.panel__link {
  font-size: var(--text-sm);
  font-weight: 600;
  display: inline-flex;
  align-items: center;
  gap: var(--space-1);
}
.panel__link .icon { width: 16px; height: 16px; }

/* Two figures side by side above 1024px, stacked below. `minmax(0, 1fr)`
   rather than `1fr` — a bare 1fr floors at min-content, and a long
   programme name in a bar label would push the column wider than the
   grid and overflow the page. */
.panel-grid {
  display: grid;
  gap: var(--space-5);
  grid-template-columns: minmax(0, 1fr);
  /* Each panel is as tall as its own content. Grid's default `stretch`
     made a one-row roster panel as tall as the four-panel column beside
     it — a third of a screen of empty card, which reads as content that
     failed to load rather than as content that is simply short. */
  align-items: start;
}
@media (min-width: 1024px) {
  .panel-grid--2 { grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); }
  .panel-grid--wide { grid-template-columns: minmax(0, 1.6fr) minmax(0, 1fr); }
}

/* ===========================================================================
   2. Figure chrome — shared by every chart
   ======================================================================== */
.viz { margin: 0; }
.viz__head {
  display: flex;
  flex-direction: column;
  gap: 2px;
  margin-bottom: var(--space-4);
}
.viz__title {
  font-family: var(--font-display);
  font-size: var(--text-base);
  font-weight: 600;
  color: var(--color-ink);
}
.viz__caption,
.viz__note {
  font-size: var(--text-xs);
  color: var(--color-muted);
  margin: 0;
}
.viz__note { margin-top: var(--space-3); }

/* The honest empty state. A chart drawn from zero rows reads as "zero";
   a chart drawn from NO rows reads as "the system is broken". They must
   not look alike, so an empty figure says so in words. */
.viz__empty {
  margin: 0;
  padding: var(--space-6) var(--space-4);
  text-align: center;
  font-size: var(--text-sm);
  color: var(--color-muted);
  background: var(--color-surface-alt);
  border: 1px dashed var(--color-border);
  border-radius: var(--radius-md);
}

.viz__table { margin-top: var(--space-4); }
.viz__table > summary {
  cursor: pointer;
  font-size: var(--text-xs);
  color: var(--color-muted);
  padding: var(--space-1) 0;
}
.viz__table > summary:hover { color: var(--color-accent); }

/* ===========================================================================
   3. Donut
   ======================================================================== */
.viz-donut {
  display: flex;
  align-items: center;
  gap: var(--space-6);
  flex-wrap: wrap;
}
.viz-donut__ring {
  position: relative;
  flex: none;
  width: 180px;
  height: 180px;
}
.viz-donut__ring svg { width: 100%; height: 100%; display: block; }
.viz-donut__seg {
  /* stroke-linecap stays BUTT: a round cap would overhang the 2px
     separator computed in charts.py and close the gap it exists to
     create. */
  stroke-linecap: butt;
  transition: opacity var(--transition-fast);
}
.viz-donut__ring:hover .viz-donut__seg { opacity: 0.45; }
.viz-donut__ring .viz-donut__seg:hover { opacity: 1; }
@media (prefers-reduced-motion: reduce) {
  .viz-donut__seg { transition: none; }
}

.viz-donut__centre {
  position: absolute;
  inset: 0;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 2px;
  pointer-events: none;   /* never steal a hover from the segment beneath */
}
.viz-donut__total {
  font-family: var(--font-mono);
  font-size: var(--text-3xl);
  font-weight: 600;
  line-height: 1;
  color: var(--color-ink);
}
.viz-donut__total-label {
  font-size: var(--text-xs);
  text-transform: uppercase;
  letter-spacing: 0.06em;
  color: var(--color-muted);
}

/* ===========================================================================
   4. Legend — always present, always labelled
   ======================================================================== */
.viz-legend {
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: var(--space-3);
  min-width: 0;
  flex: 1 1 180px;
}
.viz-legend__item {
  display: grid;
  grid-template-columns: 12px 1fr auto;
  align-items: center;
  gap: var(--space-3);
  font-size: var(--text-sm);
}
.viz-legend__swatch {
  width: 12px;
  height: 12px;
  border-radius: 3px;
}
.viz-legend__label { color: var(--color-ink); }
.viz-legend__value {
  font-family: var(--font-mono);
  font-weight: 600;
  color: var(--color-ink);
  font-variant-numeric: tabular-nums;
}
.viz-legend__share { color: var(--color-muted); font-weight: 500; }

/* ===========================================================================
   5. Horizontal bars
   ======================================================================== */
.viz-bars {
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: var(--space-4);
}
.viz-bars__row {
  display: grid;
  grid-template-columns: minmax(0, 8.5rem) minmax(0, 1fr) auto;
  align-items: center;
  gap: var(--space-3);
  font-size: var(--text-sm);
}
@media (max-width: 560px) {
  /* Below this the three-column row squeezes the track to nothing.
     Label above, bar and value below. */
  .viz-bars__row {
    grid-template-columns: minmax(0, 1fr) auto;
    grid-template-areas: "label label" "track value";
    row-gap: var(--space-1);
  }
  .viz-bars__label { grid-area: label; }
  .viz-bars__track { grid-area: track; }
  .viz-bars__value { grid-area: value; }
}
.viz-bars__label {
  color: var(--color-ink);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.viz-bars__track {
  background: var(--color-surface-alt);
  border-radius: var(--radius-sm);
  height: 14px;              /* thin mark — well under the 24px cap */
  overflow: hidden;
}
.viz-bars__fill {
  display: block;
  height: 100%;
  min-width: 3px;            /* a count of 1 must still be visible */
  background: var(--viz-series);
  /* Rounded at the data end, square at the baseline. */
  border-radius: 0 var(--radius-sm) var(--radius-sm) 0;
}
.viz-bars__row.is-max .viz-bars__fill { background: var(--viz-series); }
.viz-bars__value {
  font-family: var(--font-mono);
  font-weight: 600;
  font-variant-numeric: tabular-nums;
  color: var(--color-ink);
}
.viz-bars__share { color: var(--color-muted); font-weight: 500; }

/* ===========================================================================
   6. Area / trend
   ======================================================================== */
.viz-area__svg {
  width: 100%;
  height: 160px;
  display: block;
  overflow: visible;
}
.viz-area__fill { fill: var(--viz-series-wash); stroke: none; }
.viz-area__line {
  fill: none;
  stroke: var(--viz-series);
  stroke-width: 2;
  stroke-linejoin: round;
  stroke-linecap: round;
  /* preserveAspectRatio="none" stretches the viewBox horizontally, which
     would also stretch the stroke. This keeps it 2px however wide the
     panel gets. */
  vector-effect: non-scaling-stroke;
}
.viz-area__dot {
  fill: var(--viz-series);
  stroke: var(--color-surface);      /* the 2px surface ring */
  stroke-width: 2;
  vector-effect: non-scaling-stroke;
  opacity: 0;
  transition: opacity var(--transition-fast);
}
.viz-area__svg:hover .viz-area__dot { opacity: 1; }
.viz-area__dot.is-last { opacity: 1; }
@media (prefers-reduced-motion: reduce) {
  .viz-area__dot { transition: none; }
}
.viz-area__axis {
  display: flex;
  justify-content: space-between;
  font-size: var(--text-xs);
  color: var(--color-muted);
  margin-top: var(--space-2);
}

/* ===========================================================================
   7. Status pills — status colour NEVER travels alone
   ======================================================================== */
.pill {
  display: inline-flex;
  align-items: center;
  gap: var(--space-2);
  padding: 3px var(--space-3);
  border-radius: 999px;
  font-size: var(--text-xs);
  font-weight: 600;
  line-height: 1.6;
  white-space: nowrap;
  background: var(--tint-neutral);
  color: var(--color-muted);
}
.pill__dot {
  width: 7px;
  height: 7px;
  border-radius: 50%;
  background: currentColor;
  flex: none;
}
/* The dot carries the hue; the word carries the meaning. Approved-green
   and rejected-red are indistinguishable under deuteranopia, so the
   label is not optional decoration — it is the actual encoding. */
.pill--approved { color: var(--viz-status-approved); background: var(--tint-neutral); }
.pill--pending  { color: var(--viz-status-pending);  background: var(--tint-neutral); }
.pill--rejected { color: var(--viz-status-rejected); background: var(--tint-neutral); }

/* ===========================================================================
   8. Data table
   ======================================================================== */
.data-table {
  width: 100%;
  border-collapse: collapse;
  font-size: var(--text-sm);
}
.data-table th,
.data-table td {
  text-align: left;
  padding: var(--space-3);
  border-bottom: 1px solid var(--color-border);
  vertical-align: middle;
}
.data-table thead th {
  font-size: var(--text-xs);
  text-transform: uppercase;
  letter-spacing: 0.05em;
  color: var(--color-muted);
  font-weight: 600;
  border-bottom-width: 1px;
  white-space: nowrap;
}
.data-table tbody tr:hover { background: var(--color-surface-alt); }
.data-table tbody tr:last-child td,
.data-table tbody tr:last-child th { border-bottom: none; }
.data-table tfoot th,
.data-table tfoot td {
  font-weight: 600;
  border-bottom: none;
  border-top: 1px solid var(--color-border);
}
/* Columns of numbers DO align vertically — this is the one place
   tabular figures belong. */
.data-table .is-numeric {
  text-align: right;
  font-family: var(--font-mono);
  font-variant-numeric: tabular-nums;
}
.data-table--compact th,
.data-table--compact td { padding: var(--space-2) var(--space-3); }

/* A wide table scrolls inside its own box; the PAGE never scrolls
   sideways. This is the Day 20 finding (Status/Review pushed off-screen
   at 1440px) turned into a rule instead of a column count. */
.table-scroll {
  overflow-x: auto;
  margin-inline: calc(var(--space-5) * -1);
  padding-inline: var(--space-5);
}

/* ===========================================================================
   9. The dashboard shell
   ======================================================================== */
.dash {
  display: grid;
  grid-template-columns: minmax(0, 1fr);
  gap: var(--space-6);
  padding-block: var(--space-6) var(--space-10);
  align-items: start;
}
@media (min-width: 960px) {
  .dash { grid-template-columns: 248px minmax(0, 1fr); }
}

.dash__sidebar {
  background: var(--color-surface);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-lg);
  box-shadow: var(--shadow-sm);
  padding: var(--space-4);
  display: flex;
  flex-direction: column;
  gap: var(--space-4);
}
@media (min-width: 960px) {
  .dash__sidebar {
    position: sticky;
    top: calc(64px + var(--space-4));  /* clears the sticky site header */
    max-height: calc(100vh - 64px - var(--space-8));
    overflow-y: auto;
  }
}

/* --- Who am I ------------------------------------------------------------ */
.dash__identity {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  padding-bottom: var(--space-4);
  border-bottom: 1px solid var(--color-border);
}
.dash__avatar {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 38px;
  height: 38px;
  flex: none;
  border-radius: 50%;
  background: var(--color-primary);
  color: var(--color-on-dark);
  font-family: var(--font-display);
  font-weight: 600;
  font-size: var(--text-sm);
}
.dash__who { min-width: 0; }
.dash__name {
  display: block;
  font-weight: 600;
  font-size: var(--text-sm);
  color: var(--color-ink);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.dash__role {
  display: block;
  font-family: var(--font-mono);
  font-size: var(--text-xs);
  color: var(--color-muted);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* --- Grouped navigation -------------------------------------------------- */
.dash__group + .dash__group { margin-top: var(--space-4); }
.dash__group-title {
  font-size: var(--text-xs);
  text-transform: uppercase;
  letter-spacing: 0.07em;
  color: var(--color-muted);
  font-weight: 600;
  padding: 0 var(--space-3) var(--space-2);
  margin: 0;
}
.dash__nav {
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: 2px;
}
.dash__link {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  padding: var(--space-2) var(--space-3);
  border-radius: var(--radius-md);
  font-size: var(--text-sm);
  font-weight: 500;
  color: var(--color-ink);
  position: relative;
}
.dash__link .icon { width: 18px; height: 18px; flex: none; opacity: 0.75; }
.dash__link:hover { background: var(--color-surface-alt); color: var(--color-ink); }
.dash__link.is-active {
  background: var(--color-primary);
  color: var(--color-on-dark);
  font-weight: 600;
}
.dash__link.is-active .icon { opacity: 1; }
/* An orange marker on the active item, so "where am I" survives being
   printed in greyscale or read by someone who can't separate the navy
   from the surrounding surface. */
.dash__link.is-active::before {
  content: "";
  position: absolute;
  left: 0;
  top: 20%;
  bottom: 20%;
  width: 3px;
  border-radius: 0 2px 2px 0;
  background: var(--color-accent);
}
.dash__link--disabled {
  color: var(--color-muted);
  cursor: default;
  justify-content: space-between;
  /* The "Coming Week 5" badge must never eat the label. `flex-wrap`
     lets it drop to a second line instead of clipping a single long
     word — "Announcements" rendered as "Announcement" before this. */
  flex-wrap: wrap;
  row-gap: 2px;
}
.dash__link--disabled:hover { background: none; }
/* A LIVE link that carries a hint badge — "Opens Django admin". Same
   layout as the disabled variant (badge pushed right, wrapping instead
   of clipping the label), but it is a real link, so it keeps its hover.
   The badge's own background has to step off --color-surface-alt on
   hover or it would dissolve into the row it sits in. */
.dash__link--hinted {
  justify-content: space-between;
  flex-wrap: wrap;
  row-gap: 2px;
}
.dash__link--hinted:hover .dash__eta { background: var(--color-surface); }
.dash__eta {
  font-family: var(--font-mono);
  font-size: 0.65rem;
  padding: 1px var(--space-2);
  border-radius: 999px;
  background: var(--color-surface-alt);
  color: var(--color-muted);
  white-space: nowrap;
}

.dash__sidebar-footer {
  margin-top: auto;
  padding-top: var(--space-4);
  border-top: 1px solid var(--color-border);
}
.dash__sidebar-footer .logout-form { display: block; }
.dash__sidebar-footer .btn { width: 100%; }

/* --- Mobile: the sidebar becomes a disclosure ---------------------------- */
.dash__sidebar-toggle {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-3);
  width: 100%;
  padding: var(--space-3) var(--space-4);
  background: var(--color-surface);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-md);
  font: inherit;
  font-size: var(--text-sm);
  font-weight: 600;
  color: var(--color-ink);
  cursor: pointer;
  margin-bottom: var(--space-4);
}
@media (min-width: 960px) {
  .dash__sidebar-toggle { display: none; }
}
@media (max-width: 959px) {
  /* Collapsed by default on small screens so the content the admin came
     for is the first thing on the page, not a 12-item menu. Uses the
     [hidden] attribute, toggled in main.js, so with JavaScript off the
     nav is simply always open rather than permanently unreachable. */
  .dash__sidebar[hidden] { display: none; }
}

.dash__content { min-width: 0; }

/* --- Page head ----------------------------------------------------------- */
.dash__page-head {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: var(--space-4);
  flex-wrap: wrap;
  margin-bottom: var(--space-6);
}
.dash__page-title {
  font-family: var(--font-display);
  font-size: var(--text-2xl);
  margin: 0 0 var(--space-1);
}
.dash__page-sub {
  margin: 0;
  color: var(--color-muted);
  font-size: var(--text-sm);
}
.dash__actions {
  display: flex;
  gap: var(--space-2);
  flex-wrap: wrap;
}

/* ===========================================================================
   10. Quick actions
   ======================================================================== */
.quick-actions {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(210px, 1fr));
  gap: var(--space-3);
}
.quick-action {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  padding: var(--space-4);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-md);
  background: var(--color-surface);
  color: var(--color-ink);
  transition: border-color var(--transition-fast), transform var(--transition-fast),
    box-shadow var(--transition-fast);
}
.quick-action:hover {
  border-color: var(--color-accent);
  transform: translateY(-2px);
  box-shadow: var(--shadow-md);
  color: var(--color-ink);
}
@media (prefers-reduced-motion: reduce) {
  .quick-action { transition: none; }
  .quick-action:hover { transform: none; }
}
.quick-action__icon {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 34px;
  height: 34px;
  flex: none;
  border-radius: var(--radius-md);
  background: var(--tint-primary);
  color: var(--color-primary);
}
:root[data-theme="dark"] .quick-action__icon { color: var(--viz-series); }
@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) .quick-action__icon { color: var(--viz-series); }
}
.quick-action__icon .icon { width: 18px; height: 18px; }
.quick-action__label { font-size: var(--text-sm); font-weight: 600; }
.quick-action__hint { display: block; font-size: var(--text-xs); color: var(--color-muted); font-weight: 500; }

/* ===========================================================================
   11. Empty states
   ======================================================================== */
.empty-state {
  text-align: center;
  padding: var(--space-8) var(--space-5);
  color: var(--color-muted);
}
.empty-state__icon {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 48px;
  height: 48px;
  border-radius: 50%;
  background: var(--color-surface-alt);
  color: var(--color-muted);
  margin-bottom: var(--space-3);
}
.empty-state__icon .icon { width: 22px; height: 22px; }
.empty-state__title {
  font-family: var(--font-display);
  font-size: var(--text-base);
  font-weight: 600;
  color: var(--color-ink);
  margin: 0 0 var(--space-1);
}
.empty-state p { margin: 0; font-size: var(--text-sm); }

/* A reference number is the row's identity and its link. Monospace,
   because it is a code someone reads character by character off a
   printed form or an email. */
.data-table__ref {
  font-family: var(--font-mono);
  font-size: var(--text-xs);
  font-weight: 600;
  white-space: nowrap;
}

/* ===========================================================================
   12. Filter panel + table cell stacking
   ======================================================================== */
.filter-panel__grid {
  display: grid;
  gap: var(--space-4);
  grid-template-columns: minmax(0, 1fr);
}
@media (min-width: 640px) {
  .filter-panel__grid { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}
@media (min-width: 1100px) {
  .filter-panel__grid { grid-template-columns: repeat(3, minmax(0, 1fr)); }
}
.filter-panel .form-group { margin-bottom: 0; }
.filter-panel__actions {
  display: flex;
  gap: var(--space-2);
  flex-wrap: wrap;
  margin-top: var(--space-4);
  padding-top: var(--space-4);
  border-top: 1px solid var(--color-border);
}

/* Two or three related values in one cell instead of two or three
   columns. This is what keeps the queue at five columns — see the note
   in review_list.html. */
.cell-stack {
  display: flex;
  flex-direction: column;
  gap: 2px;
  min-width: 0;
}
.cell-stack__main { font-weight: 600; color: var(--color-ink); }
.cell-stack__sub { font-size: var(--text-xs); color: var(--color-muted); }

/* Available to assistive tech, invisible on screen. Used for the header
   of an actions column, which has no visible caption but still needs a
   name for anyone navigating the table by column. */
.sr-only {
  /* In-flow (1px inline-block), NOT the usual absolutely-positioned
     recipe. An absolutely-positioned .sr-only has no positioned
     ancestor here, so its containing block is the viewport: it sat at
     its static position INSIDE the horizontally-scrolled table and
     stretched documentElement.scrollWidth to 642px on a 420px screen —
     a phantom sideways scroll on every phone, from a span nobody can
     see. Being in-flow, this version is clipped by the same scroll
     container as the table it sits in. */
  display: inline-block;
  width: 1px;
  height: 1px;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
  border: 0;
  padding: 0;
}

/* ===========================================================================
   13. Review detail — narrow-column overrides
   ======================================================================== */
/* The Day 20 .decision-panel splits into two columns at 768px. In the
   rebuilt detail page it lives in the narrow right-hand column, where
   two columns would squeeze both forms to nothing — so inside that
   panel it stays stacked at every width. Higher specificity than the
   original rule, so it wins regardless of media-query order. */
.panel--decision .decision-panel {
  grid-template-columns: minmax(0, 1fr);
  gap: var(--space-5);
}
.panel--decision .decision-panel > form:last-child {
  border-left: 0;
  padding-left: 0;
  border-top: 1px solid var(--color-border);
  padding-top: var(--space-5);
}
.panel--decision .btn .icon { width: 16px; height: 16px; }

.panel .review-photo {
  display: block;
  margin-inline: auto;
}

/* ===========================================================================
   14. Academics screens (Day 22)
   ======================================================================== */

/* --- Section sub-navigation ---------------------------------------------
   The sidebar answers "which module am I in"; this answers "which screen
   within it". Day 13's CMS sub-nav did the same job with three .btn
   anchors and inline styles; this is the same idea as a real component,
   and the CMS screens can move onto it whenever they are next touched. */
.subnav {
  display: flex;
  gap: var(--space-1);
  flex-wrap: wrap;
  padding: var(--space-1);
  background: var(--color-surface-alt);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-md);
}
.subnav__link {
  display: inline-flex;
  align-items: center;
  gap: var(--space-2);
  padding: var(--space-2) var(--space-4);
  border-radius: var(--radius-sm);
  font-size: var(--text-sm);
  font-weight: 500;
  color: var(--color-muted);
  white-space: nowrap;
}
.subnav__link .icon { width: 16px; height: 16px; }
.subnav__link:hover { color: var(--color-ink); background: var(--color-surface); }
.subnav__link.is-active {
  background: var(--color-surface);
  color: var(--color-ink);
  font-weight: 600;
  box-shadow: var(--shadow-sm);
}

/* --- Forms: grouping and two-up rows ------------------------------------- */
.fieldset {
  border: 1px solid var(--color-border);
  border-radius: var(--radius-md);
  padding: var(--space-5);
  margin: 0 0 var(--space-5);
}
.fieldset__legend {
  padding: 0 var(--space-2);
  font-size: var(--text-xs);
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.06em;
  color: var(--color-muted);
}
.form-row {
  display: grid;
  gap: var(--space-4);
  grid-template-columns: minmax(0, 1fr);
}
@media (min-width: 640px) {
  .form-row { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}
.form-actions {
  display: flex;
  gap: var(--space-3);
  flex-wrap: wrap;
  margin-top: var(--space-5);
  padding-top: var(--space-5);
  border-top: 1px solid var(--color-border);
}

/* A checkbox whose label carries real consequence — "Free of charge",
   "Open for applications". The hint sits under the label rather than in a
   tooltip, because both of these change what the public site says. */
.check {
  display: grid;
  grid-template-columns: auto minmax(0, 1fr);
  gap: var(--space-3);
  align-items: start;
  padding: var(--space-3) 0;
  cursor: pointer;
}
.check input[type="checkbox"] {
  width: 18px;
  height: 18px;
  margin: 2px 0 0;
  accent-color: var(--color-accent);
  cursor: pointer;
}
.check strong { display: block; font-size: var(--text-sm); }
.check__hint {
  display: block;
  font-size: var(--text-xs);
  color: var(--color-muted);
  margin-top: 2px;
}

/* "not set" / "not priced" — a missing value that BLOCKS something, said
   in words as well as colour. Not a status pill: these sit inside table
   cells where a pill would fight the row's real status pill. */
.text-warn {
  color: var(--viz-status-pending);
  font-weight: 600;
  font-size: var(--text-xs);
}

/* --- Curriculum builder --------------------------------------------------- */
.outline {
  list-style: none;
  margin: 0 0 var(--space-5);
  padding: 0;
  counter-reset: none;
}
.outline__item {
  display: grid;
  grid-template-columns: 28px minmax(0, 1fr) auto;
  gap: var(--space-3);
  align-items: start;
  padding: var(--space-3) 0;
  border-bottom: 1px solid var(--color-border);
}
.outline__item:last-child { border-bottom: 0; }
@media (max-width: 560px) {
  /* The action buttons need a full row of their own below ~560px, or the
     module title is squeezed to two characters. */
  .outline__item {
    grid-template-columns: 28px minmax(0, 1fr);
    grid-template-areas: "num body" ". actions";
    row-gap: var(--space-2);
  }
  .outline__number { grid-area: num; }
  .outline__body { grid-area: body; }
  .outline__actions { grid-area: actions; justify-content: flex-start; }
}
.outline__number {
  font-family: var(--font-mono);
  font-size: var(--text-xs);
  font-weight: 600;
  color: var(--color-muted);
  background: var(--color-surface-alt);
  border-radius: var(--radius-sm);
  text-align: center;
  padding: 3px 0;
}
.outline__body { min-width: 0; }
.outline__title { display: block; font-size: var(--text-sm); font-weight: 600; }
.outline__desc {
  display: block;
  font-size: var(--text-xs);
  color: var(--color-muted);
  margin-top: 2px;
}
.outline__actions {
  display: flex;
  gap: var(--space-1);
  align-items: center;
  flex-wrap: wrap;
}
/* Each action is its own <form> (they are POSTs); the forms must not add
   layout of their own. */
.outline__actions form { display: inline-flex; margin: 0; }

.icon-btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-width: 28px;
  height: 28px;
  padding: 0 var(--space-2);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-sm);
  background: var(--color-surface);
  color: var(--color-muted);
  font: inherit;
  font-size: var(--text-xs);
  font-weight: 600;
  cursor: pointer;
  transition: border-color var(--transition-fast), color var(--transition-fast);
}
.icon-btn:hover { border-color: var(--color-accent); color: var(--color-accent); }
.icon-btn:disabled {
  opacity: 0.4;
  cursor: default;
  border-color: var(--color-border);
  color: var(--color-muted);
}
.icon-btn--danger:hover { border-color: var(--viz-status-rejected); color: var(--viz-status-rejected); }
@media (prefers-reduced-motion: reduce) { .icon-btn { transition: none; } }

.outline__add {
  padding-top: var(--space-5);
  border-top: 1px solid var(--color-border);
}

/* --- Sessions ------------------------------------------------------------- */
.session-list { list-style: none; margin: 0; padding: 0; }
.session {
  padding: var(--space-4) 0;
  border-bottom: 1px solid var(--color-border);
}
.session:first-child { padding-top: 0; }
.session:last-child { border-bottom: 0; padding-bottom: 0; }
.session__head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-3);
  margin-bottom: var(--space-3);
  flex-wrap: wrap;
}
.session__name {
  font-family: var(--font-mono);
  font-size: var(--text-sm);
  font-weight: 600;
  color: var(--color-ink);
}
.session__dates {
  display: grid;
  gap: var(--space-2) var(--space-4);
  grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
  margin: 0 0 var(--space-3);
}
.session__dates dt {
  font-size: var(--text-xs);
  text-transform: uppercase;
  letter-spacing: 0.05em;
  color: var(--color-muted);
  margin-bottom: 2px;
}
.session__dates dd { margin: 0; font-size: var(--text-sm); }
.session__actions { display: flex; gap: var(--space-2); flex-wrap: wrap; }
.session__actions form { margin: 0; }

/* Two independent facts about one row — "Open" and "Needs review" are
   not alternatives, and a program that is both is the case that matters
   most on the catalogue screen. */
.pill-stack {
  display: inline-flex;
  flex-direction: column;
  align-items: flex-start;
  gap: var(--space-1);
}

/* ===========================================================================
   15. Student portal (Day 23)
   ======================================================================== */

/* "What's next" — the one thing a student opens this page for. A date
   alone is inert; the countdown beside it is what turns it into
   something they act on. */
.deadline-list { list-style: none; margin: 0; padding: 0; }
.deadline {
  display: grid;
  grid-template-columns: 34px minmax(0, 1fr) auto;
  align-items: center;
  gap: var(--space-3);
  padding: var(--space-3) 0;
  border-bottom: 1px solid var(--color-border);
}
.deadline:first-child { padding-top: 0; }
.deadline:last-child { border-bottom: 0; padding-bottom: 0; }
.deadline__icon {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 34px;
  height: 34px;
  border-radius: var(--radius-md);
  background: var(--tint-primary);
  color: var(--color-primary);
}
:root[data-theme="dark"] .deadline__icon { color: var(--viz-series); }
@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) .deadline__icon { color: var(--viz-series); }
}
.deadline__body { min-width: 0; }
.deadline__label { display: block; font-size: var(--text-sm); font-weight: 600; }
.deadline__meta { display: block; font-size: var(--text-xs); color: var(--color-muted); }
.deadline__countdown {
  font-family: var(--font-mono);
  font-size: var(--text-xs);
  font-weight: 600;
  color: var(--color-muted);
  white-space: nowrap;
}
/* Inside two weeks. The accent is a scanning aid; the number of days is
   still written out beside it, so the urgency is never colour-only. */
.deadline.is-soon .deadline__icon {
  background: var(--tint-accent);
  color: var(--color-accent);
}
.deadline.is-soon .deadline__countdown { color: var(--color-accent); }
:root[data-theme="dark"] .deadline.is-soon .deadline__icon { color: var(--viz-status-pending); }
@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) .deadline.is-soon .deadline__icon { color: var(--viz-status-pending); }
}

/* A row that is on the record but no longer active — a withdrawn
   enrolment. Dimmed, never removed: a register that forgets everyone who
   left cannot answer the question a register exists to answer. The
   status pill still carries the word, so this is a scanning aid rather
   than the encoding. */
.data-table tbody tr.is-dimmed { opacity: 0.55; }
.data-table tbody tr.is-dimmed:hover { opacity: 1; }

/* A CheckboxSelectMultiple rendered as a readable grid rather than a
   stacked column of bare inputs — used for an Admin's roles. Django emits
   a <ul> of <li><label><input> …, so the selectors reach through that. */
.checkbox-grid ul {
  list-style: none;
  margin: 0;
  padding: 0;
  display: grid;
  gap: var(--space-2);
  grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
}
.checkbox-grid label {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  padding: var(--space-2) var(--space-3);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-md);
  font-size: var(--text-sm);
  cursor: pointer;
}
.checkbox-grid label:hover { border-color: var(--color-accent); }
/* Radios too, since Day 40: a RadioSelect renders the same
   <ul><li><label> structure, and the final-project decision uses this
   component rather than growing a second one beside it. */
.checkbox-grid input[type="checkbox"],
.checkbox-grid input[type="radio"] {
  width: 16px;
  height: 16px;
  accent-color: var(--color-accent);
  cursor: pointer;
}

/* ===========================================================================
   16. Teaching assignments (Day 24)
   ======================================================================== */
.teaching-list { list-style: none; margin: 0; padding: 0; }
.teaching {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-3);
  padding: var(--space-3) 0;
  border-bottom: 1px solid var(--color-border);
}
.teaching:first-child { padding-top: 0; }
.teaching:last-child { border-bottom: 0; padding-bottom: 0; }
.teaching form { margin: 0; }
.teaching__body { min-width: 0; }
.teaching__name {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  flex-wrap: wrap;
  font-size: var(--text-sm);
  font-weight: 600;
  color: var(--color-ink);
}
.teaching__scope {
  display: block;
  font-size: var(--text-xs);
  color: var(--color-muted);
  margin-top: 2px;
}

/* --- An instructor's class, as a card ------------------------------------
   A card rather than a table row: a class is a thing you open, not a
   record you scan a column of, and an instructor has a handful of them
   rather than a page of them. */
.class-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
  gap: var(--space-4);
}
.class-card {
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
  padding: var(--space-5);
  background: var(--color-surface);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-lg);
  box-shadow: var(--shadow-sm);
  color: var(--color-ink);
  transition: border-color var(--transition-fast), transform var(--transition-fast),
    box-shadow var(--transition-fast);
}
.class-card:hover {
  border-color: var(--color-accent);
  transform: translateY(-2px);
  box-shadow: var(--shadow-md);
  color: var(--color-ink);
}
@media (prefers-reduced-motion: reduce) {
  .class-card { transition: none; }
  .class-card:hover { transform: none; }
}
.class-card__head {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: var(--space-2);
}
.class-card__name {
  font-family: var(--font-display);
  font-size: var(--text-base);
  font-weight: 600;
}
.class-card__session { font-size: var(--text-xs); color: var(--color-muted); }
.class-card__stats {
  display: flex;
  gap: var(--space-5);
  margin-top: var(--space-2);
  padding-top: var(--space-3);
  border-top: 1px solid var(--color-border);
}
.class-card__stat { display: block; }
.class-card__value {
  display: block;
  font-family: var(--font-mono);
  font-size: var(--text-lg);
  font-weight: 600;
  color: var(--color-primary);
}
:root[data-theme="dark"] .class-card__value { color: var(--color-on-dark); }
@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) .class-card__value { color: var(--color-on-dark); }
}
.class-card__label {
  display: block;
  font-size: var(--text-xs);
  color: var(--color-muted);
}
/* A mark deadline inside two weeks. The word "marks due" stays beside it,
   so this is a scanning aid and never the encoding. */
.class-card__stat.is-soon .class-card__value { color: var(--color-accent); }
:root[data-theme="dark"] .class-card__stat.is-soon .class-card__value { color: var(--viz-status-pending); }
@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) .class-card__stat.is-soon .class-card__value { color: var(--viz-status-pending); }
}
.class-card__scope {
  font-size: var(--text-xs);
  color: var(--color-muted);
  margin-top: var(--space-1);
}

/* ===========================================================================
   17. Marks (Day 25)
   ======================================================================== */

/* Module columns are narrow and their headers are long. The order number
   sits above a truncated title with the full text in a `title`
   attribute, so the grid stays scannable without a header row three
   lines deep. */
.marks-table__module { vertical-align: bottom; }
.marks-table__module-order {
  display: block;
  font-family: var(--font-mono);
  font-size: 0.65rem;
  color: var(--color-muted);
}
.marks-table__module-title {
  display: block;
  max-width: 7rem;
  margin-left: auto;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  /* Not uppercased, unlike every other table header. A module title is a
     phrase that has already been truncated to fit the column — putting
     it in caps with letter-spacing costs several more characters and
     makes the half that survives harder to read, not easier. */
  text-transform: none;
  letter-spacing: 0;
  font-size: var(--text-xs);
}

/* Names and matricule numbers stay on one line. The student column was
   breaking BISOFTECH-2026-SD-0007 across three lines, which made every
   row three times as tall as its content and pushed the marks apart. The
   table scrolls inside .table-scroll if that costs width; the page never
   does. */
.marks-table tbody th { white-space: nowrap; }
.marks-table .cell-stack__sub {
  font-family: var(--font-mono);
  white-space: nowrap;
}

.mark-input {
  /* Narrow enough that a four-module grid keeps its Result column on
     screen at 1440px. Three characters and a decimal is the widest a
     mark out of 100 gets. */
  width: 4.25rem;
  text-align: right;
  font-family: var(--font-mono);
  font-variant-numeric: tabular-nums;
  padding: var(--space-2);
}
/* Chrome's number spinners are 20px of chrome on a 5.5rem box, and they
   invite mis-clicks that change a mark by one. Typed entry only. */
.mark-input::-webkit-outer-spin-button,
.mark-input::-webkit-inner-spin-button { -webkit-appearance: none; margin: 0; }
.mark-input { -moz-appearance: textfield; appearance: textfield; }

/* A module this instructor does not teach: visible for context, plainly
   not editable. */
.mark-readonly {
  display: inline-block;
  min-width: 4.25rem;
  padding: var(--space-2);
  font-family: var(--font-mono);
  font-variant-numeric: tabular-nums;
  color: var(--color-muted);
}

/* --- The overall result badge -------------------------------------------
   Three states, and the middle one is the reason this exists. An average
   over three of four modules is real and useful, but it is NOT a result,
   and it must never be able to pass for one. */
.result {
  display: inline-flex;
  align-items: baseline;
  gap: var(--space-2);
  padding: 3px var(--space-3);
  border-radius: 999px;
  font-family: var(--font-mono);
  font-size: var(--text-xs);
  font-weight: 600;
  white-space: nowrap;
  background: var(--tint-neutral);
  color: var(--color-muted);
}
.result__grade { font-weight: 700; }
.result__note { font-weight: 500; opacity: 0.75; }
.result--none { font-style: italic; font-family: var(--font-body); font-weight: 500; }
/* Provisional. Deliberately NOT green or red — a partial average has not
   earned either, and colouring it would be the exact misreading the
   fraction beside it exists to prevent. */
.result--partial { color: var(--color-muted); }
.result--pass { color: var(--viz-status-approved); }
.result--fail { color: var(--viz-status-rejected); }

/* "not marked yet" in a results table — a statement of absence, and it
   should not look like a value. */
.text-muted-italic {
  color: var(--color-muted);
  font-style: italic;
  font-family: var(--font-body);
  font-size: var(--text-xs);
}

/* --- Meter ----------------------------------------------------------------
   A single ratio against a limit — marking progress against a full grid.
   The unfilled track is a lighter step of the same hue rather than a
   grey, so the state reads across the whole bar. The numbers are written
   out beneath it: the bar is the glance, the sentence is the fact. */
.meter__track {
  height: 10px;
  border-radius: var(--radius-sm);
  background: var(--viz-series-wash);
  overflow: hidden;
}
.meter__fill {
  display: block;
  height: 100%;
  min-width: 2px;
  background: var(--viz-series);
  border-radius: 0 var(--radius-sm) var(--radius-sm) 0;
}
.meter__fill.is-complete { background: var(--viz-status-approved); }
.meter__caption {
  margin: var(--space-3) 0 0;
  font-size: var(--text-sm);
  color: var(--color-muted);
}
.meter__caption strong { color: var(--color-ink); font-family: var(--font-mono); }

/* The grid is dense by nature — a tighter cell keeps more of it on
   screen without making anything harder to hit. */
.marks-table th,
.marks-table td { padding-inline: var(--space-2); }

/* ===========================================================================
   18. Announcements (Day 26)
   ======================================================================== */
.notice-list { list-style: none; margin: 0; padding: 0; display: grid; gap: var(--space-4); }
.notice {
  position: relative;
  padding: var(--space-5);
  background: var(--color-surface);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-lg);
  box-shadow: var(--shadow-sm);
}
/* Unread gets the accent rail — the same device the stat cards use, so
   "there is something new here" reads the same way across the platform.
   The word "New" sits beside it, so this is a scanning aid and never the
   only signal. */
.notice.is-unread { border-color: var(--color-accent); }
.notice.is-unread::before {
  content: "";
  position: absolute;
  top: var(--space-5);
  bottom: var(--space-5);
  left: -1px;
  width: 3px;
  border-radius: 0 2px 2px 0;
  background: var(--color-accent);
}
.notice.is-pinned { background: var(--tint-primary); }
.notice__head {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--space-3);
  flex-wrap: wrap;
}
.notice__title {
  font-family: var(--font-display);
  font-size: var(--text-lg);
  font-weight: 600;
  color: var(--color-ink);
}
.notice__meta {
  margin: var(--space-1) 0 var(--space-3);
  font-size: var(--text-xs);
  color: var(--color-muted);
}
.notice__body {
  margin: 0 0 var(--space-4);
  font-size: var(--text-sm);
  line-height: var(--leading-normal);
}
.notice form { margin: 0; }

/* ---------------------------------------------------------------------------
   19. Data tables on a narrow screen — Day 26
   ---------------------------------------------------------------------------
   `.table-scroll` keeps a wide table inside its own horizontal scroller,
   which is what stops the PAGE from scrolling sideways. It does not stop
   a column from being off-screen, and on the announcements list that put
   Status and the Publish button past the right edge at 420px — an author
   on a phone could neither see whether a notice was live nor publish it,
   with nothing on screen suggesting the table scrolled at all.

   So the low-priority columns are dropped below 48rem instead. `Reach`
   stays: the number of people a notice goes to is the fact this screen
   exists to make visible. `Audience` is not dropped so much as moved —
   it reappears under the title via `.is-narrow-only`, because WHO a
   notice is for must never require a sideways scroll to check.

   A media query rather than a container query here: the dashboard
   sidebar stacks above the content on mobile, so the content column
   tracks the viewport closely, and `container-type` on `.panel` would
   make every panel a containing block for absolutely-positioned
   descendants — the Day 21 phantom-scroll bug came from exactly that
   kind of unintended containing block.
--------------------------------------------------------------------------- */

.cell-stack__sub.is-narrow-only { display: none; }

/* Tier 1 — below 48rem, drop the low-priority columns. Measured: this
   takes the announcements table from 822px to 602px, which fits a
   tablet without a sideways scroll. */
@media (max-width: 48rem) {
  .data-table th.is-optional,
  .data-table td.is-optional { display: none; }

  .cell-stack__sub.is-narrow-only { display: block; }
}

/* Tier 2 — below 34rem, STOP BEING A TABLE.
   Three attempts at squeezing the remaining columns into 420px each
   ended the same way: the last column clipped, and `scrollWidth` still
   reporting the table as fitting, so the browser was not even admitting
   the overflow. A table whose columns cannot fit is not a narrow table,
   it is the wrong shape — each announcement becomes a block, which is
   what one row of this data actually is.

   34rem, not 48rem: between the two the columns genuinely do fit, and
   stacking there wastes most of the width. */
@media (max-width: 34rem) {
  .data-table--stack-actions,
  .data-table--stack-actions tbody,
  .data-table--stack-actions tr,
  .data-table--stack-actions th,
  .data-table--stack-actions td { display: block; width: auto; }

  .data-table--stack-actions thead { display: none; }

  .data-table--stack-actions tbody tr {
    padding: var(--space-4) 0;
    border-bottom: 1px solid var(--color-border);
  }
  .data-table--stack-actions tbody tr:last-child { border-bottom: none; }

  .data-table--stack-actions th,
  .data-table--stack-actions td {
    border: none;
    padding: 0 0 var(--space-2);
    text-align: left;
  }

  /* The header row is gone, so each value carries its own label. */
  .data-table--stack-actions td[data-label]::before {
    content: attr(data-label) ": ";
    color: var(--color-muted);
    font-size: var(--text-xs);
    letter-spacing: 0.04em;
    text-transform: uppercase;
  }

  /* `.data-table .is-numeric` (two classes) outranks the block rule
     above (one class + element), so a stacked "Reach: 6" kept sitting
     right-aligned on its own line away from its own label. */
  .data-table--stack-actions td.is-numeric {
    text-align: left;
    font-variant-numeric: tabular-nums;
  }

  .data-table--stack-actions .row-actions {
    display: flex;
    flex-wrap: wrap;
    gap: var(--space-2);
    padding-top: var(--space-2);
  }
  .data-table--stack-actions .row-actions .btn { white-space: nowrap; }
}

}

/* Row-level actions, wide screens: a nowrap inline row. The narrow-screen
   stacking above overrides this. */
.row-actions {
  display: inline-flex;
  align-items: center;
  gap: var(--space-2);
  white-space: nowrap;
}
.row-actions form { display: inline; margin: 0; }

/* ---------------------------------------------------------------------------
   20. Messaging — Day 27
   ---------------------------------------------------------------------------
   Two screens: a list of conversations, and one conversation.

   The thread deliberately does NOT use the chat-bubble idiom (coloured
   pills alternating left and right). Two reasons. A bubble carries its
   meaning in colour and alignment alone, which is nothing to a screen
   reader and very little to a deuteranope; and this is correspondence
   between a student and their institute — closer to a letter than to a
   chat app. So both sides are full-width blocks, distinguished by a
   left rail, a background, and an explicit "You" in the byline. Remove
   every colour and the thread still reads correctly.
--------------------------------------------------------------------------- */

/* --- the inbox ----------------------------------------------------------- */

.thread-list { list-style: none; margin: 0; padding: 0; display: grid; gap: var(--space-3); }

.thread {
  background: var(--color-surface);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-lg);
  box-shadow: var(--shadow-sm);
}
/* The accent rail means "unread", the same device the stat cards and
   announcement notices use. The "N new" pill beside it carries the same
   fact in words, so this is a scanning aid and never the only signal. */
.thread.is-unread { border-color: var(--color-accent); }

/* `.block-link` (see base.css) is what stops the global anchor rule
   painting the whole row link-blue. Without it the person's name, the
   preview and the timestamp all render as one flat link colour. */
.thread__link {
  display: grid;
  gap: var(--space-2);
  padding: var(--space-4) var(--space-5);
  text-decoration: none;
  color: inherit;
  border-radius: inherit;
}
.thread__link:hover { background: var(--color-surface-alt); }
.thread__link:focus-visible { outline: 2px solid var(--color-accent); outline-offset: 2px; }

.thread__head {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-2) var(--space-3);
}
.thread__who { font-weight: 600; color: var(--color-ink); }
.thread__role {
  font-size: var(--text-xs);
  text-transform: uppercase;
  letter-spacing: 0.04em;
  color: var(--color-muted);
}
.thread__preview {
  color: var(--color-muted);
  font-size: var(--text-sm);
  line-height: var(--leading-normal);
}
.thread__you { font-weight: 600; color: var(--color-ink); }
.thread__time { font-size: var(--text-xs); color: var(--color-muted); }

/* --- one thread ---------------------------------------------------------- */

.msg-list {
  list-style: none;
  margin: 0 0 var(--space-5);
  padding: 0;
  display: grid;
  gap: var(--space-4);
}

.msg {
  padding: var(--space-4);
  border-left: 3px solid var(--color-border);
  border-radius: var(--radius-md);
  background: var(--color-surface-alt);
}
/* Yours: the accent rail and the plain surface. Theirs keeps the tinted
   background, so the two are told apart by rail colour, background and
   the byline — three signals, not one. */
.msg--mine {
  border-left-color: var(--color-accent);
  background: var(--color-surface);
  border-top: 1px solid var(--color-border);
  border-right: 1px solid var(--color-border);
  border-bottom: 1px solid var(--color-border);
}

.msg__body {
  margin: 0 0 var(--space-2);
  color: var(--color-ink);
  line-height: var(--leading-normal);
  /* A pasted URL or a long identifier must not push the panel wider
     than the page — the failure mode the Day 26 table taught. */
  overflow-wrap: anywhere;
}
.msg__meta {
  margin: 0;
  font-size: var(--text-xs);
  color: var(--color-muted);
}

.msg-compose { display: grid; gap: var(--space-3); margin: 0; }
.msg-compose textarea { width: 100%; }
.msg-compose__actions { display: flex; justify-content: flex-end; }

/* ---------------------------------------------------------------------------
   21. Online classes — Day 29
   ---------------------------------------------------------------------------
   A list of sessions with a state each. The state is carried by a word
   ("Upcoming", "In progress", "Cancelled", "Finished") on every card —
   the left rail is a scanning aid, never the only signal, and a
   cancelled class read with no colour at all still says it is cancelled.
--------------------------------------------------------------------------- */

.klass-list { list-style: none; margin: 0; padding: 0; display: grid; gap: var(--space-4); }

.klass {
  padding: var(--space-4);
  border: 1px solid var(--color-border);
  border-left: 3px solid var(--color-border);
  border-radius: var(--radius-md);
  background: var(--color-surface);
}
.klass--upcoming { border-left-color: var(--color-accent); }
.klass--live {
  border-left-color: var(--viz-status-approved, var(--color-accent));
  background: var(--color-surface-alt);
}
.klass--cancelled { opacity: 0.72; }
.klass--cancelled:hover { opacity: 1; }

.klass__head {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-2) var(--space-3);
  margin-bottom: var(--space-2);
}
.klass__title { font-weight: 600; color: var(--color-ink); }

.klass__when {
  margin: 0 0 var(--space-2);
  color: var(--color-ink);
  font-size: var(--text-sm);
}
.klass__meta { color: var(--color-muted); }

.klass__desc {
  margin: 0 0 var(--space-3);
  color: var(--color-muted);
  font-size: var(--text-sm);
  line-height: var(--leading-normal);
  overflow-wrap: anywhere;
}

.klass__cancelled {
  margin: 0 0 var(--space-3);
  padding: var(--space-2) var(--space-3);
  border-radius: var(--radius-md);
  background: var(--color-surface-alt);
  color: var(--color-ink);
  font-size: var(--text-sm);
}

.klass__actions {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-2) var(--space-3);
}
/* The sentence that stands in for a button when there is nothing to
   click. It is deliberately text, not a disabled button: a disabled
   button says "you may not", and the truth is "there is nothing yet". */
.klass__note { color: var(--color-muted); font-size: var(--text-sm); font-style: italic; }
.klass__manage { display: inline-flex; flex-wrap: wrap; gap: var(--space-2); margin-left: auto; }
.klass__manage form { display: inline; margin: 0; }

@media (max-width: 34rem) {
  /* `margin-left: auto` pushes the manage controls to the far edge on a
     wide card; on a phone that leaves them stranded on their own line
     with a gap above. */
  .klass__manage { margin-left: 0; width: 100%; }
}

/* ---------------------------------------------------------------------------
   22. Finance — Day 31
   ---------------------------------------------------------------------------
   Almost everything reuses the panel, table, pill and stat-card
   components. Two additions only.
--------------------------------------------------------------------------- */

/* A results-withheld panel. It carries the accent rail rather than a
   red one: this is a consequence of an unpaid balance, not a fault
   found with the student, and the difference in tone matters on the
   page a student sees when their marks are held. The words do the
   work — see templates/partials/result_withheld.html. */
.panel--held { border-left: 3px solid var(--color-accent); }

.held-facts {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-3) var(--space-6);
  margin: var(--space-4) 0;
}
.held-facts div { display: grid; gap: var(--space-1); }
.held-facts dt {
  font-size: var(--text-xs);
  text-transform: uppercase;
  letter-spacing: 0.04em;
  color: var(--color-muted);
}
.held-facts dd {
  margin: 0;
  font-family: var(--font-mono, monospace);
  font-size: var(--text-lg);
  font-weight: 600;
  color: var(--color-ink);
}

/* --- Sessions: the code, and the programme picker (Day 38) --------------- */

/* The one value on the screen that gets read aloud and copied onto an
   exam paper. Monospace and spaced out so O/0 and I/1 confusion is at
   least visible — the generated codes exclude those characters, but a
   code BISOFTECH types by hand may not. */
.session-code {
  display: inline-block;
  padding: 0.15rem 0.5rem;
  border-radius: var(--radius-sm);
  background: var(--tint-primary);
  color: var(--color-primary);
  font-family: var(--font-mono);
  font-weight: 600;
  letter-spacing: 0.12em;
}
:root[data-theme="dark"] .session-code { color: var(--color-on-dark); }

.program-picker { margin-top: var(--space-5); }
.program-picker__label { display: block; font-size: var(--text-sm); font-weight: 600; }
.program-picker__grid > ul,
.program-picker__grid > div {
  list-style: none;
  margin: var(--space-3) 0 0;
  padding: 0;
  display: grid;
  gap: var(--space-2);
  grid-template-columns: minmax(0, 1fr);
}
@media (min-width: 40rem) {
  .program-picker__grid > ul,
  .program-picker__grid > div { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}
.program-picker__grid li,
.program-picker__grid > div > div { margin: 0; }
.program-picker__grid label {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  padding: var(--space-3) var(--space-4);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-md);
  font-size: var(--text-sm);
  font-weight: 500;
  cursor: pointer;
  transition: border-color var(--transition-fast), background-color var(--transition-fast);
}
.program-picker__grid label:hover {
  border-color: var(--color-accent);
  background: var(--color-surface-alt);
}
.program-picker__grid input { flex: none; }

/* ===========================================================================
   22. The result slip (Day 40, recoloured 17 Sep 2026)
   ========================================================================
   A page whose real output is a sheet of paper, and BISOFTECH asked for
   it to be "maturely colourful". The first version was black text on
   white with one navy rule — correct and joyless, and it did not look
   like a document an institute issues.

   WHAT THE COLOUR IS DOING, because decoration on a statement of
   results is the thing to avoid:

     * the LETTERHEAD is brand navy, so the sheet is recognisably
       BISOFTECH's from across a desk;
     * each assessment part has its OWN tone, and the weighting bar under
       them is those same tones at their real proportions — so "50 / 25 /
       25" is a thing you can see rather than a sentence you have to
       parse;
     * the OUTCOME panel is keyed to pass or fail.

   Every one of those still ships its word: the parts are labelled, the
   bar segments are labelled, the verdict says "Passed" or "Not passed".
   Approved-green and rejected-red are the textbook red/green collapse
   under deuteranopia (ΔE 4.6 in OKLab, measured Day 21) and this sheet
   gets photocopied in greyscale, so hue is never the only carrier.

   Colours come from tokens that tests/test_contrast.py already measures.
   Nothing here introduces a new pair. */

.slip { padding: 0; overflow: hidden; }

/* --- Letterhead ---------------------------------------------------------- */
.slip__head {
  display: flex;
  align-items: center;
  gap: var(--space-4);
  padding: var(--space-5) var(--space-5) var(--space-4);
  background: var(--color-primary);
  color: var(--color-on-dark);
  /* The orange hairline is the brochure's own pairing. 3px, because at
     1px it disappears on a laser printer. */
  border-bottom: 3px solid var(--color-accent);
}
.slip__logo { height: auto; flex: none; }
.slip__institute {
  margin: 0;
  font-weight: 700;
  font-size: var(--text-xl);
  color: var(--color-on-dark);
  line-height: 1.2;
}
.slip__doc {
  margin: var(--space-1) 0 0;
  font-family: var(--font-mono);
  font-size: var(--text-sm);
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--color-accent);
}
.slip__body { padding: var(--space-5); }

/* --- Who it is about ----------------------------------------------------- */
.slip__identity {
  background: var(--tint-primary);
  border-radius: var(--radius-md);
  padding: var(--space-4);
  margin-bottom: var(--space-5);
}
.slip__identity .held-facts { margin: 0; }
.slip__identity .held-facts + .held-facts { margin-top: var(--space-3); }

.slip__section-title {
  margin: var(--space-5) 0 var(--space-3);
  font-size: var(--text-lg);
  color: var(--color-primary);
  border-left: 4px solid var(--color-accent);
  padding-left: var(--space-3);
}

/* --- The three parts ----------------------------------------------------- */
.slip__parts {
  display: grid;
  gap: var(--space-3);
  grid-template-columns: 1fr;
}
@media (min-width: 640px) {
  .slip__parts { grid-template-columns: repeat(3, 1fr); }
}
.slip__part {
  border: 1px solid var(--color-border);
  border-top: 4px solid var(--part-colour, var(--viz-series));
  border-radius: var(--radius-md);
  padding: var(--space-4);
}
.slip__part--coursework { --part-colour: var(--viz-series); }
.slip__part--exam       { --part-colour: var(--viz-series-soft); }
.slip__part--project    { --part-colour: var(--color-accent); }
.slip__part-label {
  margin: 0;
  font-size: var(--text-sm);
  font-weight: 600;
}
.slip__part-weight {
  margin: 0;
  font-size: var(--text-sm);
  color: var(--color-muted);
}
.slip__part-score {
  margin: var(--space-2) 0 0;
  font-family: var(--font-mono);
  font-weight: 700;
  font-size: var(--text-2xl);
  color: var(--part-colour, var(--viz-series));
}

/* --- The weighting, at its real proportions ------------------------------ */
.slip__weighting {
  display: flex;
  margin-top: var(--space-3);
  border-radius: var(--radius-sm);
  overflow: hidden;
  height: 14px;
}
.slip__weighting span { display: block; height: 100%; }
.slip__weighting-caption {
  margin: var(--space-2) 0 0;
  font-size: var(--text-sm);
  color: var(--color-muted);
}

/* --- The outcome --------------------------------------------------------- */
.slip__outcome {
  margin-top: var(--space-5);
  padding: var(--space-5);
  border-radius: var(--radius-md);
  text-align: center;
  background: var(--tint-primary);
  border: 2px solid var(--outcome-colour, var(--color-primary));
}
.slip__outcome--pass { --outcome-colour: var(--viz-status-approved); }
.slip__outcome--fail { --outcome-colour: var(--viz-status-rejected); }
.slip__outcome-label {
  margin: 0;
  font-size: var(--text-sm);
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--color-muted);
}
.slip__outcome-value {
  margin: 0;
  font-family: var(--font-mono);
  font-weight: 700;
  font-size: 3.25rem;
  line-height: 1.05;
  color: var(--outcome-colour, var(--color-primary));
}
.slip__outcome-grade {
  margin: 0;
  font-weight: 700;
  font-size: var(--text-lg);
}
.slip__outcome-verdict {
  display: inline-block;
  margin: var(--space-3) 0 0;
  padding: var(--space-2) var(--space-4);
  border-radius: 999px;
  font-weight: 600;
  font-size: var(--text-sm);
  background: var(--color-surface);
  color: var(--outcome-colour, var(--color-primary));
  border: 1px solid var(--outcome-colour, var(--color-primary));
}
.slip__note {
  margin-top: var(--space-4);
  font-size: var(--text-sm);
  color: var(--color-muted);
}

/* Dark mode. The letterhead keeps the navy — it is the brand, and the
   sheet should look the same colour whoever is reading it — but the
   section titles and the big figure need the re-stepped tokens, which
   the `--part-colour`/`--outcome-colour` variables already pull in. */
:root[data-theme="dark"] .slip__section-title { color: var(--color-accent); }

@media print {
  /* Paper has no dashboard. Class names taken from the partials that
     render them (topbar.html, header.html, footer.html,
     floating_ctas.html) rather than guessed — the first version of this
     rule guessed `.site-topbar` and `.floating-ctas`, neither of which
     exists, and the navy contact strip printed across the top of the
     statement of results. Found by printing it. */
  .topbar,
  .site-header,
  .site-footer,
  .dash__sidebar,
  .dash__sidebar-toggle,
  .dash__actions,
  .fab-stack { display: none !important; }

  /* The colour IS the document. Without this every browser drops the
     navy letterhead and the tinted panels to save toner, and what comes
     out is the joyless version this redesign replaced. */
  .slip,
  .slip * {
    -webkit-print-color-adjust: exact;
    print-color-adjust: exact;
  }

  .dash { display: block; }
  .dash__content { padding: 0; }
  /* The dashboard page heading duplicates the slip's own letterhead. */
  .dash__page-head { display: none; }
  .panel.slip {
    border: none;
    box-shadow: none;
    /* A slip that breaks across two sheets mid-table is a slip somebody
       reprints. */
    break-inside: avoid;
  }
  .table-scroll { overflow: visible; }
}
