/* Design tokens copied from design/ledger-design-system.html -- that file
   stays the reference document; this is the same system applied to a real
   app shell instead of a documentation page. */
:root {
  --cover: #1F3A32;
  --cover-ink: #EDEAE0;
  --paper: #F0EEE5;
  --paper-raised: #E7E3D6;
  --ink: #2E2A22;
  --ink-soft: #5B5648;
  --brass: #C08A2E;
  /* --brass is ~2.6:1 on --paper -- fine for rules and borders, too faint
     for text. --brass-ink is the same hue darkened for text.
     These three all cleared WCAG AA (4.5:1) at ~5.1:1 and were still hard to
     read in practice (reported 2026-10-03 against a real screen): most of
     this app's colored text is small, numeric and tabular, where a bare AA
     pass is not enough. Darkened to ~7:1, which is AAA for normal text.
     Measured on --paper #F0EEE5: oak 5.09 -> 7.15, brass-ink 5.06 -> 6.85,
     flag 5.26 -> 7.11. */
  --brass-ink: #6B4B0D;
  --flag: #7E3B25;
  --oak: #3C543C;
  --rule: rgba(46, 42, 34, 0.16);
  --rule-strong: rgba(46, 42, 34, 0.32);
}
@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) {
    --cover: #16241F;
    --cover-ink: #E7E2D3;
    --paper: #201E19;
    --paper-raised: #2A2822;
    --ink: #ECE7DA;
    --ink-soft: #B4AE9D;
    --brass: #D9A94A;
    --brass-ink: #D9A94A;
    --flag: #D68266;
    --oak: #8DB184;
    --rule: rgba(236, 231, 218, 0.16);
    --rule-strong: rgba(236, 231, 218, 0.34);
  }
}
:root[data-theme="dark"] {
  --cover: #16241F;
  --cover-ink: #E7E2D3;
  --paper: #201E19;
  --paper-raised: #2A2822;
  --ink: #ECE7DA;
  --ink-soft: #B4AE9D;
  --brass: #D9A94A;
  --brass-ink: #D9A94A;
  --flag: #D68266;
  --oak: #8DB184;
  --rule: rgba(236, 231, 218, 0.16);
  --rule-strong: rgba(236, 231, 218, 0.34);
}

* { box-sizing: border-box; }
/* .signin-screen and .app-shell each set their own `display` (flex/grid),
   which has equal-or-higher CSS specificity than the browser's default
   `[hidden] { display: none }` UA rule -- without this, toggling the
   `hidden` attribute in JS silently does nothing and both render stacked
   on top of each other regardless of sign-in state. */
[hidden] { display: none !important; }
/* Every size in this file is in rem, so bumping the root size scales text,
   padding, and spacing together everywhere at once rather than needing to
   touch each individual font-size declaration. */
html { font-size: 18px; }
html, body { height: 100%; margin: 0; }
body {
  background: var(--paper);
  color: var(--ink);
  font-family: "IBM Plex Sans", -apple-system, "Segoe UI", sans-serif;
  line-height: 1.6;
}
h1, h2, h3 {
  font-family: "Fraunces", Georgia, serif;
  font-weight: 600;
  margin: 0;
}

/* Browser-default focus rings were near-invisible on the dark rail. */
:focus-visible { outline: 2px solid var(--ink); outline-offset: 2px; }
.rail :focus-visible { outline-color: var(--brass); }
main:focus { outline: none; } /* skip-link target, not an interactive control */

.skip-link {
  position: absolute;
  left: 0.75rem;
  top: -3rem;
  z-index: 20;
  background: var(--paper);
  color: var(--ink);
  padding: 0.5rem 0.9rem;
}
.skip-link:focus { top: 0.75rem; }

/* All UI icons are inline SVGs (see ICONS in app.js) sized by font-size. */
svg.icon {
  width: 1em;
  height: 1em;
  fill: none;
  stroke: currentColor;
  stroke-width: 2;
  stroke-linecap: round;
  stroke-linejoin: round;
  display: block;
}
.inline-icon { display: inline-block; vertical-align: -0.15em; color: var(--ink-soft); }
.inline-icon svg.icon { display: inline-block; }
/* Test/quiz/exam title flag (skyward/join.py's is_urgent_title) -- brass-ink
   is the same accent used for the rail's active state elsewhere, darkened
   to clear WCAG AA for text/icon use (see the --brass-ink definition). The
   icon shape itself (an "!") carries the meaning, not just the color. */
