/* Datascope design system */
:root {
  /* ---- brand ----
     Four colors, and everything below is one of them or a tint/shade of one:
     crimson #C41230, black #000000, gray #6D6E71, light gray #E0E0E0.
     Re-branding the site is these six rows plus the tile banners in
     lessons/tilecolors.py.
     Crimson is the logo and the color of trouble (errors, wrong answers,
     destructive buttons) — not the interface. Buttons, links, hovers and
     selections take the neutral charcoal accent below, so the red that is
     left on a page means something. */
  --brand: #C41230;
  --brand-dark: #8e0c22;      /* pressed / hover crimson */
  --brand-deep: #5e0817;      /* destructive actions — same family, graver */
  --black: #000000;
  --gray: #6d6e71;
  --gray-light: #e0e0e0;

  --bg: #f4f4f5;
  --surface: #ffffff;
  --surface-2: #f7f7f8;
  --border: var(--gray-light);
  --text: var(--black);
  --text-muted: var(--gray);
  --accent: #2b2b2e;          /* charcoal: primary buttons, links, selection */
  --accent-strong: #000000;   /* pressed / hover */
  --accent-soft: #ececee;     /* hover wash, selected pill, focus ring */
  /* Verdicts are a signal, not branding: right/wrong/partial have to stay
     told apart at a glance, so green and amber survive the rebrand — muted so
     they stay quieter than an error. Wrong is the brand
     crimson itself; nothing else on a lesson page competes with it. */
  --green: #157a4d;
  --green-soft: #e6f2ec;
  --amber: #8a5a00;
  --amber-soft: #f7efdf;
  --red: var(--brand);
  --red-soft: #fbe9ec;
  --radius: 12px;
  --radius-sm: 8px;
  /* The lesson column. The editor's image stage is the same width, which is
     what makes a sized image in the editor the size students will see. */
  --lesson-width: 820px;
  --shadow: 0 1px 2px rgba(0, 0, 0, .06), 0 4px 16px rgba(0, 0, 0, .08);
  --shadow-lg: 0 8px 40px rgba(0, 0, 0, .20);
  --font: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
}

* { box-sizing: border-box; }
/* `hidden` must win over a display rule that names the element — the image
   editor toggles a flex placeholder and a block <img> that way. */
[hidden] { display: none !important; }
html, body { margin: 0; padding: 0; }
body {
  font-family: var(--font);
  background: var(--bg);
  color: var(--text);
  font-size: 15px;
  line-height: 1.55;
  min-height: 100vh;
}
a { color: var(--accent); text-decoration: none; }
a:hover { text-decoration: underline; }
h1, h2, h3, h4 { line-height: 1.25; margin: 0 0 .5em; }
h1 { font-size: 1.6rem; }
h2 { font-size: 1.25rem; }
h3 { font-size: 1.05rem; }

/* ---- layout ---- */
.topbar {
  background: var(--surface);
  border-bottom: 1px solid var(--border);
  display: flex; align-items: center; gap: 1rem;
  padding: 0 1.25rem; height: 56px;
  position: sticky; top: 0; z-index: 50;
}
.topbar .brand {
  font-weight: 800; font-size: 1.05rem; letter-spacing: .02em;
  color: var(--text); display: flex; align-items: center; gap: .5rem;
}
.topbar .brand:hover { text-decoration: none; }
.topbar .brand .logo {
  background: linear-gradient(135deg, var(--brand), var(--brand-deep));
  color: #fff; border-radius: 8px; width: 30px; height: 30px;
  display: inline-flex; align-items: center; justify-content: center;
  font-size: .85rem; font-weight: 800;
}
.topbar nav { display: flex; gap: .25rem; flex: 1; }
.topbar nav a {
  color: var(--text-muted); padding: .4rem .7rem; border-radius: var(--radius-sm);
  font-weight: 600; font-size: .9rem;
}
.topbar nav a:hover { background: var(--accent-soft); color: var(--accent); text-decoration: none; }
.topbar nav a.current { background: var(--accent-soft); color: var(--accent); }
.topbar .user { color: var(--text-muted); font-size: .85rem; display: flex; align-items: center; gap: .75rem; }
.topbar .avatar {
  background: var(--accent-soft); color: var(--accent); font-weight: 700;
  width: 32px; height: 32px; border-radius: 50%;
  display: inline-flex; align-items: center; justify-content: center; font-size: .8rem;
}

.page { max-width: 1200px; margin: 0 auto; padding: 1.5rem 1.25rem 4rem; }
.page-wide { max-width: 1500px; }
.page-header { display: flex; align-items: center; justify-content: space-between; gap: 1rem; flex-wrap: wrap; margin-bottom: 1.25rem; }
.page-header .sub { color: var(--text-muted); margin: .15rem 0 0; }

/* ---- cards / surfaces ---- */
.card {
  background: var(--surface); border: 1px solid var(--border);
  border-radius: var(--radius); box-shadow: var(--shadow);
  padding: 1.25rem;
}
.card + .card { margin-top: 1rem; }

/* ---- buttons / forms ---- */
.btn {
  display: inline-flex; align-items: center; gap: .4rem;
  background: var(--accent); color: #fff; border: none; cursor: pointer;
  padding: .5rem .95rem; border-radius: var(--radius-sm);
  font-size: .9rem; font-weight: 600; font-family: var(--font);
  transition: background .12s;
}
.btn:hover { background: var(--accent-strong); text-decoration: none; color: #fff; }
.btn.secondary { background: var(--surface); color: var(--text); border: 1px solid var(--border); }
.btn.secondary:hover { background: var(--surface-2); }
/* The primary button is charcoal, so crimson is free to mean "destructive". */
.btn.danger { background: var(--brand); }
.btn.danger:hover { background: var(--brand-dark); }
.btn.ghost { background: transparent; color: var(--text-muted); border: none; }
.btn.ghost:hover { background: var(--accent-soft); color: var(--accent); }
.btn.small { padding: .3rem .6rem; font-size: .8rem; }
.btn:disabled { opacity: .5; cursor: not-allowed; }

label { font-weight: 600; font-size: .85rem; display: block; margin-bottom: .3rem; }
input[type=text], input[type=email], input[type=password], input[type=number],
input[type=datetime-local], select, textarea {
  width: 100%; padding: .5rem .7rem; border: 1px solid var(--border);
  border-radius: var(--radius-sm); font-family: var(--font); font-size: .92rem;
  background: var(--surface); color: var(--text);
}
input:focus, select:focus, textarea:focus {
  outline: 2px solid var(--accent-soft); border-color: var(--accent);
}
.field { margin-bottom: 1rem; }
.field .help { color: var(--text-muted); font-size: .8rem; margin-top: .25rem; }
.checkbox-row { display: flex; align-items: center; gap: .5rem; margin-bottom: .6rem; }
.checkbox-row label { margin: 0; font-weight: 500; }
.checkbox-row input { width: auto; }
.form-row { display: flex; gap: 1rem; }
.form-row > * { flex: 1; }

/* ---- badges / chips ---- */
.badge {
  display: inline-flex; align-items: center; gap: .3rem;
  font-size: .72rem; font-weight: 700; letter-spacing: .02em;
  padding: .15rem .55rem; border-radius: 999px; text-transform: uppercase;
}
.badge.green { background: var(--green-soft); color: var(--green); }
.badge.amber { background: var(--amber-soft); color: var(--amber); }
.badge.red { background: var(--red-soft); color: var(--brand-dark); }
.badge.gray { background: var(--surface-2); color: var(--text-muted); border: 1px solid var(--border); }
.badge.accent { background: var(--accent-soft); color: var(--accent); }

.chips { display: flex; gap: .5rem; flex-wrap: wrap; }
.chip {
  background: var(--surface); border: 1px solid var(--border); color: var(--text-muted);
  padding: .3rem .8rem; border-radius: 999px; font-size: .82rem; font-weight: 600;
  cursor: pointer;
}
.chip:hover { border-color: var(--accent); color: var(--accent); text-decoration: none; }
.chip.selected { background: var(--accent); border-color: var(--accent); color: #fff; }
/* Edits the tab row rather than filtering by it — dashed, so it reads as a
   different kind of thing sitting in the same row. */
.chip.manage { border-style: dashed; display: inline-flex; align-items: center; gap: .3rem; }

/* ---- messages ---- */
.messages { position: fixed; top: 68px; right: 1rem; z-index: 200; display: flex; flex-direction: column; gap: .5rem; max-width: 380px; }
.message {
  background: var(--surface); border: 1px solid var(--border); border-left: 4px solid var(--accent);
  box-shadow: var(--shadow-lg); border-radius: var(--radius-sm);
  padding: .7rem 1rem; font-size: .88rem; animation: slidein .25s ease-out;
}
.message.success { border-left-color: var(--green); }
.message.error { border-left-color: var(--red); }
.message.warning { border-left-color: var(--amber); }
@keyframes slidein { from { transform: translateX(30px); opacity: 0; } to { transform: none; opacity: 1; } }

/* ---- course grid (home) ---- */
.course-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); gap: 1rem; }
.course-card {
  background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius);
  box-shadow: var(--shadow); padding: 1.25rem; display: flex; flex-direction: column; gap: .5rem;
  transition: transform .12s, box-shadow .12s; color: var(--text);
}
.course-card:hover { transform: translateY(-2px); box-shadow: var(--shadow-lg); text-decoration: none; }
.course-card .code { font-weight: 800; font-size: 1.05rem; }
.course-card .title { color: var(--text-muted); }
.course-card .meta { margin-top: auto; display: flex; gap: .5rem; align-items: center; }

