/* ==========================================================================
   Senior Tags — Maspeth Town Hall
   Design tokens, layout, components, motion
   ========================================================================== */

:root{
  /* Brand — burgundy */
  --b-900:#3E0C15;
  --b-800:#5C1622;
  --b-700:#7B1E2B;   /* primary */
  --b-600:#932738;
  --b-500:#A93446;
  --b-300:#D98B96;
  --b-200:#EBBFC6;
  --b-100:#F6E5E8;
  --b-50:#FCF6F7;

  /* Neutrals */
  --ink:#1C1418;
  --ink-2:#5B4F54;
  --ink-3:#8B8085;
  --line:#E8E1E3;
  --line-2:#F1ECED;
  --surface:#FFFFFF;
  --bg:#F5F2F3;

  /* Semantic */
  --ok:#1C7A4C; --ok-bg:#E7F5EE;
  --warn:#A6650B; --warn-bg:#FCF3E3;
  --bad:#A82330; --bad-bg:#FBEBEC;
  --info:#1F5C8B; --info-bg:#E9F2F9;
  /* The danger pair needs a border and a hover of its own — --bad is too
     strong for a 1px edge and --bad-bg does not change on hover. */
  --bad-line:#F0D2D5; --bad-bg-hover:#F7DEE0;

  /* Scrollbars: warm greys, so they sit in the burgundy palette rather than
     looking like the browser's default blue-grey. */
  --scroll:#D8CFD2; --scroll-hover:#C0B3B7;

  /* Toasts are the one dark surface in the app. */
  --dark:#33272C; --dark-2:#3B2F34; --dark-3:#54444A;

  /* Text and hairlines that sit on burgundy. Solid tints only — a translucent
     white wash over burgundy fades across pixels and reads as a gradient. */
  --on-b:#FFFFFF;
  --on-b-2:#E7C3C9;
  --on-b-3:#D3A3AC;

  --r-sm:8px; --r-md:12px; --r-lg:16px; --r-xl:22px;

  /* Motion */
  --e-out:cubic-bezier(.22,.75,.24,1);
  --e-in-out:cubic-bezier(.65,.02,.28,1);
  --e-spring:cubic-bezier(.34,1.4,.5,1);
  --t-fast:150ms; --t:240ms; --t-slow:420ms;

  --topbar-h:66px;
  --font:ui-sans-serif,system-ui,-apple-system,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif;
  --font-display:"Iowan Old Style","Palatino Linotype",Palatino,Georgia,serif;
  --font-mono:ui-monospace,"SFMono-Regular",Menlo,Consolas,monospace;

  /* Chevron used as a CSS mask for the disclosure marker */
  --chevron-mask:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='2.4' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M9.6 4.8l7.2 7.2-7.2 7.2'/%3E%3C/svg%3E");
}

*,*::before,*::after{box-sizing:border-box}

/* Icon sprite: defined once, never rendered directly. */
.sprite{position:absolute;width:0;height:0;overflow:hidden}
.ico{
  width:1.05em;height:1.05em;flex:none;
  fill:none;stroke:currentColor;stroke-width:2;
  stroke-linecap:round;stroke-linejoin:round;
  vertical-align:-.16em;
}
.ico-lg{width:34px;height:34px;stroke-width:1.7}
/* Per-context sizing so strokes stay optically even */
.btn .ico{width:15px;height:15px}
.btn-sm .ico{width:13px;height:13px}
.icon-btn .ico{width:17px;height:17px;vertical-align:0}
.toast .t-ico .ico{width:13px;height:13px;vertical-align:0}
.trend-up .ico,.trend-down .ico{width:12px;height:12px;stroke-width:2.3}
html,body{height:100%}
body{
  margin:0;
  font-family:var(--font);
  color:var(--ink);
  background:var(--bg);
  font-size:15px;
  line-height:1.5;
  -webkit-font-smoothing:antialiased;
  text-rendering:optimizeLegibility;
}
h1,h2,h3{margin:0;letter-spacing:-.015em}
h1{font-size:1.5rem;font-weight:650}
h2{font-size:1rem;font-weight:640}
p{margin:0}
button{font:inherit;color:inherit}
:focus-visible{outline:2.5px solid var(--b-500);outline-offset:2px;border-radius:4px}
::selection{background:var(--b-100)}

/* Scrollbars */
*::-webkit-scrollbar{width:10px;height:10px}
*::-webkit-scrollbar-thumb{background:var(--scroll);border-radius:99px;border:3px solid transparent;background-clip:content-box}
*::-webkit-scrollbar-thumb:hover{background:var(--scroll-hover);background-clip:content-box}
*::-webkit-scrollbar-track{background:transparent}

/* A class selector beats the browser's built-in `[hidden]` rule, so any element
   we set `hidden` on and also give a `display` needs this or it stays visible.
   That bit us once: the sign-in overlay refused to go away after signing in. */
[hidden]{display:none !important}

/* ========================= SHELL ========================= */
.app{min-height:100%;display:flex;flex-direction:column}

.topbar{
  position:sticky;top:0;z-index:40;
  height:var(--topbar-h);
  display:flex;align-items:center;gap:24px;
  padding:0 20px;
  background:var(--b-700);
  color:#fff;
  border-bottom:1px solid var(--b-800);
}

.brand{display:flex;align-items:center;gap:13px;min-width:0}
.brand-logo{
  flex:none;height:46px;
  display:grid;place-items:center;
  padding:5px 11px;border-radius:12px;
  background:#fff;
  transition:transform var(--t) var(--e-spring);
}
.brand-logo:hover{transform:translateY(-1px) scale(1.02)}
.brand-logo img{display:block;height:36px;width:auto}
.brand-text{
  display:flex;flex-direction:column;line-height:1.25;min-width:0;
  padding-left:13px;border-left:1px solid var(--b-600);
}
.brand-text strong{font-size:1.02rem;font-weight:650;letter-spacing:-.01em;white-space:nowrap}
.brand-text span{font-size:.735rem;color:var(--on-b-2);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}

/* Tabs */
.tabs{position:relative;display:flex;gap:2px;margin-left:auto}
.tab{
  position:relative;z-index:1;
  display:flex;align-items:center;gap:8px;
  padding:9px 15px;border:0;border-radius:10px;
  background:transparent;color:var(--on-b-2);
  font-size:.875rem;font-weight:530;cursor:pointer;
  transition:color var(--t-fast) var(--e-out),background var(--t-fast) var(--e-out),transform var(--t-fast) var(--e-out);
}
.tab:hover{color:#fff;background:var(--b-600)}
.tab:active{transform:scale(.97)}
.tab.is-active{color:#fff;background:var(--b-800)}
.tab .ico{width:1.15em;height:1.15em;opacity:.8;transition:opacity var(--t-fast)}
.tab:hover .ico,.tab.is-active .ico{opacity:1}
.tab-indicator{
  position:absolute;bottom:-1px;height:3px;border-radius:99px;
  background:#fff;
  transition:left var(--t) var(--e-spring),width var(--t) var(--e-spring);
  pointer-events:none;
}

/* ---------- Second tier: the screens inside a section ----------
   Deliberately quieter than the header strip. It sits on the page background
   rather than the maroon bar, so the hierarchy is obvious at a glance: the
   header says which part of the database you are in, this says which screen. */
.section{display:flex;flex-direction:column;gap:18px}
.section.is-entering{animation:viewIn var(--t-slow) var(--e-out) both}

.subtabs{
  position:relative;display:flex;gap:2px;flex-wrap:wrap;
  padding-bottom:2px;
  border-bottom:1px solid var(--line);
}
.subtab{
  position:relative;z-index:1;
  display:flex;align-items:center;gap:8px;
  padding:10px 14px;border:0;border-radius:9px 9px 0 0;
  background:transparent;color:var(--ink-3);
  font:inherit;font-size:.875rem;font-weight:540;cursor:pointer;
  transition:color var(--t-fast) var(--e-out),background var(--t-fast) var(--e-out);
}
.subtab:hover{color:var(--b-700);background:var(--b-50)}
.subtab.is-active{color:var(--b-800);font-weight:620}
.subtab .ico{width:1.1em;height:1.1em;opacity:.75}
.subtab:hover .ico,.subtab.is-active .ico{opacity:1}
.subtab-indicator{
  position:absolute;bottom:-1px;height:2.5px;border-radius:99px;
  background:var(--b-700);
  transition:left var(--t) var(--e-spring),width var(--t) var(--e-spring);
  pointer-events:none;
}

/* A sheet's row count, so you can see how big each one is before opening it. */
.subtab-count{
  margin-left:7px;padding:1px 7px;border-radius:99px;
  background:var(--line-2);color:var(--ink-3);
  font-size:.7rem;font-weight:640;font-variant-numeric:tabular-nums;
}
.subtab.is-active .subtab-count{background:var(--b-100);color:var(--b-800)}

/* These sheets run to hundreds of rows; the table paints the first page and
   this reveals the rest, rather than laying out 500 rows nobody scrolls to. */
.ds-more{padding:12px 18px 16px;display:flex;justify-content:center;
  border-top:1px solid var(--line-2)}

/* ---------- The membership sheets: every column, side-scrolled ----------
   Eighteen columns will never fit, and hiding some of them means the answer you
   want is the one that got cut. So the panel scrolls sideways and keeps the lot.

   The scrollbar is deliberately visible and chunky. A thin overlay bar that only
   appears once you are already scrolling does not tell you there is more to the
   right, which is the one thing this table has to communicate. */
/* `width:max-content` sizes each column to its own content; `min-width:100%`
   keeps a narrow sheet like Categories filling the panel. NOT
   `min-width:max-content` — against the inherited `width:100%` that resolves to
   a table more than twice its content width (5400px against 2243px here), and
   the slack is handed out to every column, so a capped cell still sat in a
   347px one. */
#dsTable{width:max-content;min-width:100%}
#section-membership .table-wrap{
  overflow-x:auto;overflow-y:visible;
  scrollbar-width:auto;scrollbar-color:var(--b-300) var(--line-2);
  overscroll-behavior-x:contain;
}
#section-membership .table-wrap::-webkit-scrollbar{height:14px}
#section-membership .table-wrap::-webkit-scrollbar-track{
  background:var(--line-2);border-radius:99px;margin:0 10px}
#section-membership .table-wrap::-webkit-scrollbar-thumb{
  background:var(--b-300);border-radius:99px;border:3px solid var(--surface)}
#section-membership .table-wrap::-webkit-scrollbar-thumb:hover{background:var(--b-500)}

/* Whose row is this? Pin the first column so the name survives the scroll. */
#dsTable .ds-pin{
  position:sticky;left:0;z-index:2;
  background:var(--surface);
  box-shadow:1px 0 0 var(--line-2);
  font-weight:560;
}
#dsTable thead .ds-pin{z-index:3;background:var(--b-50)}
#dsTable tbody tr:hover .ds-pin{background:var(--b-50)}

/* Edit stays reachable without scrolling back to the far end. */
#dsTable .ds-actions{
  position:sticky;right:0;z-index:2;
  width:1%;padding-left:10px;padding-right:14px;
  background:var(--surface);
  box-shadow:-1px 0 0 var(--line-2);
  text-align:right;
}
#dsTable thead .ds-actions{
  z-index:3;background:var(--b-50);
  font-size:.7rem;letter-spacing:.06em;text-transform:uppercase;color:var(--ink-3);
}
#dsTable tbody tr:hover .ds-actions{background:var(--b-50)}
/* Truncation lives on the span, not the cell: `max-width` on a td is advisory
   under the auto table layout and gets ignored, so a long comment was widening
   its column to fit. */
#dsTable .ds-cell{
  display:block;max-width:34ch;
  overflow:hidden;text-overflow:ellipsis;white-space:nowrap;
}
/* Eighteen columns each paying 16px a side is 570px of nothing. Tighter here
   only; the other tables have room to breathe. */
#dsTable th,#dsTable td{padding-left:10px;padding-right:10px}

/* The name column is capped hard. Auto layout sizes a column to its widest
   cell, and business memberships live in the last-name field — one
   "Masone, White, Penkava & Cristofari" made every other row pay for 35
   characters. Ordinary surnames fit in 16; the outliers ellipsize, and the
   full value is on the cell's tooltip and in the edit form. */
#dsTable .ds-pin .ds-cell{max-width:min(16ch,38vw)}

@media (max-width:700px){
  #dsTable .ds-cell{max-width:20ch}
  /* No pinned column on a phone. The auto table layout sizes a column to its
     widest cell regardless of what the contents are capped at, so the pin ended
     up wider than the screen — a reference column you cannot see past is worse
     than scrolling without one. Edit stays pinned; it is only 66px. */
  #dsTable .ds-pin{position:static;box-shadow:none}
  #section-membership .table-wrap::-webkit-scrollbar{height:10px}
}

/* ---------- Sorting by a column ----------
   The whole heading is the control, so the button fills the cell and inherits
   its type rather than looking like a button sitting inside one. */
#dsTable thead th{padding:0}
.ds-sort{
  display:flex;align-items:center;gap:5px;width:100%;
  padding:calc(10px * var(--ds-zoom)) calc(10px * var(--ds-zoom));
  border:0;background:transparent;
  font:inherit;color:inherit;letter-spacing:inherit;text-transform:inherit;
  text-align:left;cursor:pointer;white-space:nowrap;
  transition:background var(--t-fast),color var(--t-fast);
}
.ds-sort:hover{background:var(--b-100);color:var(--b-800)}
.ds-sort.is-on{color:var(--b-800)}
.ds-arrow{font-size:.9em;line-height:1}
/* Held open for the arrow, or every heading shifts sideways when one appears. */
.ds-sort:not(.is-on) .ds-arrow{opacity:0}
.ds-sort:not(.is-on) .ds-arrow::before{content:"↑"}

/* ---------- Table zoom ----------
   One factor drives the type and the cell padding together; scaling only the
   font leaves the rows as tall as they were and saves nothing. */