.urgent-flag { color: var(--brass-ink); }

/* Signed-out state */
.signin-screen {
  min-height: 100vh;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 1rem;
  text-align: center;
  padding: 1.5rem;
}
.signin-screen h1 { font-size: 2rem; }
.signin-screen p { color: var(--ink-soft); max-width: 40ch; margin: 0; }
.signin-button {
  display: inline-block;
  background: var(--cover);
  color: var(--cover-ink);
  border: none;
  padding: 0.65rem 1.4rem;
  font-size: 0.95rem;
  font-family: inherit;
  cursor: pointer;
  text-decoration: none;
}

/* App shell */
.app-shell {
  display: grid;
  grid-template-columns: 15rem minmax(0, 1fr);
  min-height: 100vh;
  /* The rail is sticky at 100vh, so below it the grid cell is empty --
     paint the column so a long page never shows paper under the rail. */
  background: linear-gradient(to right, var(--cover) 15rem, var(--paper) 15rem);
}
/* A flex column so .rail-footer's margin-top:auto actually pins it to the
   bottom (it was a block, so that margin did nothing), and sticky at viewport
   height so "bottom" means the bottom of the screen, not of a long page. */
.rail {
  background: var(--cover);
  color: var(--cover-ink);
  padding: 1.25rem 0;
  display: flex;
  flex-direction: column;
  position: sticky;
  top: 0;
  height: 100vh;
  overflow-y: auto;
}
.rail .students { padding: 0 0.75rem; margin-bottom: 1.1rem; }
.rail .student {
  display: flex;
  flex-direction: column;
  padding: 0.55rem 0.75rem;
  border-left: 3px solid transparent;
  cursor: pointer;
  background: none;
  border-top: none;
  border-right: none;
  border-bottom: none;
  color: inherit;
  font: inherit;
  text-align: left;
  width: 100%;
}
.rail .student.active {
  border-left-color: var(--brass);
  background: rgba(237, 234, 224, 0.08);
}
.rail .student .name { font-weight: 600; font-size: 0.95rem; }
.rail .student .campus { font-size: 0.78rem; opacity: 0.7; }
.rail .divider { height: 1px; background: rgba(237, 234, 224, 0.18); margin: 0.9rem 0.75rem; }
.rail nav { padding: 0 0.75rem; display: flex; flex-direction: column; gap: 0.15rem; }
.rail nav a {
  display: flex;
  justify-content: space-between;
  align-items: center;
  gap: 0.5rem;
  padding: 0.5rem 0.6rem;
  font-size: 0.92rem;
  border-left: 3px solid transparent;
  color: inherit;
  text-decoration: none;
}
.rail nav a.active {
  border-left-color: var(--brass);
  background: rgba(237, 234, 224, 0.1);
  font-weight: 500;
}
.rail nav a .count {
  font-family: "IBM Plex Mono", Consolas, monospace;
  color: var(--flag);
  background: var(--paper);
  padding: 0 0.4rem;
  font-size: 0.78rem;
}
.rail-footer {
  margin-top: auto;
  padding: 0.75rem;
  font-size: 0.82rem;
  display: flex;
  align-items: center;
  gap: 0.75rem;
}
/* Demo mode switch (demo.js): on the sign-in screen and in the rail. It
   inherits its color, so it reads on both the paper and the dark rail. */
.demo-toggle {
  display: inline-flex;
  align-items: center;
  gap: 0.45rem;
  font-size: 0.82rem;
  cursor: pointer;
  opacity: 0.85;
}
.demo-toggle input { accent-color: var(--brass); margin: 0; cursor: pointer; }
.rail .demo-toggle { padding: 0 0.75rem 0.75rem; color: var(--cover-ink); }
.signin-screen .demo-toggle { color: var(--ink-soft); }
.rail-footer #signout-button {
  background: none;
  border: none;
  color: var(--cover-ink);
  opacity: 0.7;
  cursor: pointer;
  font: inherit;
  padding: 0;
  text-decoration: underline;
}
.icon-button {
  display: inline-grid;
  place-items: center;
  background: none;
  border: none;
  color: var(--cover-ink);
  opacity: 0.85;
  cursor: pointer;
  font-size: 1.25rem;
  width: 2.2rem;
  height: 2.2rem;
  padding: 0;
  flex-shrink: 0;
}
.icon-button:hover { opacity: 1; }
.icon-button:disabled { opacity: 0.5; cursor: default; }
/* Manual Skyward refresh -- spins the icon in place while a pull is running
   (see wireRefreshButton in app.js); respects reduced-motion like every
   other animated affordance should. */
