:root {
  --bg: #eef1f5; --panel: #ffffff; --panel2: #f3f5f8; --line: #d9dee6;
  --text: #232733; --muted: #6b7280; --accent: #3b6ef5; --accent2: #7c5cff;
  --ok: #12a150; --danger: #dc2626; --warn: #d97706;
}
* { box-sizing: border-box; }
body { margin: 0; background: var(--bg); color: var(--text);
  font: 14px/1.5 -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; }
a { color: var(--accent); text-decoration: none; }
a:hover { text-decoration: underline; }

.topbar { display: flex; align-items: center; gap: 20px; padding: 0 20px; height: 54px;
  background: var(--panel); border-bottom: 1px solid var(--line); position: sticky; top: 0; z-index: 10; }
.brand { font-weight: 700; font-size: 16px; color: var(--text); flex-shrink: 0; }
.topbar nav { display: flex; align-items: center; gap: 4px; margin-right: auto; min-width: 0; }
.topbar nav a { color: var(--muted); padding: 6px 12px; border-radius: 6px; }
.topbar nav a:hover { background: var(--panel2); text-decoration: none; }
.topbar nav a.active { color: var(--text); background: var(--panel2); }
/* The current project's name, shown as a breadcrumb next to "Projects"
   instead of repeating it in a page-level <h1> on every project page —
   see project_base.html. */