#section-membership .panel{--ds-zoom:1}
#dsTable{font-size:calc(.86rem * var(--ds-zoom))}
#dsTable th,#dsTable td{
  padding-left:calc(10px * var(--ds-zoom));
  padding-right:calc(10px * var(--ds-zoom));
  padding-top:calc(10px * var(--ds-zoom));
  padding-bottom:calc(10px * var(--ds-zoom));
}
#dsTable th{font-size:calc(.71rem * var(--ds-zoom))}
/* The caps stay in `ch` and are NOT multiplied by the zoom: `ch` is already a
   fraction of the font size, so scaling both shrank the text twice over and a
   name was clipped to five characters at the smallest step. Zooming out should
   show the same words in less space, not fewer words. */

.zoom-group{display:inline-flex;border:1px solid var(--line);border-radius:9px;overflow:hidden}
.zoom-btn{
  display:grid;place-items:center;
  width:32px;height:32px;padding:0;border:0;
  background:var(--surface);color:var(--ink-2);
  font:inherit;font-size:1rem;line-height:1;cursor:pointer;
  transition:background var(--t-fast),color var(--t-fast);
}
.zoom-btn+.zoom-btn{border-left:1px solid var(--line)}
.zoom-btn:hover:not(:disabled){background:var(--b-50);color:var(--b-700)}
.zoom-btn:disabled{color:var(--line);cursor:default}

/* ---------- Membership filters ----------
   One line of pills that scrolls sideways, rather than a grid of captioned
   fields that wrapped onto three rows and took a third of a phone screen. Each
   pill names its own column while it is unset ("All category"), so the caption
   above it was saying the same thing twice. */
.ds-filters{
  display:flex;flex-wrap:nowrap;align-items:center;gap:8px;
  padding:0 18px 12px;
  overflow-x:auto;overscroll-behavior-x:contain;
  scrollbar-width:none;
}
.ds-filters::-webkit-scrollbar{display:none}
.ds-filter{display:block;flex:none;min-width:0}
.ds-filters select{
  padding:7px 30px 7px 11px;
  font-size:.82rem;font-weight:520;
  max-width:200px;text-overflow:ellipsis;
  border-radius:99px;
  background-color:var(--line-2);
  border-color:transparent;
  color:var(--ink-2);
  background-position:right 11px center;
}
.ds-filters select:hover{background-color:var(--b-50);color:var(--b-700)}

/* A filter you have set has to read differently from one you have not, or the
   only way to know why a count dropped is to open all six. */
.ds-filter.is-on select{
  background-color:var(--b-100);
  border-color:var(--b-300);
  color:var(--b-800);font-weight:620;
}
/* First in the row, not last: in a strip that scrolls, "clear the filters" is
   no use parked off the right-hand edge behind the ones you want to clear. */
.ds-filters #dsClearFilters{flex:none;order:-1;padding:7px 12px;border-radius:99px}
.ds-filters #dsClearFilters .ico{width:14px;height:14px}

/* A date that has already gone by, wherever it appears. */
#dsTable td.ds-past .ds-cell{color:var(--bad);font-weight:560}

/* ---------- Editing a cell in place ---------- */
/* The cell is the control, so it has to look like one before it is clicked. */
#dsTable td.ds-editable{cursor:cell}
#dsTable td.is-sel{
  outline:2px solid var(--b-500);outline-offset:-2px;
  background:var(--b-50);
}
#dsTable td.is-editing{padding:0;outline:2px solid var(--b-500);outline-offset:-2px}
#dsTable .ds-input{
  display:block;width:100%;min-width:120px;
  padding:9px 8px;border:0;border-radius:0;
  font:inherit;font-size:.86rem;color:var(--ink-1);
  background:var(--surface);
}
#dsTable .ds-input:focus{outline:none}
/* In flight, then briefly confirmed. A save that looks identical to no save is
   the one thing this cannot do. */
#dsTable td.is-saving{opacity:.55}
#dsTable td.is-saved{animation:cellSaved 1.1s ease-out}
@keyframes cellSaved{
  0%   {background:var(--ok-bg)}
  100% {background:transparent}
}
@media (prefers-reduced-motion:reduce){
  #dsTable td.is-saved{animation:none;background:var(--ok-bg)}
  /* The blanket rule zeroes durations but not delays, and this one delays
     hiding until the close finishes. With no close to wait for, drop it. */
  .cat-body,.cat-block.is-open .cat-body{transition-delay:0s}
}

@media (max-width:700px){
  .ds-filters{padding:0 14px 12px}
  .ds-filters select{max-width:170px}
}

/* ---------- Categories: each level is the row and the dropdown ---------- */
/* One grid shared by the legend and every row, so the columns line up without
   a table — a table cannot hold an expanding panel inside a row. */
:root{--cat-grid:18px minmax(0,1fr) 62px 84px 104px}

.cat-legend{
  display:grid;grid-template-columns:var(--cat-grid);
  align-items:center;gap:12px;
  padding:10px 18px;
  background:var(--b-50);border-bottom:1px solid var(--line);
  font-size:.71rem;font-weight:640;letter-spacing:.055em;text-transform:uppercase;
  color:var(--b-700);
}
.cat-legend span:last-child{text-align:right}

.cat-blocks{display:flex;flex-direction:column}
.cat-block{background:var(--surface);border-bottom:1px solid var(--line-2)}
.cat-block:last-child{border-bottom:0}
.cat-head{
  display:grid;grid-template-columns:var(--cat-grid);
  align-items:center;gap:12px;width:100%;
  padding:13px 18px;border:0;background:transparent;
  font:inherit;text-align:left;cursor:pointer;
  transition:background var(--t-fast) var(--e-out);
}
.cat-head:hover{background:var(--b-50)}
.cat-chev{
  width:16px;height:16px;color:var(--ink-3);
  transition:transform var(--t-fast) var(--e-out);
}
.cat-block.is-open .cat-chev{transform:rotate(90deg);color:var(--b-700)}
.cat-name{
  min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;
  font-size:.92rem;font-weight:600;color:var(--ink);
}
.cat-block.is-open .cat-name{color:var(--b-800)}
.cat-code{font-size:.82rem;color:var(--ink-3)}
.cat-amount{font-size:.88rem;font-weight:620;color:var(--ink-2)}
.cat-count{
  justify-self:end;padding:3px 10px;border-radius:99px;
  background:var(--line-2);color:var(--ink-3);
  font-size:.72rem;font-weight:600;white-space:nowrap;
}
.cat-block.is-open .cat-count{background:var(--b-100);color:var(--b-700)}

/* Opens by growing rather than appearing. `grid-template-rows: 0fr -> 1fr` is
   the one way to transition to a height nobody knows in advance — Benefactor
   is thirteen names and Senior is a hundred and eighty-five, and max-height
   guessed high enough to fit the big one makes the small one snap open. The
   child carries the padding: on the wrapper it would still be there at 0fr,
   leaving a collapsed section holding a band of empty space. */
/* Height is animated from JS, which measures it. The grid `0fr -> 1fr` trick
   was tried first and does not survive being closed again: `minmax()` is not
   reliably interpolable, so the track stranded part-open at 142px and the
   section never shut. An explicit height is one number, and it lands. */
.cat-body{
  overflow:hidden;height:0;
  opacity:0;visibility:hidden;
  transition:height var(--t) var(--e-out),
             opacity 140ms var(--e-out),
             visibility 0s linear var(--t);
}
.cat-block.is-open .cat-body{
  height:auto;opacity:1;visibility:visible;
  transition:height var(--t) var(--e-out),
             opacity 200ms var(--e-out) 60ms,
             visibility 0s;
}

/* The workbook lays each level out as a column of names; columns here too, as
   many as the screen takes. */
.cat-list{
  list-style:none;margin:0;
  padding:2px 18px 16px 44px;
  columns:4 190px;column-gap:22px;
}
.cat-body>.empty{padding:2px 18px 16px 44px}
.cat-list li{break-inside:avoid;margin:0 0 1px}
.cat-member{
  display:block;width:100%;padding:5px 8px;
  border:0;border-radius:7px;background:transparent;
  font:inherit;font-size:.85rem;color:var(--ink);text-align:left;cursor:pointer;
  overflow:hidden;text-overflow:ellipsis;white-space:nowrap;
  transition:background var(--t-fast),color var(--t-fast);
}
.cat-member:hover{background:var(--b-50);color:var(--b-700)}

@media (max-width:700px){
  /* Code and amount stack under the name rather than squeezing five columns
     across a phone; the legend goes with them. */
  /* Four, not three: hiding .cat-code takes it out of the grid flow, so the
     remaining chevron, name, amount and count need a track each. */
  :root{--cat-grid:18px minmax(0,1fr) auto auto}
  .cat-legend{display:none}
  .cat-head{padding:12px 14px;gap:10px}
  .cat-code{display:none}
  .cat-amount{font-size:.82rem}
  .cat-count{justify-self:end}
  .cat-list,.cat-body>.empty{padding:2px 14px 14px 30px}
  .cat-list{columns:1}
}

/* ---------- Placeholder for a section that has not been built yet ---------- */
.wip{
  display:flex;flex-direction:column;align-items:center;text-align:center;gap:10px;
  padding:clamp(40px,9vh,84px) 24px;
  background:var(--surface);border:1px solid var(--line);border-radius:var(--r-xl);
}
.wip .ico{width:40px;height:40px;color:var(--b-300)}
.wip h1{font-family:var(--font-display);font-size:1.7rem;font-weight:620;letter-spacing:-.02em}
.wip p{max-width:46ch;color:var(--ink-3);font-size:.9rem;line-height:1.65;text-wrap:balance}
.wip-badge{
  padding:5px 12px;border-radius:99px;
  background:var(--warn-bg);color:var(--warn);
  font-size:.7rem;font-weight:640;letter-spacing:.09em;text-transform:uppercase;
}

.topbar-right{display:flex;align-items:center;gap:14px;margin-left:8px}
.clock{display:flex;flex-direction:column;align-items:flex-end;line-height:1.2}
.clock strong{font-size:1rem;font-weight:620;font-variant-numeric:tabular-nums}
.clock span{font-size:.72rem;color:var(--on-b-2);white-space:nowrap}

.icon-btn{
  width:34px;height:34px;flex:none;
  display:grid;place-items:center;
  border:0;border-radius:10px;cursor:pointer;
  background:var(--b-600);color:#fff;font-size:.95rem;
  transition:background var(--t-fast),transform var(--t-fast) var(--e-spring);
}
.icon-btn:hover{background:var(--b-500)}
.icon-btn:active{transform:scale(.9)}
.icon-btn.is-off{opacity:.5}

/* Stage / views */
/* The tab strips bleed to the screen edges by cancelling this padding, so the
   two have to agree. As a literal repeated in four breakpoints they did not:
   the padding stepped down to 12px and the bleed stayed at 14, which pushed
   the page 3px wider than the phone. */
.stage{flex:1;--stage-pad:22px;padding:var(--stage-pad);max-width:1560px;width:100%;margin:0 auto}
.view{display:flex;flex-direction:column;gap:18px}
.view[hidden]{display:none}
.view.is-entering{animation:viewIn var(--t-slow) var(--e-out) both}
@keyframes viewIn{from{opacity:0;transform:translateY(10px)}to{opacity:1;transform:none}}

.view-head{display:flex;align-items:flex-end;justify-content:space-between;gap:16px;flex-wrap:wrap}
.view-head .sub{color:var(--ink-3);font-size:.86rem;margin-top:3px}
.view-actions{display:flex;gap:8px;flex-wrap:wrap}

.statusbar{
  display:flex;align-items:center;gap:10px;
  padding:11px 22px;
  border-top:1px solid var(--line);
  background:var(--surface);
  color:var(--ink-3);font-size:.76rem;
}
.statusbar-sep{opacity:.5}

/* ========================= PRIMITIVES ========================= */
.panel{
  background:var(--surface);
  border:1px solid var(--line);
  border-radius:var(--r-lg);
  overflow:hidden;
  transition:border-color var(--t) var(--e-out);
}
.panel:hover{border-color:var(--b-300)}
.panel-head{
  display:flex;align-items:center;justify-content:space-between;gap:12px;
  flex-wrap:wrap;
  padding:14px 18px;border-bottom:1px solid var(--line-2);
}
.panel-tools{display:flex;gap:8px;align-items:center;flex-wrap:wrap;min-width:0}
.panel-tools .input-sm{min-width:0}
/* Names a control sitting in a panel head, where a stacked field label would
   be taller than the head itself. */
.tools-label{
  font-size:.7rem;font-weight:620;letter-spacing:.055em;text-transform:uppercase;
  color:var(--ink-3);
}
.panel-tools select{font-size:.82rem;padding-top:7px;padding-bottom:7px}

.eyebrow{
  font-size:.7rem;font-weight:620;letter-spacing:.09em;text-transform:uppercase;
  color:var(--b-500);
}