#refresh-button.spinning .icon,
.refetch-recommendation.spinning .icon { animation: refresh-spin 1.1s linear infinite; }
@keyframes refresh-spin {
  to { transform: rotate(360deg); }
}
@media (prefers-reduced-motion: reduce) {
  #refresh-button.spinning .icon,
  .refetch-recommendation.spinning .icon { animation: none; }
}
.refresh-note {
  margin: 0 0.75rem 0.6rem;
  padding: 0.4rem 0.6rem;
  font-size: 0.78rem;
  color: var(--cover-ink);
  /* --rule-strong is tuned for --paper, not the sidebar's dark --cover --
     it was nearly invisible here (1.49:1). .rail .divider's own rgba is the
     one already proven to read against --cover; matched, slightly more
     opaque since this is an outlined note, not a hairline divider. */
  border: 1px solid rgba(237, 234, 224, 0.3);
}
/* Same reason #attention-count .count gives --flag text a --paper chip
   instead of using it directly on --cover: --flag measures 1.49:1 against
   --cover on its own, effectively unreadable. */
.refresh-note.error { color: var(--flag); background: var(--paper); border-color: var(--flag); }

/* Settings modal: which classes to hide from every list. */
.modal-backdrop {
  position: fixed;
  inset: 0;
  background: rgba(0, 0, 0, 0.45);
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 1rem;
  z-index: 10;
}
.modal-panel {
  background: var(--paper);
  color: var(--ink);
  border: 1px solid var(--rule-strong);
  padding: 1.5rem;
  width: 100%;
  max-width: 26rem;
  max-height: 80vh;
  overflow-y: auto;
}
.modal-panel .icon-button { color: var(--ink-soft); margin-right: -0.5rem; }
.modal-panel .icon-button:hover { color: var(--ink); }
.modal-header { display: flex; align-items: center; justify-content: space-between; gap: 1rem; }
.modal-header h2 { font-size: 1.3rem; }
.modal-note { color: var(--ink-soft); font-size: 0.88rem; margin: 0.5rem 0 1.1rem; }
.settings-course-list { display: flex; flex-direction: column; gap: 0.1rem; }
.settings-course-row {
  display: flex;
  align-items: center;
  gap: 0.6rem;
  padding: 0.45rem 0;
  border-bottom: 1px solid var(--rule);
  font-size: 0.92rem;
  cursor: pointer;
}
.settings-course-row:last-child { border-bottom: none; }
.settings-course-row .name { flex: 1; }
.settings-course-row .kid-tag { margin-right: 0; }

.page { background: var(--paper); padding: 1.75rem 2rem; }
.page-inner { max-width: 900px; }
/* Course names and assignment titles are server-supplied and can arrive as one
   unbroken token. Outside a table there's no good alternative to breaking it
   (`anywhere`, not `break-word` -- only `anywhere` lowers the min-content
   contribution that was forcing the whole document into horizontal scroll). */
.course-group summary .name,
.trend-list li span,
.attention-card .course,
.settings-course-row { overflow-wrap: anywhere; min-width: 0; }
/* One h1 per view (the page title) and h2 sections under it -- every heading
   used to be the same 1.05rem h3, so the date out-ranked the page title. */
.page h1 { font-size: 1.75rem; line-height: 1.25; margin: 0 0 1.25rem; }
.page h2 { font-family: "IBM Plex Sans", sans-serif; font-size: 1.05rem; font-weight: 600; margin: 2.25rem 0 0.9rem; }

.page-header { text-align: right; margin-bottom: 0.25rem; }
.today-date { color: var(--ink-soft); font-size: 0.88rem; }

/* Family page: per-kid summary strip */
.family-kids { border: 1px solid var(--rule); margin-bottom: 1.75rem; }
.family-kid {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 1rem;
  flex-wrap: wrap;
  width: 100%;
  background: none;
  border: none;
  border-bottom: 1px solid var(--rule);
  padding: 0.75rem 1rem;
  font: inherit;
  color: inherit;
  text-align: left;
  cursor: pointer;
}
.family-kid:last-child { border-bottom: none; }
.family-kid:hover { background: var(--paper-raised); }
.family-kid .name { font-weight: 600; }
/* margin-right:auto keeps the campus next to the name -- under the row's
   space-between it was floating in the middle of the row. */