/* ---- lesson tiles ---- */
.tile-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(250px, 1fr)); gap: 1rem; }
.tile {
  background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius);
  box-shadow: var(--shadow); overflow: hidden; display: flex; flex-direction: column;
  transition: transform .12s, box-shadow .12s, opacity .2s;
  position: relative;
}
.tile:hover { box-shadow: var(--shadow-lg); }
.tile.dragging { opacity: .4; }
.tile.drag-over { outline: 2px dashed var(--accent); outline-offset: 2px; }
.tile-banner {
  height: 84px; position: relative;
  display: flex; align-items: flex-end; padding: .75rem .9rem;
}
.tile-banner .tile-title { color: #fff; font-weight: 700; font-size: 1rem; text-shadow: 0 1px 3px rgba(0,0,0,.35); }
/* Set by `tile_ink_class` on the one kind of banner white cannot sit on: a
   custom hex color light enough to fail 4.5:1 against it. The shadow flips
   with the ink — a dark shadow under dark text is just a smudge. */
.tile-banner.ink-dark .tile-title { color: #101014; text-shadow: 0 1px 3px rgba(255,255,255,.45); }
.tile-banner.ink-dark .drag-handle { color: rgba(0,0,0,.75); background: rgba(255,255,255,.35); }
.tile-body { padding: .7rem .9rem .85rem; display: flex; flex-direction: column; gap: .5rem; flex: 1; }
.tile-desc { color: var(--text-muted); font-size: .84rem; min-height: 1.2em;
  display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; }
.tile-badges { display: flex; gap: .35rem; flex-wrap: wrap; }
.tile-actions {
  display: flex; gap: .25rem; align-items: center;
  border-top: 1px solid var(--border); padding: .45rem .6rem; background: var(--surface-2);
}
.tile-actions .spacer { flex: 1; }
.tile .drag-handle {
  cursor: grab; color: rgba(255,255,255,.9); position: absolute; top: .45rem; left: .5rem;
  display: inline-flex; padding: .2rem; border-radius: 6px; background: rgba(0,0,0,.2);
}
.tile .drag-handle:hover { background: rgba(0,0,0,.32); }
.tile .drag-handle:active { cursor: grabbing; }

/* ---- icons ---- */
/* pointer-events:none keeps the button (not the <svg>) as the event target, so
   delegated handlers that read dataset/closest() from e.target keep working. */
.icon { width: 1.15em; height: 1.15em; display: block; flex: none; pointer-events: none; }
.icon.sm { width: 1em; height: 1em; }
.icon-btn {
  background: none; border: none; cursor: pointer; color: var(--text-muted);
  width: 30px; height: 30px; padding: 0; border-radius: var(--radius-sm);
  display: inline-flex; align-items: center; justify-content: center; line-height: 1;
  transition: background .12s, color .12s;
}
.icon-btn:hover { background: var(--accent-soft); color: var(--accent); }
.icon-btn:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }
.icon-btn.danger:hover { background: var(--red-soft); color: var(--brand-deep); }
/* Held state: this account already has the power the button would remove. */
.icon-btn.amber { color: var(--amber); }
.icon-btn.amber:hover { background: var(--amber-soft); color: var(--amber); }
a.icon-btn:hover { text-decoration: none; }
.tile-schedule {
  font-size: .75rem; color: var(--text-muted);
  display: flex; align-items: center; gap: .3rem;
}

/* ---- instructor ⇄ student view ---- */
.header-actions { display: flex; align-items: center; gap: .75rem; flex-wrap: wrap; }
.view-toggle {
  display: inline-flex; background: var(--surface); border: 1px solid var(--border);
  border-radius: 999px; padding: 3px; gap: 2px;
}
.view-toggle a {
  display: inline-flex; align-items: center; gap: .35rem;
  padding: .3rem .85rem; border-radius: 999px;
  font-size: .82rem; font-weight: 600; color: var(--text-muted);
}
.view-toggle a:hover { color: var(--accent); text-decoration: none; }
.view-toggle a.current { background: var(--accent); color: #fff; }
.viewing-as { font-size: .82rem; font-weight: 600; color: var(--text-muted); }
.sv-warning {
  background: var(--amber-soft); color: var(--amber); border-radius: var(--radius-sm);
  padding: .6rem .85rem; font-size: .85rem; margin: 0 0 1rem;
}
.sv-group { margin-bottom: 1.5rem; }
.sv-group h2 { display: flex; align-items: baseline; gap: .6rem; flex-wrap: wrap; }
/* An empty tab still has to be a target you can drop a tile onto, so the grid
   keeps a tile's worth of height instead of collapsing to nothing. */
/* An empty tab is invisible to students but still has to be something you can
   drop a tile onto, so the grid keeps a tile's worth of height and says so. */
.sv-grid { min-height: 96px; border-radius: var(--radius); }
.sv-grid.empty { border: 2px dashed var(--border); }
.sv-tile .drag-handle { opacity: 0; transition: opacity .12s; }
.sv-tile:hover .drag-handle { opacity: 1; }
.sv-withheld {
  border-top: 1px solid var(--border); padding-top: 1rem; margin-top: 1.5rem;
}
.sv-withheld ul { list-style: none; margin: 0 0 .6rem; padding: 0; }
.sv-withheld li {
  display: flex; align-items: baseline; gap: .6rem; flex-wrap: wrap;
  padding: .35rem 0; border-bottom: 1px solid var(--border);
}

/* ---- modal ---- */
.modal-backdrop {
  position: fixed; inset: 0; background: rgba(0, 0, 0, .5);
  display: none; align-items: flex-start; justify-content: center; z-index: 100;
  padding: 4rem 1rem 2rem; overflow-y: auto;
}
.modal-backdrop.open { display: flex; }
.modal {
  background: var(--surface); border-radius: var(--radius); box-shadow: var(--shadow-lg);
  width: 100%; max-width: 560px; padding: 1.5rem; animation: pop .18s ease-out;
}
@keyframes pop { from { transform: scale(.97); opacity: 0; } to { transform: none; opacity: 1; } }
.modal h2 { margin-bottom: 1rem; }
.modal.narrow { max-width: 420px; }
.modal .modal-actions { display: flex; justify-content: flex-end; gap: .5rem; margin-top: 1.25rem; }
/* A dialog opened *from* a modal (the extension picker over lesson details)
   has to paint above the modal it came from, backdrop included. */
.modal-backdrop.stacked { z-index: 110; }
.form-error { color: var(--red); font-size: .85rem; margin-top: .75rem; }

/* ---- schedule stack rows ---- */
.schedule-stack { display: flex; flex-direction: column; gap: .5rem; }
.schedule-row { display: flex; gap: .5rem; align-items: center; }
.schedule-row select { max-width: 140px; }
.schedule-hint { font-size: .8rem; color: var(--text-muted); }
/* One schedule: the class's, or a set of sections running on their own times.
   The class block is unboxed until "by section" turns on and it becomes one
   schedule among several. */
#section-schedules .schedule-group, .schedule-group.boxed {
  border: 1px solid var(--border); border-radius: var(--radius-sm);
  padding: .6rem .7rem; background: var(--surface-2); margin-top: .6rem;
}
.schedule-group-head {
  display: flex; gap: .5rem; align-items: baseline; margin-bottom: .5rem;
}
.schedule-group-head .muted { flex: 1; min-width: 0; }
.schedule-group-head .icon-btn { margin-left: auto; align-self: center; }
.schedule-group select[multiple] { width: 100%; margin-bottom: .6rem; }
/* The section a tile's schedule line belongs to, before the time. */
.tile-schedule .who { font-weight: 600; color: var(--text); }
.tile-schedule .who::after { content: '·'; margin: 0 .25rem; font-weight: 400; color: var(--text-muted); }

/* ---- lesson tabs (categories) ---- */
.tab-list { display: flex; flex-direction: column; gap: .4rem; }
.tab-row {
  display: flex; gap: .5rem; align-items: center;
  border: 1px solid var(--border); border-radius: var(--radius-sm);
  padding: .35rem .4rem .35rem .3rem; background: var(--surface-2);
}
/* The name field takes the slack; min-width:0 keeps it from pushing the count
   and the remove button off the row. */
.tab-row input[type=text] { flex: 1; min-width: 0; }
.tab-row .tab-count { flex: none; white-space: nowrap; }
.tab-row .color-select { flex: none; width: 9.5rem; }
/* Narrow on purpose: it holds seven characters and shows up beside the menu
   only while that menu is asking for a hex code — which is also when the menu
   gives up some of its own width, so the tab's name keeps most of the row. */
.tab-row .hex-input { flex: none; width: 6.5rem; font-family: ui-monospace, SFMono-Regular, Menlo, monospace; }
.tab-row.picking-custom .color-select { width: 7rem; }
.tab-row .drag-handle { flex: none; display: flex; padding: .25rem; color: var(--text-muted); cursor: grab; }
.tab-row .drag-handle:active { cursor: grabbing; }
.tab-row.dragging { opacity: .4; }

/* A color menu wearing the color it names — the swatch is the control, so a
   tab strip reads at a glance without a second element per row. `.painted` is
   only ever set by JS from the gradient the server sent, and the option text
   still names the color for anyone who cannot see it (or whose browser will
   not paint a <select> at all — the menu still works, it just looks plain). */
.color-select.painted {
  color: #fff; border-color: transparent; font-weight: 600;
  text-shadow: 0 1px 2px rgba(0,0,0,.35);
}
/* The open menu is the OS's, drawn on its own ground: white-on-white options
   would be unreadable there. */
.color-select.painted option { color: var(--text); background: var(--surface); text-shadow: none; }
/* A custom color can be pale, and then the menu wears the same near-black the
   tile does — the swatch has to be readable on whatever hex was typed. */
.color-select.painted.ink-dark { color: #101014; text-shadow: 0 1px 2px rgba(255,255,255,.5); }

/* The menu and its hex field on one line, in the lesson details modal where
   the fields are otherwise stacked. */
.color-pair { display: flex; gap: .5rem; align-items: center; }
.color-pair .color-select { flex: 1; min-width: 0; }
.color-pair .hex-input { flex: none; width: 7rem; font-family: ui-monospace, SFMono-Regular, Menlo, monospace; }

/* The dot on a filter chip is the same color as the tiles the chip filters to
   — the one thing tying the strip of chips to the grid below it. */
.chip-dot {
  display: inline-block; width: .6rem; height: .6rem; border-radius: 50%;
  margin-right: .35rem; vertical-align: middle; position: relative; top: -1px;
}
.chip.selected .chip-dot { box-shadow: 0 0 0 1.5px rgba(255,255,255,.85); }

/* ---- per-student extensions ---- */
.extension-list { display: flex; flex-direction: column; gap: .5rem; }
.extension-row {
  display: flex; gap: .5rem; align-items: center;
  border: 1px solid var(--border); border-radius: var(--radius-sm);
  padding: .5rem .4rem .5rem .7rem; background: var(--surface-2);
}
/* The name column takes the slack so the buttons stay put; min-width:0 lets a
   long address ellipsize instead of pushing them off the row. */
.extension-row .extension-who { flex: 1; min-width: 0; display: flex; flex-direction: column; }
.extension-row .extension-who > * { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.extension-row .extension-times { color: var(--text); }

/* ---- tables ---- */
table.data { border-collapse: collapse; width: 100%; font-size: .88rem; }
table.data th, table.data td { padding: .55rem .75rem; border-bottom: 1px solid var(--border); text-align: left; }
table.data th { font-size: .75rem; text-transform: uppercase; letter-spacing: .04em; color: var(--text-muted); }
table.data tr:hover td { background: var(--surface-2); }
/* The buttons live in a div *inside* the cell, never on the <td> itself: a
   table-cell with display:flex is no longer a table-cell, so it drops out of
   the column grid and paints over its neighbours. The cell stays a cell and
   sizes the column; the div lays the buttons out. */
table.data td.actions { text-align: right; white-space: nowrap; }
.row-actions {
  display: flex; gap: .4rem; align-items: center;
  justify-content: flex-end; flex-wrap: nowrap;
}
.row-actions > * { flex: none; }   /* buttons keep their size; the wrap scrolls */
.row-actions .btn, .row-actions .icon-btn, .row-actions > span { white-space: nowrap; }
/* Backstop for narrow windows: scroll the table rather than the page. */
.table-wrap { overflow-x: auto; }
/* Column sizing: every column shrinks to its content except the email, which
   soaks up the leftover width. Without this the browser hands the slack to the
   button column and squeezes the identifying columns until addresses break
   mid-word. Names and addresses are identifiers — they never wrap. */
.accounts-table td, .accounts-table th { vertical-align: middle; }
.accounts-table .name, .accounts-table .email { white-space: nowrap; }
.accounts-table .email { width: 100%; }
.accounts-table .shrink { width: 1%; white-space: nowrap; }
.accounts-table .num { text-align: right; }
/* Role holds up to two badges; stacked reads better than one wide line. */
.accounts-table .role { width: 1%; }
.accounts-table .role .badge { margin: 1px 0; }
/* Hairlines between columns — with six columns of mixed content, padding alone
   doesn't make the boundaries legible. */
.accounts-table th + th, .accounts-table td + td { border-left: 1px solid var(--border); }
.accounts-table th, .accounts-table td { padding-left: .85rem; padding-right: .85rem; }
/* Inline name editor in the Name column. The column is nowrap so identifiers
   never break mid-word; the editor sets its own wrapping and a workable width,
   since two inputs plus two buttons do not fit a name's worth of space. */
.name-edit { display: none; gap: .35rem; align-items: center; flex-wrap: wrap; margin-top: .4rem; white-space: normal; }
.name-edit.open { display: flex; }
.name-edit input[type=text] { width: 9rem; font-size: .85rem; padding: .3rem .5rem; }
.name-edit .btn { flex: none; }

/* Retired accounts stay listed (their coursework is still there) but read as
   inactive rather than competing with live rows. */
tr.retired td { opacity: .6; }
tr.retired:hover td { opacity: 1; }

/* ---- course email templates ---- */
.placeholder-help summary { cursor: pointer; }
.placeholder-help[open] summary { margin-bottom: .75rem; }
.email-template + .email-template { margin-top: 1rem; }
.email-template-head { display: flex; align-items: center; gap: .6rem; flex-wrap: wrap; margin-bottom: .25rem; }
.email-template-head h2 { margin: 0; font-size: 1.05rem; }
textarea.mono { font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: .85rem; line-height: 1.5; }
.email-preview {
  margin-top: 1rem; padding: 1rem; border: 1px dashed var(--border);
  border-radius: var(--radius-sm); background: var(--surface-2);
}
.email-preview .preview-subject { margin: .4rem 0 .6rem; font-size: .9rem; }
.email-preview pre {
  margin: 0; white-space: pre-wrap; overflow-wrap: anywhere;
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: .82rem; line-height: 1.55;
}

/* progress table with sticky name + section columns and sticky header */
.progress-wrap {
  overflow: auto; max-height: 75vh; border: 1px solid var(--border);
  border-radius: var(--radius); background: var(--surface); box-shadow: var(--shadow);
}
table.progress {
  border-collapse: separate; border-spacing: 0; font-size: .85rem; min-width: 100%;
  /* The frozen columns are offset by hand, so their widths have to be fixed:
     the inner .name-cell/.section-cell divs pin them, and --pad-x below must
     stay in step with the cell padding. */
  --name-w: 15rem; --section-w: 7rem; --pad-x: .8rem;
}
table.progress th, table.progress td {
  padding: .6rem var(--pad-x); border-bottom: 1px solid var(--border); border-right: 1px solid var(--border);
  background: var(--surface); white-space: nowrap;
}
table.progress thead th { position: sticky; top: 0; z-index: 3; background: var(--surface-2); font-size: .78rem; }
table.progress .sticky-col { position: sticky; left: 0; z-index: 2; }
table.progress .sticky-col-2 {
  position: sticky; z-index: 2; box-shadow: 2px 0 0 var(--border);
  left: calc(var(--name-w) + var(--pad-x) * 2 + 1px);  /* +1px: the name cell's right border */
}
table.progress thead .sticky-col, table.progress thead .sticky-col-2 { z-index: 4; }
table.progress td.sticky-col { font-weight: 600; }
table.progress td.sticky-col-2 { color: var(--text-muted); }
table.progress .name-cell { width: var(--name-w); }
table.progress .section-cell { width: var(--section-w); }
/* Long names, addresses and section lists are clipped rather than allowed to
   widen a frozen column out of alignment with its offset. */
table.progress .name-cell > *, table.progress .section-cell {
  display: block; overflow: hidden; text-overflow: ellipsis;
}
.pct-cell { display: flex; flex-direction: column; gap: .25rem; min-width: 130px; }
.pct-bar { background: var(--border); height: 6px; border-radius: 3px; overflow: hidden; width: 110px; }
.pct-bar > div { height: 100%; background: var(--accent); border-radius: 3px; }
.pct-bar.done > div { background: var(--green); }
.pct-meta { font-size: .72rem; color: var(--text-muted); }

/* ---- lesson content ---- */
.lesson-page { max-width: var(--lesson-width); margin: 0 auto; }
.block { margin-bottom: 1.5rem; }
.block img { max-width: 100%; border-radius: var(--radius-sm); }
/* An image block: the <figure> carries the width (a percent of the column) and
   the picture fills it, so the caption sits under the picture and a narrow
   image still scales to the width it was given. */
.block-figure { margin: 0; max-width: 100%; }
.block-figure img { display: block; width: 100%; }
.block-figure figcaption { margin-top: .35rem; }
.block .video-wrap { position: relative; padding-top: 56.25%; border-radius: var(--radius-sm); overflow: hidden; }
.block .video-wrap iframe, .block .video-wrap video { position: absolute; inset: 0; width: 100%; height: 100%; border: 0; }
.question-card {
  background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius);
  box-shadow: var(--shadow); padding: 1.25rem;
}
.question-card .q-label { font-size: .72rem; font-weight: 700; letter-spacing: .05em; text-transform: uppercase; color: var(--accent); margin-bottom: .4rem; }
.question-card .q-text { font-weight: 600; margin-bottom: .8rem; }
.question-card .options { display: flex; flex-direction: column; gap: .45rem; margin-bottom: .8rem; }
.question-card .options label {
  display: flex; align-items: center; gap: .6rem; font-weight: 500;
  border: 1px solid var(--border); padding: .55rem .8rem; border-radius: var(--radius-sm); cursor: pointer;
}
.question-card .options label:hover { border-color: var(--accent); background: var(--accent-soft); }
.question-card .options input { width: auto; }
.q-hint { font-size: .8rem; color: var(--text-muted); margin: .35rem 0 0; }
.q-status { font-size: .8rem; color: var(--text-muted); margin-left: .75rem; }
.q-status.saved { color: var(--green); font-weight: 600; }

/* ---- auto-checked answers ---- */
.q-auto {
  font-size: .68rem; font-weight: 700; letter-spacing: .04em; text-transform: uppercase;
  color: var(--text-muted); background: var(--surface-2); border: 1px solid var(--border);
  border-radius: 999px; padding: .1rem .5rem; margin-left: .4rem; white-space: nowrap;
}
/* A question the lesson cannot be submitted without. Drawn like `.q-auto` — it
   sits in the same row and says the same kind of thing about the question —
   but outlined in the text color, since it is a rule rather than a note. */
.q-required {
  font-size: .68rem; font-weight: 700; letter-spacing: .04em; text-transform: uppercase;
  color: var(--text); background: var(--surface); border: 1px solid var(--text);
  border-radius: 999px; padding: .1rem .5rem; margin-left: .4rem; white-space: nowrap;
}
/* The question a submission was turned away for. The outline is what the eye
   lands on after the jump; `.q-flag.missing` is what says why. */
.question-card.needs-answer { border-color: var(--amber); box-shadow: 0 0 0 3px var(--amber-soft); }
.q-flag.missing { background: var(--amber-soft); border-color: var(--amber); color: #6d4700; }
.q-answer-row { display: flex; align-items: center; gap: .75rem; flex-wrap: wrap; }
.q-actions { margin-top: .6rem; }
/* The flag that reports the verdict. Color alone never carries it — the
   message says which one it is, for anyone who cannot see the difference. */
.q-flag {
  margin-top: .7rem; padding: .55rem .8rem; border-radius: var(--radius-sm);
  font-weight: 600; font-size: .9rem; border: 1px solid transparent;
}
.q-flag.correct { background: var(--green-soft); border-color: var(--green); color: #0f5c39; }
.q-flag.incorrect { background: var(--red-soft); border-color: var(--red); color: var(--brand-dark); }
.q-flag.partial { background: var(--amber-soft); border-color: var(--amber); color: #6d4700; }
/* Not a verdict: the answer was refused before it was saved (too short, or
   language that does not belong in coursework). Amber, like partial — nothing
   is wrong with the student's reasoning, the submission just has to change. */
.q-flag.rejected { background: var(--amber-soft); border-color: var(--amber); color: #6d4700; }
/* Still readable, visibly about the previous try — the answer has changed since. */
.q-flag.stale { opacity: .5; }
.q-solution { font-weight: 700; font-size: .9rem; white-space: nowrap; }
.q-solution.good { color: var(--green); }
.q-solution.bad { color: var(--red); }
.q-solution.block-solution { margin-top: .5rem; white-space: normal; }
.question-card .options label.opt-correct { border-color: var(--green); background: var(--green-soft); }
.question-card .options label.opt-wrong { border-color: var(--red); background: var(--red-soft); }
/* The unpicked half of the answer key, once it is being revealed: green like a
   correct pick but dashed, so it does not read as something they chose. */
.question-card .options label.opt-answer { border-color: var(--green); border-style: dashed; }
.question-card .options label { flex-wrap: wrap; }
.opt-note { flex-basis: 100%; font-size: .82rem; font-weight: 400; color: var(--text-muted); }
/* Hover must not wash the verdict off a marked choice. */
.question-card .options label.opt-correct:hover { border-color: var(--green); background: var(--green-soft); }
.question-card .options label.opt-wrong:hover { border-color: var(--red); background: var(--red-soft); }
.question-card .options label.opt-answer:hover { border-color: var(--green); background: none; }
.question-card.resolved .options label { cursor: default; }

.markdown pre { background: #141414; color: var(--gray-light); padding: 1rem; border-radius: var(--radius-sm); overflow-x: auto; }
.markdown code { background: var(--accent-soft); padding: .1em .35em; border-radius: 4px; font-size: .9em; }
.markdown pre code { background: none; padding: 0; }
.markdown a { text-decoration: underline; text-underline-offset: 2px; }
.markdown blockquote { border-left: 3px solid var(--accent); margin-left: 0; padding-left: 1rem; color: var(--text-muted); }
.markdown table { border-collapse: collapse; }
.markdown th, .markdown td { border: 1px solid var(--border); padding: .4rem .7rem; }

/* ---- math (LaTeX rendered to MathML on the server — lessons/mathmd.py) ---- */
math { font-size: 1.06em; }              /* MathML sets a shade small next to prose */
.math-block {
  margin: 1rem 0; padding: .2rem 0; text-align: center;
  overflow-x: auto;                      /* a wide formula scrolls, never stretches */
}
.math-block math { font-size: 1.22em; }
code.math-error {                        /* beats .markdown code's own styling */
  background: var(--red-soft); color: var(--red);
  border: 1px dashed var(--red); border-radius: 4px;
  padding: .05em .35em; font-size: .9em;
}

/* ---- styled text spans ([text]{red large} — lessons/textstyle.py) ---- */
/* One variable per color word, so the palette is re-pointable in one place
   rather than every lesson having to be rewritten for a background. Mid tones:
   nothing near-white and nothing near-black, so a color survives being
   projected as well as read on screen. */
:root {
  --tx-red: #c41230; --tx-orange: #bd6209; --tx-green: #16875a; --tx-teal: #0b7f8d;
  --tx-blue: #2563c9; --tx-indigo: #4f5dd4; --tx-purple: #7d3fc4; --tx-pink: #bd3184;
  --tx-gray: #6d6e71;
}
.tx-red { color: var(--tx-red); }
.tx-orange { color: var(--tx-orange); }
.tx-green { color: var(--tx-green); }
.tx-teal { color: var(--tx-teal); }
.tx-blue { color: var(--tx-blue); }
.tx-indigo { color: var(--tx-indigo); }
.tx-purple { color: var(--tx-purple); }
.tx-pink { color: var(--tx-pink); }
.tx-gray { color: var(--tx-gray); }
/* Relative, so a size word composes with the heading, list or table cell it is
   used in rather than fighting it — and so two nested sizes multiply, which is
   what makes styling a word inside an already-large run behave. */
.tx-tiny { font-size: .78em; }
.tx-small { font-size: .88em; }
.tx-large { font-size: 1.25em; }
.tx-huge { font-size: 1.6em; }
.tx-sans { font-family: var(--font); }
.tx-serif { font-family: Georgia, "Times New Roman", Times, serif; }
.tx-mono { font-family: ui-monospace, SFMono-Regular, Menlo, monospace; }

/* ---- editor ---- */
.editor-layout { display: grid; grid-template-columns: 210px 1fr; gap: 1.25rem; align-items: start; }
.editor-palette { position: sticky; top: 76px; display: flex; flex-direction: column; gap: .4rem; }
.editor-palette button {
  text-align: left; background: var(--surface); border: 1px solid var(--border);
  border-radius: var(--radius-sm); padding: .55rem .8rem; cursor: pointer;
  font-family: var(--font); font-size: .85rem; font-weight: 600; color: var(--text);
  display: flex; gap: .5rem; align-items: center;
}
.editor-palette button:hover { border-color: var(--accent); color: var(--accent); background: var(--accent-soft); }
.editor-canvas { display: flex; flex-direction: column; gap: .75rem; min-height: 300px; }
.edit-block {
  background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius);
  box-shadow: var(--shadow);
}
.edit-block.dragging { opacity: .4; }
.edit-block.drag-over { outline: 2px dashed var(--accent); outline-offset: 2px; }
.edit-block-head {
  display: flex; align-items: center; gap: .5rem; padding: .45rem .75rem;
  border-bottom: 1px solid var(--border); background: var(--surface-2);
  border-radius: var(--radius) var(--radius) 0 0;
}
.edit-block-head .type { font-size: .72rem; font-weight: 800; text-transform: uppercase; letter-spacing: .05em; color: var(--accent); }
.edit-block-head .grab { cursor: grab; color: var(--text-muted); display: inline-flex; align-items: center; }
.edit-block-head .grab:active { cursor: grabbing; }
.edit-block-body { padding: .9rem; display: flex; flex-direction: column; gap: .6rem; }
.edit-block-body textarea { min-height: 90px; font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: .85rem; }
.option-row { display: flex; gap: .5rem; align-items: center; }
.option-row input[type=text] { flex: 1; }
.option-item { display: flex; flex-direction: column; gap: .3rem; }
/* "correct" tick beside an option, only shown while auto-checking is on. */
.opt-mark {
  display: flex; align-items: center; gap: .3rem; flex: none; cursor: pointer;
  font-size: .7rem; font-weight: 700; text-transform: uppercase; letter-spacing: .04em;
  color: var(--text-muted); border: 1px solid var(--border); border-radius: 999px;
  padding: .25rem .55rem;
}
.opt-mark input { width: auto; margin: 0; }
.opt-mark:has(input:checked) { color: var(--green); border-color: var(--green); background: var(--green-soft); }
.opt-note-input { font-size: .85rem; margin-left: 1.5rem; }
.grading-panel { border-top: 1px dashed var(--border); padding-top: .6rem; margin-top: .2rem; }
.grading-body {
  border-left: 3px solid var(--accent-soft); padding-left: .8rem; margin-top: .4rem;
  display: flex; flex-direction: column; gap: .2rem;
}
.grading-messages summary { cursor: pointer; font-size: .85rem; color: var(--text-muted); margin: .3rem 0; }
.grading-messages[open] summary { margin-bottom: .6rem; }
.grading-messages code { background: var(--accent-soft); padding: .05em .3em; border-radius: 4px; }
.grading-panel > .help, .grading-body > .help, .grading-messages > .help {
  color: var(--text-muted); font-size: .8rem;
}
.help.warn { color: var(--amber); font-weight: 600; }
.editor-topbar { display: flex; align-items: center; gap: .75rem; margin-bottom: 1rem; flex-wrap: wrap; }
.editor-topbar input.lesson-title { font-size: 1.15rem; font-weight: 700; max-width: 420px; }
.save-status { font-size: .82rem; color: var(--text-muted); }
.save-status.saved { color: var(--green); }
.save-status.unsaved { color: var(--amber); }
.empty-state {
  text-align: center; color: var(--text-muted); padding: 3rem 1rem;
  border: 2px dashed var(--border); border-radius: var(--radius);
}

/* ---- image block editor ----
   The stage is one lesson column wide and holds the very markup `_block.html`
   renders, so a size chosen here is the size on the lesson page. Everything
   below is chrome *around* that figure — nothing in here restyles it. */
.image-stage {
  background: var(--surface-2); border: 1px solid var(--border);
  border-radius: var(--radius-sm); padding: .8rem;
}
/* The column is the inner `.block`, not the frame around it: a percentage
   width resolves against this box, so it is the one that has to be the lesson
   column exactly — padding and border on the stage would eat into it. */
.image-stage .block { max-width: var(--lesson-width); margin-bottom: 0; }
.image-stage .block-figure { position: relative; }
.image-stage .image-empty {
  display: flex; align-items: center; justify-content: center; text-align: center;
  min-height: 110px; padding: 1rem .6rem; color: var(--text-muted); font-size: .82rem;
  border: 2px dashed var(--border); border-radius: var(--radius-sm); background: var(--surface);
}
/* The grip on the picture's right edge: the width control that is on the
   picture itself, so a size is chosen by looking at it rather than at a number. */
.image-handle {
  position: absolute; top: 50%; right: -8px; transform: translateY(-50%);
  width: 16px; height: 46px; padding: 0; cursor: ew-resize; touch-action: none;
  border: 1px solid var(--border); border-radius: 8px; background: var(--surface);
  box-shadow: var(--shadow);
}
.image-handle::before {
  content: ''; display: block; margin: 0 auto; width: 6px; height: 18px;
  border-left: 2px solid var(--text-muted); border-right: 2px solid var(--text-muted);
}
.image-handle:hover, .image-handle:focus-visible, .image-handle.dragging { border-color: var(--accent); background: var(--accent-soft); }
.image-handle:hover::before, .image-handle:focus-visible::before, .image-handle.dragging::before {
  border-color: var(--accent);
}
.image-size { display: flex; align-items: center; gap: .6rem; flex-wrap: wrap; }
.image-size input[type=range] { flex: 1 1 140px; width: auto; min-width: 120px; accent-color: var(--accent); }
.image-size input[type=number] { width: 4.6rem; flex: none; }
.image-size .image-px { flex: none; }
.image-presets { display: flex; gap: .3rem; flex-wrap: wrap; margin-top: .35rem; }

/* ---- markdown editor (Text block) ---- */
.md-editor {
  border: 1px solid var(--border); border-radius: var(--radius-sm);
  background: var(--surface); overflow: hidden;
}
.md-editor:focus-within { border-color: var(--accent); box-shadow: 0 0 0 3px var(--accent-soft); }
.md-editor.dropping { border-color: var(--accent); box-shadow: 0 0 0 3px var(--accent-soft); }
.md-toolbar {
  display: flex; align-items: center; gap: .1rem; flex-wrap: wrap;
  padding: .3rem .35rem; background: var(--surface-2);
  border-bottom: 1px solid var(--border);
}
.md-toolbar .sep { width: 1px; height: 18px; margin: 0 .3rem; background: var(--border); }
.md-tool {
  display: inline-flex; align-items: center; justify-content: center;
  width: 30px; height: 28px; padding: 0; cursor: pointer;
  border: 1px solid transparent; border-radius: 6px;
  background: transparent; color: var(--text-muted);
}
.md-tool:hover:not(:disabled) { background: var(--accent-soft); color: var(--accent); }
.md-tool.on { background: var(--accent-soft); color: var(--accent); border-color: var(--border); }
.md-tool:disabled { opacity: .35; cursor: default; }
.md-tool .icon { width: 17px; height: 17px; }
.md-toolbar select.md-heading {
  width: auto; min-width: 116px; height: 28px; padding: .1rem 1.5rem .1rem .5rem;
  font-size: .8rem; font-weight: 600; border-radius: 6px; margin-right: .1rem;
}
.md-toolbar select.md-style {
  width: auto; height: 28px; padding: .1rem 1.3rem .1rem .45rem;
  font-size: .8rem; font-weight: 700; border-radius: 6px; margin: 0 .05rem;
}
/* The color select is given its own tx- class as the caret moves, so the
   swatch it shows is the same variable the lesson page will use. */
.md-toolbar select.md-style-color { min-width: 74px; }
.md-tabs { display: flex; gap: .15rem; margin-left: auto; }
.md-tab {
  height: 28px; padding: 0 .65rem; cursor: pointer;
  border: 1px solid transparent; border-radius: 6px; background: transparent;
  font-family: var(--font); font-size: .78rem; font-weight: 700; color: var(--text-muted);
}
.md-tab:hover { color: var(--accent); }
.md-tab.on { background: var(--surface); border-color: var(--border); color: var(--accent); }
/* Split view: source and rendered output side by side, the preview following a
   few tenths of a second behind. The toolbar spans both columns; `start` keeps
   each pane its own height, since the textarea is auto-grown to its content and
   the render beside it is whatever it is. */
.md-editor.split { display: grid; grid-template-columns: 1fr 1fr; align-items: start; }
.md-editor.split > .md-toolbar { grid-column: 1 / -1; }
.md-editor.split > textarea { border-right: 1px solid var(--border); }
.md-editor.split > .md-preview { background: var(--surface-2); }
@media (max-width: 900px) {
  .md-editor.split { grid-template-columns: 1fr; }
  .md-editor.split > textarea { border-right: none; border-bottom: 1px solid var(--border); }
}
/* A render that has stopped following the source says so rather than passing
   itself off as current — same idea as a stale answer verdict. */
.md-preview.stale { opacity: .5; }

/* Auto-grown by JS, so the drag handle would only fight it. */
.md-editor textarea { border: none; border-radius: 0; resize: none; display: block; max-height: 70vh; }
.md-editor textarea:focus { outline: none; }
.md-preview { padding: .85rem .9rem; min-height: 90px; overflow-x: auto; }
.md-preview > :first-child { margin-top: 0; }
.md-preview > :last-child { margin-bottom: 0; }

/* A question prompt's formulas, typeset under the field that holds them — only
   drawn once there is a delimiter to render, so a prompt without math is an
   ordinary field. Same server round-trip as .md-preview, no extra chrome: it
   is one line, not a pane. */
.math-field-preview {
  margin-top: .35rem; padding: .4rem .6rem; overflow-x: auto;
  border-left: 2px solid var(--border); background: var(--surface-2);
  border-radius: var(--radius-sm); font-size: .95rem;
}
.math-field-preview .math-block { margin: .3rem 0; }

/* ---- presentation mode ---- */
/* The lesson page with the topbar taken off (lessons/view.html + `present`), not
   a slide deck — so there is nothing to style except the room it is read from.
   Everything is sized in `em` off one bump, which keeps the page the students'
   page: same colors, same blocks, same widths in proportion, just legible from
   the back. */
.present-body .lesson-page { font-size: 1.15em; max-width: 940px; }
.present-body .page { padding-top: 2.25rem; }

/* ---- login ---- */
.login-wrap { min-height: 100vh; display: flex; align-items: center; justify-content: center; padding: 1rem; }
.login-card { width: 100%; max-width: 400px; }
.login-card .logo-big {
  background: linear-gradient(135deg, var(--brand), var(--brand-deep)); color: #fff;
  width: 52px; height: 52px; border-radius: 14px; font-weight: 800; font-size: 1.3rem;
  display: flex; align-items: center; justify-content: center; margin: 0 auto 1rem;
}
.login-card h1 { text-align: center; font-size: 1.3rem; }
.login-card .sub { text-align: center; color: var(--text-muted); font-size: .88rem; margin-bottom: 1.5rem; }

/* ---- misc ---- */
.toolbar { display: flex; gap: .75rem; align-items: center; flex-wrap: wrap; margin-bottom: 1rem; }
.toolbar .grow { flex: 1; }
/* The course page's notice of R packages students could not load: amber, the
   "needs attention" color, since nothing on it is an error faculty caused. */
.package-requests { border-left: 4px solid var(--amber); margin-bottom: 1.25rem; }
.package-requests h2 { font-size: 1rem; margin: 0 0 .35rem; }
.package-requests summary { cursor: pointer; white-space: nowrap; }
.searchbox { max-width: 300px; }
.muted { color: var(--text-muted); }
.small { font-size: .82rem; }
.mono { font-family: ui-monospace, SFMono-Regular, Menlo, monospace; }
.section-list { display: flex; flex-direction: column; gap: 1rem; }
.member-list { display: flex; flex-direction: column; }
.member-row {
  display: flex; align-items: center; gap: .75rem; padding: .5rem .25rem;
  border-bottom: 1px solid var(--border); font-size: .9rem; flex-wrap: wrap;
}
.member-row:last-child { border-bottom: none; }
.member-row .spacer { flex: 1; min-width: 0; }
.member-row .email { min-width: 0; overflow-wrap: anywhere; }
/* Name editor: a full-width child of the row itself, so it wraps onto its own
   line without breaking .member-row:last-child (which is what keeps the list
   from drawing a separator against the "add student" panel below it). */
.member-rename { display: none; flex: 0 0 100%; gap: .4rem; align-items: center; flex-wrap: wrap; }
.member-rename.open { display: flex; }
.member-rename input[type=text] { flex: 1 1 140px; min-width: 0; width: auto; font-size: .85rem; padding: .3rem .5rem; }
.member-rename label { flex: none; margin: 0; }   /* labels are block+margin by default */
.member-rename .btn { flex: none; }

/* Section footer: "add one student" and "import CSV" stacked, each on its own
   full-width row. Side by side they squeeze the inputs (which are width:100%)
   down to unreadable slivers. */
.roster-add {
  display: flex; flex-direction: column; gap: 1.25rem;
  margin-top: 1rem; border-top: 1px solid var(--border); padding-top: 1rem;
}
.roster-add h3 { margin: 0 0 .5rem; font-size: .9rem; }
.roster-add-fields { display: flex; gap: .5rem; flex-wrap: wrap; align-items: center; }
.roster-add-fields > input { flex: 1 1 140px; min-width: 0; }
.roster-add-fields > input.grow-2 { flex: 2 1 220px; }
.roster-add-fields > .btn { flex: none; white-space: nowrap; }
.roster-add .help { color: var(--text-muted); font-size: .8rem; margin-top: .4rem; }
.roster-add input[type=file] {
  padding: .4rem .5rem; border: 1px solid var(--border);
  border-radius: var(--radius-sm); background: var(--surface); font-size: .85rem;
}
.divider { border: none; border-top: 1px solid var(--border); margin: 1.25rem 0; }

@media (max-width: 800px) {
  .editor-layout { grid-template-columns: 1fr; }
  .editor-palette { position: static; flex-direction: row; flex-wrap: wrap; }
  .form-row { flex-direction: column; gap: 0; }
  /* Two frozen columns must still leave room for the lessons on a phone. */
  table.progress { --name-w: 8.5rem; --section-w: 4.5rem; --pad-x: .5rem; }
}

/* ---- code blocks on the lesson page (lessons/_block.html, _code_js.html) ----
   R (`.rcode`) and Python (`.pycode`) draw the same card, so every rule here is
   written for both prefixes rather than copied per language — the two blocks
   differ in what runs the code, not in how it looks. The code box is a plain
   textarea, sized to its code by the runner; output stacks beneath it as the
   interpreter printed it — console text, then messages, warnings and errors
   each in their own run, then plots. Monospace throughout, since a printed
   table lines up by character. */
.rcode, .pycode {
  border: 1px solid var(--border); border-radius: var(--radius); background: var(--surface);
  overflow: hidden;
}
.rcode-head, .pycode-head {
  display: flex; align-items: center; gap: .5rem; padding: .45rem .6rem .45rem .8rem;
  border-bottom: 1px solid var(--border); background: var(--surface-2); flex-wrap: wrap;
}
.rcode-head > .icon, .pycode-head > .icon { width: 18px; height: 18px; color: var(--accent); flex: none; }
.rcode-label, .pycode-label { font-size: .72rem; font-weight: 700; letter-spacing: .05em; color: var(--accent); }
.rcode-status, .pycode-status { font-size: .8rem; color: var(--text-muted); }
.rcode-export, .pycode-export { display: inline-flex; align-items: center; gap: .3rem; }
.rcode-export-formats:not([hidden]), .pycode-export-formats:not([hidden]) { display: inline-flex; gap: .3rem; }
.rcode-status.done, .pycode-status.done { color: var(--green); }
.rcode-status.error, .pycode-status.error { color: var(--brand-dark); }
.rcode.running .rcode-status::before, .pycode.running .pycode-status::before {
  content: ""; display: inline-block; width: .7em; height: .7em; margin-right: .4em;
  border: 2px solid currentColor; border-right-color: transparent; border-radius: 50%;
  vertical-align: -.05em; animation: code-spin .8s linear infinite;
}
@keyframes code-spin { to { transform: rotate(360deg); } }
@media (prefers-reduced-motion: reduce) { .rcode.running .rcode-status::before,
  .pycode.running .pycode-status::before { animation: none; } }
.rcode-src, .pycode-src {
  display: block; width: 100%; margin: 0; padding: .75rem .9rem; border: 0; border-radius: 0;
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: .85rem; line-height: 1.5;
  tab-size: 2; white-space: pre; overflow-x: auto; resize: vertical; min-height: 4.5em;
  background: var(--surface); color: var(--text);
}
/* The one place the two differ: a tab stop is what the language means by an
   indent, and Python means four. */
.pycode-src { tab-size: 4; }
pre.rcode-src, pre.pycode-src { resize: none; }
.rcode-src::placeholder, .pycode-src::placeholder { color: var(--text-muted); opacity: 1; }
textarea.rcode-src:focus, textarea.pycode-src:focus { outline: 2px solid var(--accent); outline-offset: -2px; }
.rcode-out, .pycode-out { border-top: 1px solid var(--border); background: var(--surface-2); padding: .5rem .9rem .75rem; }
.rcode-text, .pycode-text {
  margin: .25rem 0; white-space: pre-wrap; overflow-wrap: anywhere;
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: .82rem; line-height: 1.45;
}
/* By data-kind, not class: `.message`, `.warning` and `.error` are the toast
   styles, and a console line must not turn into a toast. */
.rcode-text[data-kind="stderr"], .pycode-text[data-kind="stderr"], .rcode-text[data-kind="message"], .pycode-text[data-kind="message"] { color: var(--text-muted); }
.rcode-text[data-kind="warning"], .pycode-text[data-kind="warning"] { color: var(--amber); }
.rcode-text[data-kind="error"], .pycode-text[data-kind="error"] { color: var(--brand-dark); font-weight: 600; }
.rcode-plot, .pycode-plot {
  display: block; max-width: 100%; height: auto; margin: .5rem 0;
  background: #fff; border: 1px solid var(--border); border-radius: var(--radius-sm);
}

/* ---- dataset link card on the lesson page (lessons/_dataset.html) ---- */
.dataset-card { border: 1px solid var(--border); border-radius: var(--radius); background: var(--surface); }
.ds-head { display: flex; align-items: center; gap: .75rem; padding: .8rem 1rem; }
.ds-head > .icon { width: 22px; height: 22px; color: var(--accent); flex: none; }
.ds-title { font-weight: 700; }
.ds-desc { margin: 0; padding: 0 1rem .8rem; }

/* ---- the dataset page (datasets/workspace.html) ----
   Opened in its own tab from the lesson's link card. The toolbox spans the
   top — one boxed section per tool group, plus the picked tool's parameter
   form — and the rest of the viewport is split left/right between the data
   table and the results area, resized by dragging the divider (--ds-split,
   written by the page's JS). By default the page never scrolls — each pane
   scrolls inside itself — until the bar under the panes sets an explicit
   height (--ds-height + ds-sized on <body>), which may run past the
   window; then the page scrolls to reach it.
   The toolbox is chrome, not the work: it is drawn as tight as it can be
   read, and its own two controls — the Toolbox strip, which folds the whole
   thing away and is also what brings it back, and the bar under the menu
   (--ds-menu-h), which caps how much of the menu shows short of that — are
   there so it can be made smaller still or taken away outright, because how
   much of it earns its space depends on the tool and the screen. */
body.ds-body { height: 100vh; display: flex; flex-direction: column; overflow: hidden; }
body.ds-body .topbar { flex: none; }
.ds-page { flex: 1 1 auto; min-height: 0; display: flex; flex-direction: column; }
.ds-toolbar {
  flex: none; min-height: 0; display: flex; flex-direction: column;
  background: var(--surface); border-bottom: 1px solid var(--border);
}
/* The dataset's identity on one line — title, dimensions and lesson run
   together — so the toolbox starts near the top of the window. */
.ds-page-head { display: flex; align-items: center; gap: .5rem; padding: .35rem 1rem .3rem; }
.ds-page-head > .icon { width: 17px; height: 17px; color: var(--accent); flex: none; }
.ds-page-head .ds-title { font-size: .95rem; }
.ds-dims { min-width: 0; overflow: hidden; white-space: nowrap; text-overflow: ellipsis; }
.ds-caret { display: inline-block; font-size: .7em; transition: transform .12s ease; }
.ds-toolbar.collapsed .ds-unfold .ds-caret { transform: rotate(180deg); }
/* The toolbox's one control, and the only part of it that is always drawn:
   a strip the width of the window naming what sits under it. Clicking it
   folds the toolbox entire — head, menu and parameter form — because a
   dataset you already know your way around is one where every pixel of the
   window belongs to the data and the results; clicking it again brings all
   three back. It stays put across both states on purpose. A control that
   hides itself has to leave a replacement behind, and a replacement is a
   second thing to find and to explain; one that never moves is neither.
   It has to *look* like a control, which a full-width band of small muted
   capitals does not — that is how every table header on the site is drawn,
   so it read as a label for the toolbox rather than the way to fold it.
   Hence a pill: bordered, on the raised surface, in sentence case, saying
   which way it goes ("Hide toolbox" / "Show toolbox") rather than naming
   the thing it acts on. The whole strip stays the click target — the pill
   is what says it is one, the band is what makes it easy to hit. */
.ds-unfold {
  flex: none; width: 100%; border: 0; border-bottom: 1px solid var(--border);
  background: var(--surface-2); font: inherit; color: inherit;
  padding: .18rem 1rem; cursor: pointer;
  display: flex; align-items: center; justify-content: center;
}
.ds-unfold-label {
  display: inline-flex; align-items: center; gap: .3rem;
  padding: .04rem .5rem; border: 1px solid var(--border);
  border-radius: 999px; background: var(--surface);
  font-size: .7rem; font-weight: 600; line-height: 1.6; color: var(--text-muted);
  transition: background .12s ease, border-color .12s ease, color .12s ease;
}
.ds-unfold:hover .ds-unfold-label, .ds-unfold:focus-visible .ds-unfold-label {
  background: var(--accent-soft); border-color: var(--accent); color: var(--accent-strong);
}
/* One word or the other, never both: open, it offers to hide. */
.ds-unfold-show { display: none; }
.ds-toolbar.collapsed .ds-unfold-show { display: inline; }
.ds-toolbar.collapsed .ds-unfold-hide { display: none; }
/* Folded: everything but the strip goes, and the toolbar's own bottom border
   would then double the strip's — so the strip keeps the only line. */
.ds-toolbar.collapsed > :not(.ds-unfold) { display: none; }
.ds-toolbar.collapsed { border-bottom: 0; }
.ds-info { padding: .1rem 1rem .6rem; border-bottom: 1px solid var(--border); }
/* The toolbox menu: one boxed section per tool group — the group's name on
   top, its tools beneath — so four groups read as four sections rather than
   one run-on line of pills. Everything here is sized to the smallest that
   still reads: the menu is how you reach the work, not the work, and the
   tighter the pills the more groups share a row.
   --ds-menu-h is how much of the menu shows, and the page's JS always writes
   it: fitted to the menu's own height by default — a cap short of that hides
   tools, which is the one thing the menu is for — and only cut back when the
   window is too short to hold the menu and leave the panes a workable
   minimum, so a long toolbox still can never push the data off screen. The
   bar under the menu overrides it. The value here is the floor for a browser
   that never runs the script. */
.ds-menubar {
  display: flex; flex-wrap: wrap; align-items: stretch; gap: .3rem .45rem;
  padding: .15rem 1rem .4rem;
  max-height: var(--ds-menu-h, 50vh); overflow-y: auto; overscroll-behavior: contain;
}
.ds-menu-section {
  display: flex; flex-direction: column; gap: .25rem;
  padding: .25rem .5rem .35rem; background: var(--surface-2);
  border: 1px solid var(--border); border-radius: var(--radius-sm);
}
.ds-menu-group {
  font-size: .6rem; font-weight: 700; text-transform: uppercase;
  letter-spacing: .06em; color: var(--text-muted); margin: 0; line-height: 1.25;
}
.ds-menu-tools { display: flex; flex-wrap: wrap; gap: .25rem; }
.ds-menu-tool {
  border: 1px solid var(--border); border-radius: 999px; background: var(--surface);
  font: inherit; font-size: .78rem; color: var(--text); padding: .16rem .55rem;
  cursor: pointer; line-height: 1.35;
}
.ds-menu-tool:hover { border-color: var(--accent); }
.ds-menu-tool.on {
  border-color: var(--accent); background: var(--accent-soft);
  color: var(--accent-strong); font-weight: 600;
}
/* Data and results share the rest of the viewport stacked one over the
   other — every computed output on top, the data table beneath it — and
   dragging the divider between them trades space. Results take the top
   seat because a chart is what a tool was run for: it should land where
   the eye already is, without scrolling a pane sideways to reach it. The
   table sits under it at full window width, which is what a table with a
   dozen columns wants. --ds-split is the results pane's share, written by
   the page's JS and remembered per browser — CSS does the actual sizing,
   so where the divider is display:none (narrow screens, below) the
   property is simply unused. */
.ds-split { flex: 1 1 auto; min-height: 0; display: flex; flex-direction: column; align-items: stretch; }
.ds-results { flex: 1 1 auto; min-width: 0; min-height: 0; overflow: auto; padding: 1rem 1.25rem; }
.ds-split.has-data .ds-results { flex: 0 0 var(--ds-split, 55%); }
/* The height bar under the panes, the divider between them and the twin
   under the tool menu — one bar, one drag, so a horizontal handle means one
   thing on this page: it moves the edge it sits on.
   ds-sized on <body> is what lets the page grow past the window: the fixed
   height/overflow come off <body>, and the split takes --ds-height instead
   of the window's remainder. */
.ds-divider, .ds-hdivider, .ds-tdivider {
  flex: none; height: 10px; cursor: row-resize; touch-action: none;
  background: var(--surface-2);
  border-top: 1px solid var(--border); border-bottom: 1px solid var(--border);
  display: flex; align-items: center; justify-content: center;
}
.ds-divider::after, .ds-hdivider::after, .ds-tdivider::after {
  content: ''; height: 2px; width: 34px; border-radius: 2px;
  background: var(--gray-light);
}
.ds-divider:hover, .ds-divider.dragging, .ds-divider:focus-visible,
.ds-hdivider:hover, .ds-hdivider.dragging, .ds-hdivider:focus-visible,
.ds-tdivider:hover, .ds-tdivider.dragging, .ds-tdivider:focus-visible { background: var(--accent-soft); }
.ds-divider:hover::after, .ds-divider.dragging::after,
.ds-hdivider:hover::after, .ds-hdivider.dragging::after,
.ds-tdivider:hover::after, .ds-tdivider.dragging::after { background: var(--accent); }
.ds-tdivider { height: 9px; }
body.ds-body.ds-sized { height: auto; min-height: 100vh; overflow: visible; }
body.ds-body.ds-sized .ds-split { flex: 0 0 var(--ds-height, 60vh); }
.ds-outputs { display: flex; flex-wrap: wrap; gap: 1rem; align-items: flex-start; }
.ds-outputs .ds-output { margin: 0; max-width: 100%; background: var(--surface); }
.ds-no-results { margin: .2rem 0; }
/* The toolbox form (datasets/workspace.html). Every tool draws the same
   shape, so a tool with one parameter and a tool with four are the same
   panel: the tool's name, then equal-width parameter columns, then the run
   button — all of them cells of one grid, so a two-field tool is one line
   rather than three. `align-items: end` is what puts every control on that
   line — the row is as tall as its tallest label, and a control is always
   the last thing in its field. A hint therefore belongs *inside* the label
   ("Bins · blank = automatic"): a note under a control would push that one
   field's bottom edge past its neighbours'. */
.ds-panel {
  display: grid; grid-template-columns: repeat(auto-fill, minmax(10rem, 1fr));
  gap: .45rem .75rem; align-items: end;
  padding: .5rem 1rem .6rem; background: var(--surface-2);
}
/* The per-tool fields are grid items of the panel itself, not of a box inside
   it — in a box of their own they stack in one narrow column and no two tools
   line up. */
.ds-fields { display: contents; }
.ds-field { display: flex; flex-direction: column; gap: .3rem; min-width: 0; }
/* :not(.ds-check) — a checkbox chip is a label too, and must not be styled
   as one of the small uppercase captions. */
.ds-field > label:not(.ds-check) {
  font-size: .66rem; font-weight: 700; color: var(--text-muted); margin: 0;
  text-transform: uppercase; letter-spacing: .04em; line-height: 1.3;
}
.ds-hint { text-transform: none; letter-spacing: 0; font-weight: 500; opacity: .85; }
.ds-hint::before { content: "\00B7"; margin: 0 .3em; }
.ds-field select, .ds-field input[type=number] {
  width: 100%; max-width: none; font-size: .85rem; padding: .3rem .45rem;
}
.ds-field select:disabled, .ds-field input:disabled {
  background: var(--surface-2); color: var(--text-muted); cursor: not-allowed;
}
/* The tool's name and the run button are ordinary cells, sitting on the same
   line as the fields — the name says which tool this is when the menu above
   is hidden, and the highlighted pill says it when it isn't. An error is the
   one thing that needs room to be read, so showing one is what takes the
   row. The variable list is wide but not a whole row: given 1/-1 it would
   push the group picker and the button onto rows of their own, which is
   three rows for the form every student uses most. Four columns fit a
   normal dataset's variables on one line and leave room beside them; the
   narrow-screen override below is what keeps `span` from inventing columns
   once the panel is down to one. */
.ds-wide { grid-column: span 4; min-width: 0; }
.ds-action-row:has(.ds-error:not([hidden])) { grid-column: 1 / -1; }
.ds-tool-name {
  font-weight: 700; font-size: .88rem; align-self: end; padding-bottom: .32rem;
  line-height: 1.3;
}
/* A checkbox is the one control whose label sits beside it rather than above.
   As a chip it stands as tall as a select, so it lands on the row's baseline
   instead of floating half a line above it. */
.ds-check, .ds-var-check {
  display: inline-flex; align-items: center; gap: .35rem; margin: 0;
  padding: .24rem .55rem; border: 1px solid var(--border); border-radius: 999px;
  background: var(--surface); color: var(--text); cursor: pointer;
  font-size: .8rem; font-weight: 500; line-height: 1.35;
  text-transform: none; letter-spacing: 0;
}
.ds-check:hover, .ds-var-check:hover { border-color: var(--accent); }
.ds-check:has(input:checked), .ds-var-check:has(input:checked) {
  border-color: var(--accent); background: var(--accent-soft);
  color: var(--accent-strong); font-weight: 600;
}
.ds-check input, .ds-var-check input { width: auto; margin: 0; accent-color: var(--accent); }
.ds-var-checks { display: flex; flex-wrap: wrap; gap: .3rem; }
.ds-action-row { display: flex; align-items: center; gap: .6rem; flex-wrap: wrap; }
.ds-error { color: var(--red); font-size: .85rem; }
.ds-output { border: 1px solid var(--border); border-radius: var(--radius-sm); margin: .8rem 0; }
.ds-output-head {
  display: flex; align-items: center; justify-content: space-between; gap: .5rem;
  padding: .4rem .3rem .4rem .75rem; border-bottom: 1px solid var(--border); background: var(--surface-2);
  border-radius: var(--radius-sm) var(--radius-sm) 0 0;
}
.ds-output-title { font-weight: 600; font-size: .9rem; }
.ds-output-body { padding: .6rem .75rem; }
.ds-kv { display: grid; grid-template-columns: max-content 1fr; gap: .25rem 1.25rem; margin: 0; font-size: .9rem; }
.ds-kv dt { color: var(--text-muted); }
.ds-kv dd { margin: 0; font-variant-numeric: tabular-nums; }
.ds-caption { font-weight: 600; font-size: .85rem; margin: .5rem 0 .25rem; }
.ds-result { font-size: .85rem; }
.ds-result td { font-variant-numeric: tabular-nums; }
.ds-note { margin: .5rem 0 0; }
.ds-plot { max-width: 640px; }
.ds-plot svg { width: 100%; height: auto; display: block; }

/* ---- the data pane ----
   The bottom half of the split: it takes whatever the results pane above it
   leaves, the rows scroll inside it, and the search/paging chrome stays put. */
.ds-data {
  flex: 1 1 auto; min-width: 0; min-height: 0;
  display: flex; flex-direction: column; background: var(--surface);
}
.ds-data-tools {
  display: flex; align-items: center; gap: .6rem; padding: .45rem 1.25rem;
  border-bottom: 1px solid var(--border); flex: none;
}
.ds-search { max-width: 12rem; font-size: .85rem; padding: .3rem .5rem; }
.ds-data-scroll { overflow: auto; flex: 1 1 auto; min-height: 0; }
.ds-table { border-collapse: collapse; width: 100%; font-size: .84rem; }
.ds-table th, .ds-table td {
  padding: .35rem .6rem; border-bottom: 1px solid var(--border);
  text-align: left; white-space: nowrap;
}
.ds-table td { font-variant-numeric: tabular-nums; }
.ds-table thead th {
  position: sticky; top: 0; z-index: 1; background: var(--surface-2);
  font-size: .72rem; text-transform: uppercase; letter-spacing: .04em;
  color: var(--text-muted); cursor: pointer; user-select: none;
}
.ds-table thead th:hover { color: var(--text); }
.ds-table td.ds-missing { color: var(--text-muted); }
.ds-table td.ds-missing::after { content: '\B7'; }

/* ---- server-rendered SVG charts (datasets/plots.py) ----
   The data-series palette is the validated colorblind-safe set, assigned in
   fixed slot order s1–s8 — deliberately not the crimson brand: crimson is UI,
   and a data mark must never look like a button. Chrome (grid/axis/ink) is a
   step off the surface, recessive by design. One place to re-theme, but the
   slot ORDER is the colorblind-safety mechanism — re-validate before touching. */
.viz {
  --vz1: #2a78d6; --vz2: #eb6834; --vz3: #1baf7a; --vz4: #eda100;
  --vz5: #e87ba4; --vz6: #008300; --vz7: #4a3aa7; --vz8: #e34948;
  --vz-grid: #e7e6e1; --vz-axis: #c3c2b7; --vz-muted: #898781; --vz-ink: #52514e;
  font-family: var(--font);
}
.viz .vz-grid { stroke: var(--vz-grid); stroke-width: 1; }
.viz .vz-axis { stroke: var(--vz-axis); stroke-width: 1; }
.viz .vz-tick { fill: var(--vz-muted); font-size: 11px; }
.viz .vz-label { fill: var(--vz-ink); font-size: 12px; font-weight: 600; }
.viz .vz-value { fill: var(--vz-ink); font-size: 11px; }
.viz .vz-fill { stroke: none; }
.viz .vz-slice { stroke: var(--surface); stroke-width: 2; }
.viz .vz-slice-label { fill: #fff; font-size: 11px; font-weight: 600; }
.viz .vz-dot { stroke: var(--surface); stroke-width: 2; }
.viz .vz-line { fill: none; stroke-width: 2; stroke-linecap: round; }
.viz .vz-path { fill: none; stroke-width: 2; stroke-linecap: round; stroke-linejoin: round; }
.viz .vz-box { fill-opacity: .12; stroke-width: 2; }
.viz .vz-median { stroke-width: 3; }
.viz .vz-fit { stroke: var(--vz-ink); stroke-width: 2; stroke-linecap: round; }
.viz .s1 { fill: var(--vz1); } .viz .s2 { fill: var(--vz2); }
.viz .s3 { fill: var(--vz3); } .viz .s4 { fill: var(--vz4); }
.viz .s5 { fill: var(--vz5); } .viz .s6 { fill: var(--vz6); }
.viz .s7 { fill: var(--vz7); } .viz .s8 { fill: var(--vz8); }
.viz .vz-line.s1, .viz .vz-path.s1, .viz .vz-box.s1, .viz .vz-median.s1 { stroke: var(--vz1); }
.viz .vz-line.s2, .viz .vz-path.s2, .viz .vz-box.s2, .viz .vz-median.s2 { stroke: var(--vz2); }
.viz .vz-line.s3, .viz .vz-path.s3, .viz .vz-box.s3, .viz .vz-median.s3 { stroke: var(--vz3); }
.viz .vz-line.s4, .viz .vz-path.s4, .viz .vz-box.s4, .viz .vz-median.s4 { stroke: var(--vz4); }
.viz .vz-line.s5, .viz .vz-path.s5, .viz .vz-box.s5, .viz .vz-median.s5 { stroke: var(--vz5); }
.viz .vz-line.s6, .viz .vz-path.s6, .viz .vz-box.s6, .viz .vz-median.s6 { stroke: var(--vz6); }
.viz .vz-line.s7, .viz .vz-path.s7, .viz .vz-box.s7, .viz .vz-median.s7 { stroke: var(--vz7); }
.viz .vz-line.s8, .viz .vz-path.s8, .viz .vz-box.s8, .viz .vz-median.s8 { stroke: var(--vz8); }
.viz .vz-line { fill: none; }
.viz .vz-box { fill-opacity: .12; }
.viz .vz-median { fill: none; }
.viz .vz-path { fill: none; }

/* ---- R code panel in the editor ---- */
.rcode-editor { width: 100%; white-space: pre; overflow-x: auto; tab-size: 2; }
.pycode-editor { width: 100%; white-space: pre; overflow-x: auto; tab-size: 4; }
.rcode-packages, .pycode-packages { margin: .5rem 0; }
.rcode-packages summary, .pycode-packages summary { cursor: pointer; }
.rcode-packages > div, .pycode-packages > div { margin: .25rem 0 0 1rem; }

/* ---- dataset panel in the editor ---- */
.ds-upload-row { display: flex; align-items: center; gap: .75rem; margin: .2rem 0 .6rem; }
.ds-tool-group { border: 1px solid var(--border); border-radius: var(--radius-sm);
  margin: 0 0 .5rem; padding: .4rem .75rem .6rem; }
.ds-tool-group legend { font-size: .8rem; font-weight: 600; padding: 0 .35rem; }
.ds-group-links { font-weight: 400; color: var(--text-muted); }
.ds-tool-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(13rem, 1fr)); gap: .15rem .75rem; }
.ds-tool { font-size: .85rem; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }

/* On a narrow screen the panes keep the same order — results over data — but
   nothing is draggable: a drag divider means nothing to a touch layout. So
   the shares are fixed instead (the table capped, results taking the rest)
   and the sized-mode overrides return the page to fill-the-window. */
@media (max-width: 900px) {
  .ds-divider, .ds-hdivider, .ds-tdivider { display: none; }
  .ds-menubar { max-height: 40vh; }
  /* Below this the panel may be narrower than four columns, and a span wider
     than the grid would have it invent its own. */
  .ds-wide { grid-column: 1 / -1; }
  .ds-split.has-data .ds-results { flex: 1 1 auto; }
  .ds-data { flex: none; max-height: 45vh; border-top: 1px solid var(--border); }
  body.ds-body.ds-sized { height: 100vh; overflow: hidden; }
  body.ds-body.ds-sized .ds-split { flex: 1 1 auto; }
}

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