.btn{
  display:inline-flex;align-items:center;justify-content:center;gap:7px;
  padding:9px 16px;border-radius:10px;border:1px solid transparent;
  font-size:.86rem;font-weight:560;cursor:pointer;white-space:nowrap;
  transition:background var(--t-fast),border-color var(--t-fast),color var(--t-fast),
             transform var(--t-fast) var(--e-spring);
}
.btn:active{transform:scale(.965)}
.btn-primary{background:var(--b-700);color:#fff}
.btn-primary:hover{background:var(--b-600)}
.btn-ghost{background:var(--surface);border-color:var(--line);color:var(--ink-2)}
.btn-ghost:hover{background:var(--b-50);border-color:var(--b-300);color:var(--b-700)}
.btn-danger{background:var(--bad-bg);color:var(--bad);border-color:var(--bad-line)}
.btn-danger:hover{background:var(--bad-bg-hover)}
.btn-sm{padding:6px 11px;font-size:.79rem;border-radius:8px}

/* Every text-like input, by exclusion rather than by listing types. Listing
   them meant `password` and `date` fell through to the browser's own styling,
   so the sign-in box looked different from the one above it. */
.input-sm,
input:not([type=checkbox]):not([type=radio]):not([type=hidden]),
select,textarea{
  padding:8px 11px;border:1px solid var(--line);border-radius:9px;
  background:var(--surface);color:var(--ink);font:inherit;font-size:.85rem;
  transition:border-color var(--t-fast),box-shadow var(--t-fast),background var(--t-fast);
}
.input-sm:focus,input:focus,select:focus,textarea:focus{
  outline:0;border-color:var(--b-500);box-shadow:0 0 0 3.5px var(--b-100);
}
input::placeholder{color:var(--ink-3)}
textarea{resize:vertical;min-height:64px;line-height:1.5}
/* The native date control keeps its own picker button; just match the frame. */
input[type=date]{min-height:38px}
input[type=date]::-webkit-calendar-picker-indicator{cursor:pointer;opacity:.55}
input[type=date]::-webkit-calendar-picker-indicator:hover{opacity:1}
select{cursor:pointer;padding-right:26px;appearance:none;
  background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='10' height='6'%3E%3Cpath d='M1 1l4 4 4-4' fill='none' stroke='%238B8085' stroke-width='1.6' stroke-linecap='round'/%3E%3C/svg%3E");
  background-repeat:no-repeat;background-position:right 10px center}
.field{display:flex;flex-direction:column;gap:5px}
.field label{font-size:.76rem;font-weight:580;color:var(--ink-2)}
.field input,.field select{width:100%}

.pill{
  display:inline-flex;align-items:center;gap:5px;
  padding:3px 9px;border-radius:99px;
  font-size:.71rem;font-weight:600;letter-spacing:.02em;
  background:var(--b-100);color:var(--b-700);
}
.pill-muted{background:var(--line-2);color:var(--ink-3)}
.pill::before{content:"";width:5px;height:5px;border-radius:99px;background:currentColor;opacity:.75}
.pill-muted::before{display:none}
#feedPill::before{animation:blip 1.8s var(--e-in-out) infinite}
@keyframes blip{0%,100%{opacity:.25;transform:scale(.8)}50%{opacity:1;transform:scale(1.15)}}

.badge{
  display:inline-flex;align-items:center;
  padding:2.5px 8px;border-radius:6px;
  font-size:.71rem;font-weight:620;letter-spacing:.01em;
}
.badge-ok{background:var(--ok-bg);color:var(--ok)}
.badge-warn{background:var(--warn-bg);color:var(--warn)}
.badge-bad{background:var(--bad-bg);color:var(--bad)}
.badge-info{background:var(--info-bg);color:var(--info)}
.badge-muted{background:var(--line-2);color:var(--ink-3)}

.chip{
  display:inline-block;padding:2.5px 8px;margin:2px 3px 2px 0;
  border-radius:6px;background:var(--b-50);border:1px solid var(--b-100);
  color:var(--b-700);font-size:.71rem;font-weight:560;white-space:nowrap;
}

.avatar{
  width:38px;height:38px;flex:none;
  display:grid;place-items:center;border-radius:50%;
  background:var(--b-700);
  color:#fff;font-size:.78rem;font-weight:640;letter-spacing:.02em;
}
.avatar-sm{width:30px;height:30px;font-size:.68rem}
.avatar-lg{width:60px;height:60px;font-size:1.15rem}

.mono{font-family:var(--font-mono);font-size:.79rem;letter-spacing:-.02em;color:var(--ink-2)}
.empty{padding:34px 18px;text-align:center;color:var(--ink-3);font-size:.86rem}
.hint{font-size:.78rem;color:var(--ink-3)}

/* ========================= BOOT SPLASH ========================= */
.booting{
  position:fixed;inset:0;z-index:210;
  display:grid;place-items:center;
  background:var(--b-700);
}
.booting[hidden]{display:none}
.booting-inner{display:flex;flex-direction:column;align-items:center;gap:18px}
.booting-inner img{height:56px;width:auto;background:#fff;padding:8px 14px;border-radius:12px}
.booting-inner p{color:var(--on-b-2);font-size:.84rem}
.booting-bar{display:block;width:150px;height:3px;border-radius:99px;background:var(--b-800);overflow:hidden}
.booting-bar i{
  display:block;width:40%;height:100%;border-radius:99px;background:#fff;
  animation:bootSlide 1.1s var(--e-in-out) infinite;
}
@keyframes bootSlide{
  0%{transform:translateX(-110%)}
  100%{transform:translateX(360%)}
}

/* ========================= SIGN IN =========================
   The dashboard is not rendered at all until a session is confirmed, so this
   screen stands alone rather than sitting on top of a blurred app. */
.signin{
  position:fixed;inset:0;z-index:200;
  display:grid;place-items:center;
  padding:20px;
  background:var(--b-700);
  overflow:auto;
}
.signin-card{
  width:min(400px,100%);
  background:var(--surface);
  border-radius:var(--r-xl);
  padding:30px 30px 24px;
  animation:signinIn var(--t-slow) var(--e-out) both;
}
@keyframes signinIn{from{opacity:0;transform:translateY(14px)}to{opacity:1;transform:none}}
.signin-brand{display:grid;place-items:center;margin-bottom:18px}
.signin-brand img{display:block;height:54px;width:auto}
.signin-card h1{font-size:1.3rem;font-weight:650;text-align:center}
.signin-sub{margin-top:4px;text-align:center;color:var(--ink-3);font-size:.85rem}
.signin-card form{display:flex;flex-direction:column;gap:14px;margin-top:22px}
.signin-card .field input{padding:11px 13px;font-size:.95rem}
.btn-block{width:100%;padding:12px 16px;font-size:.92rem}
.signin-error{
  padding:9px 12px;border-radius:9px;
  background:var(--bad-bg);color:var(--bad);
  font-size:.82rem;font-weight:520;
  border:1px solid var(--bad-line);
}
.signin-foot{
  margin-top:20px;padding-top:16px;border-top:1px solid var(--line-2);
  text-align:center;font-size:.8rem;color:var(--ink-3);
}
.signin-foot a{color:var(--b-700);font-weight:560}

/* ========================= WHO IS SIGNED IN ========================= */
.whoami{position:relative}
.whoami-btn{
  display:flex;align-items:center;gap:8px;
  padding:5px 9px 5px 5px;border:0;border-radius:99px;cursor:pointer;
  background:var(--b-600);color:#fff;
  transition:background var(--t-fast);
}
.whoami-btn:hover{background:var(--b-500)}
.whoami-btn .ico{width:14px;height:14px;color:var(--on-b-2)}
.whoami-avatar{
  width:26px;height:26px;flex:none;
  display:grid;place-items:center;border-radius:99px;
  background:var(--b-800);
  font-size:.7rem;font-weight:660;letter-spacing:.02em;
}
.whoami-name{font-size:.82rem;font-weight:540;max-width:120px;
  overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.whoami-menu{
  position:absolute;top:calc(100% + 8px);right:0;z-index:60;
  min-width:220px;padding:6px;
  background:var(--surface);border:1px solid var(--line);border-radius:var(--r-md);
  animation:menuIn var(--t-fast) var(--e-out) both;
}
@keyframes menuIn{from{opacity:0;transform:translateY(-5px)}to{opacity:1;transform:none}}
/* Leaves the way it came, a little quicker — an exit that matches the entrance
   in length reads as sluggish, because nobody is waiting to see it. */
.whoami-menu.is-out{animation:menuOut 130ms var(--e-in-out) both}
@keyframes menuOut{to{opacity:0;transform:translateY(-4px) scale(.985)}}
.whoami-head{
  padding:9px 11px 10px;margin-bottom:4px;border-bottom:1px solid var(--line-2);
  display:flex;flex-direction:column;line-height:1.35;
}
.whoami-head strong{font-size:.87rem;color:var(--ink)}
.whoami-head span{font-size:.73rem;color:var(--ink-3);text-transform:capitalize}
.whoami-item{
  width:100%;display:flex;align-items:center;gap:9px;
  padding:9px 11px;border:0;border-radius:8px;cursor:pointer;
  background:transparent;color:var(--ink-2);
  font-size:.83rem;font-weight:520;text-align:left;
  transition:background var(--t-fast),color var(--t-fast);
}
.whoami-item:hover{background:var(--b-50);color:var(--b-700)}
.whoami-item .ico{width:15px;height:15px}

.select-wrap{position:relative;display:block}
.select-wrap select{width:100%}
.select-wrap .chev{display:none}

/* ========================= BULK EDITING =========================
   The checkbox column only exists while selecting, so the table looks exactly
   as it did before the moment you turn selection off. */
/* "2 days ago" broke across two lines once the checkbox column narrowed the
   row on a phone. */
.nowrap{white-space:nowrap}

.col-check{display:none;width:44px}
.table.is-selecting .col-check{display:table-cell}
.table.is-selecting tbody tr{cursor:pointer}
.col-check input[type=checkbox]{width:17px;height:17px;margin:0;cursor:pointer;accent-color:var(--b-700)}
tr.is-picked > td{background:var(--b-50)}
tr.is-picked > td:first-child{box-shadow:inset 3px 0 0 var(--b-700)}

.bulkbar{
  display:flex;align-items:center;justify-content:space-between;gap:14px;flex-wrap:wrap;
  padding:11px 18px;
  background:var(--b-50);
  border-bottom:1px solid var(--b-200);
  animation:bulkIn var(--t) var(--e-out) both;
}
@keyframes bulkIn{from{opacity:0;transform:translateY(-6px)}to{opacity:1;transform:none}}
.bulkbar-left{display:flex;align-items:center;gap:9px;flex-wrap:wrap;min-width:0}
.bulkbar-count{
  font-size:.83rem;font-weight:600;color:var(--b-700);
  font-variant-numeric:tabular-nums;white-space:nowrap;
}
.bulkbar-actions{display:flex;align-items:center;gap:7px;flex-wrap:wrap}
.bulkbar-actions .btn[disabled]{opacity:.4;pointer-events:none}
.bulkbar .chip-btn{background:var(--surface)}

@media (max-width:700px){
  .bulkbar{align-items:stretch;flex-direction:column;gap:10px}
  .bulkbar-actions{display:grid;grid-template-columns:1fr 1fr;gap:7px}
  .bulkbar-actions .btn{justify-content:center}
}

/* ========================= NOTICE ========================= */
.notice{
  display:flex;align-items:flex-start;gap:12px;
  padding:14px 18px;
  background:var(--b-50);
  border-color:var(--b-200);
}
.notice:hover{border-color:var(--b-200)}
.notice > .ico{width:19px;height:19px;flex:none;margin-top:1px;color:var(--b-700)}
.notice p{font-size:.85rem;color:var(--ink-2);line-height:1.55}
.notice a{color:var(--b-700);font-weight:560}

/* ========================= TABLES ========================= */
.table-wrap{overflow-x:auto}
.table{width:100%;border-collapse:collapse;font-size:.86rem}
.table th{
  position:sticky;top:0;z-index:1;
  padding:10px 16px;text-align:left;white-space:nowrap;
  background:var(--b-50);
  font-size:.71rem;font-weight:640;letter-spacing:.055em;text-transform:uppercase;
  color:var(--b-700);
  border-bottom:1px solid var(--line);
}
.table td{padding:11px 16px;border-bottom:1px solid var(--line-2);vertical-align:middle}
.table tbody tr{animation:rowIn 380ms var(--e-out) both;animation-delay:calc(var(--i,0) * 22ms)}
@keyframes rowIn{from{opacity:0;transform:translateY(6px)}to{opacity:1;transform:none}}
.table tbody tr:last-child td{border-bottom:0}
.table-hover tbody tr{cursor:pointer;transition:background var(--t-fast)}
.table-hover tbody tr:hover{background:var(--b-50)}
.cell-person{display:flex;align-items:center;gap:10px}
.cell-person strong{font-weight:570}
.cell-person small{display:block;color:var(--ink-3);font-size:.74rem;font-weight:400}
.num{font-variant-numeric:tabular-nums}
.table td.num{white-space:nowrap}
.row-arrow{width:1.05em;height:1.05em;color:var(--ink-3);
  transition:transform var(--t-fast) var(--e-out),color var(--t-fast)}
.table-hover tr:hover .row-arrow{transform:translateX(3px);color:var(--b-700)}

/* ========================= KIOSK ========================= */
.kiosk{display:grid;grid-template-columns:minmax(0,1fr) 348px;gap:18px;align-items:start}
.kiosk-main{display:flex;flex-direction:column;gap:14px}

.kiosk-card{
  position:relative;overflow:hidden;
  border-radius:var(--r-xl);
  background:var(--b-700);
  color:#fff;border:1px solid var(--b-800);
}
.kiosk-head{
  position:relative;
  display:flex;align-items:flex-start;justify-content:space-between;gap:18px;
  padding:20px 24px;
  border-bottom:1px solid var(--b-600);
}
.kiosk-head .eyebrow{color:var(--on-b-3)}

/* The kiosk shows the class, it no longer chooses it — that moved to the
   dashboard, which is behind a login. Same size and weight as the old picker
   so the screen reads identically from across the room. */
.kiosk-classname{
  margin-top:4px;
  font-family:var(--font-display);font-size:1.6rem;font-weight:600;
  letter-spacing:-.02em;color:#fff;line-height:1.15;
}
.kiosk-classmeta{margin-top:4px;font-size:.82rem;color:var(--on-b-2)}

/* Nothing is in session: dim the target so nobody taps expecting a result. */
.scanner.is-closed .scan-rings span{animation-play-state:paused;opacity:.25}
.scanner.is-closed .scan-core{opacity:.35}
.scanner.is-closed .scan-title{opacity:.5}

.kiosk-count{
  flex:none;text-align:center;
  padding:10px 18px;border-radius:14px;
  background:var(--b-800);border:1px solid var(--b-600);
}
.kiosk-count span{display:block;font-size:1.9rem;font-weight:680;line-height:1;font-variant-numeric:tabular-nums}
.kiosk-count small{font-size:.68rem;letter-spacing:.07em;text-transform:uppercase;color:var(--on-b-3)}

.scanner{
  position:relative;
  padding:44px 28px 40px;
  display:flex;flex-direction:column;align-items:center;text-align:center;
}
.scan-rings{position:relative;width:150px;height:150px;display:grid;place-items:center}
.scan-rings span{
  position:absolute;inset:0;border-radius:50%;
  border:2px solid rgba(255,255,255,.75);
  animation:ripple 3s linear infinite;
}
.scan-rings span:nth-child(2){animation-delay:1s}
.scan-rings span:nth-child(3){animation-delay:2s}

/* Three things about this were wrong, and all three only really showed up on a
   phone, where the whole target is a third of the size:

   - It started at scale .52. The disc in the middle is 56% of the box, so the
     brightest moment of the ripple happened *behind* the disc — the ring only
     emerged once it had already started fading.
   - `ease-out` meant it decelerated hard and then sat almost still at the outer
     edge for most of the cycle. Parked, faint, and barely moving reads exactly
     like a ring that is not animating at all.
   - Past 70% it was down to .12 opacity through a .5-alpha border — about 6%
     white. Invisible in daylight.

   So: start outside the disc, travel at a constant rate, and stay bright until
   the very end of the trip. */
@keyframes ripple{
  0%{transform:scale(.62);opacity:0}
  10%{opacity:.85}
  75%{opacity:.4}
  100%{transform:scale(1.34);opacity:0}
}
.scan-core{position:absolute;inset:0;display:grid;place-items:center}
.scan-core::before{
  content:"";position:absolute;width:84px;height:84px;border-radius:50%;
  background:rgba(255,255,255,.14);border:1px solid rgba(255,255,255,.3);
  animation:breathe 3s var(--e-in-out) infinite;
}
@keyframes breathe{0%,100%{transform:scale(.97);opacity:.78}50%{transform:scale(1.07);opacity:1}}
.nfc-glyph{position:relative;width:46px;height:46px;color:#fff}
.scanner.is-busy .scan-rings span{animation-duration:1.1s}

.scan-title{
  margin-top:22px;
  font-family:var(--font-display);font-size:2.1rem;font-weight:600;letter-spacing:-.02em;
  /* Keeps the line lengths even instead of leaving one word stranded on the
     last line, which is what "…to sign / in" was doing on a phone. */
  text-wrap:balance;
}
.scan-sub{margin-top:8px;font-size:1rem;color:var(--on-b-2);max-width:44ch;text-wrap:balance}

/* Reader status, shown in the footer status bar (light background). */
.reader-state{display:inline-flex;align-items:center;gap:7px;min-width:0}
.reader-state .dot{width:8px;height:8px;border-radius:50%;flex:none;background:var(--warn)}
.reader-state.is-live .dot{background:var(--ok);animation:blip 2s infinite}
.reader-state.is-off .dot{background:var(--bad)}
.reader-state span{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}

/* ========================= KPIs ========================= */
.kpis{display:grid;grid-template-columns:repeat(auto-fit,minmax(196px,1fr));gap:14px}
/* A button, not a card: each one opens the working behind its number, so it
   has to be reachable by keyboard and announce itself as a control. */
.kpi{
  position:relative;overflow:hidden;
  display:block;width:100%;text-align:left;
  padding:16px 18px;
  background:var(--surface);border:1px solid var(--line);border-radius:var(--r-lg);
  font:inherit;color:inherit;cursor:pointer;
  animation:viewIn var(--t-slow) var(--e-out) both;animation-delay:calc(var(--i,0) * 55ms);
  transition:transform var(--t) var(--e-out),border-color var(--t);
}
.kpi>*{display:block}
.kpi:hover{transform:translateY(-3px);border-color:var(--b-300)}
.kpi:active{transform:translateY(-1px) scale(.995)}

/* Sits in the corner and only commits on hover — five permanent arrows across
   the top of the dashboard would be louder than the numbers. */
.kpi-go{
  position:absolute;top:14px;right:14px;
  width:15px;height:15px;color:var(--b-300);
  opacity:0;transform:translateX(-3px);
  transition:opacity var(--t-fast) var(--e-out),transform var(--t-fast) var(--e-out);
}
.kpi:hover .kpi-go,.kpi:focus-visible .kpi-go{opacity:1;transform:none}
.kpi::after{
  content:"";position:absolute;left:0;top:0;bottom:0;width:3px;
  background:var(--b-700);
  transform:scaleY(0);transform-origin:top;
  transition:transform var(--t-slow) var(--e-out);
}
.kpi:hover::after{transform:scaleY(1)}
.kpi-label{font-size:.72rem;font-weight:600;letter-spacing:.06em;text-transform:uppercase;color:var(--ink-3)}
.kpi-value{margin-top:7px;font-size:2.05rem;font-weight:670;line-height:1;letter-spacing:-.025em;
  font-variant-numeric:tabular-nums;color:var(--b-800)}
.kpi-value .unit{font-size:.9rem;font-weight:560;color:var(--ink-3);margin-left:3px;letter-spacing:0}
.kpi-foot{margin-top:8px;font-size:.76rem;color:var(--ink-3);display:flex;align-items:center;gap:6px}

/* ---------- The panel behind a KPI ---------- */
/* Tables inside a dialog lose the panel's border and rounded corners, so they
   need their own edge or they run into the modal's sides. */
.kpi-detail{margin-top:14px}
/* overflow-x stays `auto`, not `hidden`. Hidden was enough to clip the rounded
   corners, and it also silently cancelled the scrolling this inherits — the
   five-column Late table is wider than the dialog on a phone, so the last
   columns were cut off with no way to reach them. */
.kpi-detail .table-wrap{
  border:1px solid var(--line);border-radius:var(--r-md);
  overflow-x:auto;overflow-y:hidden;
  margin-top:8px;
}
.kpi-detail .table th{background:var(--b-50)}
.kpi-detail .table td,.kpi-detail .table th{padding:9px 12px}
.kpi-detail .table tbody tr:hover td{background:var(--b-50)}
.kpi-detail .empty{padding:26px 18px;line-height:1.7}
/* One heading per class when a detail view is grouped. */
.kpi-group{
  display:flex;align-items:center;gap:10px;flex-wrap:wrap;
  margin:20px 0 2px;font-size:.95rem;font-weight:620;
}
.kpi-group:first-child{margin-top:0}
.kpi-detail>.hint+.table-wrap{margin-top:10px}
.trend-up{color:var(--ok);font-weight:620}
.trend-down{color:var(--bad);font-weight:620}

/* ========================= DONUT + SPARK ========================= */
.grid-2{display:grid;grid-template-columns:minmax(0,1fr) minmax(0,1.35fr);gap:18px}

.donut-wrap{position:relative;padding:20px;display:flex;align-items:center;gap:22px;flex-wrap:wrap}
.donut{width:148px;height:148px;flex:none;transform:rotate(-90deg)}
.donut-track{fill:none;stroke:var(--line-2);stroke-width:14}
.donut-value{
  fill:none;stroke:var(--b-700);stroke-width:14;stroke-linecap:round;
  stroke-dasharray:339.29;stroke-dashoffset:339.29;
  transition:stroke-dashoffset 1s var(--e-out);
}
.donut-center{
  position:absolute;left:20px;top:20px;width:148px;height:148px;
  display:flex;flex-direction:column;align-items:center;justify-content:center;pointer-events:none;
}
.donut-center strong{font-size:1.75rem;font-weight:680;color:var(--b-800);font-variant-numeric:tabular-nums;letter-spacing:-.02em}
.donut-center span{font-size:.74rem;color:var(--ink-3)}
.legend{list-style:none;margin:0;padding:0;flex:1;min-width:170px;display:flex;flex-direction:column;gap:9px}
.legend li{display:flex;align-items:center;gap:9px;font-size:.83rem;
  animation:viewIn 420ms var(--e-out) both;animation-delay:calc(var(--i,0) * 60ms)}
.legend .swatch{width:9px;height:9px;border-radius:3px;flex:none;background:var(--b-700)}
.legend .lg-name{flex:1;min-width:0;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;color:var(--ink-2)}
.legend .lg-val{font-weight:620;font-variant-numeric:tabular-nums;color:var(--ink)}

/* ========================= SPARK ========================= */
.spark{padding:20px;display:flex;align-items:flex-end;gap:12px;height:188px}
.spark-col{
  flex:1;display:flex;flex-direction:column;align-items:center;gap:8px;
  height:100%;justify-content:flex-end;
}
.spark-bar{
  position:relative;width:100%;max-width:52px;border-radius:8px 8px 3px 3px;
  background:var(--b-500);
  transform-origin:bottom;animation:barGrow 700ms var(--e-out) both;
  animation-delay:calc(var(--i,0) * 70ms);
  transition:background var(--t-fast);
  min-height:4px;
}
.spark-bar.is-today{background:var(--b-800)}
@keyframes barGrow{from{transform:scaleY(0);opacity:.3}to{transform:scaleY(1);opacity:1}}
.spark-bar .bar-val{
  position:absolute;top:-20px;left:50%;transform:translateX(-50%);
  font-size:.74rem;font-weight:640;color:var(--b-800);font-variant-numeric:tabular-nums;
}
.spark-lbl{font-size:.72rem;color:var(--ink-3);text-align:center;line-height:1.3;display:block}
.spark-lbl b{display:block;color:var(--ink-2);font-weight:600;font-size:.75rem}

/* A whole panel that is one control. Only spans inside, so the head's heading
   is a class rather than an <h2>. */
.panel-tap{
  display:block;width:100%;text-align:left;padding:0;
  font:inherit;color:inherit;cursor:pointer;
}
.panel-h{font-size:1rem;font-weight:640;letter-spacing:-.015em}
.panel-go{
  display:inline-flex;align-items:center;gap:5px;
  font-size:.74rem;font-weight:620;letter-spacing:.03em;color:var(--b-300);
  opacity:0;transform:translateX(-4px);
  transition:opacity var(--t-fast) var(--e-out),transform var(--t-fast) var(--e-out),
             color var(--t-fast);
}
.panel-go .ico{width:14px;height:14px}
.panel-tap:hover .panel-go,.panel-tap:focus-visible .panel-go{
  opacity:1;transform:none;color:var(--b-600);
}
.panel-tap:hover .spark-bar{background:var(--b-600)}
.panel-tap:hover .spark-bar.is-today{background:var(--b-900)}
.panel-tap:active{transform:scale(.995)}

/* ========================= CALENDAR =========================
   The month grid on the dashboard and in History. Six rows always: a grid
   that shrinks in February and grows again in March makes the whole panel
   jump every time you page, and two spare cells cost less than that. */
.cal{padding:14px 16px 15px}
/* In a dialog the body already supplies the padding. */
.cal-modal{padding:0}
.cal-modal .cal-head{padding-top:0}
.cal-head{display:flex;align-items:center;justify-content:space-between;gap:10px;padding:0 2px 12px}
.cal-month{
  font-size:.95rem;font-weight:640;letter-spacing:-.01em;color:var(--ink);
  font-variant-numeric:tabular-nums;
}
.cal-turn{
  width:30px;height:30px;flex:none;display:grid;place-items:center;
  border:1px solid var(--line);border-radius:9px;cursor:pointer;
  background:var(--surface);color:var(--ink-2);
  transition:background var(--t-fast),color var(--t-fast),border-color var(--t-fast),
             transform var(--t-fast) var(--e-spring);
}
.cal-turn .ico{width:15px;height:15px;vertical-align:0}
.cal-turn:hover:not(:disabled){background:var(--b-50);border-color:var(--b-200);color:var(--b-700)}
.cal-turn:active:not(:disabled){transform:scale(.88)}
.cal-turn:disabled{opacity:.32;cursor:default}

.cal-dow{display:grid;grid-template-columns:repeat(7,minmax(0,1fr));gap:4px;margin-bottom:6px}
.cal-dow span{
  text-align:center;font-size:.65rem;font-weight:640;letter-spacing:.07em;
  text-transform:uppercase;color:var(--ink-3);
}

.cal-grid{display:grid;grid-template-columns:repeat(7,minmax(0,1fr));gap:4px}
/* Paging slides the whole month in from the side you came from — the cells
   are staggered on arrival only, and 42 of them staggering on every turn
   would read as a page load rather than a step. */
.cal-grid.turn-next{animation:calNext 340ms var(--e-out) both}
.cal-grid.turn-prev{animation:calPrev 340ms var(--e-out) both}
@keyframes calNext{from{opacity:0;transform:translateX(18px)}to{opacity:1;transform:none}}
@keyframes calPrev{from{opacity:0;transform:translateX(-18px)}to{opacity:1;transform:none}}

.cal-day{
  height:42px;padding:0;
  display:flex;flex-direction:column;align-items:center;justify-content:center;gap:2px;
  border:1px solid transparent;border-radius:9px;
  background:var(--surface);font:inherit;color:var(--ink-2);cursor:pointer;
  animation:viewIn 380ms var(--e-out) both;animation-delay:calc(var(--r,0) * 45ms);
  transition:background var(--t-fast),color var(--t-fast),border-color var(--t-fast),
             box-shadow var(--t-fast),transform var(--t-fast) var(--e-out);
}
.cal-num{font-size:.78rem;font-weight:560;line-height:1;font-variant-numeric:tabular-nums}
.cal-count{font-size:.66rem;font-weight:680;line-height:1;font-variant-numeric:tabular-nums;color:var(--b-700)}

/* Shading ramp. Five steps, and the legend shows all five: every past day
   sits on this ramp and nothing else. A day off used to get a grey of its
   own — a shade the key never explained, so it read as a mystery rather
   than a distinction. Zero is zero, whether a class ran or not. */
.cal-day[data-lvl="0"]{border-color:var(--line)}
.cal-day[data-lvl="1"]{background:var(--b-100);border-color:var(--b-100)}
.cal-day[data-lvl="2"]{background:var(--b-200);border-color:var(--b-200)}
.cal-day[data-lvl="3"]{background:var(--b-300);border-color:var(--b-300);color:var(--b-900)}
.cal-day[data-lvl="3"] .cal-count{color:var(--b-900)}
.cal-day[data-lvl="4"]{background:var(--b-600);border-color:var(--b-600);color:#fff}
.cal-day[data-lvl="4"] .cal-count{color:var(--on-b-2)}

.cal-day:hover:not(:disabled){transform:translateY(-2px);border-color:var(--b-500)}

/* Today and the selected day are different facts and can be true at once, so
   they get different marks: today an inset ring, selection a ring around the
   outside. The selected fill alone was not enough — --b-700 against a busy
   day's --b-600 is a shade apart, and on the History picker you could not see
   which day you had chosen. */
.cal-day.is-today{outline:2px solid var(--b-700);outline-offset:-2px}
.cal-day.is-today[data-lvl="3"],.cal-day.is-today[data-lvl="4"],.cal-day.is-sel.is-today{
  outline-color:var(--on-b-2);
}
/* Spread only, no offset and no blur: that is a ring, not elevation. */
.cal-day.is-sel{
  background:var(--b-700);border-color:var(--b-700);color:#fff;
  box-shadow:0 0 0 2px var(--b-900);
}
.cal-day.is-sel .cal-count{color:var(--on-b-2)}
.cal-day.is-future{opacity:.38;cursor:default}
/* Days either side of the month: shown so the weeks read as weeks, dimmed
   enough that they never look clickable. */
.cal-day.is-pad{background:transparent;border-color:transparent;color:var(--ink-3);opacity:.32}

.cal-foot{
  display:flex;align-items:center;justify-content:space-between;gap:12px;flex-wrap:wrap;
  margin-top:12px;padding-top:11px;border-top:1px solid var(--line-2);
  font-size:.73rem;color:var(--ink-3);
}
.cal-sum strong{color:var(--b-800);font-weight:660;font-variant-numeric:tabular-nums}
.cal-scale{display:inline-flex;align-items:center;gap:4px}
.cal-scale i{width:11px;height:11px;border-radius:3px;display:block}
.cal-scale i[data-lvl="0"]{border:1px solid var(--line)}
.cal-scale i[data-lvl="1"]{background:var(--b-100)}
.cal-scale i[data-lvl="2"]{background:var(--b-200)}
.cal-scale i[data-lvl="3"]{background:var(--b-300)}
.cal-scale i[data-lvl="4"]{background:var(--b-600)}

/* ========================= CLASS GRID ========================= */
.class-grid{padding:16px;display:grid;grid-template-columns:repeat(auto-fill,minmax(268px,1fr));gap:14px}
.class-card{
  padding:15px 16px;border:1px solid var(--line);border-radius:var(--r-md);
  background:var(--surface);
  animation:viewIn 460ms var(--e-out) both;animation-delay:calc(var(--i,0) * 55ms);
  transition:transform var(--t) var(--e-out),border-color var(--t);
}
.class-card:hover{transform:translateY(-2px);border-color:var(--b-300)}
.cc-top{display:flex;align-items:flex-start;justify-content:space-between;gap:10px}
.cc-name{font-size:.95rem;font-weight:620;letter-spacing:-.01em}
.cc-meta{margin-top:3px;font-size:.76rem;color:var(--ink-3)}
.cc-bar{margin-top:13px;height:7px;border-radius:99px;background:var(--line-2);overflow:hidden}
.cc-bar i{
  display:block;height:100%;border-radius:99px;
  background:var(--b-700);
  transform-origin:left;animation:fillIn 900ms var(--e-out) both;
  animation-delay:calc(var(--i,0) * 55ms + 120ms);
}
@keyframes fillIn{from{transform:scaleX(0)}to{transform:scaleX(1)}}
.cc-foot{margin-top:9px;display:flex;align-items:center;justify-content:space-between;font-size:.78rem}
.cc-foot .cc-count{font-weight:620;color:var(--b-800);font-variant-numeric:tabular-nums}
.cc-foot .cc-of{color:var(--ink-3);font-weight:400}

/* ========================= CLASSES ========================= */
.week-grid{
  display:grid;grid-template-columns:repeat(7,minmax(0,1fr));
  gap:1px;background:var(--line-2);border-top:1px solid var(--line-2);
}
.week-col{background:var(--surface);padding:12px 10px;min-height:132px;
  animation:viewIn 380ms var(--e-out) both;animation-delay:calc(var(--i,0) * 45ms)}
.week-col.is-today{background:var(--b-50)}
.week-day{
  font-size:.72rem;font-weight:640;letter-spacing:.06em;text-transform:uppercase;
  color:var(--ink-3);margin-bottom:9px;display:flex;align-items:center;gap:6px;
}
.week-col.is-today .week-day{color:var(--b-700)}
.week-col.is-today .week-day::after{
  content:"Today";padding:1px 5px;border-radius:4px;
  background:var(--b-100);color:var(--b-700);font-size:.62rem;letter-spacing:.02em;
}
.week-slot{
  width:100%;text-align:left;cursor:pointer;
  display:block;padding:7px 9px;margin-bottom:5px;
  border:1px solid var(--b-100);border-left:3px solid var(--b-700);
  border-radius:7px;background:var(--b-50);
  transition:background var(--t-fast),transform var(--t-fast) var(--e-out),border-color var(--t-fast);
}
.week-slot:hover{background:var(--b-100);transform:translateX(2px)}
.week-slot strong{display:block;font-size:.775rem;font-weight:590;color:var(--b-800);line-height:1.25}
.week-slot span{display:block;font-size:.7rem;color:var(--ink-3);font-variant-numeric:tabular-nums;margin-top:1px}
.week-empty{font-size:.73rem;color:var(--ink-3);opacity:.7}

/* ---------- dragging a class to another day ---------- */
.week-slot{cursor:grab}
.week-slot:active{cursor:grabbing}

/* The chip left behind while its copy follows the pointer. */
.week-slot.is-dragging{opacity:.35;transform:none}
.week-slot.is-dragging:hover{background:var(--b-50);transform:none}

/* The copy under the pointer. */
.week-ghost{
  position:fixed;z-index:300;margin:0;pointer-events:none;
  cursor:grabbing;
  border-color:var(--b-500);background:var(--surface);
  box-shadow:0 6px 16px rgba(62,12,21,.22);
  transform-origin:top left;
}

/* Where it will land, and where it cannot. */
.week-col.is-drop{background:var(--b-100);outline:2px dashed var(--b-500);outline-offset:-3px}
.week-col.is-drop-bad{outline:2px dashed var(--line);outline-offset:-3px}
.week-col.is-drop .week-empty{color:var(--b-700);opacity:1}

/* While a drag is in flight, stop the pointer selecting text across the grid. */
body.is-dragging-class{user-select:none;-webkit-user-select:none}
body.is-dragging-class .week-slot:hover{transform:none}

/* Touch: let a finger scroll the page vertically, but claim sideways movement
   so a class can be dragged across the week. */
@media (pointer:coarse),(hover:none){
  .week-slot{touch-action:pan-y}
}

.check-inline{
  display:inline-flex;align-items:center;gap:6px;
  font-size:.8rem;color:var(--ink-2);cursor:pointer;white-space:nowrap;
}
.check-inline input{width:auto;margin:0;cursor:pointer}

.row-actions{display:flex;gap:5px;justify-content:flex-end}
.act-btn{
  width:30px;height:30px;flex:none;
  display:grid;place-items:center;cursor:pointer;
  border:1px solid var(--line);border-radius:8px;
  background:var(--surface);color:var(--ink-3);
  transition:background var(--t-fast),color var(--t-fast),border-color var(--t-fast),
             transform var(--t-fast) var(--e-spring);
}
.act-btn:hover{background:var(--b-50);color:var(--b-700);border-color:var(--b-300)}
.act-btn:active{transform:scale(.9)}
.act-btn.is-danger:hover{background:var(--bad-bg);color:var(--bad);border-color:var(--bad-line)}
.act-btn .ico{width:15px;height:15px;vertical-align:0}
tr.is-archived{opacity:.62}

/* Time picker — replaces <input type="time">, whose dropdown is browser chrome
   and cannot be themed. Fixed positioning keeps it clear of the modal's clipping. */
.timefield{position:relative}
.time-trigger{
  display:flex;align-items:center;justify-content:space-between;gap:8px;width:100%;
  padding:8px 11px;border:1px solid var(--line);border-radius:9px;
  background:var(--surface);color:var(--ink);font:inherit;font-size:.85rem;
  cursor:pointer;text-align:left;
  transition:border-color var(--t-fast),box-shadow var(--t-fast),background var(--t-fast);
}
.time-trigger:hover{border-color:var(--b-300);background:var(--b-50)}
.time-trigger .ico{width:15px;height:15px;color:var(--ink-3);vertical-align:0}
.time-trigger .time-value{font-variant-numeric:tabular-nums;font-weight:540}
.timefield.is-open .time-trigger{border-color:var(--b-500);box-shadow:0 0 0 3.5px var(--b-100)}
.timefield.is-open .time-trigger .ico{color:var(--b-700)}

/* Opening the popover moves it onto <body>: `position:fixed` is resolved
   against the nearest animated ancestor, and the modal it sits in is animated,
   so anchored to the field it drifted off toward the bottom-right corner. */
.time-pop{
  position:fixed;z-index:95;
  display:flex;flex-direction:column;gap:8px;padding:8px;
  background:var(--surface);border:1px solid var(--line);border-radius:var(--r-md);
  box-shadow:0 10px 30px rgba(62,12,21,.18);
  animation:popIn 180ms var(--e-spring) both;
}
.time-pop[hidden]{display:none}
.time-cols{display:flex;gap:4px}
@keyframes popIn{from{opacity:0;transform:translateY(-6px) scale(.97)}to{opacity:1;transform:none}}
.time-pop.is-out{animation:popOut 140ms var(--e-in-out) both}
@keyframes popOut{to{opacity:0;transform:translateY(-4px) scale(.98)}}
.time-col{
  /* Also the offset parent the scroll-into-view measures against — without it
     the numbers were measured from the popover and scrolled under the header. */
  position:relative;
  display:flex;flex-direction:column;gap:2px;
  min-width:58px;max-height:196px;overflow-y:auto;
  scrollbar-width:thin;
}
.time-col + .time-col{border-left:1px solid var(--line-2);padding-left:4px}
.time-head{
  position:sticky;top:0;z-index:1;padding:2px 0 5px;background:var(--surface);
  font-size:.63rem;font-weight:640;letter-spacing:.08em;text-transform:uppercase;
  color:var(--ink-3);text-align:center;
}
.time-opt{
  padding:7px 10px;border:0;border-radius:7px;cursor:pointer;
  background:transparent;color:var(--ink-2);
  font-size:.84rem;font-weight:540;font-variant-numeric:tabular-nums;text-align:center;
  transition:background var(--t-fast),color var(--t-fast);
}
.time-opt:hover{background:var(--b-50);color:var(--b-700)}
.time-opt.is-on{background:var(--b-700);color:#fff}

/* Phone: a sheet across the bottom of the screen, where a thumb already is,
   instead of a small menu squeezed beside a full-width field. */
.time-pop.is-sheet{
  left:10px;right:10px;bottom:10px;top:auto;
  padding:14px;border-radius:var(--r-xl);
  animation:sheetIn 220ms var(--e-out) both;
}
@keyframes sheetIn{from{opacity:0;transform:translateY(16px)}to{opacity:1;transform:none}}
/* A sheet came up from the bottom, so it goes back down — not the popover's
   shrink, which on a full-width sheet reads as a glitch. */
.time-pop.is-sheet.is-out{animation:sheetOut 160ms var(--e-in-out) both}
@keyframes sheetOut{to{opacity:0;transform:translateY(14px)}}
.time-pop.is-sheet .time-cols{gap:8px}
.time-pop.is-sheet .time-col{flex:1 1 0;min-width:0;max-height:34vh}
.time-pop.is-sheet .time-opt{padding:12px 6px;font-size:.95rem}
.time-sheet-head{
  display:flex;align-items:baseline;justify-content:space-between;gap:12px;
  padding-bottom:10px;border-bottom:1px solid var(--line-2);
  font-size:.72rem;font-weight:640;letter-spacing:.06em;text-transform:uppercase;
  color:var(--ink-3);
}
.time-sheet-head strong{
  font-size:1.05rem;font-weight:640;letter-spacing:0;text-transform:none;
  color:var(--b-700);font-variant-numeric:tabular-nums;
}
.time-done{width:100%;justify-content:center}

/* Day-of-week picker in the class form */
.day-picker{display:flex;gap:6px;flex-wrap:wrap;margin-top:2px}
.day-toggle{
  min-width:44px;padding:8px 4px;cursor:pointer;
  border:1px solid var(--line);border-radius:9px;
  background:var(--surface);color:var(--ink-2);
  font-size:.76rem;font-weight:600;text-align:center;
  transition:background var(--t-fast),color var(--t-fast),border-color var(--t-fast),
             transform var(--t-fast) var(--e-spring);
}
.day-toggle:hover{border-color:var(--b-300);background:var(--b-50)}
.day-toggle:active{transform:scale(.93)}
.day-toggle.is-on{background:var(--b-700);border-color:var(--b-700);color:#fff}
/* Full day names where there is room; three letters when there is not. */
.day-toggle .day-long{display:none}
@media (min-width:560px){
  .day-picker{display:grid;grid-template-columns:repeat(7,minmax(0,1fr))}
  .day-toggle{min-width:0;padding:9px 2px}
  .day-toggle .day-short{display:none}
  .day-toggle .day-long{display:block;overflow:hidden;text-overflow:ellipsis}
}

/* One-tap answers for the schedules that actually recur. */
.day-presets{display:flex;gap:6px;flex-wrap:wrap;margin-top:8px}
.chip-btn{
  padding:5px 10px;border-radius:99px;cursor:pointer;
  border:1px solid var(--line);background:var(--surface);color:var(--ink-2);
  font-size:.75rem;font-weight:540;
  transition:background var(--t-fast),border-color var(--t-fast),color var(--t-fast);
}
.chip-btn:hover{background:var(--b-50);border-color:var(--b-300);color:var(--b-700)}

.field-note{margin-top:8px;font-size:.78rem;color:var(--ink-3)}
.field-note.is-warn{color:var(--warn)}

.label-opt{font-weight:450;color:var(--ink-3);text-transform:none;letter-spacing:0}

/* ---------- scanning a tag straight into a field ---------- */
.scan-row{display:flex;gap:8px;align-items:stretch}
.scan-row input{flex:1;min-width:0}
.scan-btn{flex:none;white-space:nowrap}
.scan-btn.is-listening{
  background:var(--b-700);border-color:var(--b-700);color:#fff;
}
.scan-btn.is-listening:hover{background:var(--b-600);border-color:var(--b-600);color:#fff}

/* A slow pulse, so "listening" is obvious from across a desk. */
.scan-pulse{
  width:9px;height:9px;flex:none;border-radius:99px;background:#fff;
  animation:scanPulse 1.1s var(--e-in-out) infinite;
}
@keyframes scanPulse{
  0%,100%{opacity:1;transform:scale(1)}
  50%{opacity:.35;transform:scale(.72)}
}

/* Feedback under the field, in place of the resting hint. */
.hint.is-wait{color:var(--b-700)}
.hint.is-ok{color:var(--ok)}
.hint.is-bad{color:var(--bad)}

@media (max-width:560px){
  .scan-row{flex-direction:column}
  .scan-btn{justify-content:center}
}
.picker-compact{max-height:200px}

/* Pushes destructive actions away from the confirming ones in a dialog footer,
   so "Remove" is never next to "Save". */
.foot-spacer{flex:1 1 auto}

/* When a decision has real consequences, spell the options out rather than
   hiding them behind a yes/no. Used for removing someone who has history. */
.choice-list{display:flex;flex-direction:column;gap:10px;margin-top:16px}
.choice{
  display:flex;align-items:flex-start;gap:13px;width:100%;
  padding:14px 15px;cursor:pointer;text-align:left;
  border:1px solid var(--line);border-radius:var(--r-md);
  background:var(--surface);
  transition:border-color var(--t-fast),background var(--t-fast),transform var(--t-fast) var(--e-spring);
}
.choice:hover{border-color:var(--b-300);background:var(--b-50)}
.choice:active{transform:scale(.99)}
.choice-ico{
  width:34px;height:34px;flex:none;display:grid;place-items:center;
  border-radius:9px;background:var(--b-100);color:var(--b-700);
}
.choice-ico .ico{width:17px;height:17px}
.choice-body{
  display:flex;flex-direction:column;gap:3px;min-width:0;
  font-size:.81rem;color:var(--ink-2);line-height:1.55;
}
.choice-body strong{font-size:.9rem;font-weight:600;color:var(--ink)}
.choice-body b{font-weight:600;color:var(--ink-2)}
.choice-body em{font-style:italic}
.choice.is-danger:hover{border-color:var(--bad-line);background:var(--bad-bg)}
.choice.is-danger .choice-ico{background:var(--bad-bg);color:var(--bad)}
.choice.is-danger .choice-body strong{color:var(--bad)}

/* Empty states that say what to do next, instead of only what is missing. */
.empty-cta{
  display:flex;flex-direction:column;align-items:center;gap:12px;
  padding:34px 18px;text-align:center;
}
.empty-cta .ico{width:26px;height:26px;color:var(--b-300)}
.empty-cta p{color:var(--ink-3);font-size:.86rem;max-width:38ch;line-height:1.6}
.empty-cta strong{display:block;color:var(--ink);font-size:.95rem;font-weight:600;margin-bottom:2px}

/* Validation that stays put instead of a toast that fades. */
.form-error{
  margin-top:14px;padding:9px 12px;border-radius:9px;
  background:var(--bad-bg);color:var(--bad);
  border:1px solid var(--bad-line);
  font-size:.82rem;font-weight:520;
}

/* Roster / enrollment picker */
.picker-head{
  display:flex;align-items:center;justify-content:space-between;gap:10px;
  margin-bottom:10px;flex-wrap:wrap;
}
.picker-head .picker-count{font-size:.8rem;font-weight:600;color:var(--b-700)}
.picker-actions{display:flex;gap:6px}
.picker-list{
  list-style:none;margin:0;padding:5px;
  max-height:330px;overflow-y:auto;
  border:1px solid var(--line);border-radius:var(--r-md);
}
.picker-list li{margin:0}
.picker-row{
  display:flex;align-items:center;gap:10px;width:100%;
  padding:8px 9px;border-radius:9px;cursor:pointer;
  border:1px solid transparent;background:transparent;text-align:left;
  transition:background var(--t-fast),border-color var(--t-fast);
}
.picker-row:hover{background:var(--b-50)}
.picker-row.is-on{background:var(--b-50);border-color:var(--b-100)}
.picker-row input{width:auto;margin:0;flex:none;cursor:pointer}
.picker-body{min-width:0;flex:1}
.picker-body strong{display:block;font-size:.85rem;font-weight:550}
.picker-body small{display:block;font-size:.73rem;color:var(--ink-3)}
@media (max-width:900px){
  .week-grid{grid-template-columns:repeat(2,minmax(0,1fr))}
  .week-col{min-height:0}
}
@media (max-width:560px){
  .week-grid{grid-template-columns:1fr}
}

/* ========================= HISTORY ========================= */
/* 330px, not 270: seven day columns and a month name have to fit the picker. */
.history-layout{display:grid;grid-template-columns:330px minmax(0,1fr);gap:18px;align-items:start}

.mini-stats{display:flex;gap:0;border-bottom:1px solid var(--line-2);flex-wrap:wrap}
.mini-stat{
  flex:1;min-width:120px;padding:13px 18px;border-right:1px solid var(--line-2);
  animation:viewIn 380ms var(--e-out) both;animation-delay:calc(var(--i,0) * 50ms);
}
.mini-stat:last-child{border-right:0}
.mini-stat span{display:block;font-size:.7rem;font-weight:600;letter-spacing:.055em;text-transform:uppercase;color:var(--ink-3)}
.mini-stat strong{display:block;margin-top:4px;font-size:1.3rem;font-weight:660;color:var(--b-800);
  font-variant-numeric:tabular-nums;letter-spacing:-.02em}

/* ========================= OVERLAY ========================= */
.overlay{
  position:fixed;inset:0;z-index:90;
  display:grid;place-items:center;
  background:var(--b-800);
  animation:ovIn 260ms var(--e-out) both;
}
.overlay[hidden]{display:none}
.overlay.is-out{animation:ovOut 220ms var(--e-in-out) both}
@keyframes ovIn{from{opacity:0}to{opacity:1}}
@keyframes ovOut{to{opacity:0}}

.overlay-card{
  position:relative;
  width:min(520px,calc(100vw - 28px));padding:38px 28px 32px;
  border-radius:26px;text-align:center;
  background:var(--surface);
  border:1px solid var(--line);
  animation:cardPop 520ms var(--e-spring) both;
}
.overlay.is-out .overlay-card{animation:cardOut 220ms var(--e-in-out) both}
@keyframes cardPop{from{opacity:0;transform:translateY(26px) scale(.9)}to{opacity:1;transform:none}}
@keyframes cardOut{to{opacity:0;transform:translateY(-10px) scale(.97)}}

.ov-badge{
  width:82px;height:82px;margin:0 auto 18px;
  display:grid;place-items:center;border-radius:50%;
  background:var(--ok-bg);color:var(--ok);
}
.ov-badge.is-warn{background:var(--warn-bg);color:var(--warn)}
.ov-badge.is-bad{background:var(--bad-bg);color:var(--bad)}
.ov-badge.is-info{background:var(--info-bg);color:var(--info)}
.ov-badge .ico-lg{animation:cardPop 500ms 120ms var(--e-spring) both}
.tick{width:52px;height:52px}
.tick-ring{fill:none;stroke:currentColor;stroke-width:2.5;opacity:.28;
  stroke-dasharray:145;stroke-dashoffset:145;
  animation:draw 620ms 60ms var(--e-out) forwards}
.tick-path{fill:none;stroke:currentColor;stroke-width:4.5;stroke-linecap:round;stroke-linejoin:round;
  stroke-dasharray:44;stroke-dashoffset:44;
  animation:draw 420ms 300ms var(--e-out) forwards}
@keyframes draw{to{stroke-dashoffset:0}}

.ov-kicker{font-size:.76rem;font-weight:620;letter-spacing:.11em;text-transform:uppercase;color:var(--b-500)}
.ov-name{margin-top:7px;font-family:var(--font-display);font-size:2.35rem;font-weight:640;
  letter-spacing:-.025em;line-height:1.1;overflow-wrap:break-word;hyphens:auto}
.ov-meta{margin-top:9px;font-size:1rem;color:var(--ink-2)}
.ov-facts{
  margin-top:22px;padding-top:18px;border-top:1px solid var(--line-2);
  display:grid;grid-template-columns:repeat(3,minmax(0,1fr));
}
.ov-fact{min-width:0;padding:0 8px;border-right:1px solid var(--line-2);
  animation:viewIn 420ms var(--e-out) both;animation-delay:calc(var(--i,0) * 70ms + 260ms)}
.ov-fact:last-child{border-right:0}
.ov-fact strong{display:block;font-size:1.15rem;font-weight:660;color:var(--b-800);font-variant-numeric:tabular-nums}
.ov-fact span{display:block;margin-top:2px;font-size:.71rem;font-weight:580;letter-spacing:.04em;
  text-transform:uppercase;color:var(--ink-3);overflow-wrap:anywhere;line-height:1.25}
.ov-progress{
  position:absolute;left:26px;right:26px;bottom:14px;height:3px;
  border-radius:99px;background:var(--line-2);overflow:hidden;
}
.ov-progress i{display:block;height:100%;background:var(--b-500);transform-origin:left;
  animation:countdown var(--ov-dur,3200ms) linear forwards}
@keyframes countdown{from{transform:scaleX(1)}to{transform:scaleX(0)}}

/* ========================= MODAL ========================= */
.modal-scrim{
  position:fixed;inset:0;z-index:80;
  display:grid;place-items:center;padding:22px;
  background:rgba(28,20,24,.6);
  animation:ovIn 200ms var(--e-out) both;
}
.modal-scrim[hidden]{display:none}
.modal-scrim.is-out{animation:ovOut 180ms both}
.modal{
  width:min(620px,100%);max-height:88vh;overflow:hidden;
  display:flex;flex-direction:column;
  background:var(--surface);border-radius:var(--r-xl);
  border:1px solid var(--line);
  animation:modalIn 340ms var(--e-spring) both;
}
.modal-scrim.is-out .modal{animation:cardOut 180ms both}
@keyframes modalIn{from{opacity:0;transform:translateY(18px) scale(.96)}to{opacity:1;transform:none}}
.modal-head{
  display:flex;align-items:center;justify-content:space-between;gap:14px;
  padding:16px 20px;border-bottom:1px solid var(--line-2);
  background:var(--b-700);color:#fff;
}
.modal-head h2{font-size:1.05rem}
.modal-body{padding:20px;overflow-y:auto}
.form-grid{display:grid;grid-template-columns:1fr 1fr;gap:14px}
.form-grid .span-2{grid-column:span 2}
.modal-foot{display:flex;flex-wrap:wrap;justify-content:flex-end;gap:8px;
  margin-top:20px;padding-top:16px;border-top:1px solid var(--line-2)}
/* "Back" belongs at the far end from the action that closes things. */
.modal-foot .foot-left{margin-right:auto}

/* A dialog that swaps its own contents — calendar to a day and back — cuts
   with nothing to follow otherwise. Same slide as paging a month. */
.modal-body.swap-in{animation:calNext 280ms var(--e-out) both}
.modal-body.swap-back{animation:calPrev 280ms var(--e-out) both}

.detail-head{display:flex;align-items:center;gap:15px;padding-bottom:18px;border-bottom:1px solid var(--line-2)}
.detail-head .dh-body h3{font-family:var(--font-display);font-size:1.4rem;font-weight:620;letter-spacing:-.02em}
.detail-head .dh-body p{margin-top:3px;font-size:.82rem;color:var(--ink-3)}
.detail-rows{margin-top:16px;display:flex;flex-direction:column;gap:0}
.detail-row{
  display:flex;gap:14px;padding:10px 0;border-bottom:1px solid var(--line-2);font-size:.86rem;
  animation:viewIn 360ms var(--e-out) both;animation-delay:calc(var(--i,0) * 40ms);
}
.detail-row:last-child{border-bottom:0}
.detail-row dt{flex:0 0 132px;color:var(--ink-3);font-size:.79rem;font-weight:560}
.detail-row dd{margin:0;flex:1;min-width:0}
.visit-list{list-style:none;margin:10px 0 0;padding:0;display:flex;flex-direction:column;gap:1px}
.visit-list li{display:flex;justify-content:space-between;gap:12px;padding:8px 10px;border-radius:8px;font-size:.82rem;
  transition:background var(--t-fast)}
.visit-list li:hover{background:var(--b-50)}
.visit-list .vl-date{font-weight:560}
.visit-list .vl-meta{color:var(--ink-3);font-size:.78rem}
.section-label{margin-top:20px;font-size:.72rem;font-weight:640;letter-spacing:.07em;
  text-transform:uppercase;color:var(--b-500)}

/* ========================= TOASTS ========================= */
.toasts{position:fixed;right:20px;bottom:20px;z-index:100;display:flex;flex-direction:column;gap:9px;
  pointer-events:none}
.toast{
  display:flex;align-items:center;gap:11px;
  min-width:250px;max-width:360px;padding:12px 15px;
  background:var(--ink);color:#fff;border-radius:12px;
  border:1px solid var(--dark-2);
  font-size:.85rem;
  animation:toastIn 380ms var(--e-spring) both;
}
.toast.is-out{animation:toastOut 260ms var(--e-in-out) both}
@keyframes toastIn{from{opacity:0;transform:translateX(28px) scale(.94)}to{opacity:1;transform:none}}
@keyframes toastOut{to{opacity:0;transform:translateX(20px) scale(.96)}}
.toast .t-ico{flex:none;width:22px;height:22px;display:grid;place-items:center;border-radius:6px;
  background:var(--dark);font-size:.78rem}
.toast.is-ok .t-ico{background:rgba(92,214,154,.24)}
.toast.is-bad .t-ico{background:rgba(240,154,140,.24)}

/* A toast carrying an Undo. The stack sets pointer-events:none so toasts never
   block the page — this one has to opt back in or its button is dead. */
.toast.has-undo{padding-right:7px;pointer-events:auto}
.toast-undo{
  display:inline-flex;align-items:center;gap:5px;flex:none;margin-left:auto;
  padding:6px 11px;border:0;border-radius:8px;cursor:pointer;
  background:var(--dark-2);color:#fff;
  font-size:.77rem;font-weight:600;white-space:nowrap;
  transition:background var(--t-fast);
}
.toast-undo:hover{background:var(--dark-3)}
.toast-undo .ico{width:12px;height:12px}

/* ========================= ASSISTANT =========================
   The chat bubble. Floating, so it earns a shadow (§4) — the popover value,
   not a new one. z-index 70: under the modal scrim (80), so a confirmation
   the assistant's own edit triggers still lands on top of it. */
.ai-fab{
  position:fixed;right:18px;bottom:18px;z-index:70;
  width:54px;height:54px;border-radius:50%;
  display:grid;place-items:center;border:0;cursor:pointer;
  background:var(--b-700);color:#fff;
  box-shadow:0 10px 30px rgba(62,12,21,.18);
  animation:fabIn 420ms var(--e-spring) both;
  transition:background var(--t-fast),transform var(--t-fast) var(--e-spring);
}
.ai-fab:hover{background:var(--b-600)}
.ai-fab:active{transform:scale(.9)}
.ai-fab.is-open{background:var(--b-800)}
.ai-fab .ico{width:24px;height:24px;stroke-width:1.8}
@keyframes fabIn{from{opacity:0;transform:scale(.5)}to{opacity:1;transform:none}}

.ai-panel{
  position:fixed;right:18px;bottom:84px;z-index:70;
  width:min(390px,calc(100vw - 28px));
  height:min(560px,calc(100dvh - 120px));
  display:flex;flex-direction:column;overflow:hidden;
  background:var(--surface);border:1px solid var(--line);border-radius:var(--r-lg);
  box-shadow:0 10px 30px rgba(62,12,21,.18);
  transform-origin:bottom right;
  animation:aiIn 300ms var(--e-spring) both;
}
.ai-panel[hidden]{display:none}
.ai-panel.is-out{animation:aiOut 160ms var(--e-in-out) both}
@keyframes aiIn{from{opacity:0;transform:translateY(16px) scale(.92)}to{opacity:1;transform:none}}
@keyframes aiOut{to{opacity:0;transform:translateY(12px) scale(.95)}}

.ai-head{
  display:flex;align-items:center;justify-content:space-between;gap:10px;
  padding:12px 14px;flex:none;
  background:var(--b-700);color:#fff;
}

/* The resize grip, on the panel's one free corner. The hit area is a
   generous invisible square; the visible part is a small corner bracket. */
.ai-resize{
  position:absolute;top:0;left:0;z-index:1;
  width:22px;height:22px;cursor:nw-resize;touch-action:none;
}
.ai-resize::before{
  content:"";position:absolute;top:7px;left:7px;width:9px;height:9px;
  border-top:2px solid var(--on-b-3);border-left:2px solid var(--on-b-3);
  border-top-left-radius:4px;
  transition:border-color var(--t-fast);
}
.ai-resize:hover::before{border-color:#fff}
/* Mid-drag only transitions and selection are suspended. The open animation
   is left alone deliberately: toggling animation-name off and back on REPLAYS
   it, which both flashed the panel and, at fill-mode's scale(.92) mid-replay,
   made the drag handler save a size 8% smaller than where you dropped it. */
.ai-panel.is-resizing{transition:none;user-select:none}
.ai-head h2{font-size:.95rem;display:flex;align-items:center;gap:8px}
.ai-head .ico{width:16px;height:16px}

.ai-msgs{
  flex:1;overflow-y:auto;padding:14px;
  display:flex;flex-direction:column;gap:9px;
  background:var(--bg);
}
.ai-msg{
  max-width:86%;padding:9px 13px;border-radius:14px;
  font-size:.86rem;line-height:1.5;overflow-wrap:break-word;
}
/* Only what just arrived animates: the list is rebuilt per message, and the
   whole history replaying its entrance on every reply reads as a page load. */
.ai-msgs > :nth-last-child(-n+2){animation:aiMsgIn 260ms var(--e-out) both}
@keyframes aiMsgIn{from{opacity:0;transform:translateY(7px)}to{opacity:1;transform:none}}
.ai-msg.from-user{align-self:flex-end;background:var(--b-700);color:#fff;border-bottom-right-radius:5px}
.ai-msg.from-ai{align-self:flex-start;background:var(--surface);border:1px solid var(--line);border-bottom-left-radius:5px}
.ai-msg.is-err{border-color:var(--bad-line);background:var(--bad-bg);color:var(--bad)}

/* One quiet line per tool call, so a pause is visibly work and not a hang. */
.ai-act{
  align-self:flex-start;display:flex;align-items:center;gap:6px;
  padding:0 4px;font-size:.72rem;color:var(--ink-3);
}
.ai-act .ico{width:12px;height:12px}

.ai-typing{display:flex;gap:4px;padding:12px 14px}
.ai-typing i{
  width:6px;height:6px;border-radius:50%;background:var(--b-300);
  animation:aiDot 900ms var(--e-in-out) infinite;
}
.ai-typing i:nth-child(2){animation-delay:150ms}
.ai-typing i:nth-child(3){animation-delay:300ms}
@keyframes aiDot{0%,60%,100%{opacity:.35;transform:none}30%{opacity:1;transform:translateY(-3px)}}

.ai-form{
  display:flex;gap:8px;padding:10px;flex:none;
  border-top:1px solid var(--line-2);background:var(--surface);
}
.ai-form input{flex:1;min-width:0}
.ai-form .icon-btn{width:38px;height:38px;flex:none}
.ai-form .icon-btn:disabled{opacity:.45;cursor:default}

@media (max-width:560px){
  .ai-fab{right:14px;bottom:calc(var(--tabbar-h,59px) + 14px)}
  /* Anchored bottom-right only — the left edge stays free so the same
     corner grip as the desktop can set width AND height. Full width is
     merely the default. */
  .ai-panel{
    right:10px;width:calc(100vw - 20px);
    bottom:calc(var(--tabbar-h,59px) + 80px);
    height:min(500px,calc(100dvh - var(--tabbar-h,59px) - 96px));
  }
  /* The desktop corner bracket, grown to a finger-sized target. */
  .ai-resize{width:34px;height:34px}
  .ai-resize::before{top:9px;left:9px;width:12px;height:12px}

  /* While the chat input is focused, iOS pans the layout viewport to keep
     it visible; app.js measures the pan (--kb-pan) and the keyboard's
     height (--kb-inset). Translating .app back down freezes the page where
     it was — and because a transformed ancestor contains its fixed
     descendants, the tab bar rides along into the keyboard's shadow for
     free. The panel alone is re-pinned to the keyboard's top edge. */
  body.kb-open .app{transform:translateY(var(--kb-pan,0px))}
  body.kb-open .ai-panel{
    bottom:calc(var(--kb-inset,0px) + 10px);
    max-height:calc(100dvh - var(--kb-inset,0px) - 20px);
  }
  /* The bubble would float mid-keyboard; typing does not need it. */
  body.kb-open .ai-fab{display:none}
}

/* ========================= RESPONSIVE ========================= */
@media (max-width:1180px){
  .kiosk{grid-template-columns:1fr}
  .grid-2{grid-template-columns:1fr}
  .history-layout{grid-template-columns:1fr}
}
@media (max-width:900px){
  .topbar{height:auto;flex-wrap:wrap;gap:12px;padding:12px 16px}
  .tabs{order:3;width:100%;margin-left:0;overflow-x:auto}
  .tab-indicator{display:none}
  .tab.is-active{background:var(--b-800)}
  .topbar-right{margin-left:auto}
  .stage{--stage-pad:16px}
  .scan-title{font-size:1.7rem}
  .ov-name{font-size:1.9rem}
  .form-grid{grid-template-columns:1fr}
  .form-grid .span-2{grid-column:span 1}
  .detail-row{flex-direction:column;gap:3px}
  .detail-row dt{flex:none}
}
@media (max-width:560px){
  .brand-text span{display:none}
  .kiosk-classname{font-size:1.25rem}
  .kiosk-head{flex-direction:column}
  /* Compact badge, not a full-width bar with the label pushed to the far edge. */
  .kiosk-count{align-self:flex-start;display:inline-flex;align-items:baseline;gap:8px;
    padding:8px 14px}
  .kiosk-count span{font-size:1.5rem}
  .kiosk-count small{font-size:.62rem}
  .view-head{flex-direction:column;align-items:stretch}
  /* Seven columns have to fit 375px: trim the padding, not the cells. */
  .cal{padding:12px 12px 13px}
  .cal-scale{font-size:.7rem}
  .toasts{left:16px;right:16px;bottom:16px}
  .toast{max-width:none}
}

/* ========================= QUIET RE-RENDERS =========================
   Cards and rows fade up as they arrive, which is right when you open the app
   or switch tabs — and wrong every other time, because the dashboard rebuilds
   itself on every sync and after every edit. Restaging the whole page for a
   data change looked like the app refreshing on its own: dropping a class on
   another day blinked the entire schedule out and back.

   app.js sets data-anim="off" on <body> for those repaints, and clears it when
   you actually arrive somewhere. Scoped to .stage so modals, which are always
   an arrival, keep theirs. */
body[data-anim="off"] .stage :is(
  .kpi, .class-card, .week-col, .spark-bar, .cal-day, .mini-stat,
  .table tbody tr, .legend li, .cc-bar i
){animation:none !important}
/* The ring is a transition, and it is recreated on every render — so it swept
   from empty to full each time too. */
body[data-anim="off"] .stage .donut-value{transition:none}

/* ========================= REDUCED MOTION ========================= */
@media (prefers-reduced-motion:reduce){
  *,*::before,*::after{
    animation-duration:.01ms !important;animation-iteration-count:1 !important;
    transition-duration:.01ms !important;scroll-behavior:auto !important;
  }
  .donut-value{stroke-dashoffset:var(--dash,0) !important}

  /* One exception. The pulsing ring on the tap screen is not decoration, it is
     the instruction — it is how a senior knows the reader is live and where to
     hold the card. Blanking it leaves a dead circle and the screen looks broken.

     Reduced motion is a request about *movement*, so the ring keeps breathing
     but stops travelling: one ring, fading in place, nothing sliding or growing.
     Selectors beat `*`, so these win despite the !important above. */
  .scan-rings span{
    animation:ringBreathe 2.6s var(--e-in-out) infinite !important;
    transform:scale(.94);
  }
  .scan-rings span:nth-child(n+2){display:none}
  .scan-core::before{
    animation:ringBreathe 2.6s var(--e-in-out) .6s infinite !important;
    transform:none;
  }
}
@keyframes ringBreathe{0%,100%{opacity:.22}50%{opacity:.9}}

/* ========================= PRINT ========================= */
@media print{
  .topbar,.statusbar,.view-actions,.toasts{display:none !important}
  body{background:#fff}
  .panel{break-inside:avoid}
}


/* ==========================================================================
   RESPONSIVE — phones and tablets
   ========================================================================== */

/* Columns that fold away on small screens. Nothing is lost: hidden fields are
   still reachable through the row's detail modal or the class edit form. */
/* Secondary detail folds away on tablets and below. */
@media (max-width:900px){
  .col-md-hide{display:none !important}
}

/* Shown only where the equivalent columns have folded away. */
.sm-only{display:none}

@media (max-width:700px){
  .col-sm-hide{display:none !important}
  .sm-only{display:block}

  /* Panel toolbars get their own full-width row rather than overflowing. */
  .panel-head{align-items:stretch}
  .panel-tools{width:100%}
  .panel-tools .input-sm,.panel-tools select,.panel-tools input{flex:1 1 140px}

  .stage{--stage-pad:14px}
  .view-head h1{font-size:1.3rem}
  .table th,.table td{padding:10px 12px}

  /* The sub-nav scrolls sideways rather than stacking into three ragged rows.
     Five screens will not fit across a phone at any readable size, and a strip
     you can swipe is easier than a block that pushes the page down. */
  .subtabs{
    flex-wrap:nowrap;overflow-x:auto;scrollbar-width:none;
    /* bleed to the screen edges by cancelling the stage padding exactly */
    margin:0 calc(var(--stage-pad, 14px) * -1);
    padding:0 var(--stage-pad, 14px) 2px;
  }
  .subtabs::-webkit-scrollbar{display:none}
  .subtab{flex:none;white-space:nowrap;padding:10px 12px;font-size:.82rem}
}

/* Touch devices: fingers need bigger targets than a mouse pointer, and this
   runs on a tablet used by seniors. 44px is the accessibility floor.
   `hover:none` is included because an iPad with a keyboard attached can report
   a fine pointer while still being driven by touch. */
@media (pointer:coarse), (hover:none){
  .btn{padding:12px 18px;font-size:.9rem}
  .btn-sm{padding:11px 15px;font-size:.84rem}
  .check-inline{min-height:44px;padding:0 4px}
  .icon-btn{width:44px;height:44px}
  /* Send has to re-state the touch floor: .ai-form .icon-btn is more
     specific than the rule above and was quietly shrinking it back. */
  .ai-form .icon-btn{width:44px;height:44px}
  /* 16px, the exact threshold: anything smaller makes iOS zoom the whole
     page when the input focuses — inside a fixed panel that zoom never
     quite undoes itself. The :not() chain only exists to outweigh the
     generic coarse input rule above, which carries three of them. */
  .ai-form input:not([type=checkbox]):not([type=radio]):not([type=hidden]){font-size:1rem}
  .act-btn{width:42px;height:42px;border-radius:10px}
  .act-btn .ico{width:18px;height:18px}
  /* Matches the base rule's specificity, or the 8px padding there would win. */
  input:not([type=checkbox]):not([type=radio]):not([type=hidden]),
  select,textarea,.input-sm{
    padding:12px 12px;font-size:.9rem;
  }
  select{padding-right:30px}
  .time-trigger{padding:12px 12px;font-size:.9rem}
  .time-opt{padding:11px 12px;font-size:.9rem}
  .time-col{min-width:66px;max-height:230px}
  .day-toggle{min-width:54px;padding:13px 6px;font-size:.82rem}
  .check-inline input,.picker-row input[type=checkbox],.form-grid input[type=checkbox]{
    width:20px;height:20px;
  }
  .picker-row{padding:12px 10px}
  .picker-list{max-height:44vh}
  /* 44px is the touch floor, and a day cell is a target like any other. */
  .cal-day{height:44px}
  .cal-turn{width:38px;height:38px}
  /* The arrow is a hover affordance, and a touch screen has no hover — so on
     one it simply stays put. It also takes the hover colour: --b-300 is a
     resting tint for something about to be revealed, and at 2.3:1 it is under
     the contrast floor for text that never goes away. */
  .panel-go{opacity:1;transform:none;color:var(--b-600)}
  .week-slot{padding:11px 10px}
  .table-hover tbody tr td{padding-top:14px;padding-bottom:14px}
  .tab{padding:12px 15px}
}

/* Phone layout: the five tabs leave the header and become a bar across the
   bottom of the screen — a thumb reaches it without stretching, it stays put
   while the page scrolls, and the header collapses to a single row.

   It is still inside <header>, which is why it sits under a modal without
   needing a z-index of its own: the header's stacking context is below the
   scrim's, so `z-index:45` here only has to beat the page content. */
@media (max-width:560px){
  /* The document itself does not scroll on a phone; .app does.

     This is the fix for the bar drifting off the bottom. Safari hides and
     shows its toolbar as the DOCUMENT scrolls, and each time it does the
     visible area changes height while `position:fixed` stays measured against
     the layout viewport — so the bar ended up stranded above the toolbar with
     page content under it. Scrolling an element inside the page instead means
     the toolbar never moves, the two viewports stay the same size, and
     `bottom:0` is genuinely the bottom of what you can see.

     dvh rather than vh: vh is the tallest the viewport ever gets, which is
     already too tall while the controls are on screen. */
  html,body{height:100%;overflow:hidden;overscroll-behavior:none}
  /* Safari paints its own chrome — the status bar strip and the space around
     the bottom toolbar — by sampling the DOCUMENT's background, and ignores
     theme-color for it. So on a phone the document is burgundy to match the
     header and tab bar, and .app carries the light background the content
     actually sits on. Desktop keeps body's --bg untouched. */
  html,body{background:var(--b-700)}
  .app{
    height:100vh;height:100dvh;
    overflow-y:auto;overflow-x:hidden;
    overscroll-behavior-y:contain;
    -webkit-overflow-scrolling:touch;
    background:var(--bg);
  }

  .tabs{
    position:fixed;left:0;right:0;bottom:0;z-index:45;
    /* Auto columns rather than a fixed five: a non-admin has no Staff tab, and
       a hard-coded track count left them a dead cell on the end. */
    display:grid;grid-auto-flow:column;grid-auto-columns:1fr;gap:3px;
    overflow:visible;width:auto;margin:0;
    padding:5px 8px;
    padding-bottom:calc(5px + env(safe-area-inset-bottom));
    background:var(--b-700);
    border-top:1px solid var(--b-800);
    box-shadow:0 -6px 20px rgba(28,10,14,.16);
  }
  /* `bottom:0` is the bottom of the layout viewport, which on a phone is not
     where the screen ends: the browser's own toolbar sits over the last strip
     of it, and page background showed between the bar and the toolbar — the
     bar read as floating rather than anchored. This paints the bar's colour on
     down past the edge. Off-screen when there is no gap, so it costs nothing
     and does not depend on getting the arithmetic right on every device. */
  .tabs::after{
    content:"";
    position:absolute;top:100%;left:0;right:0;height:120px;
    background:var(--b-700);
    pointer-events:none;
  }
  /* Room underneath, or the status line and the last card hide behind the bar.
     The height is measured from the bar itself (--tabbar-h, set in app.js): as
     a constant it was 64px against a 59px bar, and that 5px strip of page
     showing under the bar is what made it look detached from the bottom. The
     fallback matters only for the instant before the first measurement.
     Not on the tap screen — it has no tabs, and the space would just shrink the
     target. */
  body:not(.kiosk-page) .app{padding-bottom:var(--tabbar-h,59px)}
  body:not(.kiosk-page) .toasts{bottom:calc(var(--tabbar-h,59px) + 12px)}

  .tab{
    flex-direction:column;gap:4px;padding:9px 2px;
    font-size:.66rem;font-weight:560;line-height:1.1;text-align:center;
    border-radius:9px;min-width:0;
  }
  .tab .ico{width:1.35em;height:1.35em}

  .topbar{gap:10px;padding:10px 14px}
  .brand-logo{height:40px;padding:4px 9px}
  .brand-logo img{height:30px}
  .clock strong{font-size:.9rem}
  .clock span{font-size:.66rem}

  .stage{--stage-pad:12px}
  .view{gap:14px}

  /* Density. Everything below is sized for a 375px screen rather than scaled
     down from a desk: the type was set for a monitor and the touch rules then
     added padding on top, so a table row came out 71px tall and seven of them
     filled the phone. Tap targets that are actually tapped — buttons, tabs,
     inputs — keep their size; rows of text do not need it. */
  .view-head h1{font-size:1.15rem}
  .view-head .sub{font-size:.76rem}
  .panel-head{padding:12px 14px}
  .panel-head h2{font-size:.94rem}
  .section{gap:14px}

  .table{font-size:.78rem}
  .table th{padding:8px 10px;font-size:.62rem;letter-spacing:.04em}
  .table td{padding:9px 10px}
  /* Beats the touch rule above, which is the one that made rows 71px. */
  .table-hover tbody tr td{padding-top:9px;padding-bottom:9px}
  .table td .badge,.table td .pill{font-size:.66rem}

  #dsTable{font-size:calc(.78rem * var(--ds-zoom))}
  #dsTable td{
    padding-top:calc(8px * var(--ds-zoom));
    padding-bottom:calc(8px * var(--ds-zoom));
  }
  .ds-sort{padding:calc(8px * var(--ds-zoom)) calc(10px * var(--ds-zoom))}
  .zoom-btn{width:36px;height:36px}

  /* The touch rules give every icon button a 42px box, and in a table that one
     button — not the text, which is 19px — decided the height of every row.
     34px is still a comfortable target for a secondary action, and it buys
     four more records on the screen. */
  /* Scales with the zoom too, or it becomes the floor on the row height and
     zooming out stops buying anything. Never below 28px — past that it is a
     mis-tap that opens the wrong person's record. */
  #dsTable .act-btn{
    height:max(28px,calc(34px * var(--ds-zoom)));
    width:max(28px,calc(34px * var(--ds-zoom)));
    min-height:0;padding:0;
  }
  #dsTable .ds-actions{padding-left:6px;padding-right:8px}

  /* Prose about where the sheet came from is worth 31px on a monitor and not
     on a phone. */
  #dsNote{display:none}
  /* Export and Add are one line, not two stacked full-width blocks. */
  #section-membership .view-actions .btn{padding:9px 13px;font-size:.82rem}

  .cat-head{font-size:.9rem}
  .cat-member{font-size:.82rem;padding:6px 8px}

  .modal-scrim{padding:10px}
  .modal{max-height:94vh}
  .modal-body{padding:16px}
  .modal-foot{flex-direction:column-reverse;align-items:stretch}
  .modal-foot .btn{width:100%}

  /* Stretched full-width, the number and its label drift to opposite edges and
     stop reading as one badge. Keep it compact and left-aligned with the name
     above it. */
  .kiosk-count{align-self:flex-start;display:inline-flex;align-items:baseline;gap:8px;
    padding:8px 14px}
  .kiosk-count span{font-size:1.5rem}
  .kiosk-count small{font-size:.62rem}
  .scan-title{font-size:1.5rem}
  .scan-sub{font-size:.9rem}
  .overlay-card{padding:26px 16px 24px;border-radius:20px}
  .ov-badge{width:64px;height:64px;margin-bottom:14px}
  .ov-badge .ico-lg,.tick{width:30px;height:30px}
  .ov-kicker{font-size:.68rem;letter-spacing:.07em}
  .ov-name{font-size:1.5rem}
  .ov-meta{font-size:.9rem}
  .ov-facts{margin-top:16px;padding-top:14px}
  .ov-fact{padding:0 5px}
  .ov-fact strong{font-size:1rem}
  .ov-fact span{font-size:.58rem;letter-spacing:.02em}
  /* Wrap rather than stack: three items each on their own row made the footer
     tall enough to push itself off the bottom of a phone screen. */
  .statusbar{flex-wrap:wrap;align-items:center;gap:4px 12px;padding:9px 14px}
  .statusbar-sep{display:none}
  .statusbar > span,.statusbar > a{font-size:.72rem}
}


/* ==========================================================================
   TWO PAGES
   checkin.html is the tablet kiosk: no navigation, bigger targets, nothing a
   senior can wander into. index.html is the staff dashboard.
   ========================================================================== */

/* Discreet way back to the dashboard from the kiosk footer. */
.staff-link{color:var(--ink-3);text-decoration:none;border-bottom:1px dotted var(--line)}
.staff-link:hover{color:var(--b-700);border-bottom-color:var(--b-300)}

/* Between a phone and a laptop the header runs out of room. The tab strip and
   the account menu size to their contents and will not shrink, so every pixel
   of the shortfall came out of the brand — at 900px it was a 14px column of
   stacked letters overlapping the tabs. The logo already says whose building
   this is, and the section tabs are what you came to the header for. */
@media (max-width:1024px){
  .brand-text{display:none}
}

@media (max-width:560px){
  /* Phone header: one row of "who and what" — name on the left, the controls
     on the right — and one row of tabs. Nothing floats in the middle of its
     own line the way three auto margins used to leave it. */
  .topbar{gap:8px;padding:9px 12px}
  .brand{margin-right:auto}
  /* Beside the tab strip there is no room for the label, so it is the icon
  .topbar-right{margin-left:auto;gap:7px}

  /* The phone already shows the time in its own status bar, and the full date
     is the subtitle of every screen. In the header it was just bulk. */
  .clock{display:none}
  /* The avatar is the button; the menu it opens says who you are in full. */
  .whoami-name{display:none}
  .whoami-btn .ico{display:none}
  .whoami-btn{padding:5px}
  .whoami-avatar{width:30px;height:30px;font-size:.74rem}

  .brand-logo{height:38px;padding:4px 8px}
  .brand-logo img{height:28px}
  .brand-text{padding-left:10px}
  .brand-text strong{font-size:.92rem}
  .brand-text span{font-size:.68rem}
  /* Action buttons read better as a full-width stack than as ragged pills. */
  .view-actions{width:100%}
  .view-actions .btn{flex:1 1 auto;justify-content:center}

  /* Export and Reset are things you do occasionally, not the reason you opened
     the dashboard — on a phone they sat above the numbers. `display:contents`
     lifts the header's children into the view's own flex column so the actions
     can be ordered last, without moving them on a desktop.
     Only the Dashboard: "Add class" and "Add member" belong at the top. */
  #view-dashboard .view-head{display:contents}
  #view-dashboard .view-head > div:first-child{order:-1}
  #view-dashboard .view-actions{order:99;margin-top:2px}
}

/* ==========================================================================
   The tap screen sizes itself to the screen it is on

   It is the only thing on the page, so the card fills whatever the header and
   the status line leave, and everything inside it is measured from the viewport
   instead of from a handful of fixed breakpoints. Three hard-coded sizes meant
   every tablet that wasn't one of the three got a target that was either
   floating in empty space or crowding the edges.

   `min(vh, vw)` rather than plain `vmin` so the target can grow further on a
   short wide screen than the shorter axis alone would allow, and every inner
   piece is a percentage of the ring box — so the glyph, the disc and the rings
   always keep their proportions to each other, at any size.
   ========================================================================== */
/* `align-items:stretch` undoes the dashboard's `start` — here the single column
   should fill the row, not sit at the top of it. */
.kiosk-page .kiosk{
  grid-template-columns:minmax(0,1fr);max-width:960px;margin:0 auto;width:100%;
  flex:1;min-height:0;align-items:stretch;
}

/* One flex chain from the page down to the scanner, so the card can stretch. */
.kiosk-page .stage{
  max-width:1200px;
  display:flex;flex-direction:column;
  padding:clamp(10px,2vmin,22px);
}
.kiosk-page #view-kiosk{flex:1;display:flex;min-height:0;gap:0}
.kiosk-page .kiosk-main{flex:1;gap:0;min-height:0}
.kiosk-page .kiosk-card{flex:1;display:flex;flex-direction:column;min-height:0}
.kiosk-page .scanner{
  flex:1;justify-content:center;
  padding:clamp(16px,4vmin,60px) clamp(16px,3vmin,32px);
}

.kiosk-page .kiosk-head{padding:clamp(12px,2.2vmin,22px) clamp(14px,2.6vmin,26px)}
.kiosk-page .kiosk-classname{font-size:clamp(1.15rem,min(3.4vh,4.2vw),2.2rem)}
.kiosk-page .kiosk-classmeta{font-size:clamp(.74rem,min(1.7vh,2.1vw),1rem)}
.kiosk-page .kiosk-count span{font-size:clamp(1.35rem,min(3.6vh,4vw),2.6rem)}

.kiosk-page .scan-rings{
  width:clamp(128px,min(32vh,42vw),300px);
  height:clamp(128px,min(32vh,42vw),300px);
}
.kiosk-page .scan-core::before{width:56%;height:56%}
.kiosk-page .nfc-glyph{width:31%;height:31%}
.kiosk-page .scan-title{
  margin-top:clamp(12px,2.6vmin,28px);
  font-size:clamp(1.35rem,min(5.4vh,6.2vw),3rem);
}
.kiosk-page .scan-sub{font-size:clamp(.86rem,min(2.2vh,2.6vw),1.2rem)}