.family-kid .campus { color: var(--ink-soft); font-size: 0.85rem; margin-left: -0.5rem; margin-right: auto; }
.family-kid .tally { font-size: 0.88rem; color: var(--ink-soft); white-space: nowrap; }
.family-kid .tally b { font-family: "IBM Plex Mono", Consolas, monospace; color: var(--ink); font-weight: 500; }
.family-kid .tally b.missing { color: var(--flag); }
.family-kid .tally b.soon { color: var(--brass-ink); }

.kid-tag {
  display: inline-block;
  font-size: 0.75rem;
  color: var(--ink-soft);
  border: 1px solid var(--rule-strong);
  padding: 0.05rem 0.4rem;
  margin-right: 0.3rem;
  white-space: nowrap;
}

/* Overview stat tiles */
.stat-row { display: flex; flex-wrap: wrap; gap: 1px; background: var(--rule); border: 1px solid var(--rule); margin-bottom: 1.75rem; }
.stat-tile { background: var(--paper); padding: 1rem 1.25rem; min-width: 140px; flex: 1; }
.stat-tile.clickable {
  border: none;
  font: inherit;
  color: inherit;
  text-align: left;
  cursor: pointer;
}
.stat-tile.clickable:hover { background: var(--paper-raised); }
.stat-tile .value { font-family: "IBM Plex Mono", Consolas, monospace; font-size: 1.8rem; }
.stat-tile .value.missing { color: var(--flag); }
.stat-tile .label { color: var(--ink-soft); font-size: 0.85rem; }

.trend-list { list-style: none; margin: 0; padding: 0; }
.trend-list li {
  display: flex;
  justify-content: space-between;
  padding: 0.5rem 0;
  border-bottom: 1px solid var(--rule);
  font-size: 0.92rem;
}
.trend-list .direction-down { color: var(--flag); }
.trend-list .direction-up { color: var(--oak); }
.trend-list .direction-flat { color: var(--ink-soft); }

/* Overview: per-category grade breakdown, collapsed under each course.
   Same native <details> pattern as the Assignments page's course groups --
   no JS state, and the flat list stays flat for anyone not digging in. */
.trend-list li.has-breakdown { display: block; padding: 0; }
.category-breakdown > summary {
  display: flex;
  justify-content: space-between;
  gap: 0.5rem;
  padding: 0.5rem 0;
  cursor: pointer;
  list-style: none;
}
.category-breakdown > summary::-webkit-details-marker { display: none; }
.category-breakdown > summary::after {
  content: "▸";
  color: var(--ink-soft);
  font-size: 0.8rem;
  order: 3;
}
.category-breakdown[open] > summary::after { content: "▾"; }
.category-list { list-style: none; margin: 0 0 0.5rem; padding: 0 0 0 0.75rem; }
/* 0.92rem matches .trend-list li rather than shrinking below it: these rows
   are mostly numbers, and at the 0.86rem they started at they were reported
   as hard to read even though every token clears WCAG AA. Small tabular
   figures in a soft ink is the combination that fails in practice. */
.category-list li {
  display: grid;
  grid-template-columns: minmax(7rem, 1.4fr) auto auto auto;
  align-items: baseline;
  gap: 0.35rem 0.75rem;
  padding: 0.4rem 0;
  border-bottom: none;
  font-size: 0.92rem;
}
.cat-name { font-weight: 600; }
.cat-weight, .cat-recent { color: var(--ink-soft); }
/* .grade / .grade-good|warn|bad come along too -- a category percentage is a
   grade and reads in the same colors as every other grade in the app. */
.cat-earned { text-align: right; font-variant-numeric: tabular-nums; }
/* The number the row exists for, so full-strength ink rather than
   --ink-soft. .drag flags whichever category is costing the most (the API
   sorts biggest-drag first, so that is the top row); .gain is the extra
   credit case, where a category earned above 100% and is handing points
   back rather than losing them. */
.cat-lost { text-align: right; font-variant-numeric: tabular-nums; color: var(--ink); font-weight: 500; }
.cat-lost.drag { color: var(--flag); font-weight: 600; }
.cat-lost.gain { color: var(--oak); }

/* Weighted trend arrow, sat beside the course average in the summary. Reuses
   the direction-up/down/flat colors the trend list already defines. */