.crumb-sep { color: var(--muted); flex-shrink: 0; }
.crumb-project { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.who { display: flex; align-items: center; gap: 10px; font-size: 13px; color: var(--muted); }
.topbar-toggle { display: none; }
.who .email { color: var(--text); }
.logout { color: var(--muted); }

.container { max-width: 1100px; margin: 0 auto; padding: 24px 20px 60px; }
.container-wide { max-width: none; margin: 0; padding: 24px 20px 60px; }
h1 { font-size: 22px; margin: 6px 0 12px; }
h2 { font-size: 16px; margin: 22px 0 10px; }
.muted { color: var(--muted); }
.small { font-size: 12px; }

.role { text-transform: uppercase; font-size: 10px; font-weight: 700; letter-spacing: .5px;
  padding: 2px 7px; border-radius: 10px; }
.role-admin { background: rgba(124,92,255,.14); color: #6d4bd6; }
.role-reviewer { background: rgba(59,110,245,.12); color: var(--accent); }

.flash { padding: 10px 14px; border-radius: 8px; margin: 10px 0; }
.flash.ok { background: rgba(18,161,80,.12); color: #0f7a3d; border: 1px solid rgba(18,161,80,.28); }
.flash.err { background: rgba(220,38,38,.1); color: #b42318; border: 1px solid rgba(220,38,38,.28); }
.flash.warn { background: rgba(217,119,6,.12); color: #92400e; border: 1px solid rgba(217,119,6,.28); }

table { width: 100%; border-collapse: collapse; margin: 8px 0; }
th, td { text-align: left; padding: 9px 10px; border-bottom: 1px solid var(--line); vertical-align: top; }
th { color: var(--muted); font-weight: 600; font-size: 12px; text-transform: uppercase; letter-spacing: .4px; }
tr.dim { opacity: .55; }

.card { background: var(--panel); border: 1px solid var(--line); border-radius: 10px; padding: 16px 18px; margin: 12px 0;
  box-shadow: 0 1px 2px rgba(16,24,40,.04); }
.stat-cards { display: grid; grid-template-columns: repeat(auto-fit, minmax(160px, 1fr)); gap: 12px; margin: 12px 0; }
.stat-card { background: var(--panel); border: 1px solid var(--line); border-radius: 10px; padding: 14px 16px;
  box-shadow: 0 1px 2px rgba(16,24,40,.04); }
.stat-card .stat-value { font-size: 26px; font-weight: 700; line-height: 1.2; }
.stat-card .stat-label { font-size: 12px; color: var(--muted); text-transform: uppercase; letter-spacing: .4px; margin-top: 4px; }
.row-form { display: flex; gap: 14px; align-items: flex-end; flex-wrap: wrap; margin-bottom: 10px; }
.row-form .grow { flex: 1; min-width: 220px; }
.row-form label { display: block; font-size: 12px; color: var(--muted); margin-bottom: 4px; }

/* Required-field marker. On a form where most fields are optional, which
   four are not has to be visible before someone types, not discovered by a
   browser tooltip on submit — see _student_add_block.html. */
.req::after { content: " *"; color: var(--danger); font-weight: 700; }
.req-note { font-size: 12px; color: var(--muted); }
.req-note::before { content: "*"; color: var(--danger); font-weight: 700; margin-right: 4px; }

.form-section { background: var(--panel2); border: 1px solid var(--line); border-radius: 10px;
  padding: 16px 18px; margin: 14px 0; }
.form-section + .form-section { margin-top: 14px; }
.form-section .row-form:last-child { margin-bottom: 0; }
.form-section-title { font-size: 14px; font-weight: 700; color: var(--text); margin: 0 0 12px;
  display: flex; align-items: center; gap: 8px; }
.form-section-title .muted { font-weight: 400; font-size: 12px; }

.chip-check:has(input:checked) { background: rgba(59,110,245,.12); border-color: var(--accent); color: var(--accent); font-weight: 600; }
.task-card { border-left: 3px solid var(--accent); }

.task-select-list { max-height: 260px; overflow-y: auto; border: 1px solid var(--line); border-radius: 7px;
  background: var(--panel); margin-top: 4px; }
.task-select-row { display: flex; align-items: center; gap: 8px; padding: 9px 12px; margin: 0;
  font-size: 14px; cursor: pointer; border-bottom: 1px solid var(--line); }
.task-select-row:last-child { border-bottom: none; }
.task-select-row:hover { background: var(--panel2); }
.task-select-row:has(input:checked) { background: rgba(59,110,245,.1); }
.task-select-row input { width: auto; flex-shrink: 0; }

.combo { position: relative; }
.combo .task-select-list { display: none; position: absolute; top: 100%; left: 0; right: 0; z-index: 30;
  box-shadow: 0 8px 24px rgba(0,0,0,.15); }
.combo .task-select-list.combo-open { display: block; }

input, select { width: 100%; background: var(--panel); border: 1px solid var(--line); color: var(--text);
  padding: 8px 10px; border-radius: 7px; font-size: 14px; }
input:focus, select:focus { outline: none; border-color: var(--accent); box-shadow: 0 0 0 3px rgba(59,110,245,.12); }
input[type="checkbox"], input[type="radio"] { width: auto; padding: 0; }

.choice-cell { padding: 0 !important; }
.choice-cell label { display: flex; align-items: center; gap: 8px; padding: 10px; margin: 0;
  cursor: pointer; border-radius: 6px; }
.choice-cell label:hover { background: var(--panel2); }
.choice-cell label:has(input:checked) { background: rgba(59,110,245,.12); }
.select-all-link { font-size: 11px; font-weight: 600; text-transform: none; letter-spacing: normal;
  cursor: pointer; color: var(--accent); margin-left: 6px; }
.select-all-link:hover { text-decoration: underline; }
.cta-row { display: flex; gap: 10px; align-items: center; margin-top: 10px; }
/* A row of actions that stays on one line and scrolls rather than wrapping
   — see the calling grid, where wrapping cost a second row above the table
   that is the whole point of the page. app.js fades whichever edge has
   something past it. */
.action-row { display: flex; flex-wrap: nowrap; overflow-x: auto; align-items: center; gap: 8px;
  margin-bottom: 8px; scrollbar-width: none; -webkit-overflow-scrolling: touch; }
.action-row::-webkit-scrollbar { display: none; }
.action-row > * { flex-shrink: 0; }

/* A guest's campaign page has no sidenav (see project_base.html), so the
   layout collapses to a single full-width column instead of leaving the
   sidebar's empty track. */
.project-layout-bare { display: block; }

/* Phone number in the calling grid: a button, not a bare number, because
   tapping it offers Call / WhatsApp / Add to contacts rather than only the
   browser's own dial action. Styled as a link so the grid still reads as
   data. See openPhoneMenu in app.js. */
.phone-btn { background: none; border: none; padding: 0; font: inherit; color: var(--accent);
  cursor: pointer; text-decoration: underline; text-underline-offset: 2px; }
/* The dial button a campaign shows instead of the parent's number when it
   goes through mCube (CallCampaign.mcube_enabled). Deliberately a real
   button rather than the link-styled .phone-btn above: it performs an action
   with a side effect — it makes a phone ring — instead of opening a menu.
   See mcubeDial in app.js. */
.mcube-btn { font: inherit; font-size: 12.5px; padding: 3px 10px; cursor: pointer;
  color: var(--accent); background: var(--panel); border: 1px solid var(--accent);
  border-radius: 6px; white-space: nowrap; }
.mcube-btn:hover:not(:disabled) { background: var(--panel2); }
.mcube-btn:disabled { color: var(--muted); border-color: var(--line); cursor: not-allowed; }
/* Held for a few seconds after a dial is accepted, then reverted — a caller
   whose phone did not ring has to be able to press again. */
.mcube-btn.save-ok { color: #0f7a3d; border-color: var(--ok); background: rgba(18,161,80,.08); }

.phone-menu { position: fixed; z-index: 60; display: flex; flex-direction: column; padding: 4px;
  min-width: 170px; background: var(--panel); border: 1px solid var(--line); border-radius: 10px;
  box-shadow: 0 8px 24px rgba(16,24,40,.18); }
.phone-menu a { padding: 11px 12px; border-radius: 6px; color: var(--text); font-size: 14px; }
.phone-menu a:hover { background: var(--panel2); text-decoration: none; }

.status-dot { display: inline-flex; align-items: center; gap: 6px; font-size: 12.5px; color: var(--muted); white-space: nowrap; }
.status-dot::before { content: ""; width: 7px; height: 7px; border-radius: 50%; flex-shrink: 0; background: var(--muted); }
.status-dot.ok { color: #0f7a3d; }
.status-dot.ok::before { background: var(--ok); }
.status-dot.pending { color: #92400e; }
.status-dot.pending::before { background: var(--warn); }
.status-dot.bad { color: #b42318; }
.status-dot.bad::before { background: var(--danger); }

.chip { display: inline-block; background: var(--panel2); border: 1px solid var(--line);
  padding: 1px 8px; border-radius: 10px; font-size: 12px; margin: 1px 3px 1px 0; }
.chip-check { display: inline-flex; align-items: center; gap: 6px; background: var(--panel2);
  border: 1px solid var(--line); padding: 6px 10px; border-radius: 8px; font-size: 13px; width: auto; }
.chip-check input { width: auto; }
.thumb { width: 32px; height: 32px; object-fit: cover; border-radius: 6px; border: 1px solid var(--line); }
.qcell { max-width: 220px; }
.meta-pre { white-space: pre-wrap; max-height: 200px; overflow: auto; font-size: 11px;
  margin: 6px 0 0; padding: 6px 8px; background: var(--panel2); border-radius: 6px; }
/* Caps a multi-line table cell's own height instead of letting long content
   (many contacts, many resolved course ids) stretch the whole row — applying
   overflow directly on a <td> doesn't reliably clip in every browser, so this
   wraps the cell's content in a block-level element to constrain instead. */
.cell-scroll { display: block; max-height: 72px; overflow-y: auto; white-space: pre-line; }
/* Same multi-line rendering as .cell-scroll but no height cap — the row
   just grows to fit instead of scrolling internally. */
.cell-wrap { white-space: pre-line; }

.pill { font-size: 11px; font-weight: 700; padding: 2px 8px; border-radius: 10px; text-transform: uppercase; }
.pill-ok { background: rgba(18,161,80,.15); color: #0f7a3d; }
.pill-off { background: rgba(220,38,38,.14); color: #b42318; }
.pill-pending { background: rgba(217,119,6,.15); color: #92400e; }

/* Inline loading indicator for an ajax form's submit button — see
   setFormLoading in app.js. Sized off font-size/em so it drops into a
   .pill button (11px) or a normal .btn (14px) without extra tuning, and
   currentColor picks up whatever text color the button already has. */
.btn-spinner {
  display: inline-block; width: 1em; height: 1em; vertical-align: -0.15em;
  border: 2px solid currentColor; border-right-color: transparent;
  border-radius: 50%; opacity: 0.6; animation: btn-spin 0.6s linear infinite;
}
@keyframes btn-spin { to { transform: rotate(360deg); } }
@media (prefers-reduced-motion: reduce) {
  .btn-spinner { animation-duration: 1.5s; }
}

.list-head { display: flex; align-items: center; justify-content: space-between; gap: 12px; }
/* A summed row that sits at the top of a table rather than the bottom, so
   it stays on screen while the rows below scroll. */
tr.row-total > td { background: var(--panel2); border-bottom: 2px solid var(--line); }

/* Attendance and school-wise grids. Column freezing is the shared enhanced
   table's job (data-enh-freeze-cols), not hand-rolled sticky rules — two
   mechanisms fighting over `position: sticky` on the same cells is how a
   frozen column ends up scrolling out from under its own header. What is
   left here is cell colour and density. */
.attendance td, .attendance th { padding: 4px 8px; font-size: 13px; white-space: nowrap; }
.attendance td { text-align: center; }
.attendance .att-email { text-align: left; }
.att-pending { background: #fadbd8; }
.att-off { background: #e6e6e6; color: #666; }
.att-leave { background: #fdf3d0; }
.att-key { padding: 1px 6px; border-radius: 4px; font-size: 11px; }
.school-wise .sw-school { min-width: 220px; }
.school-wise .sw-list { white-space: normal; min-width: 170px; font-size: 12px; }
/* The From/To window above a report's table. Centre-aligned, not
   baseline/end: a date input and a button have different intrinsic
   heights, so anything but centre leaves the buttons sitting proud. */
.report-window { display: flex; gap: 8px; align-items: center; flex-wrap: wrap; margin: 10px 0; }
.report-window input { width: auto; }
.report-window label { margin: 0; }
/* Colour key for the attendance grid, below its table rather than inside
   the shared table toolbar — the toolbar's other controls all DO
   something, so parking three inert swatches among them read as buttons
   that were broken rather than as a legend. */
.att-legend { display: flex; gap: 10px; align-items: center; flex-wrap: wrap; margin-top: 8px; }

/* Adoption's funnel. Steps sit in one row and narrow left to right, so the
   drop between two of them is visible as shape before anyone reads a number. */
.funnel { display: flex; gap: 8px; align-items: stretch; flex-wrap: wrap; margin: 8px 0 4px; }
.funnel-step { flex: 1 1 150px; background: var(--panel); border: 1px solid var(--line);
  border-radius: 8px; padding: 11px 13px; border-left: 3px solid var(--accent); }
.funnel-step:nth-child(2) { border-left-color: #5b86f0; }
.funnel-step:nth-child(3) { border-left-color: var(--warn); }
.funnel-step:nth-child(4) { border-left-color: #d2691e; }
.funnel-step:nth-child(5) { border-left-color: var(--ok); }
.funnel-l { font-size: 12px; color: var(--muted); text-transform: uppercase; letter-spacing: .4px; }
.funnel-v { font-size: 24px; font-weight: 700; line-height: 1.25; font-variant-numeric: tabular-nums; }
.funnel-note { margin-top: 2px; opacity: .75; }

.chart-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(320px, 1fr)); gap: 12px; }
.chart-sub { margin: 0 0 6px; font-size: 13px; color: var(--muted); font-weight: 600; }
/* Chart.js sizes a responsive canvas to its container, so the container is
   what has to have a height — and JS sets it per chart from the bar count.
   position:relative is what Chart.js measures against. */
.chart-box { position: relative; width: 100%; }

@media (max-width: 720px) {
  /* One chart per row below the breakpoint the rest of the app already uses:
     two 320px columns do not fit a phone, and a squeezed bar chart is worse
     than a scrolled one. */
  .chart-grid { grid-template-columns: 1fr; gap: 10px; }
  /* Five funnel steps two-up rather than five-up: 150px each across a phone
     leaves no room for a seven-figure number and its two percentages. */
  .funnel-step { flex: 1 1 132px; padding: 9px 11px; }
  .funnel-v { font-size: 20px; }
  .funnel-note { display: none; }
}

/* Column formatting from the table toolbar: fill, text colour, alignment
   (per column) and text size (whole table). Saved per user in table prefs.
   Each token defines --enh-fill/--enh-ink once and both the swatch in the
   toolbar and the cells in the table read it, so the button can never show
   a different colour from the column it paints.
   Fills are pale on purpose: a column still has to be readable with the
   app's normal text on top of it, and it sits next to five others. */
.enh-fill-1 { --enh-fill: #fff3c4; }   /* yellow */
.enh-fill-2 { --enh-fill: #d7f2e3; }   /* green  */
.enh-fill-3 { --enh-fill: #fde2e1; }   /* red    */
.enh-fill-4 { --enh-fill: #dce7fd; }   /* blue   */
.enh-fill-5 { --enh-fill: #e9e2fd; }   /* purple */
.enh-fill-6 { --enh-fill: #e6e9ef; }   /* grey   */
.enh-ink-1 { --enh-ink: #b42318; }
.enh-ink-2 { --enh-ink: #0f7a3d; }
.enh-ink-3 { --enh-ink: #2b52c9; }
.enh-ink-4 { --enh-ink: #6b7280; }

/* Two classes deep so this beats `.enh-table th, .enh-table td`, which sets
   a background of its own so frozen columns do not show the table through. */
.enh-table td.enh-fill-1, .enh-table th.enh-fill-1,
.enh-table td.enh-fill-2, .enh-table th.enh-fill-2,
.enh-table td.enh-fill-3, .enh-table th.enh-fill-3,
.enh-table td.enh-fill-4, .enh-table th.enh-fill-4,
.enh-table td.enh-fill-5, .enh-table th.enh-fill-5,
.enh-table td.enh-fill-6, .enh-table th.enh-fill-6 { background: var(--enh-fill); }
.enh-table td.enh-ink-1, .enh-table th.enh-ink-1,
.enh-table td.enh-ink-2, .enh-table th.enh-ink-2,
.enh-table td.enh-ink-3, .enh-table th.enh-ink-3,
.enh-table td.enh-ink-4, .enh-table th.enh-ink-4 { color: var(--enh-ink); }
.enh-table td.enh-ink-1 a, .enh-table td.enh-ink-2 a,
.enh-table td.enh-ink-3 a, .enh-table td.enh-ink-4 a { color: inherit; }

.enh-table td.enh-align-l, .enh-table th.enh-align-l { text-align: left; }
.enh-table td.enh-align-c, .enh-table th.enh-align-c { text-align: center; }
.enh-table td.enh-align-r, .enh-table th.enh-align-r { text-align: right; }

.enh-table.enh-text-sm td, .enh-table.enh-text-sm th { font-size: 12px; padding: 6px 8px; }
.enh-table.enh-text-lg td, .enh-table.enh-text-lg th { font-size: 16px; padding: 11px 12px; }

/* Toolbar controls */
.enh-swatches, .enh-seg { display: flex; gap: 3px; align-items: center; }
.enh-sw { width: 17px; height: 17px; border: 1px solid var(--line); border-radius: 3px;
          padding: 0; cursor: pointer; background: var(--enh-fill, #fff); }
.enh-sw.enh-ink-1, .enh-sw.enh-ink-2, .enh-sw.enh-ink-3, .enh-sw.enh-ink-4 {
  background: var(--enh-ink); }
/* "None" reads as a cleared cell, not as another colour to choose. */
.enh-sw-none { background: linear-gradient(to top left, transparent calc(50% - 1px),
               var(--danger) calc(50% - 1px), var(--danger) calc(50% + 1px),
               transparent calc(50% + 1px)) #fff; }
.enh-sw.on, .enh-seg button.on { outline: 2px solid var(--accent); outline-offset: 1px; }
.enh-seg button { min-width: 22px; height: 19px; padding: 0 5px; font-size: 11px; font-weight: 600;
                  border: 1px solid var(--line); border-radius: 3px; background: var(--panel);
                  color: var(--muted); cursor: pointer; }
.enh-swatches[aria-disabled="true"], .enh-seg[aria-disabled="true"] { opacity: .4; pointer-events: none; }

/* Instant tutoring's "Today" view sits beside Weekly and Daily but is not
   the same kind of thing — it is read off the replica on demand, where the
   other two come from the synced window. The dot says so without a word. */
.chip-live::before { content: ""; display: inline-block; width: 6px; height: 6px;
  border-radius: 50%; background: var(--ok); margin-right: 5px; vertical-align: middle; }

/* Export menu in the table toolbar.
   The trigger is styled as toolbar text, not as a .btn: it sits beside
   "Table options", which is a plain muted label, and a filled/bordered
   button next to that reads as the one important thing on a toolbar of
   equals. Position is fixed, set in JS — .enh-toolbar scrolls sideways
   (overflow-x: auto), and an overflow ancestor clips an absolutely
   positioned menu, which is what put this behind the table. */
.enh-export { position: relative; }
.enh-text-btn { background: none; border: none; padding: 4px 6px; border-radius: 6px;
  font: inherit; font-size: 12px; color: var(--muted); cursor: pointer; white-space: nowrap; }
.enh-text-btn:hover { background: var(--panel2); color: var(--text); }
.enh-text-btn[aria-expanded="true"] { background: var(--panel2); color: var(--text); }
.enh-caret { font-size: 9px; }
.export-menu { position: fixed; z-index: 60;
  display: flex; flex-direction: column; min-width: 190px; padding: 4px;
  background: var(--panel); border: 1px solid var(--line); border-radius: 9px;
  box-shadow: 0 8px 24px rgba(16,24,40,.18); }
.export-menu[hidden] { display: none; }
.export-menu a, .export-menu button { display: block; width: 100%; text-align: left;
  padding: 9px 11px; border: none; background: none; border-radius: 6px;
  font: inherit; font-size: 13px; color: var(--text); cursor: pointer; }
.export-menu a:hover, .export-menu button:hover { background: var(--panel2); text-decoration: none; }
.export-menu button:disabled { color: var(--muted); cursor: default; }
.filters { display: flex; gap: 8px; align-items: center; margin: 8px 0 16px; }
.filters input { width: 260px; }

.btn { background: var(--accent); color: #fff; border: none; padding: 8px 14px; border-radius: 7px;
  font-weight: 600; cursor: pointer; font-size: 14px; display: inline-block; }
.btn:hover { filter: brightness(1.06); text-decoration: none; }
.btn-sm { padding: 5px 10px; font-size: 13px; }
.btn-light { background: var(--panel2); color: var(--text); border: 1px solid var(--line); }
.btn-danger { background: var(--danger); color: #fff; }
.inline-form { display: inline-block; }

/* App-themed confirm dialog — replaces window.confirm() (see appConfirm
   in app.js). Built once and reused, hidden via display:none/flex rather
   than added/removed from the DOM. */
.app-confirm-overlay { display: none; position: fixed; inset: 0; background: rgba(15,18,25,.45);
  align-items: center; justify-content: center; z-index: 1000; padding: 20px; }
.app-confirm-box { background: var(--panel); border: 1px solid var(--line); border-radius: 12px;
  padding: 20px 22px; max-width: 420px; width: 100%; box-shadow: 0 12px 32px rgba(16,24,40,.18); }
.app-confirm-message { margin: 0 0 18px; font-size: 14px; line-height: 1.5; color: var(--text); white-space: pre-line; }
.app-confirm-actions { display: flex; justify-content: flex-end; gap: 8px; }
.add-toggle { cursor: pointer; display: inline-block; background: var(--panel2); color: var(--text);
  border: 1px solid var(--line); border-radius: 7px; padding: 5px 10px; font-size: 13px; font-weight: 600;
  margin: 8px 0; list-style: none; }
.add-toggle::-webkit-details-marker { display: none; }
.add-toggle:hover { filter: brightness(1.06); }
details[open] > .add-toggle { border-radius: 7px 7px 0 0; margin-bottom: 0; }
.section-toggle { cursor: pointer; list-style: none; font-size: 16px; font-weight: 700; margin: 22px 0 10px;
  display: flex; align-items: center; gap: 8px; color: var(--text); }
.section-toggle::-webkit-details-marker { display: none; }
.section-toggle::before { content: '▸'; display: inline-block; font-size: 12px; color: var(--muted);
  transition: transform .1s; }
details[open] > .section-toggle::before { transform: rotate(90deg); }
.link-btn { background: none; border: none; color: var(--accent); cursor: pointer; font-size: 12px; padding: 0 4px; }

/* A collapsible section heading — see the collapsible-sections block in
   app.js. The caret is on the heading rather than a separate control so the
   whole title is the hit area. */
h3.section-head { cursor: pointer; display: flex; align-items: center; gap: 9px; user-select: none; }
h3.section-head::before { content: '▾'; font-size: 11px; color: var(--muted); transition: transform .12s; }
h3.section-head.collapsed::before { transform: rotate(-90deg); }
h3.section-head:hover { color: var(--accent); }
h3.section-head:focus-visible { outline: 2px solid var(--accent); outline-offset: 3px; border-radius: 4px; }
@media (prefers-reduced-motion: reduce) { h3.section-head::before { transition: none; } }
.link-btn:hover { text-decoration: underline; }
.config-box { width: 100%; font-family: ui-monospace, monospace; font-size: 12.5px; resize: vertical; }

.project-layout { display: flex; gap: 28px; align-items: flex-start; }
.project-sidenav { width: 180px; flex-shrink: 0; display: flex; flex-direction: column; gap: 2px;
  position: sticky; top: 74px; align-self: flex-start; transition: width .12s; }
.project-sidenav a { padding: 7px 10px; border-radius: 6px; color: var(--text); font-size: 13.5px; }
.project-sidenav a:hover { background: var(--panel2); text-decoration: none; }
.project-sidenav a.active { background: var(--panel2); font-weight: 600; color: var(--accent); }
.project-sidenav .sub { display: flex; flex-direction: column; gap: 2px; margin: 2px 0 2px 10px;
  padding-left: 10px; border-left: 2px solid var(--line); }
.project-sidenav .sub a { font-size: 13px; padding: 5px 10px; color: var(--muted); }
.project-content { flex: 1; min-width: 0; }
/* Collapse toggle lives inside the sidenav itself so it stays visible (and
   in the same place) whether expanded or collapsed — see toggleSidenav()
   in app.js. Collapsed state hides the links/labels and shrinks the rail
   down to just the toggle button; .project-content's flex:1 reclaims the
   freed width automatically. */
.sidenav-toggle { align-self: flex-end; background: none; border: 1px solid var(--line); border-radius: 6px;
  width: 26px; height: 26px; cursor: pointer; color: var(--muted); font-size: 13px; margin-bottom: 4px; flex-shrink: 0; }
.sidenav-toggle:hover { background: var(--panel2); color: var(--text); }
/* The glyph follows the state class rather than being written by JS, so
   it is correct on first paint instead of after app.js runs. */
.sidenav-toggle::before { content: '\00AB'; }
.project-sidenav.collapsed .sidenav-toggle::before { content: '\00BB'; }
.sidenav-toggle-label { display: none; }
.project-sidenav.collapsed { width: 34px; }
.project-sidenav.collapsed a, .project-sidenav.collapsed .sub { display: none; }
.project-sidenav.collapsed .sidenav-toggle { align-self: center; }
@media (max-width: 720px) {
  /* At 390px the brand, nav, email and logout cannot share one 54px row:
     the flex row crushed .crumb-project down to 24px, so the project name
     — the single most useful piece of context on a phone — was an
     unreadable sliver overlapping the email. Let the bar wrap to as many
     rows as it needs and drop the email (Logout still ends the session),
     which costs ~40px and makes nothing unreachable. */
  /* The bar carries brand, up to six nav links, email and logout. At 390px
     that wrapped to four rows for a platform admin (137px of chrome above
     every page), and before that it crushed .crumb-project to 24px — the
     project name, the context that matters most on a phone, unreadable.
     So on a phone the bar shows only where you are: the one .active link,
     which is the project crumb on a project page and "Jobs"/"Perf" on those.
     Everything else is one tap away behind the ⋯ button, which is why the
     links can be hidden rather than dropped. The email goes for good;
     Logout still ends the session. */
  .topbar { height: auto; min-height: 54px; flex-wrap: wrap; gap: 4px 12px; padding: 8px 14px; }
  .topbar nav { flex-wrap: wrap; }
  .who .email { display: none; }
  .topbar-toggle { display: inline-flex; align-items: center; justify-content: center;
    width: 30px; height: 30px; flex-shrink: 0; background: none; color: var(--muted);
    border: 1px solid var(--line); border-radius: 6px; font-size: 15px; line-height: 1; cursor: pointer; }
  .topbar:not(.topbar-open) nav a:not(.active) { display: none; }
  .topbar:not(.topbar-open) .crumb-sep { display: none; }
  .crumb-project { max-width: 48vw; }
  /* Expanded, the full list takes its own line under the brand rather than
     wrapping around it, so it reads as a list instead of a jumble. */
  .topbar.topbar-open nav { flex-basis: 100%; margin-right: 0; }
  .topbar.topbar-open .topbar-toggle { background: var(--panel2); color: var(--text); }


  .project-layout { flex-direction: column; gap: 12px; }

  /* A column, not a wrapping row. Wrapped rows put a block-level .sub
     mid-line, so "Reports" rendered *after* its own children and Field
     reports' submenu interleaved with "Services" and "Members". A column
     keeps every child under its parent. */
  .project-sidenav { width: 100%; flex-direction: column; position: static; }

  /* Collapsed is the default on a phone, and it is expressed here rather
     than by a class JS adds on load — expanded, this menu is 430px of the
     844px screen, and letting that paint before snapping shut is worse
     than not having fixed it. .sidenav-open is what toggleSidenav()
     switches; .collapsed stays desktop-only. */
  .project-sidenav:not(.sidenav-open) a,
  .project-sidenav:not(.sidenav-open) .sub { display: none; }

  /* Collapsed here is a full-width bar naming the section you are in, not
     the 34px desktop rail — when it is the only nav on screen, a bare
     chevron tells you nothing about where you are, and there is room. */
  .project-sidenav .sidenav-toggle {
    align-self: stretch; width: 100%; height: auto; min-height: 44px;
    display: flex; align-items: center; gap: 10px; padding: 0 14px;
    background: var(--panel); border-radius: 8px; margin-bottom: 0;
    font-size: 14px; font-weight: 600; color: var(--text); text-align: left; }
  .project-sidenav.sidenav-open .sidenav-toggle { margin-bottom: 6px; }
  .project-sidenav .sidenav-toggle::before { content: '\2630'; font-size: 15px; color: var(--muted); }
  .project-sidenav .sidenav-toggle::after { content: '\25BE'; margin-left: auto; font-size: 11px; color: var(--muted); }
  .project-sidenav.sidenav-open .sidenav-toggle::after { content: '\25B4'; }
  .sidenav-toggle-label { display: block; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }

  /* Finger-sized rows once the menu is open. */
  .project-sidenav a { padding: 10px 12px; font-size: 14px; }
  .project-sidenav .sub a { padding: 8px 12px; font-size: 13.5px; }

  /* .filters and .list-head are non-wrapping flex rows, and .filters input
     carries a fixed 260px width — so their min-content width is the sum of
     their children and they refuse to shrink. Since .project-layout is a
     *column* flex container here, that min-content width becomes the width
     of .project-content: measured at 1050px inside a 350px parent on the
     Students list, 1235px on Administration.
     Everything else on the page then inherits it, including .tab-nav —
     which has flex-wrap:wrap and would wrap perfectly well, but never gets
     the chance because its parent is 1050px wide, leaving 3 of 6 tabs off
     the right edge of a 390px screen. Constrain the content column and let
     these two rows wrap, and the page comes back to the viewport. */
  .project-content { min-width: 0; max-width: 100%; }
  .filters, .list-head { flex-wrap: wrap; }
  .filters input { width: 100%; }
}

/* Horizontal tab strip — see _tab_nav.html. */
.tab-nav { display: flex; gap: 4px; flex-wrap: wrap; border-bottom: 1px solid var(--line); margin-bottom: 16px; }
.tab-nav a { padding: 9px 14px; color: var(--muted); font-size: 13.5px; border-bottom: 2px solid transparent;
  margin-bottom: -1px; }
.tab-nav a:hover { color: var(--text); text-decoration: none; }
.tab-nav a.active { color: var(--accent); border-bottom-color: var(--accent); font-weight: 600; }

/* Mobile tab strips stay on one line and scroll. Six tabs carrying counts
   ("File attachments (3812)") wrapped to three rows on a 390px screen and
   pushed the table that far down the page; sideways scrolling inside the
   strip costs one row instead. app.js centres the active tab on load, so
   the strip never opens showing only tabs you are not on.
   This block must live *after* the .tab-nav rules above, not up with the
   rest of the mobile CSS: same specificity, and the later rule wins — put
   it earlier and .tab-nav's own flex-wrap:wrap silently cancels it. */
@media (max-width: 720px) {
  .tab-nav { flex-wrap: nowrap; overflow-x: auto; scrollbar-width: none; -webkit-overflow-scrolling: touch; }
  .tab-nav::-webkit-scrollbar { display: none; }
  .tab-nav a { flex-shrink: 0; }
  /* Softens the cut edges so a tab sliced mid-word ("...hool/grade") reads
     as "there is more this way" rather than as a rendering fault. Applied
     only to strips app.js found to actually overflow, so a short strip
     keeps its first and last tabs at full contrast. */
}
.btn-dev { background: var(--accent2); color: #fff; }

.login-card { max-width: 340px; margin: 12vh auto; text-align: center; background: var(--panel);
  border: 1px solid var(--line); border-radius: 14px; padding: 32px 28px; box-shadow: 0 4px 20px rgba(16,24,40,.08); }
.login-card h1 { font-size: 26px; }
.login-card .btn { display: block; width: 100%; margin: 10px 0; padding: 11px; }

/* Click-to-edit fields: click the value, an input replaces it in place,
   Enter/blur saves, Escape cancels. See toggleInlineEdit's cousins in app.js
   (startFieldEdit/saveField) and school_detail.html for usage. */
.ed-field { border-bottom: 1px dashed var(--line); cursor: text; padding: 0 1px; }
.ed-field:hover { background: var(--panel2); border-bottom-color: var(--accent); }
.ed-field input, .ed-field select { display: inline-block; width: auto; min-width: 70px;
  padding: 2px 5px; font-size: inherit; border-radius: 4px; }

.compact-list { display: flex; flex-direction: column; }
.compact-row { display: flex; align-items: center; gap: 10px; padding: 6px 0; border-bottom: 1px solid var(--line); font-size: 13px; }
.compact-row:last-child { border-bottom: none; }
.compact-row .col { min-width: 0; }
.compact-row .name-col { flex: 2; min-width: 90px; font-weight: 500; }
.compact-row .field-col { flex: 1; min-width: 60px; color: var(--muted); }
.compact-row .spacer { flex: 1; }

.icon-btn { background: none; border: none; cursor: pointer; font-size: 14px; line-height: 1;
  padding: 4px 6px; border-radius: 5px; color: var(--muted); }
.icon-btn:hover { background: rgba(220,38,38,.12); color: var(--danger); }

.bulk-add-row { display: flex; gap: 8px; align-items: center; padding: 4px 0; }
.bulk-add-row input { padding: 5px 8px; font-size: 13px; }
.bulk-add-row .col { min-width: 0; }
.bulk-add-actions { display: flex; gap: 8px; margin-top: 6px; }

/* Sheet-style column filter dropdown (Excel/Sheets-style autofilter) — see
   toggleColumnFilter/applyColumnFilter in app.js and
   _report_school_data_grade_table.html for usage. */
.col-filter-btn { background: none; border: none; cursor: pointer; color: var(--muted);
  font-size: 11px; padding: 2px 3px; margin-left: 2px; border-radius: 3px; }
.col-filter-btn:hover { background: var(--panel2); }
/* A filtered column needs to read at a glance across a wide table, not
   just on close inspection of its tiny ▾ icon — so the whole header cell
   gets a tint too, not just the button. */
th.col-filtered { background: rgba(59,110,245,.12); }
.col-filter-btn.filtered { color: #fff; font-weight: 700; background: var(--accent); border-radius: 3px; }
.col-filter-btn.filtered:hover { background: var(--accent2); }
.col-filter-panel { position: absolute; top: 100%; left: 0; z-index: 20; background: var(--panel);
  border: 1px solid var(--line); border-radius: 8px; box-shadow: 0 6px 20px rgba(16,24,40,.14);
  padding: 8px; min-width: 160px; max-width: 260px; text-transform: none; font-weight: 400; }
.col-filter-search { width: 100%; box-sizing: border-box; font-size: 12.5px; padding: 4px 6px; margin-bottom: 6px; }
.col-filter-actions { display: flex; gap: 10px; margin-bottom: 6px; }
.col-filter-options { max-height: 220px; overflow-y: auto; display: flex; flex-direction: column; gap: 3px; }
.col-filter-option { display: flex; align-items: center; gap: 6px; font-size: 12.5px; font-weight: 400;
  color: var(--text); cursor: pointer; }
.col-filter-option input { width: auto; }
.col-filter-apply { display: flex; gap: 6px; margin-top: 8px; padding-top: 8px; border-top: 1px solid var(--line); }
/* On-demand options: the panel opens before its list exists. See
   loadColumnOptions in app.js. */
.col-filter-loading { display: flex; align-items: center; gap: 7px; font-size: 12.5px;
  color: var(--muted); padding: 6px 2px; font-weight: 400; }
.col-filter-note { font-size: 11.5px; color: var(--muted); font-weight: 400;
  padding: 6px 2px 2px; line-height: 1.45; }
/* The list is fetched, so it can be empty for a moment — hold the height to
   stop the Apply row jumping up and back down under the pointer. */
.col-filter-options { min-height: 34px; }

/* Column "bands": the group header row (e.g. "Sessions scheduled", "Grade
   9") only reads as a boundary if the leaf columns under it are visibly
   tied to it — these tint both the group cell and its matching leaf
   header cells the same color, with a colored bottom border capping the
   group row so the two-row header reads as one unit. See column_header's
   `band` param in _sheet_filter_macros.html and each flattened report
   table for which band goes where. */
thead tr:first-child .col-band-a { border-bottom: 2px solid var(--accent); }
thead tr:first-child .col-band-b { border-bottom: 2px solid var(--accent2); }
thead tr:first-child .col-band-c { border-bottom: 2px solid var(--ok); }
thead tr:first-child .col-band-d { border-bottom: 2px solid var(--warn); }
.col-band-a { background: rgba(59,110,245,.07); }
.col-band-b { background: rgba(124,92,255,.07); }
.col-band-c { background: rgba(18,161,80,.07); }
.col-band-d { background: rgba(217,119,6,.07); }

/* Group-collapse toolbar, sits above the table — see toggleColumnGroup in
   app.js and the group_toolbar/colgroup macros in _sheet_filter_macros.html.
   Deliberately outside the table: a toggle placed inside a group's own
   header cell disappears along with it once collapsed. */
.col-group-toolbar { display: flex; flex-wrap: wrap; gap: 6px; align-items: center; margin-bottom: 8px; }
.col-toggle-chip { background: var(--panel2); border: 1px solid var(--line); cursor: pointer;
  color: var(--text); font-size: 12px; padding: 3px 9px; border-radius: 12px; }
.col-toggle-chip:hover { border-color: var(--accent); }
.col-toggle-chip.col-collapsed { background: transparent; color: var(--muted); text-decoration: line-through; }

/* A shadowed row in the New students grid (see _student_row_macros.html's
   new_row) — stays in place rather than moving to a separate list, just
   dimmed and struck through so it still reads at a glance as "excluded". */
.row-ignored { opacity: .5; }
.row-ignored td { text-decoration: line-through; }
.row-ignored td:last-child { text-decoration: none; }

/* Click-to-edit rows (New students / Invalid — see data-click-to-edit in
   app.js). Excludes cells with their own interactive control (checkbox,
   the Shadow button) — those keep the default cursor via the child
   selector below so it's clear they're a separate action, not "click the
   row". */
.row-editable { cursor: pointer; }
.row-editable:hover { background: rgba(0,0,0,.03); }
.row-editable td:has(input, button, a) { cursor: default; }

/* Attachment thumbnails (see _attachment_gallery.html) + the lightbox
   they open into, built by initLightbox() in app.js. */
.attachment-gallery { display: inline-flex; gap: 4px; flex-wrap: wrap; vertical-align: middle; }
.attachment-thumb { width: 36px; height: 36px; object-fit: cover; border-radius: 5px;
  border: 1px solid var(--line); cursor: pointer; }
.attachment-thumb:hover { border-color: var(--accent); }
.attachment-pdf { display: inline-flex; align-items: center; justify-content: center;
  background: var(--panel2); font-size: 16px; }
.attachment-file-link { font-size: 12px; }

#lightbox-overlay { display: none; position: fixed; inset: 0; background: rgba(15,18,26,.88);
  z-index: 1000; align-items: center; justify-content: center; }
#lightbox-overlay img { max-width: 90vw; max-height: 88vh; border-radius: 6px;
  box-shadow: 0 8px 30px rgba(0,0,0,.4); }
.lightbox-frame { width: 88vw; height: 88vh; border: none; border-radius: 6px;
  background: #fff; box-shadow: 0 8px 30px rgba(0,0,0,.4); display: none; }
.lightbox-close { position: absolute; top: 18px; right: 26px; color: #fff; font-size: 32px;
  cursor: pointer; line-height: 1; user-select: none; }
.lightbox-nav { position: absolute; top: 50%; transform: translateY(-50%); color: #fff;
  font-size: 42px; cursor: pointer; user-select: none; padding: 0 18px; opacity: .8; }
.lightbox-nav:hover { opacity: 1; }
.lightbox-prev { left: 6px; }
.lightbox-next { right: 6px; }

/* Enhanced sheet table: wrap-per-column, always-visible resize handles,
   freeze rows/columns, column drag-reorder, multi-select — see
   initEnhancedTable() in app.js. Progressive enhancement over an
   existing server-rendered <table>; every class here is scoped to a
   table this has actually been applied to (never global th/td rules),
   so it's opt-in per page. */
/* One line at every width, with the row count on it: the toolbar wrapped
   to three rows on a phone, and the flat label/stepper siblings split
   across them so a stepper appeared to belong to the label above it. It
   scrolls sideways instead — the controls stay in their reading order and
   the row count keeps its place at the head of the line. */
.enh-toolbar { display: flex; flex-wrap: nowrap; overflow-x: auto; align-items: center; gap: 8px;
  scrollbar-width: none; -webkit-overflow-scrolling: touch;
  border-bottom: 1px solid var(--line); padding: 4px 2px 8px; margin-bottom: 8px; font-size: 12px; }
/* Anything the page wants on the same bar as the table controls — the row
   count and Clear filters, which would otherwise sit on their own line
   directly above and read as a separate strip. */
.enh-toolbar-lead { display: flex; align-items: center; gap: 8px; }
.enh-collapse-btn { background: none; border: none; cursor: pointer; color: var(--muted); font-size: 10px;
  padding: 2px 4px; border-radius: 4px; display: inline-flex; align-items: center; transition: transform .12s; }
.enh-collapse-btn:hover { background: var(--panel2); color: var(--text); }
.enh-collapse-btn.collapsed { transform: rotate(-90deg); }
.enh-toolbar::-webkit-scrollbar { display: none; }
/* Softens the cut edge of anything that scrolls sideways (tab strips, this
   toolbar) so content sliced mid-word reads as "there is more this way"
   rather than as a rendering fault. Each side is faded only when there is
   really something past it — app.js tracks scroll position, not just
   overflow — because a left fade at scrollLeft 0 dims the first item (the
   row count) to hint at content that is not there. */
.edge-fade-left {
  -webkit-mask-image: linear-gradient(to right, transparent 0, #000 16px);
  mask-image: linear-gradient(to right, transparent 0, #000 16px); }
.edge-fade-right {
  -webkit-mask-image: linear-gradient(to left, transparent 0, #000 16px);
  mask-image: linear-gradient(to left, transparent 0, #000 16px); }
.edge-fade-left.edge-fade-right {
  -webkit-mask-image: linear-gradient(to right, transparent 0, #000 16px, #000 calc(100% - 16px), transparent 100%);
  mask-image: linear-gradient(to right, transparent 0, #000 16px, #000 calc(100% - 16px), transparent 100%); }
.enh-toolbar > * { flex-shrink: 0; }
.enh-toolbar-controls { display: flex; flex-wrap: nowrap; align-items: center; gap: 8px; }
.enh-toolbar-controls > * { flex-shrink: 0; }
.enh-toolbar-controls.enh-hidden { display: none; }
.enh-tool-label { font-size: 11.5px; color: var(--muted); }
.enh-tool-group { display: flex; align-items: center; gap: 8px; }
.enh-divider { width: 1px; align-self: stretch; background: var(--line); margin: 0 4px; opacity: .7; }
.enh-stepper { display: flex; align-items: center; gap: 4px; }
.enh-stepper button { width: 19px; height: 19px; border-radius: 5px; border: 1px solid var(--line);
  background: var(--panel); color: var(--muted); font-size: 12px; line-height: 1; cursor: pointer; }
.enh-stepper button:hover { border-color: var(--accent); color: var(--text); }
.enh-stepper .enh-count { min-width: 14px; text-align: center; font-variant-numeric: tabular-nums;
  font-size: 12px; color: var(--text); }
.enh-icon-btn { background: var(--panel); border: 1px solid var(--line); color: var(--muted);
  width: 24px; height: 24px; border-radius: 5px; cursor: pointer; display: inline-flex;
  align-items: center; justify-content: center; }
.enh-icon-btn:hover:not(:disabled) { border-color: var(--accent); color: var(--text); }
.enh-icon-btn:disabled { opacity: .4; cursor: default; }
.enh-icon-btn.on { background: var(--accent); border-color: var(--accent); color: #fff; }

/* enh-clip-col (applied at init to plain-text/"sheet" columns only — see
   opts.plainColumns in initEnhancedTable) clips to one line by default,
   respecting the <col> width — same as a spreadsheet defaulting to
   Overflow/Clip. enh-wrap-col (toggled per column from the toolbar,
   mutually exclusive with clip) opts back into full multi-line display.
   Columns holding live interactive markup (the school-picker combo, whose
   dropdown is position:absolute and must not be clipped; the Grades
   checkboxes; the Confirm/Discard buttons) deliberately get neither class
   — clipping those would cut off or hide working controls, not just text. */
.enh-table { border-collapse: separate; border-spacing: 0; width: max-content; min-width: 100%; }
.enh-table th, .enh-table td { position: relative; background: var(--panel); }
/* Body cells only. A <th> must NOT get overflow:hidden: every filterable
   header holds a position:absolute .col-filter-panel, and clipping the
   header clips the dropdown to the header's own box — which reads as
   "the filter doesn't open at all". Header labels are short and already
   white-space:nowrap, so they don't need clipping to stay on one line. */
.enh-table td.enh-clip-col {
  max-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.enh-table td.enh-wrap-col, .enh-table th.enh-wrap-col {
  white-space: normal; text-overflow: clip; overflow: visible; max-width: none;
}

.enh-resize-handle { position: absolute; top: 0; right: 0; width: 9px; height: 100%;
  cursor: col-resize; z-index: 4; }
.enh-resize-handle::after { content: ""; position: absolute; top: 4px; bottom: 4px; left: 4px;
  width: 2px; background: var(--line); border-radius: 1px; }
.enh-resize-handle:hover::after, .enh-resize-handle.active::after { background: var(--accent); width: 3px; }

.enh-selectable { cursor: pointer; }
.enh-col-selected { background: rgba(59,110,245,.09) !important; }
th.enh-col-selected { box-shadow: inset 0 -2px 0 var(--accent); }

.enh-row-gutter { width: 34px; min-width: 34px; max-width: 34px; text-align: center; cursor: pointer;
  color: var(--muted); font-size: 11px; user-select: none; font-variant-numeric: tabular-nums;
  position: relative; }
/* Row number and its checkbox occupy the same 34px: the number reads while
   you scan, the checkbox appears where your cursor already is. The checkbox
   is never display:none — it stays focusable, so the column is reachable by
   keyboard, and :focus-within is one of the states that reveals it. */
.enh-row-gutter input[type="checkbox"] { position: absolute; left: 50%; top: 50%;
  transform: translate(-50%, -50%); margin: 0; opacity: 0; pointer-events: none;
  transition: opacity .1s; }
.enh-row-num { transition: opacity .1s; }
tr:hover > .enh-row-gutter .enh-row-num,
.enh-row-gutter:focus-within .enh-row-num,
.enh-row-gutter.enh-checked .enh-row-num { opacity: 0; }
tr:hover > .enh-row-gutter input[type="checkbox"],
.enh-row-gutter:focus-within input[type="checkbox"],
.enh-row-gutter.enh-checked input[type="checkbox"] { opacity: 1; pointer-events: auto; }
/* The header's select-all has no number to trade places with. */
th.enh-row-gutter input[type="checkbox"] { opacity: 1; pointer-events: auto; }
/* A touch screen has no hover, so the reveal-above never fires on its own:
   the first tap only synthesises a hover state and the second does the
   actual selecting. Selecting a row took two taps. Show the box permanently
   where there is no hover and drop the row number — the number is the
   decoration here, selection is the function. */
@media (hover: none) {
  .enh-row-gutter input[type="checkbox"] { opacity: 1; pointer-events: auto; }
  .enh-row-gutter .enh-row-num { opacity: 0; }
}
.enh-row-selected .enh-row-gutter, tr.enh-row-selected td { background: rgba(59,110,245,.09) !important; }
.enh-row-selected .enh-row-gutter { color: var(--accent); font-weight: 700; }

.enh-dragging { opacity: .45; }
th.enh-drop-before { box-shadow: inset 2px 0 0 var(--accent); }
th.enh-drop-after { box-shadow: inset -2px 0 0 var(--accent); }
/* Must out-specify `.enh-table th, .enh-table td { position: relative }`
   above (class+element beats a bare class). When it loses, the cell keeps
   position:relative and the `left` the freeze code sets *shifts* it right
   instead of pinning it — every frozen column slides out of line with its
   header, which is what "freeze columns is broken" looks like. Only the row
   gutter escaped it, because row freezing sets position:sticky inline. */
.enh-table th.enh-frozen, .enh-table td.enh-frozen { position: sticky; z-index: 2; }
.enh-table thead .enh-frozen { z-index: 6; }
.enh-frozen-col-edge { box-shadow: 6px 0 8px -6px rgba(16,24,40,.18); }
.enh-frozen-row-edge td, .enh-frozen-row-edge { box-shadow: 0 6px 8px -6px rgba(16,24,40,.18); }

/* Always-on grid cells (sheet.live_cell). Flat until hovered or focused, so a
   screen full of them reads as a table rather than a wall of form controls. */
.ed-live { width: 100%; min-width: 60px; padding: 3px 5px; font-size: inherit;
  background: transparent; border: 1px solid transparent; border-radius: 4px;
  color: var(--text); box-shadow: none; }
.ed-live:hover { border-color: var(--line); background: var(--panel2); }
.ed-live:focus { border-color: var(--accent); background: var(--panel); outline: none;
  box-shadow: 0 0 0 3px rgba(59,110,245,.12); }
.ed-live.save-ok { border-color: var(--ok); }
.ed-live.save-err { border-color: var(--danger); }

/* Numeric column filter: a min/max range, shown alongside the checkbox list
   when there is one and on its own when the column has too many distinct
   values for a list to be usable. */
.col-filter-range { display: flex; align-items: center; gap: 6px; flex-wrap: nowrap;
  padding: 8px 10px; border-bottom: 1px solid var(--line); }
.col-filter-range input { width: 0; flex: 1 1 0; min-width: 0; padding: 3px 6px; font-size: 12px; }

/* Perf page: severity is carried by colour AND weight, so the eye lands on the
   slow rows without reading every number. Bands are in app/perf.py. */
.sev-ok { color: var(--muted); }
.sev-warn { color: var(--warn); }
.sev-bad { color: var(--danger); }
.sev-worst { color: var(--danger); font-weight: 700; }
tr.row-warn td:first-child { box-shadow: inset 3px 0 0 var(--warn); }
tr.row-bad td:first-child, tr.row-worst td:first-child { box-shadow: inset 3px 0 0 var(--danger); }
/* Share of total time, as its own fixed-width track so the bars are comparable
   down the column — a tint behind right-aligned text was invisible at the
   widths this table actually renders at. */
.share-track { width: 110px; height: 8px; border-radius: 4px; background: var(--line);
  overflow: hidden; }
.share-fill { display: block; height: 100%; border-radius: 4px; background: var(--danger); }
.share-fill.low { background: var(--accent); opacity: .55; }
.stat-card.alarm { border-color: var(--danger); }
.stat-card.alarm .stat-value { color: var(--danger); }
.stat-value { font-size: 22px; font-variant-numeric: tabular-nums; }

/* Row-on-row record comparison (student_batch_review's Possible match /
   Conflict / Matched-no-conflict widgets, and same-document duplicate
   groups). One column per field, one row per record, so agreement reads
   as a shape before anything is read: green where the values match, red
   where they contradict each other, plain where only one side holds a
   value — a blank is missing information, not a disagreement, and
   colouring it red would drown the real conflicts. Kept light: these
   fill most of a card, and saturated blocks at that size are unreadable
   rather than emphatic. */
.cmp-grid { width: 100%; border-collapse: collapse; font-size: 13px; margin: 8px 0 0; }
.cmp-grid th, .cmp-grid td { padding: 5px 9px; border: 1px solid var(--line); text-align: left;
  white-space: nowrap; }
.cmp-grid thead th { background: var(--panel2); color: var(--muted); font-weight: 600; font-size: 12px; }
.cmp-grid tbody th { background: var(--panel2); color: var(--muted); font-weight: 600;
  font-size: 12px; position: sticky; left: 0; }
.cmp-grid td.cmp-same { background: rgba(18, 161, 80, .10); color: #0f7a3d; }
.cmp-grid td.cmp-diff { background: rgba(220, 38, 38, .10); color: #b42318; font-weight: 600; }
/* Differs, but settles itself on commit: a phone number, where both are
   kept. Not red — red is reserved for the contradictions that actually hold
   a row up, and spending it here made a Matched-no-conflict row look stuck.
   The note under the grid says what happens. */
.cmp-grid td.cmp-add { background: rgba(59, 110, 245, .09); color: #2b52c0; }
/* Only one side holds a value. Nothing is in conflict — a blank incoming
   value is ignored outright — so this stays unpainted. */
.cmp-grid td.cmp-one { color: var(--muted); }

/* Instant tutoring report — charts and their filter chips. Server-rendered
   SVG (app/instant_charts.py) reads these custom properties, so the charts
   follow the theme rather than carrying a palette of their own. */
:root { --teal: #0f8ea3; --heat: 59,110,245; }
.filter-group { display: inline-flex; align-items: center; gap: 5px; }
.filter-label { font-size: 11px; color: var(--muted); text-transform: uppercase;
  letter-spacing: .05em; margin-right: 2px; }
.chip { border: 1px solid var(--line); background: var(--panel2); color: var(--muted);
  border-radius: 3px; padding: 3px 10px; font-size: 12px; text-decoration: none; }
.chip:hover { border-color: var(--accent); text-decoration: none; }
.chip-on { background: var(--accent); border-color: var(--accent); color: #fff; font-weight: 600; }
.kpis { display: grid; grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
  gap: 8px; margin: 12px 0; }
.kpi { background: var(--panel); border: 1px solid var(--line); padding: 11px 13px; }
.kpi-l { color: var(--muted); font-size: 10.5px; text-transform: uppercase; letter-spacing: .05em; }
.kpi-v { font-size: 23px; font-weight: 600; margin-top: 2px; font-variant-numeric: tabular-nums; }
.chart-h { font-size: 12px; margin: 22px 0 5px; color: var(--muted);
  text-transform: uppercase; letter-spacing: .06em; }
.chart-card { padding: 12px 14px; }
.chart-card svg { display: block; width: 100%; height: auto; }
.chart-legend { display: flex; gap: 14px; flex-wrap: wrap; color: var(--muted);
  font-size: 11.5px; margin-top: 7px; }
.chart-legend .sw { display: inline-block; width: 11px; height: 11px; border-radius: 2px;
  vertical-align: -1px; margin-right: 5px; }
/* A chart with no data says so. An empty grid reads as a broken chart. */
.chart-empty { color: var(--muted); font-size: 13px; margin: 26px 0; text-align: center; }

/* Batch review's Phones block. Collapsed on landing — a reviewer opens that
   page to work conflicts, and a thousand-row table above the fold is in the
   way of it; the count in the summary is what they need at a glance. */
.phones-block > summary { cursor: pointer; list-style: none; padding: 6px 0; }
.phones-block > summary::-webkit-details-marker { display: none; }
.phones-block > summary::before { content: "\25B8 "; color: #888; }
.phones-block[open] > summary::before { content: "\25BE "; }
.mini-summary td, .mini-summary th { padding: 3px 14px 3px 0; }
.mini-summary .strong-row td { font-weight: 600; }
