/* ─────────────────────────────────────────────────────────────────────────
   app.css — composite, app-level layout & data surfaces: the responsive app
   shell (sidebar ≥768px / bottom-nav <768px), data tables (list + matrix
   modes), the filter bar, KPI tiles, the login card, and view/edit toggles.
   var(--token) only.
   ───────────────────────────────────────────────────────────────────────── */

/* ═══════════════ App shell ════════════════════════════════════════════════
   Mobile-first: single column with a fixed bottom tab-bar + a mobile topbar.
   At ≥768px the bottom-nav/topbar drop out and the desktop sidebar rail takes
   over. Full-height uses 100dvh (100vh fallback first). Assumes
   viewport-fit=cover so the bottom-nav pads the iOS home-bar inset. */
.app { display: grid; grid-template-columns: 1fr; min-height: 100vh; min-height: 100dvh; }

.main {
  min-width: 0; width: 100%;
  padding: var(--space-4);
  /* clear the fixed bottom-nav (+ safe-area) so content never hides under it */
  padding-bottom: calc(var(--bottomnav-h) + var(--space-5) + env(safe-area-inset-bottom, 0px));
}

/* Mobile topbar: brand left, quick actions right. Hidden ≥768px. */
.mobile-topbar {
  display: flex; align-items: center; justify-content: space-between; gap: var(--space-3);
  padding: var(--space-2) var(--space-4); min-height: 56px;
  background: var(--bg-page); border-bottom: 1px solid var(--border);
  position: sticky; top: 0; z-index: 20;
}

/* Desktop sidebar — hidden on mobile, revealed as a left rail at ≥768px. */
.sidebar { display: none; }

/* Bottom tab-bar (mobile only). Fixed to the bottom, five thumb-reachable
   targets, each an icon + short label, padded by the home-bar safe-area. */