.momentum {
  margin-left: auto;
  padding-right: 0.6rem;
  font-family: "IBM Plex Mono", Consolas, monospace;
  font-size: 0.9rem;
  font-weight: 500;
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
}
.momentum.direction-up { color: var(--oak); }
.momentum.direction-down { color: var(--flag); }
.momentum.direction-flat { color: var(--ink-soft); }
/* Recent scores in this category, oldest first -- the direction the snapshot
   above can't show. Spans the row under the numbers rather than squeezing a
   fifth column in at phone width. */
.cat-recent { grid-column: 1 / -1; font-variant-numeric: tabular-nums; font-size: 0.88rem; }
.cat-maxed {
  grid-column: 1 / -1;
  color: var(--ink-soft);
  font-size: 0.86rem;
  font-style: italic;
}
.cat-note {
  margin: 0 0 0.6rem;
  padding-left: 0.75rem;
  color: var(--ink-soft);
  font-size: 0.82rem;
}

/* Assignments page: one collapsible <details> per course instead of a
   single flat table -- native marker/toggle behavior, no JS state to
   manage, and it stays flat/ruled rather than introducing a card. */
.course-group { border-bottom: 1px solid var(--rule); }
.course-group:last-child { border-bottom: none; }
.course-group summary {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 1rem;
  flex-wrap: wrap;
  padding: 0.75rem 0;
  cursor: pointer;
  list-style: none;
}
.course-group summary::-webkit-details-marker { display: none; }
.course-group summary::before {
  /* Unicode escapes, not the raw glyphs -- a raw non-ASCII byte here
     depends on the CSS file's declared charset, which the app's other
     unicode characters (arrows, interpuncts) never needed to care about
     since those live inside app.js template strings, decoded differently
     than an external stylesheet. Confirmed live: without this, the
     triangle mojibakes even though everything else on the page is fine. */
  content: "\25B8"; /* ▸ */
  display: inline-block;
  width: 1em;
  color: var(--ink-soft);
}
.course-group[open] summary::before { content: "\25BE"; } /* ▾ */
/* The summary is space-between across three items (arrow, name, tally),
   which centered the name. Growing the name pins it beside the arrow; the
   8rem basis means a long name wraps inside itself instead of dropping to
   its own line and stranding the arrow. */
.course-group .name { font-weight: 600; flex: 1 1 8rem; }
.course-group .tally { font-size: 0.88rem; color: var(--ink-soft); white-space: nowrap; }
.course-group .tally b { font-family: "IBM Plex Mono", Consolas, monospace; color: var(--ink); font-weight: 500; }
.course-group .tally b.missing { color: var(--flag); }
.course-group .tally b.soon { color: var(--brass-ink); }
.course-group .tally b.new { color: var(--oak); }
.course-group .tally b.past { color: var(--ink-soft); }
.course-group table.ledger { margin: 0 0 1rem 1.5rem; width: calc(100% - 1.5rem); }

/* Row-level icon buttons (details, find study material, delete). Were three
   copy-pasted 0.7rem rules with uneven padding, so each rendered as a ~13px
   glyph in a squashed oval; now one fixed square with the icon centered. */
.row-icon {
  display: inline-grid;
  place-items: center;
  width: 1.65rem;
  height: 1.65rem;
  margin-left: 0.4rem;
  vertical-align: middle;
  padding: 0;
  background: none;
  border: 1px solid var(--rule-strong);
  border-radius: 50%;
  color: var(--ink-soft);
  font-size: 0.95rem;
  cursor: pointer;
  flex-shrink: 0;
}
.row-icon:hover { color: var(--ink); border-color: var(--ink-soft); }
.row-icon:disabled { opacity: 0.5; cursor: default; }
.delete-entry { margin-left: 0; }
.delete-entry:hover { color: var(--flag); border-color: var(--flag); }
/* A lookup already exists for this exact assignment -- distinguishes the
   open-book icon from an un-looked-up row's plain search icon at a glance,
   not just by shape. Same "ready" meaning --oak carries everywhere else
   (grade-good, direction-up, .pill.new). */
.find-study-material.has-material { color: var(--oak); border-color: var(--oak); }
.find-study-material.has-material:hover { color: var(--ink); border-color: var(--ink-soft); }

.description-row td { color: var(--ink-soft); font-size: 0.88rem; white-space: pre-wrap; }

.ledger-scroll { overflow-x: auto; }
/* ...and keep a floor, or the cells' own wrapping lets the table collapse to
   an unreadable column of single characters rather than triggering the
   scroll. Fixed layout + the <colgroup> widths below give every table the
   same columns, so separate course groups line up with each other. */
table.ledger { width: 100%; min-width: 34rem; border-collapse: collapse; font-size: 0.92rem; table-layout: fixed; }
table.ledger .col-category { width: 8rem; }
table.ledger .col-due { width: 7.5rem; }
table.ledger .col-grade { width: 4.5rem; }
table.ledger .col-status { width: 7rem; }
table.ledger .col-kid { width: 6.5rem; }
/* Baseline, not the default middle: a one-line cell next to a wrapped
   two-line title used to float halfway down the row. */
table.ledger th, table.ledger td { overflow-wrap: anywhere; vertical-align: baseline; }
table.ledger th {
  text-align: left;
  font-weight: 500;
  color: var(--ink-soft);
  border-bottom: 1px solid var(--rule-strong);
  padding: 0.4rem 0.6rem 0.4rem 0;
}
table.ledger td { padding: 0.55rem 0.6rem 0.55rem 0; border-bottom: 1px solid var(--rule); }
/* Short fixed-format values never wrap -- `anywhere` above was splitting
   "Sep 24" and the status pills ("Missin / g") whenever a long course name
   squeezed the table. The fixed column widths are sized to fit them. */
table.ledger td.due { font-family: "IBM Plex Mono", Consolas, monospace; font-variant-numeric: tabular-nums; white-space: nowrap; }
table.ledger td.due.posted { font-family: inherit; font-style: italic; color: var(--ink-soft); }
.grade { font-family: "IBM Plex Mono", Consolas, monospace; font-variant-numeric: tabular-nums; white-space: nowrap; }
.grade-good { color: var(--oak); }
.grade-warn { color: var(--brass-ink); }
.grade-bad { color: var(--flag); }
.pill { display: inline-block; padding: 0.12rem 0.55rem; border: 1px solid currentColor; font-size: 0.78rem; white-space: nowrap; }
.pill.missing { color: var(--flag); }
.pill.soon { color: var(--brass-ink); }
.pill.new { color: var(--oak); }
.pill.past { color: var(--ink-soft); }


/* Study Material page: reuses the Assignments course-group shell, with its
   own entry layout (no table -- each lookup has a topic, an optional pair
   of resource links, and a done checkbox rather than uniform columns). */
.study-entries { margin: 0 0 1rem 1.5rem; }
.study-entry { padding: 0.75rem 0; border-bottom: 1px solid var(--rule); }
.study-entry:last-child { border-bottom: none; }
.study-entry .topic { font-weight: 500; }
.study-entry .collected-date { color: var(--ink-soft); font-size: 0.78rem; margin-top: 0.15rem; }
.study-entry .teks-match { color: var(--ink-soft); font-size: 0.85rem; font-style: italic; margin-top: 0.35rem; }
.study-entry .pending-note,
.attention-card .pending-note { color: var(--ink-soft); font-size: 0.9rem; font-style: italic; margin-top: 0.3rem; }
.study-entry .rec { font-size: 0.9rem; margin-top: 0.4rem; }
.study-entry .rec b { color: var(--brass-ink); }
.entry-header { display: flex; align-items: center; justify-content: space-between; gap: 0.75rem; }
.entry-actions { display: flex; gap: 0.3rem; flex-shrink: 0; }
/* Marks a Study Material entry that came from Needs Attention's daily grade
   check rather than a parent's request. Text, not color alone. */
.source-tag {
  display: inline-block;
  font-family: "IBM Plex Mono", Consolas, monospace;
  font-size: 0.72rem;
  color: var(--flag);
  border: 1px solid var(--rule-strong);
  padding: 0 0.35rem;
  margin-right: 0.45rem;
}
.done-toggle { display: flex; align-items: center; gap: 0.5rem; cursor: pointer; }
.done-toggle input[disabled] { cursor: default; }

.attention-card { position: relative; border: 1px solid var(--rule-strong); border-left: 3px solid var(--flag); padding: 0.9rem 1.1rem; margin-top: 1.25rem; }
.attention-card:first-child { margin-top: 0; }
.attention-card .course { font-weight: 600; margin-bottom: 0.2rem; }
.attention-card .why { color: var(--ink-soft); font-size: 0.9rem; margin-bottom: 0.6rem; }
.attention-card .rec { font-size: 0.9rem; }
.attention-card .rec b { color: var(--brass-ink); }
/* Dismiss / view-plan icons -- positioned over the card rather than laid
   out inline with card-head's own content, since card-head is a <button>
   on the Family page (see renderFamily) and these must be siblings of it,
   not children -- a button can't nest inside another button. Reserve room
   on the right of card-head's own text so the course/why lines never run
   underneath. */