.bottom-nav {
  position: fixed; left: 0; right: 0; bottom: 0; z-index: 40;
  display: flex; background: var(--bg-page);
  border-top: 1px solid var(--border); box-shadow: var(--shadow-lg);
  padding-bottom: env(safe-area-inset-bottom, 0px);
}
.bottom-nav-item {
  flex: 1 1 0; min-width: 0;
  display: flex; flex-direction: column; align-items: center; justify-content: center;
  gap: 2px; min-height: var(--bottomnav-h); padding: var(--space-2) var(--space-1);
  color: var(--text-soft); font-size: var(--fs-xs); font-weight: 500; text-align: center;
  text-decoration: none;
}
.bottom-nav-item .ico { width: 22px; height: 22px; }
.bottom-nav-item > span { max-width: 100%; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.bottom-nav-item:hover { color: var(--text); text-decoration: none; }
.bottom-nav-item:active { background: var(--bg-sunken); }   /* press feedback (tap-highlight off) */
.bottom-nav-item.active { color: var(--color-accent); }

@media (min-width: 768px) {
  .app { grid-template-columns: var(--sidebar-w) 1fr; }
  .bottom-nav, .mobile-topbar { display: none; }
  .main { padding: var(--space-5); padding-bottom: var(--space-6); }

  .sidebar {
    display: flex; flex-direction: column;
    /* align-self: start is what MAKES the sticky work. .app is a grid, and a
       grid item stretches to its row by default — a stretched item is already
       as tall as the page, so sticky has nothing to move within and the rail
       scrolls away with the content. The explicit height alone is not enough:
       anything that later sets height:auto re-breaks it silently. */
    align-self: start;
    position: sticky; top: 0; height: 100vh; height: 100dvh; overflow-y: auto;
    background: var(--color-primary); color: var(--color-on-primary);
    padding: var(--space-4) 0;
  }
  .sidebar-brand { display: flex; align-items: center; gap: var(--space-3); padding: var(--space-2) var(--space-5) var(--space-5); font-weight: 700; font-size: var(--fs-lg); }
  .sidebar-section { padding: var(--space-3) var(--space-5) var(--space-2); font-size: var(--fs-xs); text-transform: uppercase; letter-spacing: .06em; color: rgba(255, 255, 255, .55); }
  .nav-item, .nav-link {
    display: flex; align-items: center; gap: var(--space-3);
    padding: var(--space-3) var(--space-5);
    color: rgba(255, 255, 255, .8); font-size: var(--fs-sm); font-weight: 500;
    border-left: 3px solid transparent; white-space: nowrap;
    transition: background var(--transition), color var(--transition);
  }
  .nav-item .ico, .nav-link .ico { width: 20px; height: 20px; flex: none; }
  .nav-item:hover, .nav-link:hover { background: rgba(255, 255, 255, .07); color: var(--color-on-primary); text-decoration: none; }
  .nav-item:active, .nav-link:active { background: rgba(255, 255, 255, .12); }
  .nav-item.active, .nav-link.active { background: rgba(255, 255, 255, .1); color: var(--color-on-primary); border-left-color: var(--color-accent); }
  /* NO margin-top:auto here. .sidebar-user already claims the free space; a
     second auto margin would split it and leave a gap between the signed-in
     name and the footer that is meant to sit directly under it. */
  .sidebar-foot { padding: 0 var(--space-5) var(--space-4); font-size: var(--fs-xs); color: rgba(255, 255, 255, .45); }
}

/* ═══════════════ Data tables ══════════════════════════════════════════════
   Two modes share .data-table (ported from farmbelt §10–11):
     • LIST tables (default): each <tr> is an independent record. Below 640px
       every row reflows into a labeled card (labels from data-label attrs).
     • MATRIX tables (.data-table--matrix): schedules/financial grids where
       rows × columns are one structure — these keep the grid, scroll
       horizontally inside .table-scroll, and pin a sticky first column.
   Only the .col-primary cell wraps; identifiers, numbers, dates, badges and
   .col-actions stay on one line. */
.table-scroll {
  overflow-x: auto; -webkit-overflow-scrolling: touch;
  scrollbar-gutter: stable; scrollbar-width: thin;
  border: 1px solid var(--border); border-radius: var(--radius-lg); background: var(--bg-raised);
}
.table-scroll::-webkit-scrollbar { height: 10px; }
.table-scroll::-webkit-scrollbar-track { background: var(--bg-sunken); border-radius: 5px; }
.table-scroll::-webkit-scrollbar-thumb { background: var(--border-strong); border-radius: 5px; }

.data-table { width: 100%; border-collapse: collapse; font-size: var(--fs-table); }
.data-table thead th {
  padding: var(--space-2) var(--space-2); text-align: left;
  font-weight: 600; font-size: var(--fs-table-head); text-transform: uppercase; letter-spacing: .03em;
  color: var(--text-soft); border-bottom: 1px solid var(--border-strong); white-space: nowrap;
}
.data-table td { padding: var(--space-2) var(--space-2); border-bottom: 1px solid var(--border); vertical-align: middle; }
.data-table tbody tr:nth-child(even) { background: color-mix(in srgb, var(--bg-sunken) 60%, transparent); }   /* zebra */
.data-table tbody tr:hover { background: var(--bg-sunken); cursor: pointer; }
.data-table tbody tr:nth-child(even):hover { background: var(--bg-sunken); }
.data-table tbody tr:last-child td { border-bottom: none; }
.data-table .num, .data-table td.num, .data-table th.num { text-align: right; font-variant-numeric: tabular-nums; white-space: nowrap; }

/* Only .col-primary wraps; everything else holds one line. */
.data-table th:not(.col-primary):not(.col-actions),
.data-table td:not(.col-primary):not(.col-actions) { white-space: nowrap; }
.col-actions { text-align: right; white-space: nowrap; }

/* Sortable headers — click to sort; the [aria-sort] arrow is the state. */
.data-table th.sortable { cursor: pointer; }
.data-table th.sortable[aria-sort="ascending"]::after  { content: " ↑"; color: var(--color-primary); }
.data-table th.sortable[aria-sort="descending"]::after { content: " ↓"; color: var(--color-primary); }

/* Whole-row click target (stretched-link): the .table-link inside .col-primary
   overlays the entire <tr>; other interactive cells sit above via z-index. */
.data-table tbody tr { position: relative; }
.data-table tbody .col-primary .table-link { position: relative; z-index: 2; font-weight: 600; color: var(--text); }
.data-table tbody .col-primary .table-link::before { content: ""; position: absolute; inset: 0; z-index: 1; }
.data-table tbody td a:not(.table-link),
.data-table tbody td button,
.data-table tbody td input,
.data-table tbody td select,
.data-table tbody td form,
.data-table tbody td.col-actions { position: relative; z-index: 2; }
.data-table tbody tr:focus-within { background: var(--bg-sunken); outline: 2px solid var(--color-accent); outline-offset: -2px; }

/* The headline column absorbs slack, then truncates rather than forcing the
   table wider than the viewport (auto-layout truncation idiom). */
@media (min-width: 641px) {
  .data-table .col-primary { width: 100%; max-width: 0; }
  .data-table td.col-primary { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
}

/* ── Matrix mode: sticky first column while scrolling across columns ────────*/
.data-table--matrix th:first-child,
.data-table--matrix td:first-child {
  position: sticky; left: 0; z-index: 1; background: var(--bg-raised);
  box-shadow: 8px 0 8px -8px rgba(15, 23, 42, .1);
}
.data-table--matrix thead th:first-child { z-index: 2; background: var(--bg-sunken); }

/* ── List mode → cards on phones (matrix tables opt out) ───────────────────*/
@media (max-width: 640px) {
  .data-table:not(.data-table--matrix) { border: 0; background: transparent; }
  .data-table:not(.data-table--matrix) thead { position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; }
  .data-table:not(.data-table--matrix) tbody tr {
    display: grid; grid-template-columns: 1fr; gap: var(--space-1);
    margin-bottom: var(--space-3); padding: var(--space-3) var(--space-4);
    background: var(--bg-raised); border: 1px solid var(--border);
    border-radius: var(--radius-md); box-shadow: var(--shadow-sm);
  }
  .data-table:not(.data-table--matrix) tbody tr:nth-child(even),
  .data-table:not(.data-table--matrix) tbody tr:hover { background: var(--bg-raised); }
  .data-table:not(.data-table--matrix) tbody td {
    display: flex; justify-content: space-between; align-items: baseline; gap: var(--space-3);
    border: 0; padding: 1px 0; text-align: right; min-width: 0;
    font-size: var(--fs-sm); color: var(--text-soft); white-space: normal;
  }
  .data-table:not(.data-table--matrix) tbody td::before {
    content: attr(data-label); flex-shrink: 0; text-align: left;
    font-size: var(--fs-xs); font-weight: 500; color: var(--text-muted); opacity: .8;
  }
  /* headline cell = card title */
  .data-table:not(.data-table--matrix) tbody td.col-primary,
  .data-table:not(.data-table--matrix) tbody td:first-child {
    display: block; text-align: left; font-size: var(--fs-md); font-weight: 600; color: var(--text);
    padding: 0 0 var(--space-2); margin-bottom: var(--space-2); border-bottom: 1px solid var(--border);
  }
  .data-table:not(.data-table--matrix) tbody td.col-primary::before,
  .data-table:not(.data-table--matrix) tbody td:first-child::before { content: none; }
  /* actions cell = card footer */
  .data-table:not(.data-table--matrix) tbody td.col-actions {
    display: flex; justify-content: flex-end; gap: var(--space-2); flex-wrap: wrap;
    padding: var(--space-3) 0 0; margin-top: var(--space-2); border-top: 1px solid var(--border);
  }
  .data-table:not(.data-table--matrix) tbody td.col-actions::before { content: none; }
  .data-table:not(.data-table--matrix) tbody td:empty { display: none; }
}

/* ═══════════════ Filter bar (search + selects + count, ONE live row) ══════*/
.filter-bar { display: flex; flex-wrap: wrap; align-items: center; gap: var(--space-2); margin-bottom: var(--space-4); }
.filter-bar .search {
  flex: 1 1 200px; display: flex; align-items: center; gap: var(--space-2);
  padding: 0 var(--space-3); min-height: var(--control-compact);
  background: var(--bg-page); border: 1px solid var(--border-strong); border-radius: var(--radius-sm);
}
.filter-bar .search .ico { width: 16px; height: 16px; color: var(--text-muted); flex: none; }
.filter-bar .search input { border: none; min-height: auto; padding: 0; background: none; }
.filter-bar .search input:focus { outline: none; }
.filter-bar select, .filter-bar input[type="search"] { width: auto; min-width: 150px; flex: 0 0 auto; min-height: var(--control-compact); }
.filter-count { margin: 0; align-self: center; white-space: nowrap; color: var(--text-soft); font-size: var(--fs-sm); }
@media (max-width: 560px) { .filter-bar .search { flex-basis: 100%; } .filter-bar select { flex: 1 1 auto; } }

/* Overflow → modal (JS toggles .filters-collapsed / .filters-open on the shell; see
   initFilterbars in app.js). Inline while the row fits; a "Filters" button + centered
   modal when a control would wrap. */
.filterbar-shell { position: relative; margin-bottom: var(--space-4); }
.filterbar-shell > .filter-bar { margin-bottom: 0; }
.filter-trigger, .filter-foot, .filter-backdrop { display: none; }
.filter-badge {
  display: inline-flex; min-width: 1.25rem; justify-content: center;
  padding: 0 .35rem; border-radius: var(--radius-pill);
  background: var(--color-primary); color: var(--color-on-primary);
  font-size: var(--fs-xs); line-height: 1.4;
}
.filters-collapsed > .filter-bar { display: none; }
.filters-collapsed .filter-trigger { display: inline-flex; }
.filters-collapsed.filters-open .filter-backdrop {
  display: block; position: fixed; inset: 0; z-index: 60; background: rgba(20, 25, 32, .45);
}
.filters-collapsed.filters-open .filter-bar {
  display: flex; flex-direction: column; align-items: stretch; gap: var(--space-3);
  position: fixed; inset: 0; margin: auto; z-index: 61;
  width: min(420px, calc(100vw - 2 * var(--space-4))); max-height: 86vh; overflow-y: auto;
  background: var(--bg-raised); border-radius: var(--radius-lg); box-shadow: var(--shadow-lg);
  padding: var(--space-5);
}
.filters-collapsed.filters-open .filter-bar .search,
.filters-collapsed.filters-open .filter-bar select { width: 100%; min-width: 0; flex: none; }
.filters-collapsed.filters-open .filter-count { display: none; }
.filters-collapsed.filters-open .filter-foot {
  display: flex; justify-content: flex-end; gap: var(--space-2);
  padding-top: var(--space-3); border-top: 1px solid var(--border);
}

/* ═══════════════ KPI tiles ════════════════════════════════════════════════
   Container-query ready: the grid establishes a query container, and the card
   restacks itself when ITS OWN width drops below 260px (not the viewport's). */
.kpi-grid {
  display: grid; gap: var(--space-4); margin-bottom: var(--space-6);
  grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
  container-type: inline-size;
}
.kpi-card {
  display: flex; flex-direction: column; gap: var(--space-2);
  padding: var(--space-4) var(--space-5);
  background: var(--bg-raised); border: 1px solid var(--border);
  border-radius: var(--radius-lg); box-shadow: var(--shadow-sm);
  color: inherit; text-decoration: none;
  transition: border-color var(--transition), box-shadow var(--transition);
}
a.kpi-card:hover { border-color: var(--border-strong); box-shadow: var(--shadow-md); }
.kpi-label { order: 1; font-size: var(--fs-sm); font-weight: 600; color: var(--text-soft); line-height: 1.25; }
.kpi-value { order: 2; font-family: var(--font-display); font-size: var(--fs-2xl); font-weight: 700; line-height: 1.1; color: var(--text); font-variant-numeric: tabular-nums; letter-spacing: -.01em; }
.kpi-card--warn   .kpi-value { color: var(--st-warning-fg); }
.kpi-card--danger .kpi-value { color: var(--st-danger-fg); }
@container (max-width: 260px) {
  .kpi-card { padding: var(--space-3) var(--space-4); }
  .kpi-value { font-size: var(--fs-xl); }
}

/* ═══════════════ View ⇄ Edit toggle ═══════════════════════════════════════
   Detail pages ship both the read view and the edit form; JS flips which is
   shown by toggling .is-hidden on the paired .js-view / .js-edit blocks. */
/* ONE MECHANISM: .is-hidden, toggled by app.js. There is no CSS default here on
   purpose — the markup ships `class="js-edit is-hidden"` and the JS takes that
   class off.

   This block used to carry `.js-edit { display: none }` plus a parallel
   `.is-editing` scheme that nothing ever set. The blanket rule won regardless of
   the class the JS was actually toggling, so Edit hid the read view and then
   failed to reveal the form — every detail page in the app looked like it had
   erased the record. Two mechanisms for one job, and the dead one was in
   front. */

/* ═══════════════ Login (the one bespoke branded surface) ══════════════════*/
.login-shell { min-height: 100vh; min-height: 100dvh; display: grid; place-items: center; padding: var(--space-4); background: var(--color-primary); }
.login-card {
  width: 100%; max-width: 380px;
  padding: var(--space-6);
  background: var(--bg-raised); border-radius: var(--radius-lg); box-shadow: var(--shadow-lg);
}
.login-card h1 { text-align: center; font-size: var(--fs-xl); margin-bottom: var(--space-5); }
.login-card .form-group:last-of-type { margin-bottom: var(--space-4); }
.login-card .btn { width: 100%; }
.demo-creds { margin-top: var(--space-4); padding: var(--space-3); background: var(--bg-sunken); border-radius: var(--radius-sm); font-size: var(--fs-xs); color: var(--text-soft); text-align: center; }

/* Signup wears the login card, one size up: it carries a school name, a time
   zone and a role that the sign-in form does not. */
.login-card-wide { max-width: 460px; }
.login-alt { margin-top: var(--space-4); font-size: var(--fs-sm); color: var(--text-soft); text-align: center; }
.login-alt a { color: var(--color-primary); font-weight: 600; }
.signup-done { text-align: center; }

/* Two ways in, side by side, because the only question a new person has is
   whether anyone from their program is already here. */
.signup-modes { display: flex; gap: var(--space-2); margin-bottom: var(--space-5); }
.signup-mode {
  flex: 1; padding: var(--space-3); text-align: center;
  font-size: var(--fs-sm); font-weight: 600; color: var(--text-soft);
  background: var(--bg-sunken); border: 1px solid var(--border); border-radius: var(--radius-sm);
}
.signup-mode.is-on { color: var(--color-primary); background: var(--bg-raised); border-color: var(--color-primary); }
/* The button in this form is full-width via .login-card .btn; the mode switches
   are links and must not inherit that. */
.login-card .signup-mode { width: auto; }

/* ═══════════════ No-JS flash notice ═══════════════════════════════════════*/
.flash-noscript {
  padding: var(--space-3) var(--space-4); margin-bottom: var(--space-4);
  background: var(--st-warning-bg); color: var(--st-warning-fg);
  border: 1px solid color-mix(in srgb, var(--st-warning-fg) 30%, transparent);
  border-radius: var(--radius-sm); font-size: var(--fs-sm);
}

/* ═══════════════ Shell internals (added for templates/layout.php) ═════════*/
/* Mobile topbar brand (shown <768px). */
.mobile-topbar-brand { font-weight: 700; font-size: var(--fs-lg); color: var(--text); letter-spacing: -.01em; }
.mobile-topbar-brand:hover { text-decoration: none; }

/* Page header (pageHeader() helper) — list/dashboard title block. Title (+ optional
   subtitle) sits left; the primary CTA is pinned top-right; margin below separates it
   from the content (KPIs, tabs, tables). Without this the ported classes are unstyled
   and everything stacks left and touches. */
.admin-content-header { margin-bottom: var(--space-5); }
.admin-content-header-row {
  display: flex; align-items: flex-start; justify-content: space-between;
  gap: var(--space-3) var(--space-4); flex-wrap: wrap;
}
.admin-content-header h1 { font-size: var(--fs-h1); line-height: 1.15; }
.admin-content-header p { margin-top: var(--space-1); color: var(--text-muted); font-size: var(--fs-sm); }
.admin-content-header .btn { flex: none; }
/* Right-side action cluster in a page header (e.g. tabbed Settings: Edit/Cancel/Save). */
.admin-content-header-actions { display: flex; align-items: center; gap: var(--space-2); flex: none; }

/* Right-aligned tabular-number table cell. */
.num { text-align: right; font-variant-numeric: tabular-nums; }

/* Sidebar internals — dark rail (only rendered ≥768px). */
@media (min-width: 768px) {
  .sidebar-brand a { color: var(--color-on-primary); }
  .sidebar-brand a:hover { text-decoration: none; opacity: .9; }
  .sidebar-nav { display: flex; flex-direction: column; }
  .sidebar-user { margin-top: auto; display: flex; flex-direction: column; gap: 2px; padding: var(--space-4) var(--space-5) var(--space-2); border-top: 1px solid rgba(255, 255, 255, .1); }
  .sidebar-user-name { font-size: var(--fs-sm); font-weight: 600; color: var(--color-on-primary); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
  .sidebar-user-logout { font-size: var(--fs-xs); color: rgba(255, 255, 255, .65); }
  .sidebar-user-logout:hover { color: var(--color-on-primary); }
}

/* ═══════════════ Login brand + demo block (login.php) ═════════════════════*/
main.login-card { margin: var(--space-7) auto; }   /* center when used as the page's <main> */
.login-brand { text-align: center; font-family: var(--font-display); font-size: var(--fs-xl); font-weight: 700; letter-spacing: .02em; color: var(--text); margin-bottom: var(--space-5); }
.login-demo { margin-top: var(--space-4); padding: var(--space-3); background: var(--bg-sunken); border-radius: var(--radius-sm); font-size: var(--fs-xs); color: var(--text-soft); text-align: center; }

/* Unread badge. Two placements from one rule: pinned to the icon on the bottom
   bar, and pushed to the end of the row in the desktop rail. The base is the
   INLINE case — .nav-link is already a flex row, so margin-left:auto parks it
   on the right — and only the bottom bar overrides to absolute. Getting that
   the other way round put the pill on top of the sidebar's own label. */
.bottom-nav-icon { position: relative; display: inline-flex; }
.nav-badge {
  min-width: 16px; height: 16px; padding: 0 4px;
  display: inline-flex; align-items: center; justify-content: center;
  margin-left: auto;
  border-radius: 999px;
  background: var(--st-danger-fg); color: #fff;
  font-size: 10px; font-weight: 700; line-height: 1;
}
.bottom-nav-icon .nav-badge {
  position: absolute; top: -5px; left: 11px; margin-left: 0;
}

/* The blink, kept to a slow breath rather than a flash. Anything at or above
   three cycles a second is a seizure risk (WCAG 2.3.1) and a headache for
   everyone else; this is one soft pulse every 1.6s, and it stops entirely for
   anyone who has asked their system for less motion. The badge stays fully
   visible at both ends of the cycle, so the count is always readable — the
   pulse draws the eye, it does not carry the meaning. */
@keyframes nav-badge-pulse {
  0%, 100% { transform: scale(1);    box-shadow: 0 0 0 0 rgba(220, 38, 38, .55); }
  50%      { transform: scale(1.12); box-shadow: 0 0 0 5px rgba(220, 38, 38, 0); }
}
.nav-badge.is-pulsing { animation: nav-badge-pulse 1.6s ease-in-out infinite; }
@media (prefers-reduced-motion: reduce) {
  .nav-badge.is-pulsing { animation: none; }
}

/* The school code is read aloud off a screen, so it is set large and tracked
   out rather than sized like body copy. */
.join-code {
  font-family: var(--font-mono, ui-monospace, monospace);
  font-size: var(--fs-xl); font-weight: 700; letter-spacing: 0.12em;
  padding: var(--space-3); margin-bottom: var(--space-3);
  background: var(--bg-sunken); border-radius: var(--radius-sm); text-align: center;
}