.attention-card .card-actions { position: absolute; top: 0.9rem; right: 1.1rem; display: flex; gap: 0.3rem; }

/* Resource links (khan_resource/web_resource, both Study Material and
   Needs Attention) had no color of their own -- they were falling back to
   the browser default blue/purple-visited, which reads badly against this
   theme's dark palette. oak matches the rest of the "good/positive" accent
   uses (grade-good, direction-up) and stays put after visiting instead of
   switching to a jarring default purple. */
.rec a { color: var(--oak); }
.rec a:visited { color: var(--oak); }
.rec a:hover { color: var(--brass-ink); }

/* Practice-set questions the model wrote itself when no citable resource
   was found (see recommend.py's _craft_questions()) -- a plain numbered
   list, not links, so it reads distinctly from a cited resource line. */
.practice-questions { margin: 0.4rem 0 0; padding-left: 1.4rem; }
.practice-questions li { margin-top: 0.25rem; }

/* Family page: a recommendation callout doubles as a button to that kid's
   Attention page. Only the head is the button -- the card itself is a div,
   because resourceLine emits an <a> and an <a> inside a <button> is invalid
   (see renderFamily in app.js). Reset the head's button chrome, keep the
   flag accent on the left, and signal "clickable" by lighting the card's
   other three sides brass on hover, as it did when the card was the button. */
.attention-card .card-head {
  width: 100%;
  display: block;
  padding: 0;
  /* Room for .card-actions, which sits on top of the card, not inline. */
  padding-right: 4.5rem;
  border: 0;
  background: none;
  font: inherit;
  color: inherit;
  text-align: left;
  cursor: pointer;
}
.attention-card.clickable:hover {
  border-top-color: var(--brass);
  border-right-color: var(--brass);
  border-bottom-color: var(--brass);
}

.empty-note { color: var(--ink-soft); font-size: 0.92rem; }
.error-banner { color: var(--flag); border: 1px solid currentColor; padding: 0.6rem 0.9rem; font-size: 0.9rem; margin-bottom: 1rem; }

@media (max-width: 720px) {
  .app-shell { grid-template-columns: minmax(0, 1fr); background: var(--paper); }
  .rail {
    flex-direction: row; flex-wrap: wrap; align-items: center; gap: 0.5rem; padding: 0.9rem;
    position: static; height: auto; overflow: visible;
  }
  .rail .students, .rail nav { display: flex; flex-direction: row; flex-wrap: wrap; padding: 0; gap: 0.4rem; }
  .rail .student { width: auto; }
  .rail .divider { display: none; }
  .rail-footer { margin-top: 0; }

  .page { padding: 1.25rem 1rem; }
  .page h1 { font-size: 1.5rem; }
  .course-group table.ledger, .study-entries { margin-left: 0; width: 100%; }

  /* Stacked rows instead of a sideways-scrolling table: at 390px the old
     table hid Grade and Status -- the columns that matter most -- off the
     right edge. Each row becomes a small grid: the title on its own line,
     the short values underneath. */
  table.ledger, table.ledger tbody, table.ledger td { display: block; min-width: 0; }
  table.ledger colgroup, table.ledger thead { display: none; }
  table.ledger tr {
    display: grid;
    align-items: baseline;
    gap: 0.3rem 0.9rem;
    padding: 0.65rem 0;
    border-bottom: 1px solid var(--rule);
  }
  table.ledger td { padding: 0; border-bottom: none; }
  /* Assignments: title / category, due, grade, status */
  .ledger-assignments tr { grid-template-columns: minmax(0, 1fr) auto auto auto; }
  .ledger-assignments td:first-child { grid-column: 1 / -1; }
  /* Family: kid, course / assignment, due, status */
  .ledger-family tr { grid-template-columns: auto minmax(0, 1fr) auto auto; }
  .ledger-family td:nth-child(2) { grid-column: 2 / -1; font-weight: 500; }
  .ledger-family td:nth-child(3) { grid-column: 1 / 3; }
  .description-row td { grid-column: 1 / -1; }
}
