/* TheraGrid - the pages: grids, chips, forms, dialogs.
   app.css holds the shell and the palette; this holds everything inside it.
   Extra tokens are defined here so the two sheets stay independent. */

:root {
  --stripe:#FBFCFD;
  --ind:#2F6FB5;      /* individual  */
  --ind-bg:#E9F0FA;
  --grp:#7A4FA8;      /* group       */
  --grp-bg:#F2EBF9;
  --rs:#0E7FB8;       /* rescheduled - sky blue, the clinic's own reading */
  --rs-bg:#E3F3FB;
  --pkg:#0F6E58;
  --pkg-bg:#E4F2EE;
  /* ONE COLOUR PER THERAPY, handed out by the order the clinic put them in
     on Settings rather than by their letters. A palette keyed on "OT" and
     "SE" would be this clinic's palette; keyed on position it is anybody's,
     and the app is meant to be sold.

     Six, because six is what a therapy list gets to before the differences
     stop being tellable apart at eleven pixels. A seventh therapy starts the
     six again, which is honest: it says "not one of the first six" rather
     than inventing a colour nobody can name.

     Chosen to sit apart from the therapist colours, which are the stripe down
     the side of the same chip - those come from the spreadsheet and are
     saturated, so these are quieter and read as a label rather than as a
     second person. */
  --th1:#1C6FA8; --th1-bg:#E6F0F8;
  --th2:#8A4FA0; --th2-bg:#F3EAF7;
  --th3:#0F7A5A; --th3-bg:#E4F3EE;
  --th4:#9E5417; --th4-bg:#FAEFE4;
  --th5:#1F6E6E; --th5-bg:#E4F1F1;
  --th6:#8A5A2B; --th6-bg:#F7EFE6;
  --cancel:#C0392B;
  --snum-bg:#E6EBF1;
  --snum-ink:#243747;
  --shadow:0 10px 34px rgba(16,24,32,.18);
}

@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]):not([data-theme="lavender"]):not([data-theme="blush"]) {
    --stripe:#141A20;
    --ind:#5A9BE0; --grp:#B084E0; --rs:#5FBCEA; --rs-bg:#0F2A38;
    --ind-bg:#16232F; --grp-bg:#231C31;
    --pkg:#4BB98D; --pkg-bg:#12291F;
  --th1:#6FB2E8; --th1-bg:#14242F;
  --th2:#C08FD8; --th2-bg:#231A2B;
  --th3:#5CC6A0; --th3-bg:#122720;
  --th4:#E0A05C; --th4-bg:#2B2116;
  --th5:#6FC2C2; --th5-bg:#12272A;
  --th6:#D0A472; --th6-bg:#2A2118;
    --cancel:#E8705F;
    --snum-bg:#222C36; --snum-ink:#D6DEE7;
    --shadow:0 10px 34px rgba(0,0,0,.55);
  }
}

:root[data-theme="dark"] {
  --stripe:#141A20;
  --ind:#5A9BE0; --grp:#B084E0; --rs:#5FBCEA; --rs-bg:#0F2A38;
  --ind-bg:#16232F; --grp-bg:#231C31;
  --pkg:#4BB98D; --pkg-bg:#12291F;
  --th1:#6FB2E8; --th1-bg:#14242F;
  --th2:#C08FD8; --th2-bg:#231A2B;
  --th3:#5CC6A0; --th3-bg:#122720;
  --th4:#E0A05C; --th4-bg:#2B2116;
  --th5:#6FC2C2; --th5-bg:#12272A;
  --th6:#D0A472; --th6-bg:#2A2118;
  --cancel:#E8705F;
  --snum-bg:#222C36; --snum-ink:#D6DEE7;
  --shadow:0 10px 34px rgba(0,0,0,.55);
}

/* ---------------------------------------------------------------- bars --- */

.toolbar, .filterbar {
  display:flex; flex-wrap:wrap; gap:9px; align-items:center;
  background:var(--surface); border:1px solid var(--line);
  border-radius:var(--radius); padding:10px 12px; box-shadow:var(--shadow-xs);
}
.filterbar { margin-top:9px; }
.grow { flex:1; }

/* A segmented switch, not four buttons that happen to be next to each other.

   The four were separately bordered with the chosen one filled in the accent
   colour, which put a second solid green block in a toolbar that already had
   the primary action in it - so "Week" competed with "+ Booking" for the eye,
   and neither won. A recessed track with the current segment raised out of it
   says the same thing without shouting, and reads as one control because it
   is drawn as one. */
/* Plain by default: three buttons that do three things, which is what the
   dashboard's range shortcuts are.

   It wraps. Settings puts twelve tabs in one of these and the row was 1151px
   wide inside a 948px bar, so the last two sat past the right-hand edge of the
   window with nothing to scroll and nothing to say they were there. */
.seg { display:inline-flex; flex-wrap:wrap; gap:4px; }
.seg .btn { padding:0 12px; }
/* And if one of them were marked, this is what marks it - written so that a
   browser without :has() still shows the choice, just without the track. */
.seg .btn[aria-pressed="true"] {
  background:var(--accent-bg); border-color:var(--accent);
  color:var(--accent); font-weight:600;
}

/* A recessed track, only where the group is genuinely a choice - which is
   exactly the groups whose buttons carry aria-pressed. */
.seg:has(.btn[aria-pressed]) {
  gap:2px; padding:3px;
  background:var(--sunken); border:1px solid var(--line);
  border-radius:var(--radius-sm);
}
.seg:has(.btn[aria-pressed]) .btn {
  border:0; background:none; box-shadow:none; color:var(--muted);
  min-height:30px; padding:0 12px; border-radius:var(--radius-sm);
}
.seg:has(.btn[aria-pressed]) .btn:hover { background:var(--surface); color:var(--ink); }
.seg:has(.btn[aria-pressed]) .btn[aria-pressed="true"] {
  background:var(--surface); color:var(--accent); font-weight:650;
  box-shadow:var(--shadow-xs);
}
/* THE SAME CHOICE, SAID LOUDLY. A `.seg` is a quiet recessed track made for
   a group of two or three sitting at the end of a toolbar. Messages puts
   EIGHT in it, each a different kind of work - a day's run, a record, two
   pages of settings - and at the right-hand end of a bar they read as an
   afterthought to the page's title rather than as the page's own navigation.

   So a row of its own, full width, under the title: pills with a mark each,
   and the one you are on filled rather than merely lighter. Asked for with a
   mockup on 9 October 2026 - "make Day messages page something like this" -
   with the condition the four redesigns before it carried, "you don't have to
   change any utility of functionality its just UI change". The buttons, their
   data-mtab values and the handler on them did not move. */
.tabrow {
  display:flex; flex-wrap:wrap; gap:6px; margin:0 2px 14px;
}
.tabrow .btn {
  padding:0 13px; min-height:38px; color:var(--muted);
  background:var(--surface); border-color:var(--line);
}
.tabrow .btn .navicon { width:15px; height:15px; }
/* Packages & Billing's two tabs are LINKS (#packages, #billing), each half
   its own page, so a link's underline has to go. */
.tabrow a.btn { text-decoration:none; }
.tabrow .btn:hover { color:var(--ink); }
/* FILLED, not tinted. A tab bar is read at a glance from across a desk and
   one of eight has to win outright; the tinted version of this was the
   `.seg` it came from, and eight of those look like eight switches. */
.tabrow .btn[aria-pressed="true"], .tabrow .btn[aria-current="page"] {
  background:var(--accent); border-color:var(--accent); color:#fff;
  font-weight:650; box-shadow:var(--shadow-xs);
}
.tabrow .btn[aria-pressed="true"]:hover, .tabrow .btn[aria-current="page"]:hover { color:#fff; }
/* BESIDE THE PAGE'S NAME (14 October 2026, "move the tabs upward because
   there is space"): the .headside rule - it fits beside the title or wraps
   whole onto a line of its own, never squeezed in beside it. The band's own
   margin is the gap under it now. */
.pagetop > .tabrow.headtabs {
  flex:0 0 auto; max-width:100%; margin:0 0 0 auto; align-self:center;
}
/* The title takes its own width beside them rather than the band's 260px
   floor: "Messages" and its line need about 190, and the 70 between is what
   puts eight tabs beside it on a 1366px laptop instead of under it. */
.pagetop:has(> .tabrow.headtabs) > .pagehead { flex-basis:auto; }

/* THE APPROVALS PAGE'S SECTIONS (26 October 2026): Cancellations, Stock and
   Other. The Waiting / Sent back / Decided switch, See the day, the filters -
   wherever toolbarFilters has put them - and their Filters button belong to
   the cancellation table, so Stock and Other hide them. By the section the
   page writes on the bar; with no section written nothing is hidden. The
   numbers on the tabs are the menu's own badge (.navcount). */
#apTools[data-section]:not([data-section="cancel"])
  > :is(#apTabs, #apDay, #apQuery, #apFromPair, #apToPair, #apClear, #apCount, .foldbtn),
#apFilters[data-section]:not([data-section="cancel"]) { display:none; }
#apSections { margin-bottom:10px; }

/* A switch standing on its own, not one of a set - the Calendar's "Full
   colour". It needs marking exactly as a chosen `.seg` button does, and every
   rule above is scoped to `.seg`, so without this the press changes the chips
   and leaves the button looking untouched. */
.btn.toggle[aria-pressed="true"] {
  background:var(--accent-bg); border-color:var(--accent);
  color:var(--accent); font-weight:600;
}

/* The range and the two arrows that move it, as one control. Apart, the
   arrows were a separate group and read as belonging to nothing in
   particular; together they read as "this week, and the ones either side". */
.rangenav { display:inline-flex; align-items:center; gap:2px; flex:none; }
.rangenav .btn.icon { min-width:30px; padding:0 8px; font-size:15px; }
/* A button that is only a symbol. Square rather than the text button's
   padding, so four of them in a row read as a group of tools and not as four
   words somebody forgot to write. The glyph carries the meaning, so every one
   of these also carries a title and an aria-label - see the toolbar markup. */
.toolbar .btn.icon { min-width:36px; padding:0 8px; }
.toolbar .btn.icon .navicon { width:17px; height:17px; }
/* No rule recolouring a pressed icon. The rainbow carries its own hues and
   must keep them in both states, and the button already says it is on the
   way every other toggle does - accent background, accent border. */

.range {
  font-family:"IBM Plex Mono", ui-monospace, monospace; font-size:13px;
  font-weight:500; white-space:nowrap; padding:0 4px; color:var(--ink);
}
.dateinput, .search {
  border:1px solid var(--line); background:var(--ground); color:var(--ink);
  border-radius:var(--radius-sm); padding:7px 10px; font:inherit; font-size:13px;
  min-height:36px;
}
.dateinput:hover, .search:hover { border-color:var(--faint); }
/* The field itself carries the focus, so the global outline is stood down for
   these: an outline two pixels outside a ring two pixels outside a border is
   three concentric rectangles and looks like an error state.

   background-COLOR, not background: the shorthand resets background-image with
   it, and on a <select> that is where the arrow is drawn - so focusing a
   dropdown made its arrow vanish. */
.dateinput:focus, .search:focus,
.dateinput:focus-visible, .search:focus-visible {
  outline:none; border-color:var(--accent); box-shadow:var(--ring);
  background-color:var(--surface);
}

/* The app's own arrow, on every dropdown that is a single choice. Two 45-degree
   gradients meeting at a point; `currentColor` means it follows the text, and
   therefore the theme, without a second definition anywhere.

   The selector names the classes as well as the element because .dateinput
   sets its own padding a few lines up: a bare `select` loses to it, and the
   arrow would have been drawn underneath the longest option's text. */
select:not([multiple]), select.dateinput,
table.rows.edit select, .form2 select {
  appearance:none; -webkit-appearance:none;
  padding-right:27px;
  background-image:
    linear-gradient(45deg, transparent 50%, currentColor 50%),
    linear-gradient(135deg, currentColor 50%, transparent 50%);
  background-position:right 13px center, right 9.5px center;
  background-size:4.5px 4.5px, 4.5px 4.5px;
  background-repeat:no-repeat;
}
/* Nothing to choose from, or not yours to choose: no arrow to invite a click. */
select:disabled { background-image:none; }
.search { min-width:180px; }
.dateinput { font-family:"IBM Plex Mono", ui-monospace, monospace; font-size:12px; }

/* AND IN THE TOOLBAR, where toolbarFilters() moves a pair to when it fits -
   the Approvals dates travel with their captions, and a caption that lost its
   small type on the way up reads as a stray word. */
.filterbar .clword, .filterbar label, .toolbar .fpair label, .toolbar .fpair .clword {
  font-size:10px; font-family:"IBM Plex Mono", ui-monospace, monospace;
  letter-spacing:.11em; text-transform:uppercase; color:var(--faint);
}
.filterbar .sep { width:1px; align-self:stretch; background:var(--line); margin:0 3px; }
/* A tick box standing beside a button in an .actions row: the answer to a
   question about the press that is one click away, so it reads as a sentence
   rather than as a form field, and it is vertically centred on the button. */
.actions label.check.sendopt, .sendgo label.check.sendopt {
  display:inline-flex; align-items:center; gap:7px; cursor:pointer;
  font-size:12.5px; color:var(--muted); margin-right:12px; user-select:none;
}
.actions label.check.sendopt input { width:auto; flex:none; margin:0; }
.actions label.check.sendopt:hover { color:var(--ink); }
.pills { display:flex; flex-wrap:wrap; gap:5px; }
.pill-btn {
  appearance:none; cursor:pointer; font:inherit; font-size:12px;
  display:inline-flex; align-items:center; gap:6px;
  border:1px solid var(--line); background:var(--ground); color:var(--muted);
  border-radius:var(--radius-pill); padding:5px 11px;
}
.pill-btn:hover { border-color:var(--faint); background:var(--sunken); color:var(--ink); }
.pill-btn[aria-pressed="true"] {
  border-color:var(--accent); background:var(--accent-bg);
  color:var(--accent); font-weight:600;
}
/* The therapist's own colour, so the ring keeps it legible when the dot is
   nearly the colour of the pill behind it. */
.pill-btn .dot {
  width:8px; height:8px; border-radius:50%; flex:none;
  box-shadow:0 0 0 1px rgba(0,0,0,.12) inset;
}

/* ---------------------------------------------------------------- grid --- */

.gridwrap {
  margin-top:10px; background:var(--surface); box-shadow:var(--shadow-xs);
  border:1px solid var(--line); border-radius:var(--radius);
  /* Scrolls in BOTH directions, which is also what makes the sticky header and
     time column work: a sticky cell is positioned against its nearest scroll
     container, and overflow-x alone already makes this one. Offsetting the
     header to clear the page nav pinned it 52px below the grid's own top
     instead - an empty band across the table, with the first slot label hidden
     underneath it. */
  /* A FALLBACK, not the answer. fitGrids() in ui.js measures the real
     distance from the grid's top to the bottom of the window and sets
     max-height from it, because what sits above the grid varies with the
     window's width, with what the person may see, and with whether they have
     opened the filters. This number holds the shape until that runs, and is
     what a page ui.js does not touch gets. */
  overflow:auto; max-height:calc(100vh - 230px);
}
table.cal { border-collapse:collapse; width:100%; }
table.cal th, table.cal td {
  border-right:1px solid var(--line-soft); vertical-align:top; padding:0;
}
/* Slot rows are tall, so the divider has to be findable without counting cards. */
table.cal tbody tr > td { border-bottom:3px solid var(--line); }
table.cal tbody tr:nth-child(even) > td { background:var(--stripe); }

table.cal thead th {
  position:sticky; top:0; z-index:5;
  background:var(--sunken); font-size:12px; font-weight:600; padding:8px 10px;
  text-align:left; border-bottom:1px solid var(--line); white-space:nowrap;
}
table.cal thead th.today { color:var(--accent); box-shadow:inset 0 -2px 0 var(--accent); }
/* ON THE SAME LINE as the date, not above it. Stacked, the weekday cost the
   header a whole second line - about 14px across the top of a grid people are
   trying to fit chips into, on every view. "MON 21 Sep" is one phrase anyway;
   reading it needed two lines only because it was drawn on two.

   Still mono, still small, still faint: it is the part you scan past on the
   way to the date, and making them one line does not make them one thing. */
table.cal thead th .wd {
  display:inline; margin-right:6px;
  font-family:"IBM Plex Mono", ui-monospace, monospace;
  font-size:10px; letter-spacing:.09em; text-transform:uppercase;
  color:var(--faint); font-weight:500;
}
.shutlabel {
  display:block; font-size:10px; font-weight:500; color:var(--bad);
  text-transform:none; letter-spacing:0;
}
/* Who is off that day, under the date: a pill each in her colour, dashed
   because she is not there - the solid pills elsewhere on the grid are people
   who are. Wraps inside the heading rather than widening the day. */
table.cal thead th .offline {
  display:flex; flex-wrap:wrap; align-items:center; gap:3px;
  margin-top:4px; white-space:normal; font-weight:500;
}
.offline .offword {
  font-family:"IBM Plex Mono", ui-monospace, monospace;
  font-size:9.5px; letter-spacing:.08em; text-transform:uppercase;
  color:var(--bad); margin-right:2px;
}
.offpill {
  font-size:10.5px; line-height:1.5; padding:0 7px; border-radius:var(--radius-pill);
  border:1px dashed var(--a); color:var(--ink);
  background:color-mix(in srgb, var(--a) 16%, transparent);
}
.offpill b { font-weight:600; }

td.slotcell, th.slotcell {
  background:var(--sunken); padding:8px 10px; white-space:nowrap; width:1%;
  font-family:"IBM Plex Mono", ui-monospace, monospace; font-size:13px;
  position:sticky; left:0; z-index:4;
}
/* The corner belongs on top of both, or the row it crosses paints over it. */
table.cal thead th.slotcell { z-index:6; }
/* ONE TIME WIDE, ON EVERY SCREEN (21 October 2026): "you can save space in
   laptop view also if you follow the same strategy (of mobile view) for
   'Time' column of calendar page & planning page". Measured on a 1920px
   laptop: the column was 151px of the grid, because a merged row's label
   ("11:00 / 11:15 / 11:45", rowLabelHtml) sat on one nowrap line and the
   Calendar's lunch label ("Lunch 2 - 2:30 30 min") did the same, two rows
   below. Each " / " is its own element, which becomes a line break with the
   slash hidden - so each time sits on its own line, and the lunch label
   stacks its three parts - and the column is as wide as one time. The cell
   stays nowrap, so a time is never split, and its text is unchanged, which
   the "now" button reads. Planning's lunch band spans the whole row and is
   not in this column, so it keeps its one line. */
td.slotcell .slotsep { display:block; height:0; overflow:hidden; }
.lunchband.withcells > td.slotcell .lunchin {
  display:flex; flex-direction:column; align-items:flex-start; gap:1px;
  white-space:normal;
}
.lunchband.withcells > td.slotcell .lut { white-space:nowrap; }
td.daycell { padding:5px; min-width:172px; }
td.daycell.today { background:var(--accent-bg); }
td.shut, th.shut { background:repeating-linear-gradient(45deg,
  transparent, transparent 7px, var(--line-soft) 7px, var(--line-soft) 14px); }

/* --------------------------------------------------------------- chips --- */

ul.sessions { list-style:none; margin:0; padding:0; display:flex; flex-direction:column; gap:5px; }
/* The therapist's colour is a marker, not a wall. At 8px it was a stripe as
   wide as the session number beside it, in colours taken straight from the
   old sheet - #9900FF among them - so a week of chips read as a colour chart
   with some text in it. It went to 3px, and the clinic has since asked for it
   back wider: 6px reads as a band down a column at arm's length without
   getting near the width of the text beside it. 8px is on record as too
   much and this is deliberately short of it.

   It is drawn with a pseudo-element rather than a left border so the chip can
   have one radius all the way round: a thick coloured border and a 7px radius
   produce a wedge at each corner where the two colours meet. */
.sess {
  position:relative; overflow:hidden;
  border:1px solid var(--line); border-radius:var(--radius-sm);
  background:var(--surface); padding:6px 9px 6px 15px; cursor:pointer;
  box-shadow:var(--shadow-xs);
}
.sess::before {
  content:""; position:absolute; left:0; top:0; bottom:0; width:6px;
  background:var(--kidcolour, var(--line));
}
.sess:hover {
  border-color:var(--faint); box-shadow:var(--shadow-sm);
  transform:translateY(-1px);
}
.sess.dim { opacity:.3; filter:saturate(.35); }
.sess.st-cancelled .sess-name { text-decoration:line-through; }
/* TWO GROUPS, AND ONLY ONE OF THEM WRAPS.

   A calendar column is about 200px when six days share a laptop, and a chip
   carrying a number, a name and four badges cannot put all of that and three
   buttons on one line at that width. Something has to give, and which thing
   gives is the whole question: as one wrapping row the BUTTONS dropped to a
   line of their own, which is precisely the row this change removed, arriving
   back by another route.

   So the name and its badges wrap inside their own box and the buttons are
   pinned to the top right, where they stay whatever the column is doing. On a
   wide column - a single day, or a tablet in landscape - it is all one line,
   which is the shape this was asked for as. */
/* THE GAPS ARE THE WHOLE DIFFERENCE, which was not obvious and was measured
   rather than guessed. Counted over the 423 chips of a real week:

     gap:5 / gap:5 / gap:2   147 chips wrap their head, 33,647px of chips
     gap:3 / gap:4 / gap:0    67 chips wrap,            32,204px

   Eighty chips stop needing a second line for eleven pixels of slack. The
   button group is holding width the badges need, and a badge is either on the
   line or it is not - there is no partial credit, so a few pixels decides a
   whole 17px row.

   Shrinking the buttons was tried alongside this and bought NOTHING: 20px
   icons with the same gaps gave byte-identical totals, because the gaps had
   already freed everything the badges could use. So they stay 22px with their
   34px pressable area, and the "+" keeps the 30px an audit set deliberately.

   The two icons touching is not as tight as it reads: a 15px glyph in a 22px
   button leaves 3.5px of its own either side, so adjacent icons sit 7px
   apart. */
.sess-top { display:flex; align-items:flex-start; gap:3px; flex-wrap:nowrap; }
.sess-head {
  display:flex; align-items:center; gap:4px; flex-wrap:wrap;
  flex:1 1 auto; min-width:0;
}
.sess-btns { display:flex; align-items:center; gap:0; flex:none; }

/* AND THE LAST OF THE WRAPPING, bought with two pixels of padding either side.

   A badge is 1px 6px everywhere else in the app and that is right there - it
   is usually one of two or three on a wide row. On a calendar chip they are
   competing with a name and three buttons inside 260px, and a chip either
   fits its top line or it spends a whole 17px row not fitting. Of the 42
   chips carrying three badges, 38 were wrapping.

   Measured over a real week, 423 chips, before and after:

     wrapping chips   67 -> 39
     the grid       8,325 -> 8,127px
     11:15 row      1,052 -> 1,006px      2:40 row  976 -> 899px

   The FONT is untouched at 11.5px. Only the padding goes, so the badges read
   exactly as they did and simply stop shouldering each other onto a second
   line. Scoped to .sess-head so the same badge in a panel, a dialog or on
   Packages keeps the room it has.

   This is the lever the clinic chose. A row is as tall as the busiest day in
   it - Saturday has eleven sessions at 11:15 where the other days have six -
   and capping a cell would have taken 2,000px off the week, but it would have
   done it by putting four children behind a click. Shorter chips shrink every
   row and hide nothing. */
.sess-head .badge { padding:0 4px; letter-spacing:.02em; }
/* A button, because it opens the child's last seven days and a span cannot be
   reached from a keyboard. Everything a button brings with it is taken back
   off here - it has to sit in the chip's top line exactly as the plain name
   did, or 422 chips all change height at once. */
.sess-name {
  font-size:12.5px; font-weight:600; letter-spacing:-.01em;
  appearance:none; border:0; background:none; padding:0; margin:0;
  font-family:inherit; color:inherit; text-align:left; cursor:pointer;
  line-height:inherit;
}
/* The name is 34x19 on a phone - smaller than the History and Edit links
   directly beneath it in the same chip, which were given a real target years
   ago (.linkbtn::after below) precisely because 10.5px text is a coin toss
   under a thumb. This button arrived later, opening the child's last seven
   days, and was missed: so the hardest thing to hit on a chip became the one
   people reach for by instinct, and missing it taps the chip instead, which
   lights up the whole week for that child.

   Same technique, same reason: a transparent ::after takes the tap, the text
   does not move, and no row of the grid gets taller. The numbers are measured
   at 375px with two chips stacked - 13px of clear space above, 51px below,
   65px to the right - so 6px and 8px cannot reach another target. */
.sess-name { position:relative; }
.sess-name::after { content:""; position:absolute; inset:-6px -8px; }
.sess-name:hover { text-decoration:underline; text-underline-offset:2px; }
.sess-name:focus-visible { outline:2px solid var(--accent); outline-offset:2px; }
/* The time, half as big again. A chip is mostly empty space and the time was
   set at 10px - smaller than anything else on it, and the one line reception
   reads across a row to find a slot.

   The therapist's name shares that line and stays where it was, at 10px. It
   went up with the time at first and the clinic asked for it back: the time
   is what you scan for, the name is what you check once you have stopped, and
   sizing them together made the chip shout two things at once. */
/* A ROW now, because the outcome shares it. The baseline alignment is what
   keeps a 15px mono time and an 11.5px uppercase tag looking like they are
   on the same line rather than merely near each other. */
.sess-meta {
  display:flex; align-items:baseline; gap:6px;
  font-family:"IBM Plex Mono", ui-monospace, monospace; font-size:15px;
  color:var(--muted); margin-top:2px;
}
.sess-meta .grow { flex:1; }
/* The tag does not stretch the line it has joined. */
.sess-meta .outcome { flex:none; align-self:center; }
/* AND NOTHING ON THIS LINE WRAPS. A flex row that does not wrap still lets
   its items shrink, and a shrunken item wraps its own text: the therapist's
   name was squeezed to 46px and broke over two lines, which put the 27px
   back a line lower than it started. The name is the one that gives - it
   ellipsises - because the time and the outcome are each a fixed short thing
   that means nothing half-read. */
.sess-meta { white-space:nowrap; }
.sess-meta .sess-who { overflow:hidden; text-overflow:ellipsis; min-width:0; }
.sess-meta .sess-who { font-size:10px; }
.sess-note { font-size:11px; color:var(--wait); margin-top:3px; word-break:break-word; }

/* A QUESTION MARK IN A NOTE IS A FLAG, so it is drawn as one.

   Asked for with the chip in hand: a note holding the single character "?",
   typed against a booking nobody had confirmed, drawn as a stray mark in 11px
   amber under the name. "i want if user adds question mark(?) in the notes, it
   should be highlighted or shown as a question mark(?) tag."

   IT CARRIES ITS OWN BACKING, for the reason .rstag does. A chip's background
   is the therapist's own colour, so nothing here can know what it is sitting
   on - and in `fullcolour` that note is forced to the chip's computed ink
   because on a mid-luminance colour the best contrast available is 4.58:1 and
   there is no headroom to spend. A tag with its own plate does not spend any:
   the pairing is --wait on --wait-bg, both theme-aware, which is the same
   pairing .stat.warn and the NOT CONFIRMED badge already use. The ring is what
   keeps it from dissolving into a chip whose colour is near the tint.

   NOT A BADGE IN THE ROW ABOVE, which is where this would naturally go. That
   row already has a `?` badge and it means the time on this session is not the
   time of the row it is drawn in - so a second identical mark an inch away
   would be two different facts wearing one symbol, which is the condition the
   clinic set for the owed badge: it must not look like the one next to it. */
.qmark {
  font-weight:800; font-style:normal; padding:0 4px; line-height:1.3;
  border-radius:var(--radius-xs);
  color:var(--wait); background:var(--wait-bg);
  box-shadow:0 0 0 1px var(--wait);
}

/* THE TIME A CHILD IS ACTUALLY SEEN, when it is not the row's own.

   The sheet has written these in brackets for years - Reyan(10:25am) in the
   10:30 row - and reception books to the bracket, not to the row. Only the
   exceptions carry one: a chip with no time on it is at the row's time, which
   is almost all of them, and annotating those would bury the few that matter.

   Monospaced and quiet. It is a correction to the row label rather than a
   part of the child's name, so it sits beside the name at the weight of a
   footnote - loud enough to catch the eye of somebody scanning for it, not
   loud enough to compete with who the chip is about. */
.sess-at {
  font-family:"IBM Plex Mono", ui-monospace, monospace;
  font-size:9.5px; font-weight:600; letter-spacing:-.02em;
  padding:1px 4px; border-radius:var(--radius-xs);
  background:var(--sunken); color:var(--muted); white-space:nowrap;
}

/* ONE LINE PER CHILD, on a grid that is six days of them at once.

   The calendar's chip is four lines because it is read one cell at a time.
   This one is read by scanning down a column - "which days does he come" -
   and every line of height halves how much of the week is on screen. So who,
   what, when, how much and the way to change it all share a row.

   The note is the one thing that does not fit, and it does not have to: it
   becomes a dot, with the text on the chip's own hover. A note is a thing to
   notice, and noticing is what a dot is for. */
/* AS WIDE AS THE LINE NEEDS, AND NO WIDER.

   table.cal is width:100%, so the six day columns share whatever the page
   gives them - and when the page stopped being capped at 1340px they shared
   528px more. Every chip grew from about 213px to 267px without a word of it
   being decided: the name is the flexible part, so all of it went into the
   gap between the name and the badges. Reported with an arrow drawn across
   that gap.

   Measured on 396 real chips at 267px: the widest line needs 226px, half of
   them need 150px or less, and not one was tight. Rendered at the 220px cap,
   exactly ONE name of the 396 shortens - the longest in the book - and that
   chip keeps the whole name in its own title, which the grid has always
   relied on for long ones. 46px off every chip for one ellipsis.

   ON THE CHIP RATHER THAN THE CELL, which matters on a small screen. A cell
   capped at 220px would force a horizontal scroll on a 1366px laptop, where
   six columns of 220 plus the time column do not fit; a chip capped at 220
   simply fills whatever narrower cell it is given.

   AND 220 STOPPED BEING TRUE (21 October 2026). The chip has since gained
   the copy mark and the "..." menu, and measured again on a 1920px laptop
   138 of 415 names were cut at 220 - "Anu...", "Aya..." - with 80px of the
   cell beside them. The widest line in the book now needs 291px, so the cap
   is 300: every name whole, a 1920px cell (302px) filled, and a wider screen
   still not stretching a chip into a gap. The reasoning above stands; only
   the measurement moved. A phone takes the cap off entirely (0b below). */
.sess.plan.one {
  display:flex; align-items:center; gap:5px; flex-wrap:nowrap;
  padding:4px 6px 4px 13px; overflow:hidden; max-width:300px;
}
/* The name takes what is left and gives way first - a long name ellipsises
   rather than pushing the package size or the pencil off the end, because
   those are the things being scanned for. */
.sess.plan.one .sess-name {
  flex:1 1 auto; min-width:0; overflow:hidden;
  text-overflow:ellipsis; white-space:nowrap;
}
.sess.plan.one .badge, .sess.plan.one .sess-at,
.sess.plan.one .sess-copy { flex:none; }

/* WHAT THE LINE IS FOR, in the order somebody reads it.

   The name is the subject. The time is the answer to the question this page
   gets opened with - "when does he come" - and was the smallest thing on the
   row at 9.5px, which is exactly backwards. The therapy code and the package
   size are labels: they are scanned for, not read, and colour does that work
   now, so they can be smaller than the words around them without being lost.

   11px for the time against 10px for the badges is a small difference on
   purpose. A line of five competing sizes reads as a ransom note; this is two
   steps, and the one that matters is the larger. */
.sess.plan.one .sess-at { font-size:11px; padding:1px 5px; letter-spacing:-.01em; }
.sess.plan.one .badge.code,
.sess.plan.one .badge.pkgflag { font-size:10px; padding:0 4px; letter-spacing:.02em; }
.sess.plan.one .badge.g { font-size:10px; padding:0 4px; }
/* A CHILD ON LEAVE HAD NO NAME (21 October 2026). The leave badge, "AWAY
   17-21 Oct", was drawn at 11.5px - larger than every badge beside it - and
   could not shrink, while the name is the part that gives way; so on all
   nine such chips in the week the name was squeezed to 0px and the chip said
   who was away without saying who. It is the badges' size now, and it gives
   way BEFORE the name (twenty times as fast) down to "AWAY", with the dates
   still whole in its title. */
.sess.plan.one .badge.leave {
  font-size:10px; padding:0 4px; flex:0 20 auto; min-width:4em;
  overflow:hidden; text-overflow:ellipsis; white-space:nowrap;
}
/* A DAY ASKED TO COME OFF THE PLAN (0361) is still on it until allowed, and
   says so - on the chip and in the plan dialog, where it takes the cross's
   place. The leave badge's own size and give, so the name still wins. */
.badge.asked { color:var(--wait); background:var(--wait-bg); border:1px dashed var(--wait); }
.sess.plan.one .badge.asked {
  font-size:10px; padding:0 4px; flex:0 20 auto; min-width:4em;
  overflow:hidden; text-overflow:ellipsis; white-space:nowrap;
}

/* THE THERAPY AND THE PACKAGE, IN COLOUR - on this grid only.

   The calendar's note on .badge.code says a therapy code cannot be loud
   because it is on every chip, and something that never varies carries no
   information by being loud. That is right THERE: a calendar column is one
   therapist, so every code down it is the same letters.

   Here it is the opposite. One cell of the plan holds four or five children
   with four or five different therapies, and the question somebody brings to
   this page - which days does he come, who else is in at that hour - is
   answered faster by colour than by reading two letters five times. So the
   code is tinted by WHICH therapy it is, and the package size by being a
   package at all.

   Scoped to .sess.plan.one so the calendar chip keeps the reasoning it was
   given. The two pages disagree on purpose. */
.sess.plan.one .badge.code { border-color:transparent; font-weight:700; }
.sess.plan.one .badge.code.th1 { color:var(--th1); background:var(--th1-bg); }
.sess.plan.one .badge.code.th2 { color:var(--th2); background:var(--th2-bg); }
.sess.plan.one .badge.code.th3 { color:var(--th3); background:var(--th3-bg); }
.sess.plan.one .badge.code.th4 { color:var(--th4); background:var(--th4-bg); }
.sess.plan.one .badge.code.th5 { color:var(--th5); background:var(--th5-bg); }
.sess.plan.one .badge.code.th6 { color:var(--th6); background:var(--th6-bg); }
/* A therapy the list does not have - a therapist whose code was typed rather
   than chosen. It keeps the old quiet treatment, which is the right answer
   for something the clinic has not named. */
.sess.plan.one .badge.code:not([class*="th"]) {
  color:var(--muted); background:var(--sunken);
}
.sess.plan.one .badge.pkgflag {
  color:var(--pkg); background:var(--pkg-bg); border-color:transparent;
  font-weight:700;
}
.sess.plan.one .sess-dot {
  flex:none; color:var(--wait); font-size:15px; line-height:0; cursor:help;
}

/* The pencil. Sized to the row rather than given a button's padding: it is
   the last thing on a line that is already tight, and it only has to be
   findable. It stays faint until the chip is under the pointer, so a grid of
   three hundred of them is not a grid of three hundred icons. */
.planedit {
  flex:none; appearance:none; border:0; background:none; cursor:pointer;
  padding:0; margin-left:auto; width:20px; height:20px; border-radius:var(--radius-xs);
  display:inline-flex; align-items:center; justify-content:center;
  color:var(--faint); opacity:.55; transition:opacity .12s, color .12s;
}
.planedit .navicon { width:13px; height:13px; stroke-width:1.9; }
.sess:hover .planedit, .planedit:focus-visible { opacity:1; color:var(--ink); }
.planedit:hover { color:var(--accent); background:var(--accent-bg); opacity:1; }

/* A finger cannot find a 20px target, and on a touch screen there is no hover
   to reveal it with either - so it is full strength and full size there. */
@media (pointer: coarse) {
  .planedit { width:32px; height:32px; opacity:1; color:var(--muted); }
  .planedit .navicon { width:15px; height:15px; }
}

/* The session number is what staff read first, so it gets real weight. */
.snum {
  font-family:"IBM Plex Mono", ui-monospace, monospace; font-size:13px; font-weight:700;
  background:var(--snum-bg); color:var(--snum-ink); border-radius:var(--radius-xs);
  padding:0 6px; letter-spacing:-.02em;
}
/* WHICH PIECE of a split reschedule, after the number it belongs to: #17-1.
   Inside the same plate rather than a badge of its own, because it is part of
   the number and not a second fact about the session - and a touch lighter,
   so the eye still lands on the 17 first. */
.snum .pnum { font-style:normal; opacity:.75; }
/* Every tag a quarter bigger: the therapy code, G, RS, the package and LAST.
   At 9px they were legible only to somebody who already knew what they said,
   which defeats a label whose whole job is to be read in passing. */
.badge {
  font-size:11.5px; font-weight:700; letter-spacing:.04em; padding:1px 6px;
  border-radius:var(--radius-xs); border:1px solid var(--line); color:var(--muted);
}
/* Colour, still read at a glance - but as text on a tint of itself rather
   than white on a solid block. Solid fill is loud, and when everything on a
   chip is loud nothing on it is. It is kept for the two badges that are
   genuinely exceptions: a cancellation, and the last session of a package. */
.badge.g  { color:var(--grp); background:var(--grp-bg); border-color:transparent; }
.badge.i  { color:var(--ind); background:var(--ind-bg); border-color:transparent; }
.badge.rs { color:var(--rs);  background:var(--rs-bg);  border-color:transparent; }
.badge.cx { color:#fff; background:var(--cancel); border-color:transparent; }
/* AWAY (0234): the family said they would be away on this day. The waiting
   amber, because it is a fact about the day rather than a warning about the
   session, and it reads on every chip colour the way the owes-check badge
   does. On the Planning grid and the slot panel the dates ride on it. */
.badge.leave { color:var(--wait); background:var(--wait-bg); border-color:transparent; }
/* The block in the session dialog that records one: two dates, a note and a
   button on one line where there is room, stacked where there is not. */
.leaveform { display:flex; flex-wrap:wrap; gap:8px; align-items:flex-end; margin-top:6px; }
.leaveform > label { display:flex; flex-direction:column; gap:4px; font-size:12px; color:var(--muted); }
.leaveform input[type="date"] { max-width:10.5rem; }
.leaveform input[type="text"] { flex:1 1 12rem; }
/* A tick with its words beside it, on a form whose labels are otherwise
   blocks above their boxes. */
.tickline { display:flex; align-items:center; gap:8px; font-size:13px; color:var(--ink); cursor:pointer; }
.tickline input { margin:0; }
/* A leave asked for, on the Approvals page: the request as a queue item, the
   booked sessions inside it as a small table with a word beside each, and
   the two buttons under it. */
.leaveask { padding:8px 0 4px; border-top:1px solid var(--line); }
.leaveask:first-of-type { border-top:0; }
.leaveask .gridwrap { margin-top:6px; }
.leaveask table.rows select { width:100%; min-width:14rem; }
.leaveask .actions { display:flex; gap:8px; margin-top:8px; }
/* A reschedule recorded by hand, waiting on Approvals (19 October 2026):
   what was cancelled, what was booked, and the decision on one line. */
.handask .handline { margin:4px 0 0; font-size:13.5px; }
.handask .handline > b { display:inline-block; min-width:5.5rem; color:var(--muted); font-weight:600; }
.handask .handdecide { flex-wrap:wrap; align-items:center; }
.handask .handdecide select { min-width:16rem; max-width:100%; }
.handask .handdecide .search { flex:1 1 14rem; min-width:0; }

/* WHAT THIS CHILD IS STILL OWED, and deliberately not shaped like RS - which
   was the condition the clinic attached to asking for it. RS is two capital
   letters about THIS session; these are a word and a number about the CHILD,
   owed on some other day. Two similar badges an inch apart, one meaning the
   opposite of the other, is worse than neither - so these are lower case, in
   a pill, with a number they are read for.

   `owes` borrows the accent rather than a colour of its own: it is news, not
   a warning. `check` takes the same ink the flag already uses in the queue bar
   and on Reschedules, because it is the same flag. */
.badge.owes, .badge.owescheck {
  border-radius:var(--radius-pill); letter-spacing:0; text-transform:none;
  font-weight:650; padding:1px 7px 1px 5px; border-color:transparent;
  display:inline-flex; align-items:center; gap:3px;
}
/* The glyph rides with the number rather than sitting above it. 12px, which
   is where the two arrows were still legible when they were checked against
   the emoji at 48, 24 and 12. */
.badge.owes svg, .badge.owescheck svg {
  width:12px; height:12px; display:block; flex:none;
}
.badge.owes { color:var(--accent); background:var(--accent-bg); }
.badge.owescheck { color:#fff; background:var(--cancel); }
/* GROW THE TARGET, NOT THE PILL. These two are the only badges that are
   BUTTONS - the rest are spans saying what a session is - and at 37x25 on a
   375px phone they were under the 28px below which a thumb starts missing,
   on a control whose whole point is being reached for from the grid.

   The pill cannot simply get bigger: it sits in a row of badges on a chip and
   the note above explains why it is deliberately quiet and small. So the same
   transparent ::after the chip's name and icons already use. 3px, measured:
   there is 12px of clear space to the chip above, 37 below, 31 to the name on
   its left - of which that name's own pad takes 8 - and 50 to the chip menu.

   BOTH VARIANTS, because they are one control with two meanings. Fixing only
   `owes` would leave the flagged one small, which is the "it looked like one
   call site" mistake this repository keeps recording. */
.badge.owes, .badge.owescheck { position:relative; }
.badge.owes::after, .badge.owescheck::after {
  content:""; position:absolute; inset:-3px;
}
.badge.ok { color:var(--ok); background:var(--ok-bg); border-color:transparent; }
.badge.warn { color:#fff; background:var(--cancel); border-color:transparent; font-size:11.5px; }
.badge.pkgflag {
  background:var(--pkg-bg); color:var(--pkg); border-color:transparent;
  font-family:"IBM Plex Mono", ui-monospace, monospace;
}

.cellfoot { display:flex; gap:5px; margin-top:6px; }
/* Book this slot, and leave a note on it. Both are 26px tall, which is under
   a thumb's width, and the "+" is the way most bookings start.

   Height only, and only 4px of it: the cell above has 5px of clear space and
   the one below 11px, so this is what there is without one cell's target
   reaching into another's. Growing the buttons themselves instead would add
   10px to every row of a nine-row grid on the one device where vertical space
   is scarcest. */
.addbtn, .notebtn { position:relative; }
.addbtn::after, .notebtn::after { content:""; position:absolute; inset:-4px -2px; }
/* Except to the RIGHT of the note button, where the next cell's "+" can be a
   single pixel away - it sits hard against the cell's edge and the table adds
   no gap of its own. A tap that lands there belongs to the other day. */
.notebtn::after { right:0; }
/* It carries words now, so it is sized for words: a 16px glyph centred in a
   flex:1 box was right for a "+" and leaves the text cramped and oversized. */
.addbtn {
  flex:1; border:1px dashed var(--line); background:none; color:var(--faint);
  border-radius:var(--radius-sm); padding:4px 6px; cursor:pointer;
  font-size:11.5px; line-height:1.3; white-space:nowrap;
}
/* Declared twice, identically, for long enough that nobody noticed. */
.addbtn:hover { border-color:var(--accent); color:var(--accent); background:var(--accent-bg); }
.notebtn {
  appearance:none; border:1px dashed var(--line); background:none; cursor:pointer;
  color:var(--muted); border-radius:var(--radius-sm); padding:4px 7px;
  font-size:11.5px; line-height:1.3; white-space:nowrap;
}
.notebtn:hover, .notebtn.has {
  border-style:solid; border-color:var(--wait); color:var(--wait); background:var(--wait-bg);
}
.cellnote {
  background:var(--wait-bg); border-left:3px solid var(--wait); border-radius:var(--radius-sm);
  padding:4px 7px; margin-bottom:6px; font-size:11.5px; cursor:pointer;
  word-break:break-word;
}
/* THE NOTE'S OWN LINE BREAKS. They have always been saved - the editor is a
   textarea and the column is text - and HTML folds a newline into a space, so
   a note typed as three separate things was drawn as one run of words. This
   one line is the whole of that fix. */
.cellnote .tx { white-space:pre-wrap; display:block; }
.cellnote .tx b { font-weight:700; color:var(--ink); }
/* WHICH SITE A NOTE IS FOR, while both are on screen (21 October 2026): a
   cell under All branches can hold one of each site's, and a note about a
   therapist on leave reads the same whichever site wrote it. */
.cellnote .notetag {
  display:inline-block; margin-right:5px; padding:0 6px; border-radius:999px;
  border:1px solid currentColor; color:var(--wait); background:var(--surface);
  font-size:10px; font-weight:650; line-height:1.5; vertical-align:1px;
}
.cellnote + .cellnote { margin-top:-2px; }

/* WHICH BRANCHES A NOTE IS FOR (0349): a tick box each and All branches, in
   one row that wraps on a phone, above the note itself. */
.notesites { display:flex; flex-wrap:wrap; gap:4px 18px; margin:2px 0 10px; }
.notesites .check { margin:0; }

/* CLAMPED TO TWO LINES until somebody asks for the rest. A note with four
   breaks is five lines at the top of a cell, pushing every chip down in the
   tallest grid on the screen - and the clinic asked for the lines without the
   space. Two is less than the three this note wrapped to as one paragraph. */
.cellnote:not(.open) .tx {
  display:-webkit-box; -webkit-line-clamp:2; -webkit-box-orient:vertical;
  overflow:hidden;
}
.notemore {
  appearance:none; border:0; background:none; padding:2px 0 0; cursor:pointer;
  font:inherit; font-size:10.5px; letter-spacing:.03em; color:var(--wait);
  text-decoration:underline; text-underline-offset:2px;
}
/* Same again, and this one is worse: 10.5px underlined text, 68x18, which
   is the smallest thing on the calendar and the only way to the rest of a
   note that has been clamped to two lines.

   Padding is not available here either - the clamp exists because "the clinic
   asked for the lines without the space", and every pixel added to this box
   pushes every chip in the column down. Measured at 375px: 105px of clear
   space above, 10px below to the first session chip, 91 and 162 either side.

   NOT SYMMETRIC, and the reason is the 10px. Below this button is a session
   chip, and tapping one of those lights up the whole week for that child - so
   the gap that must not be crossed is the small one. The space is taken from
   above, where there is a hundred pixels of nothing: 8 up and 3 down leaves 7
   still clear beneath.

   It is also a pixel more than it looks like it needs. The button is 17.6px
   tall, not 18 - the figure mobilecheck prints is rounded and the threshold it
   tests is not - so a 5px pad each side came to 27.6 and failed while
   reporting "84x28". */
.notemore { position:relative; }
.notemore::after { content:""; position:absolute; inset:-8px -8px -3px; }
.notemore:hover { color:var(--ink); }
.cellnote.done .notemore { color:var(--faint); }
.cellnote.done {
  background:var(--sunken); border-left-color:var(--faint);
  color:var(--faint); text-decoration:line-through;
}

/* ------------------------------------------------------------- summary --- */

tr.summary td {
  background:var(--sunken); padding:8px 10px; vertical-align:top;
  font-family:"IBM Plex Mono", ui-monospace, monospace; font-size:11px;
}
tr.summary .big {
  display:block; margin-bottom:5px; font-family:"IBM Plex Sans", system-ui, sans-serif;
  font-size:13px; font-weight:600;
}
.openrow { display:flex; align-items:center; gap:3px; flex-wrap:wrap; margin-bottom:3px; }
.openrow .slotlabel { color:var(--faint); width:38px; flex:none; font-size:10px; }
.tchip {
  font-size:9px; font-weight:700; color:#fff; padding:1px 5px; border-radius:var(--radius-xs);
  white-space:nowrap; letter-spacing:.03em;
}
.opencell .none { color:var(--faint); }

/* THE STATS ROW, under the Open row it sits beside in every way: same sunken
   band, same monospace, same .tchip for a therapist in her own colour. It
   answers the opposite question - Open says what was free, Stats says what the
   day came to.

   The three states carry the clinic's own colours, the same --good / --wait /
   --bad every other state in the app uses, so a cancellation is the orange it
   is everywhere else rather than an orange invented here. */
tr.statsrow td {
  background:var(--sunken); padding:8px 10px; vertical-align:top;
  border-top:1px solid var(--line);
  font-family:"IBM Plex Mono", ui-monospace, monospace; font-size:11px;
}
tr.statsrow .big {
  display:block; margin-bottom:7px; font-family:"IBM Plex Sans", system-ui, sans-serif;
  font-size:15px; font-weight:700;
}
.statscell .none { color:var(--faint); }
.statstots { display:flex; flex-wrap:wrap; gap:5px; margin-bottom:7px; }
/* THE NUMBER FIRST, IN A BOX OF ITS OWN, and big enough to read across a
   desk. Written as one run of small text the figure was the part that
   disappeared: "62 actual" on a green pill reads as a word with something in
   front of it, and the number is the whole point of the row. Reported from the
   desk with a design attached.

   The count sits on a darkened panel rather than a colour of its own, which is
   what lets ONE rule serve both pills: the state colours come from the app's
   own --ok / --wait / --bad, and a therapist's comes off her own row, so
   nothing here can know in advance what it is sitting on. A translucent black
   darkens any of them by the same amount and stays legible on all of them. */
.stat, .tchip.pair, .misskid {
  display:inline-flex; align-items:stretch; overflow:hidden; padding:0;
  border-radius:7px; white-space:nowrap; line-height:1;
  font-size:12px; font-weight:700; letter-spacing:.02em;
}
.stat > b, .tchip.pair > b {
  padding:5px 7px; background:rgba(0, 0, 0, .34);
  /* So a column of counts lines up rather than shuffling with the digits. */
  font-variant-numeric:tabular-nums;
}
.stat > span, .tchip.pair > span { padding:5px 8px; }

.stat.good  { background:var(--ok-bg);   color:var(--ok); }
.stat.warn  { background:var(--wait-bg); color:var(--wait); }
.stat.bad   { background:var(--bad-bg);  color:var(--bad); }
.stat.plain { background:var(--line);    color:var(--faint); }
.statswho { display:flex; flex-wrap:wrap; gap:5px; }
/* A therapist who lost the whole day to cancellations, and one who is on
   leave. Both keep her colour - that is how this clinic says who somebody is -
   but drained of it, so the eye goes to the people who worked. They must not
   look like each other either: "0" is a day that was booked and did not
   happen, "away" is a day that was never going to. */
.tchip.nil  { opacity:.55; }
.tchip.away { opacity:.45; font-style:italic; }

/* ---- WHO IS EXPECTED AND NOT WRITTEN DOWN ----------------------------
   The space under a cell's chips, asked for with it circled: the child in
   their therapist's colour with the number they would be booked at. It
   shares the pair pill above rather than having a shape of its own, because
   the two say the same KIND of thing - a figure about a person - and
   because this must not be read as a chip. A chip means a session exists,
   and the Stats row at the foot of this grid counts those.

   SMALLER THAN BOTH, which is what keeps it furniture rather than record: a
   cell is the tallest thing on the screen, so a line of these costs about
   what the free-therapist pills under it cost - 20px against a chip's 46.
   The ink is kidStyle()'s and not white, because half the therapists'
   colours here came off a spreadsheet and are pale. The plate keeps the
   translucent black above, which darkens any colour by the same amount and
   is the darker half of the two-tone the clinic drew. */
.misskid {
  font-size:10.5px; cursor:pointer; font-family:inherit;
  background:var(--kidcolour, #8C98A4); color:var(--kidink, #fff);
  /* Positioned for the hit area at the foot of this block: an absolute
     ::after inside a static element anchors to some ancestor instead and
     grows nothing. */
  position:relative;
  /* THE EDGE IS THE SAME ON EVERY PILL, which is what makes the row read as
     one group of things rather than ten unrelated colours - the fills come
     off ten different therapists and have nothing in common. Two pixels, in
     --miss; see the note beside it in app.css for why it is neither --canc
     nor --wait. */
  border:2px solid var(--miss);
}
/* THE NAME, not the copy mark beside it. Unscoped this matched both - and
   at (0,1,1) it beat `.misscopy` (0,1,0), so the mark's own padding was dead
   and it measured 18px tall where 22 was written. */
.misskid > span:not(.misscopy) { padding:3px 6px; }
/* ---- COPYING THE NAME OFF A PILL ------------------------------------
   Asked for with the WhatsApp search box open and a name typed into it by
   hand: the clinic reads a "not booked" pill, then goes looking for that
   parent's chat. The filing name is what that search matches.

   POSITIONED, AND ABOVE THE TAP PAD. This pill's hit area is an absolutely
   positioned ::after, which paints over in-flow content - so without a
   stacking order of its own this mark would be a button that looks
   pressable and never fires, every press landing on the pill behind it.

   A SPAN WITH role="button", not a button: a button inside a button is
   markup the browser un-nests, which would put this outside the pill. */
/* `.misskid > .misscopy` rather than `.misscopy`: (0,2,0), so it outranks
   the name's rule above on its own merits rather than with !important. */
.misskid > .misscopy {
  position:relative; z-index:1; cursor:pointer;
  display:inline-flex; align-items:center; justify-content:center;
  /* AS TALL AS THE PILL'S OWN CONTENT, so the ::after below reaches the 28px
     floor without reaching past the pill's. See the note on that rule. */
  padding:5px 5px 5px 3px; opacity:.72;
}
.misscopy svg { width:12px; height:12px; }
/* BIG ENOUGH FOR A FINGER, and mobilecheck cannot say so: it measures BUTTON,
   A and SELECT, and this is a span with role="button" - which it has to be,
   since a button inside a button is markup the browser un-nests. A thumb that
   misses this by three pixels lands on the pill and OPENS THE PANEL rather
   than copying, which is worse than nothing happening.

   ±4 MATCHES THE PILL'S OWN PAD and must not grow. The rows are 9px apart
   because two pads that touch hand the tap to whichever came later in the
   markup - 65 overlapping pairs at 5px, none at 9 - so a ±6 here would put
   this mark's area over the pill above it, and the mark is later in the
   markup than that pill: a finger aimed at the bottom of one child's pill
   would copy another child's name.

   SO THE MARK IS TALLER INSTEAD, AND THE PILL GREW 22px TO 26. That is the
   one thing this costs and the note on .misskid::after above does not cover
   it - that note is about the PAD, which is still height-only and still free.
   Four pixels per row of pills, in cells that have a missing child in them.
   Measured after: mark 28x30, nothing under the floor, no pad overlapping
   any other. */
.misscopy::after {
  content:''; position:absolute; left:-4px; right:-4px; top:-4px; bottom:-4px;
}
.misscopy:hover { opacity:1; }
.misscopy:focus-visible {
  outline:2px solid var(--miss); outline-offset:-2px; opacity:1;
}
/* ---- "COPIED", WHERE THE FINGER WAS --------------------------------
   The app's toast is bottom centre, and this pill can be anywhere on the
   tallest grid here - so the confirmation for a 12px mark landed a screen
   away from it and read as nothing having happened. It matters more on this
   mark than most, because the same press on the pill BODY opens a panel.

   FIXED, and placed by flash() in calendar.js from the mark's own rectangle:
   the pill lives in `.gridwrap`, which clips anything positioned inside it,
   and a keyboard press has no pointer to put this under. Above the ordinary
   panel layer, because a pill can sit behind one. */
.copytip {
  position:fixed; z-index:120; pointer-events:none;
  padding:3px 8px; border-radius:var(--radius-pill);
  background:var(--ink); color:var(--ground);
  font-size:11.5px; font-weight:700; letter-spacing:.01em;
  box-shadow:var(--shadow-xs); white-space:nowrap;
  opacity:1; transition:opacity .18s ease, transform .18s ease;
}
.copytip.bad { background:var(--bad); color:#fff; }
/* It leaves the way it came: up from a mark it sat above, down from one it
   sat below, so the movement always reads as going back to the mark. */
.copytip.going { opacity:0; transform:translateY(-4px); }
.copytip.below.going { transform:translateY(4px); }
@media (prefers-reduced-motion: reduce) {
  .copytip { transition:opacity .18s ease; }
  .copytip.going, .copytip.below.going { transform:none; }
}

.misskid:hover { filter:brightness(1.08); }
.misskid:focus-visible { outline:2px solid var(--accent); outline-offset:2px; }
/* BIG ENOUGH FOR A FINGER WITHOUT BEING BIGGER ON THE PAGE. The pill draws at
   21px, and below about 28 a finger misses more often than it lands - found
   on the clinic's own phone, where this was the only control on the calendar
   under that floor while the cell's own buttons passed at 25px, because they
   already do this. Height only, so a row of pills costs exactly what it did.
   The pill is positioned above for it. */
.misskid::after {
  content:''; position:absolute; left:0; right:0; top:-4px; bottom:-4px;
}
/* UNDER THE CELL'S BUTTONS AND BEHIND A RULE. This row is the only thing in
   a cell that is a job rather than a record, and the second mockup put it
   below the controls that do the job. The rule is what stops it reading as a
   third row of the buttons above it. */
.missline {
  display:flex; flex-wrap:wrap; align-items:center;
  /* THE ROW GAP IS PART OF THE TAP TARGET ABOVE, which is why it is not 5
     like the column gap. Four pixels of pad above and below reaches 8px
     between two rows of pills; at 5px apart those pads overlap, and two hit
     areas that touch hand the tap to whichever came later in the markup - a
     finger aimed at one child opening another. Measured on the phone over 70
     pills: 65 overlapping pairs at 5, none at 9. The column gap needs none of
     this, because the pad is height only. */
  column-gap:5px; row-gap:9px;
  margin-top:7px; padding-top:7px; border-top:1px solid var(--line);
}
/* THE WORD, NOT THE COLOUR, SAYS WHAT THEY ARE. A row of coloured pills sits
   six pixels above another row of coloured pills - the therapists who are
   free - and the two mean opposite things. Small and lower case, on the same
   line as the first pill so it costs no height of its own. */
.misswhy {
  font-size:10px; font-weight:700; letter-spacing:.04em;
  color:var(--miss); text-transform:uppercase;
}

/* --------------------------------------------------------------- modal --- */

.modal {
  position:fixed; inset:0; z-index:100; background:rgba(12,16,20,.5);
  /* NOT ANIMATED, on purpose (24 October 2026, "app seems slow" the night the
     motion went live). This is a full-screen blur of the page; fading it in
     makes the browser redo the blur on every frame, which is the stutter a
     tablet shows. It appears at once; only the box settles (motion, at the
     foot of this file). */
  display:flex; align-items:flex-start; justify-content:center;
  padding:40px 16px; overflow:auto;
  /* The page behind a dialog is a dense grid of coloured chips, and a flat
     tint over it leaves every one of them legible and competing with the form
     in front. A small blur puts it definitively behind. */
  backdrop-filter:blur(3px); -webkit-backdrop-filter:blur(3px);
}
.modal-box {
  /* Positioned so the resize corner and edges (.mgrip) sit on its own edges
     before it floats. */
  position:relative;
  width:100%; max-width:540px; background:var(--surface);
  border:1px solid var(--line); border-radius:var(--radius-lg);
  box-shadow:var(--shadow);
}
.modal-box.wide { max-width:860px; }
/* One step wider, for a dialog whose content is a ROW rather than a form.
   860px is the right size for reading down a form and the wrong size for
   reading along a week - it fits three days of the child's record where the
   same dialog at 1120 fits four. Nothing else asks for this and nothing else
   should: a form this wide walks the eye too far between a label and its
   box, which is why `wide` stops where it does. */
.modal-box.widest { max-width:1120px; }
/* A child's name that opens their record. It has to read as the name it is -
   a table of ninety rows with a blue underlined link on every one of them is
   a table nobody can scan - so it stays the cell's own text and says what it
   does on hover, the way the calendar chip's name does. */
.kidlink {
  appearance:none; border:0; background:none; padding:0; margin:0;
  font:inherit; font-weight:600; color:var(--ink); cursor:pointer;
  text-align:left;
  /* GROW THE TARGET, NOT THE INK - the same trick .chipicon and .linkbtn
     use, and for the same reason. Measured on a 375px phone this was 20px
     tall, under the 28px below which a finger starts missing, and it is the
     way into a child's record from a table of ninety rows.

     Padding would have done it and would have made every row taller, on the
     page whose other complaint is that it is long. An invisible ::after
     stretches the pressable area past the text without moving anything: the
     name stays exactly where it was and the box around it is 10px taller
     either side. */
  position:relative;
}
.kidlink::after { content:""; position:absolute; inset:-10px -6px; }
.kidlink:hover { text-decoration:underline; text-underline-offset:2px; }
.kidlink:focus-visible { outline:2px solid var(--accent); outline-offset:2px; }

/* A DIALOG THAT HAS BEEN PICKED UP. Until then it is centred by the backdrop's
   flexbox, as it always was; once dragged it carries its own fixed box and the
   backdrop stops having an opinion about where it sits.

   The backdrop keeps its blur and its click-to-close, so nothing about how a
   dialog is dismissed changes. */
/* A WINDOW RATHER THAN A DIALOG. Asked for about the Packages grid: "i want a
   window popup to fill the values, it will be draggable and background screen
   will be visible". Dragging was never the missing half - every dialog here
   has moved since the history and edit windows were asked to float - so what
   goes is the four things that make a dialog MODAL, and each one is in the way
   of somebody copying values in from the table behind it:

     the tint and the blur   hide the row they are reading from
     pointer-events          swallow every click meant for the page
     the body scroll lock    stops them reaching the rest of the grid
     the click-away close    throws the form away when they touch the table

   The backdrop element stays and simply passes everything through, because it
   is what positions the box before anybody drags it - and because liveSafe()
   in main.js looks for `.modal` to hold the live repaint while somebody is
   typing, which is as true of a window as of a dialog. */
.modal.bare {
  background:none; backdrop-filter:none; -webkit-backdrop-filter:none;
  pointer-events:none;
}
.modal.bare .modal-box {
  pointer-events:auto;
  /* It floats over a WORKING page rather than over a blur, so it needs an
     edge of its own to read as being in front. --shadow-md is the deepest
     this theme defines and both themes define it; an undefined var is not
     a fallback - CSS drops the whole declaration - so no invented name. */
  box-shadow:var(--shadow-md);
  border-color:var(--faint);
  /* Tall enough to hold a package and never taller than the window, since
     the backdrop no longer scrolls to reach the foot of it. `free` takes over
     the moment it is dragged. */
  max-height:calc(100vh - 80px); display:flex; flex-direction:column;
}
.modal.bare .modal-box .modal-body { flex:1 1 auto; overflow:auto; }

/* SAVE, DISCARD OR KEEP EDITING (ui.js, leaveDialog) - asked before a dialog
   holding unsaved work goes. On the body and over the whole screen, one step
   above the dialogs' 100: the session dialog is taller than a laptop's window,
   and on the dialog's own backdrop it scrolled away with it. */
.unsavedask {
  position:fixed; inset:0; z-index:101; pointer-events:auto;
  display:flex; align-items:center; justify-content:center; padding:16px;
  background:rgba(12,16,20,.45);
}
.unsavedask-card {
  width:100%; max-width:400px; padding:16px 18px;
  background:var(--surface); border:1px solid var(--line);
  border-radius:var(--radius-lg); box-shadow:var(--shadow-md);
}
.unsavedask-q { margin:0 0 4px; font-weight:700; font-size:15px; }
.unsavedask .actions { display:flex; align-items:center; gap:8px; flex-wrap:wrap; margin-top:12px; }
.unsavedask .actions .grow { flex:1; }

.modal-box.free {
  position:fixed; margin:0; max-width:none; max-height:none;
  display:flex; flex-direction:column;
}
.modal-box.free .modal-body { flex:1 1 auto; overflow:auto; }

/* The head says it can be moved with the cursor rather than with a grip
   nobody would look for - the same way a panel's head does. */
.modal-head { cursor:grab; user-select:none; }
.modal.dragging .modal-head { cursor:grabbing; }
.modal.dragging .modal-box { transition:none; }
/* Nothing inside must take the drag: a dialog that moves when somebody
   selects the title is a dialog that moves by accident. */
.modal.dragging { user-select:none; }

/* THE CORNER AND THE TWO EDGES (22 October 2026, "its difficult to change
   size of popup"). The corner was 18px at half strength and hidden until the
   dialog had been dragged by its head, so a dialog just opened could not be
   resized at all. It is drawn from the start - pressing it pins the dialog
   where it stands first (ui.js) - as three ridges a hand can find, and the
   right and bottom edges take a drag as well. The box is positioned (above,
   on .modal-box) so they sit on its own edges before it floats. */
.mgrip {
  position:absolute; right:0; bottom:0; width:24px; height:24px; z-index:5;
  appearance:none; border:0; background:none; padding:0; cursor:nwse-resize;
  color:var(--muted); opacity:.75; line-height:1; border-bottom-right-radius:var(--radius-lg);
}
.mgrip::before {
  content:""; position:absolute; right:5px; bottom:5px; width:12px; height:12px;
  background:linear-gradient(135deg, transparent 0 45%, currentColor 45% 52%, transparent 52% 63%,
    currentColor 63% 70%, transparent 70% 81%, currentColor 81% 88%, transparent 88%);
}
.mgrip:hover, .mgrip:focus-visible { opacity:1; color:var(--accent); }
.mgrip-e, .mgrip-s { position:absolute; z-index:4; }
.mgrip-e { top:14px; bottom:24px; right:-4px; width:9px; cursor:ew-resize; }
.mgrip-s { left:14px; right:24px; bottom:-4px; height:9px; cursor:ns-resize; }
/* No room to resize on a phone (ui.js, roomFor), so nothing offers it. */
@media (max-width: 760px) { .mgrip, .mgrip-e, .mgrip-s { display:none; } }
.modal-head {
  display:flex; align-items:center; gap:12px; padding:15px 19px;
  border-bottom:1px solid var(--line);
}
.modal-head h3 { margin:0; font-size:16px; font-weight:650; flex:1; letter-spacing:-.015em; }
/* A close control has to be reachable as well as visible: a 24px glyph with
   4px of padding is a 15px target, which is under half what a finger needs. */
.modal-head .x {
  appearance:none; border:0; background:none; cursor:pointer;
  color:var(--muted); font-size:22px; line-height:1;
  width:32px; height:32px; border-radius:var(--radius-sm); flex:none;
  display:flex; align-items:center; justify-content:center;
}
.modal-head .x:hover { color:var(--ink); background:var(--sunken); }
/* The way back to the window this one was opened from (offerBack in
   billing.js): first in the head, before the mark, where a back arrow is
   looked for. It does not drag the dialog. */
.modal-head .modalback {
  flex:none; cursor:pointer; display:inline-flex; align-items:center; gap:6px;
  min-height:30px; white-space:nowrap;
}
.modal-head .modalback svg { width:15px; height:15px; }
.modal-body { padding:16px 18px 18px; }

/* --------------------------------------------------------------- forms --- */

.form2 { display:grid; grid-template-columns:1fr 1fr; gap:12px; }
/* A GRID ITEM WILL NOT SHRINK BELOW ITS CONTENT unless it is told it may.
   `1fr` is shorthand for `minmax(auto, 1fr)`, and that `auto` minimum is the
   item's MIN-CONTENT - so one wide thing anywhere inside a cell makes the
   whole track that wide, and the track is what the dialog is measured by.

   The Planning dialog had it: a .gridwrap with overflow-x:auto round a
   five-column table. The wrapper scrolls, so the table is not the problem -
   but overflow:auto only zeroes the automatic minimum of a FLEX OR GRID ITEM,
   and this wrapper is an ordinary block two levels down. Its min-content
   reached the grid track through its parent, the track went to 535px, and on
   a 375px phone the entire dialog - every label, every input, the buttons -
   sat 160px off the right edge with nothing to scroll it back.

   Measured before and after on a 375px viewport: 535px, then 333px, with the
   table scrolling inside its own wrapper as it was always meant to. The
   action buttons were checked too, because min-width is exactly what stops a
   button shrinking below its label - they still measure wider than their
   text. */
.form2 > * { min-width:0; }
.form1 { display:flex; flex-direction:column; gap:9px; }
.form2 .span2 { grid-column:1 / -1; }
.form2 label, .form1 label {
  display:block; font-size:12px; font-weight:500; color:var(--muted); margin-bottom:4px;
}
/* A FIELD HAS TO LOOK LIKE A FIELD. These were outlined in --line, which is
   the colour of a table rule - measured against the dialog behind them it is
   1.23 on the dark theme and 1.25 on the light one, where 1.0 is the same
   colour. So the boundary was there and could not be seen, and the fill does
   not help: --ground on --surface is 1.10 and 1.07.

   --faint is 3.80 and 2.94 against the same backgrounds - a line somebody can
   actually find, and still a good deal quieter than the text inside it.
   Reported on the therapist picker, which was the worst of them and is fixed
   below, but the picker was only the faintest of a set that were all too
   faint to be sure of. */
.form2 input, .form2 select, .form1 input, .form1 select, .form1 textarea {
  width:100%; border:1px solid var(--faint); background:var(--ground); color:var(--ink);
  border-radius:var(--radius-sm); padding:8px 10px; font:inherit; font-size:14px;
}
.form2 input:focus, .form2 select:focus, .form1 textarea:focus {
  outline:2px solid var(--accent); outline-offset:-1px;
}
.form1 textarea { resize:vertical; font-family:inherit; }
.form1 label.check {
  display:flex; align-items:center; gap:7px; font-size:13px; color:var(--ink);
}
.form1 label.check input { width:auto; }
.actions { display:flex; gap:8px; align-items:center; margin-top:2px; }

/* --------------------------------------------------------------- toast --- */

.toast {
  position:fixed; left:50%; bottom:22px; transform:translate(-50%, 18px);
  z-index:200; padding:10px 16px; border-radius:var(--radius-sm); font-size:13px;
  background:var(--ink); color:var(--ground); box-shadow:var(--shadow);
  opacity:0; pointer-events:none; transition:opacity .16s, transform .16s;
}
.toast.show { opacity:1; transform:translate(-50%, 0); }
.toast.bad { background:var(--bad); color:#fff; }

/* -------------------------------------------------------------- mobile --- */

@media (max-width: 760px) {
  .toolbar, .filterbar { padding:8px; }
  /* A FLOOR THE ROW CANNOT TAKE, so a search box sharing a line wraps onto
     its own instead of being squeezed. It was `min-width:0`, which let
     Packages' "Find a child…" shrink to 27px beside "Any policy" - a blank
     box, too narrow for its own placeholder, on the filter people reach for
     first. min() rather than a length, so the floor can never be wider than
     the bar and push the page sideways, which is what the 0 was guarding. */
  .search { min-width:min(100%, 11rem); flex:1; }
  /* `.range { width:100%; order:9 }` was here, and both halves of it were
     left over from when the label was a direct child of .toolbar with the
     arrows somewhere else entirely. Once it moved inside .rangenav they
     stopped meaning what they used to and started doing harm:

       order:9    made it the LAST of rangenav's three children, so a phone
                  drew "< > Monday, 21 September" - the arrows bunched
                  together and the date after them, which is the layout the
                  clinic asked to be rid of. A desktop drew it correctly, so
                  it was invisible to anybody not holding a phone.
       width:100% stretched rangenav across the toolbar and cost the bar a
                  whole row for a label 145px wide.

     Nothing replaces it. .rangenav is already inline-flex with flex:none and
     lays out correctly at any width. */
  td.daycell { min-width:150px; }
  /* 180px is a guess at the furniture above the grid, and it is only ever read
     by a grid fitGrids leaves alone - one with something underneath it, like
     Planning's actions row. On a handset held LANDSCAPE the window is about
     370px tall, so the guess reserves half of it and the cap comes out at
     186px: a porthole over a grid of several thousand pixels, which is the
     fault gridRoom() in web/js/ui.js was written to stop. The max() is the
     same floor that function keeps - a window smaller than 220px is not worth
     having - and it changes nothing at any ordinary height, where the calc is
     already several times larger. */
  .gridwrap { max-height:max(220px, calc(100vh - 180px)); }
  .form2 { grid-template-columns:1fr; }
  .modal { padding:14px 10px; }
}

/* A provisional booking: pencilled in, and deliberately allowed to sit on top
   of a real one. It has to look unlike a confirmed session at a glance, or
   staff will read the grid as more certain than it is. */
.badge.tent {
  color:#fff; background:var(--wait); border-color:transparent;
  font-size:10px; padding:0 5px;
}
/* DOTTED, AND IN A COLOUR THAT CAN BE SEEN. It was `border-style:dashed`
   over the chip's ordinary `1px solid var(--line)`, and --line is #242D36 on
   the dark theme - within a few percent of the surface behind it. So the
   boundary was there, correct, and invisible: the dashes were the same colour
   as the background they were drawn on, and the whole point of the rule is
   that a provisional booking must not read as a certain one at a glance.
   Reported as the chips simply not having it, which was the literal truth:
   measured against the surface behind it that edge was 1.23 on the dark theme
   and 1.25 on the light one, where 1.0 is the same colour. It is 7.44 and 3.64
   now. Anything put here has to keep clearing 3, or the rule goes back to
   being decoration.

   --wait is the amber the NOT CONFIRMED badge above already uses, so the edge
   and the badge say the same thing in the same colour rather than being two
   unrelated hints.

   The WIDTH stays at 1px on purpose. Chips are sized by their content, so a
   2px border makes an unconfirmed chip two pixels taller than the confirmed
   one beside it - a row of chips that do not line up, to fix a boundary. */
.sess.tentative {
  border-style:dashed; border-color:var(--tent); background:transparent;
  /* AND A SECOND, THICKER RING INSIDE IT. One pixel of dots is a boundary in
     name only - reported with the chip in hand, "its dotted boundary but its
     not even visible". An outline is drawn without taking room, so it can be
     thick without the chip growing; pulled inside by its offset, so the
     chip's own overflow:hidden is not in its way and it sits just inside the
     edge. The focus ring on the name is its own outline on its own element,
     and is not disturbed.

     THREE PIXELS OF DASHES IN THE CLINIC'S YELLOW, from a picture: "made the
     #FFFF33 color dotted lines thicker so that it can be clearly visible as
     in attached pic". Two pixels of amber dots had been the second answer and
     the picture was the third. --tent is that hex, in app.css. */
  outline:3px dashed var(--tent); outline-offset:-5px;
}
/* THE TWO CANCELLATIONS WEAR THE SAME RING, SOLID, in their own colours -
   asked for with the two chips pictured, the day after the pencilled-in
   ring: "for cancelled chips also there should be a continuous boundary (not
   dotted) on the chip of ED880C color ... for immediate cancelled chips ...
   of F51902 color". The same inset outline, so the chip keeps its height and
   its own overflow:hidden does not clip it; the badge on the chip is the same
   two colours, so the edge and the word agree. */
.sess.st-cancelled { border-color:var(--canc); outline:3px solid var(--canc); outline-offset:-5px; }
.sess.st-immediate { border-color:var(--imm); outline:3px solid var(--imm); outline-offset:-5px; }

/* ----------------------------------------------------------- conflicts --- */

/* Contradictions the spreadsheet allowed: a therapist in two places, an
   individual sharing a therapist with a group. They are not errors in
   TheraGrid, and they cannot be resolved by software - someone has to decide
   which booking is real. So they are loud, and they carry the cell text needed
   to find the row in the sheet. */

.conflictbar {
  margin-top:10px; border:1px solid var(--bad);
  background:var(--bad-bg); border-radius:var(--radius); overflow:hidden;
}
.conflictbar .chead {
  display:flex; align-items:center; gap:9px; width:100%;
  appearance:none; border:0; background:none; cursor:pointer;
  font:inherit; color:var(--bad); text-align:left; padding:10px 13px;
}
.conflictbar .chead .flag { font-size:15px; line-height:1; }
.conflictbar .chead .sub { color:var(--muted); font-weight:400; }
.conflictbar .chead .chev { margin-left:auto; transition:transform .15s; }
.conflictbar[data-open="true"] .chead .chev { transform:rotate(180deg); }

.conflictbar .clist { display:none; list-style:none; margin:0; padding:0 13px 12px; }
.conflictbar[data-open="true"] .clist { display:block; }
.conflictbar .clist li {
  padding:8px 0; border-top:1px solid var(--line); font-size:13px;
  display:flex; flex-wrap:wrap; gap:8px; align-items:baseline;
}
.conflictbar .clist .when {
  font-family:"IBM Plex Mono", ui-monospace, monospace; font-size:11px;
  color:var(--faint); min-width:82px;
}
.conflictbar .clist .why { color:var(--muted); flex:1 1 260px; }
.conflictbar .clist code, .clashchip code {
  font-family:"IBM Plex Mono", ui-monospace, monospace; font-size:11px;
  background:var(--sunken); color:var(--ink); padding:1px 6px; border-radius:var(--radius-xs);
  word-break:break-word;
}

/* The cell it belongs to, so it is findable from the grid and not only a list. */
td.daycell.hasclash { box-shadow:inset 0 0 0 2px var(--bad); }
.clashchip {
  border:1px dashed var(--bad); border-left:8px solid var(--bad);
  background:var(--bad-bg); border-radius:var(--radius-sm); padding:4px 7px; margin-bottom:6px;
  font-size:11.5px; color:var(--bad);
}
.clashchip b { font-size:12.5px; }
.clashchip .why { display:block; color:var(--muted); margin:2px 0 3px; }
/* The way to put a refused-import note away, in the corner of the note. A
   28px square so it is a tap target on the tablet, with the glyph small so
   it does not shout over the note it removes. */
.clashchip { position:relative; padding-right:54px; }
.clashdrop, .clashedit {
  position:absolute; top:0; width:28px; height:28px;
  appearance:none; border:0; background:none; cursor:pointer;
  font:inherit; font-size:15px; line-height:28px; color:var(--bad); opacity:.75;
}
.clashdrop { right:0; }
.clashedit { right:26px; font-size:13px; }
.clashdrop:hover, .clashdrop:focus-visible,
.clashedit:hover, .clashedit:focus-visible { opacity:1; background:var(--surface); border-radius:var(--radius-sm); }
.conflictbar .clist li[data-jump] { cursor:pointer; }
.conflictbar .clist li[data-jump]:hover { background:var(--surface); }

/* ------------------------------------------------------------- planning --- */

.toolbar b { font-size:14px; }
.toolbar .sub { color:var(--muted); font-size:12px; }

/* The weekly grid has no dates, so the columns are wider and calmer than the
   calendar's - there is less in each cell and more reason to read across. */
table.cal.plan td.daycell { min-width:190px; }
table.cal.plan thead th { text-transform:none; }

.openline {
  display:flex; flex-wrap:wrap; gap:4px; margin-top:6px;
}
.openpill {
  /* THE INK IS COMPUTED, NOT WHITE. A therapist's colour comes from the
     spreadsheet and half of them are pale - #00FF00 and #00FFFF among them -
     so white on the colour was white on near-white and the clinic could not
     read the names of the people who were free. Measured on the Calendar and
     the Staff schedule: 1.59, 1.61, 2.01, 2.10, 2.11 against the 4.5 that
     counts as legible.

     kidStyle() in chip.js sets both of these from the same luminance test the
     chips use, so a pill and a chip of one colour can never disagree about
     what shows on it. The fallbacks are what an older caller that still sets
     `background:` inline gets, and are the pre-existing look.

     This was fixed for the Planning page's named pills first and not for the
     three other places that draw one, which is how it survived: the page that
     had been looked at was correct. */
  background:var(--kidcolour, #8C98A4); color:var(--kidink, #fff);
  font-size:9px; font-weight:700; letter-spacing:.04em;
  padding:1px 7px; border-radius:var(--radius-pill); white-space:nowrap;
  /* It is a <button> now - clicking a free therapist narrows the page to her -
     and nothing in this app resets buttons globally, so a default border and
     the browser's own font would otherwise land on every one of them. The
     dashed .openpill.partly border still wins on specificity. */
  border:0; appearance:none; font-family:inherit; line-height:1.55;
  /* A SPAN HOLDING TWO CONTROLS when a reschedule is owed: the name opens her
     diary, as it always has, and RS:3 beside it opens what she owes - a
     button cannot hold another button. inline-flex rather than inline-block
     so the two halves share the pill's height; a cell with nothing owed draws
     one child and looks exactly as it did. */
  display:inline-flex; align-items:stretch; overflow:hidden;
}

/* ---------------------------------------------------- the open-slots view ---
   The same grid with the bookings taken out, so nothing here changes the
   shape of a cell - only what is allowed to sit in one. The pills grow
   because they are no longer a footnote under a stack of chips: they are the
   content, and at 9px a column of first names is a colour swatch with
   something written on it. */
td.daycell.openonly { vertical-align:top; }
td.daycell.openonly .openline { margin-top:0; }
td.daycell.openonly .openpill { font-size:11px; padding:3px 10px; }
/* Nothing free is a fact worth seeing, not a blank. Dimmed so a column that
   is solid reads as solid at arm's length without having to count. */
td.daycell.openonly.full { background:var(--sunken); }
td.daycell.openonly .none { color:var(--faint); font-size:11px; }
/* The "+" sits under the pills rather than in a footer row: there is no note
   button beside it here, so a whole bar for one control is wasted height. */
td.daycell.openonly .addbtn { margin-top:6px; }

/* Filters that only mean something when sessions are on screen. A control
   that cannot change what is drawn is worse than a missing one - somebody
   sets it, sees nothing happen, and stops trusting the rest of the bar. */
/* Two bars now carry `sessonly` controls: the filter bar, and the toolbar
   since the child search moved up into it. The rule was written for one
   and reads as a filter-bar rule; naming both is cheaper than a copy that
   drifts. */
.filterbar.openonly .sessonly,
.toolbar.openonly .sessonly { display:none; }

/* In the toolbar it sits in the gap the date arrows leave, so it takes
   what is spare and gives it back first when the bar is tight - the
   buttons either side are fixed things with fixed words. */
.toolbar .search { flex:0 1 240px; min-width:120px; }

/* ------------------------------------------------------------- packages --- */

table.rows { border-collapse:collapse; width:100%; font-size:13px; }

/* A table too wide for its box scrolls INSIDE its own box.
   Panels are the case: on a phone one is a 359px sheet, and a history table
   wants about 400px once the date, the therapy code, the therapist, the
   session number, the status pill and an Edit button are all on the row. The
   panel body is overflow-y:auto, which makes overflow-x compute to auto as
   well - so the whole body scrolled sideways and took the heading and the
   "12 of 20 done" line off the screen with it. Only the table should move.
   Every full page already gets this from .gridwrap; the panels had nothing. */
.tscroll { overflow-x:auto; -webkit-overflow-scrolling:touch; }
.tscroll > table.rows { min-width:max-content; }
table.rows thead th {
  position:sticky; top:0; z-index:2; text-align:left; white-space:nowrap;
  background:var(--sunken); color:var(--faint); font-weight:600; font-size:11px;
  letter-spacing:.06em; text-transform:uppercase;
  padding:9px 12px; border-bottom:1px solid var(--line);
}
/* The control that orders a column.

   In the heading rather than being the heading: a heading that is secretly a
   button is found by accident or never. It is deliberately quiet until it is
   the one in use - a table with two of these must not look like it has two
   things switched on - and it says which way it is pointing, because an arrow
   with only one state leaves somebody clicking to find out what they have.

   The ::after is an invisible pad: the glyph is 9px and a finger is not.

   11px VERTICALLY, 10px SIDEWAYS, and the sideways figure is the one that was
   wrong. At -8px the pressable box was 27 wide - one pixel under the 28 a
   thumb needs - and tools/mobilecheck.py agreed it was fine because it read
   only the first number of the shorthand, doubled it, and credited 22px to
   both axes. Measured on the Packages header at 375px there is nothing
   pressable above this or to its left, 19px below to a filter box and 599px
   to the next sort arrow, so the sideways pad has room to be generous. */
.sortbtn {
  appearance:none; border:0; background:none; padding:0 2px; margin-left:2px;
  position:relative; cursor:pointer; color:var(--faint);
  font-size:9px; line-height:1; vertical-align:1px;
}
.sortbtn::after { content:""; position:absolute; inset:-11px -10px; }
.sortbtn:hover, .sortbtn:focus-visible { color:var(--ink); }
.sortbtn.on { color:var(--accent); font-size:10px; }

table.rows td { padding:11px 13px; border-bottom:1px solid var(--line-soft); vertical-align:middle; }
table.rows tbody tr { cursor:pointer; }
/* --raised, not --sunken, was the hover colour: #FAFBFC on a #FFFFFF row, a
   difference of five in one channel. It was there in the stylesheet and
   invisible on the screen, so a long table gave no feedback at all about which
   row the pointer was on - which is what that colour is for. */
table.rows tbody tr:hover td { background:var(--sunken); }
/* The last row's rule would otherwise draw a line just inside the card's own
   rounded edge, which reads as a misprint. */
table.rows tbody tr:last-child td { border-bottom:0; }
table.rows .num { text-align:right; font-family:"IBM Plex Mono", ui-monospace, monospace; }
table.rows .mono { font-family:"IBM Plex Mono", ui-monospace, monospace; font-size:12px; color:var(--muted); }
table.rows .dot {
  display:inline-block; width:9px; height:9px; border-radius:50%;
  margin-right:7px; vertical-align:-1px;
}

/* The bar carries the answer at a glance; the numbers are for confirming it. */
.progress { min-width:210px; }
.progress .bar {
  height:6px; border-radius:var(--radius-pill); background:var(--sunken); overflow:hidden; margin-bottom:4px;
}
.progress .bar i { display:block; height:100%; background:var(--accent); }
.progress .bar.near i { background:var(--wait); }
.progress .bar.over i { background:var(--bad); }
.progress .count {
  font-family:"IBM Plex Mono", ui-monospace, monospace; font-size:11px; color:var(--muted);
}
.progress .muted { font-size:11px; color:var(--faint); margin-left:8px; }

.panel.empty { text-align:center; padding:38px 22px; }
.panel.empty .lead { max-width:520px; margin:6px auto 0; }

/* --------------------------------------------------- policy and settings --- */

/* Where a package stands against what was agreed. "Not agreed" is deliberately
   quiet: nothing agreed is not the same as nothing allowed, and colouring it
   like a breach would put every family in the wrong over a rule nobody made. */
.standing {
  font-size:11px; font-weight:600; padding:2px 9px; border-radius:var(--radius-pill);
  background:var(--sunken); color:var(--muted);
}
.standing.within { background:var(--ok-bg); color:var(--ok); }
.standing.spent  { background:var(--wait-bg); color:var(--wait); }
.standing.over   { background:var(--bad-bg); color:var(--bad); }

.hint {
  font-size:12px; color:var(--muted); margin:0 0 7px; font-weight:400;
  max-width:62ch;
}
.checkrow { display:flex; flex-wrap:wrap; gap:6px 16px; }
.checkrow .check {
  display:flex; align-items:center; gap:6px; font-size:13px;
  color:var(--ink); font-weight:400; margin:0;
}
.checkrow .check input { width:auto; }

.form2 textarea {
  width:100%; border:1px solid var(--line); background:var(--ground); color:var(--ink);
  border-radius:var(--radius-sm); padding:8px 10px; font:inherit; font-size:14px;
  font-family:"IBM Plex Mono", ui-monospace, monospace; resize:vertical;
}
.form2 label.check { display:flex; align-items:center; gap:7px; color:var(--ink); }
.form2 label.check input { width:auto; }

/* A closure in the past is history, not something to act on. */
table.rows tr.past td { opacity:.5; }

/* ------------------------------------------------------- hosting usage --- */

/* Two panels side by side on a desk, stacked on a phone. Not a grid of four:
   there are exactly two suppliers and inventing a third column to fill the
   row would make the page look like it was missing something. */
.usagegrid {
  display:grid; gap:14px;
  grid-template-columns:repeat(auto-fit, minmax(min(100%, 380px), 1fr));
  align-items:start;
}

.usagerow { margin:14px 0 18px; }
.usagehead {
  display:flex; align-items:baseline; justify-content:space-between;
  gap:10px; margin-bottom:6px; font-size:13px;
}

/* THE COLOUR CHANGES AT THRESHOLDS, not smoothly. A bar faintly warmer at 60%
   than at 50% tells nobody anything; these are the two moments somebody
   should act - amber to plan, red to hurry.

   The track is always visible, so a bar at under 1% still reads as "a bar
   with almost nothing in it" rather than as a missing control. */
.usagebar {
  height:9px; border-radius:var(--radius-pill); overflow:hidden;
  background:var(--sunken); border:1px solid var(--line);
}
.usagebar i {
  display:block; height:100%; border-radius:var(--radius-pill);
  background:var(--ok); transition:width .3s ease;
}
.usagebar.warn i { background:var(--wait); }
.usagebar.bad i  { background:var(--bad); }

/* A heading inside a panel, under a bar. Smaller than the panel's own h2 and
   further from what came before it than from what follows, because it belongs
   to the thing underneath. */
.usagegrid h3 {
  margin:22px 0 8px; font-size:12px; letter-spacing:.09em;
  text-transform:uppercase; color:var(--faint);
}

/* ----------------------------------------------------------- slot editor --- */

/* The session times are the rows of the calendar, in the order of the clinic's
   day. A textarea could hold them but said none of that. */
.slotlist { display:flex; flex-direction:column; gap:6px; margin-bottom:9px; }
.slotrow { display:flex; flex-wrap:wrap; align-items:center; gap:7px; }
.slotrow .ord {
  width:22px; flex:none; text-align:right; color:var(--faint);
  font-family:"IBM Plex Mono", ui-monospace, monospace; font-size:11px;
}
/* It GROWS now rather than stopping at 150px. The card holding these was the
   full width of the page and is a third of it, so a box fixed at 150 left a
   ragged gap between the time and its three buttons in a narrow column. */
/* A FLOOR, so the box is never squeezed to nothing: a row with more in it
   than its card can hold now wraps (above) rather than taking the width out
   of the time somebody is typing - which is what it did, to a sliver, the
   moment the "what will move" note appeared beside it. */
.slotrow input {
  flex:1 1 90px; min-width:76px;
  font-family:"IBM Plex Mono", ui-monospace, monospace;
}

/* Which site's times are being edited. One list, several sites, and the
   difference between them is invisible in the list itself - nine times look
   the same whether they are this branch's or the centre's. */
.slotwho { margin:0 0 7px; }

/* WHAT IS SITTING IN THE ROW, and what will move if it is changed. These are
   the whole of the move strategy: renaming 4:15 to 4:30 carries a hundred and
   two sessions with it, and the row says so at the moment somebody is typing
   rather than in a summary afterwards.

   The count is quiet and the move is not, because one is background and the
   other is a consequence somebody is about to cause. */
.slotheld { font-size:11px; color:var(--faint); white-space:nowrap; }
/* WHAT WILL MOVE GOES UNDER ITS ROW, ALWAYS (14 October 2026). It sat on the
   row's own line, unwrapped, and wrapped only below a 620px SCREEN - but the
   card is a third of a wide screen, so renaming 10:30 to 10:15 at Sector 87
   put "92 sessions and 5 plan lines move from 10:30 to 10:15" and its
   "choose who moves" button beside the box, squeezed the box to a sliver and
   ran out of the card. Reported with it circled. A line of its own under the
   time, lined up with the box, whatever the width; its words wrap and its
   button stays with them. */
.slotmove {
  flex-basis:100%; margin-left:29px;
  display:flex; flex-wrap:wrap; align-items:center; gap:4px 8px;
  font-size:11px; font-weight:600; color:var(--accent);
  background:var(--accent-bg); border-radius:var(--radius-sm);
  padding:4px 9px; white-space:normal;
}
.slotacts { display:flex; flex-wrap:wrap; gap:7px; align-items:center; }
/* Times drawn in one row: a line per shared row - what the grid calls it, its
   times as chips saying which branch runs each, and the ways to change it. */
.mergelist { display:flex; flex-direction:column; gap:8px; margin-bottom:8px; }
.mergerow {
  border:1px solid var(--line-soft); border-radius:var(--radius-sm);
  background:var(--raised); padding:8px 10px;
}
.mergerow.short { border-color:var(--wait); }
.mergehead { display:flex; align-items:center; gap:8px; margin-bottom:7px; }
.mergehead .mergelabel { font-size:13.5px; font-variant-numeric:tabular-nums; }
.mergehead .mergedrop { margin-left:auto; }
.mergetimes { display:flex; flex-wrap:wrap; align-items:center; gap:6px; }
.mergetime {
  display:inline-flex; align-items:center; gap:6px;
  padding:3px 3px 3px 10px; border-radius:var(--radius-pill);
  border:1px solid var(--line); background:var(--sunken);
}
.mergetime b { font-variant-numeric:tabular-nums; }
.mergetime .sub { font-size:10.5px; }
.mergex {
  border:0; background:none; color:var(--muted); cursor:pointer;
  font-size:15px; line-height:1; padding:0 5px;
}
.mergex:hover { color:var(--bad); }
.mergetimes .mergeadd { width:auto; font-size:12px; padding:4px 8px; min-height:0; }
.mergeshort { color:var(--wait); margin:6px 0 0; }
/* One new time on the "new row under All branches?" question: the time and
   its site, then the rows it could sit in and a row of its own. */
.rowask {
  border:1px solid var(--line-soft); border-radius:var(--radius-sm);
  padding:10px 12px; margin:10px 0;
}
.rowask > p { margin:0 0 6px; }
.rowask .check { display:flex; flex-wrap:wrap; align-items:baseline; gap:2px 8px; margin:5px 0; }
.rowask .sub { color:var(--faint); font-size:12px; }

/* A narrow screen cannot hold a time, three buttons and two notes on one
   line, and wrapping them silently puts the count under the wrong row. */
@media (max-width: 620px) {
  .slotheld { flex-basis:100%; margin-left:29px; }
}
.iconbtn {
  appearance:none; cursor:pointer; flex:none;
  border:1px solid var(--line); background:var(--surface); color:var(--muted);
  border-radius:var(--radius-sm); width:30px; height:34px; font-size:11px; line-height:1;
}
.iconbtn:hover:not(:disabled) { border-color:var(--accent); color:var(--accent); }
.iconbtn:disabled { opacity:.3; cursor:default; }
.iconbtn.danger { font-size:16px; }
.iconbtn.danger:hover { border-color:var(--bad); color:var(--bad); background:var(--bad-bg); }

/* A second scrollbar above a wide grid. The real one is at the bottom of a
   table taller than the window, so reaching it means scrolling past what you
   were trying to look at. */
.topscroll {
  overflow-x:auto; overflow-y:hidden; height:12px; margin-top:10px;
  border:1px solid var(--line); border-bottom:0;
  border-radius:var(--radius) var(--radius) 0 0; background:var(--surface);
}
.topscroll > div { height:1px; }
.topscroll + .gridwrap { margin-top:0; border-top-left-radius:0; border-top-right-radius:0; }

/* ------------------------------------------------------------- messages --- */

/* A limitation stated plainly, rather than a button that quietly cannot work. */
.panel.notice { border-left:5px solid var(--wait); background:var(--wait-bg); }
.panel.notice h2 { color:var(--wait); }
.panel.notice .lead { color:var(--ink); margin-bottom:0; }

.tile.warn .n { color:var(--wait); }

/* A COUNT WITH A MARK ON IT. Five figures in a row, each a different fact
   about the same sixty-four sessions, and as five bare numbers the eye has to
   read all five labels to find the one it wants. The mark is what it finds
   instead - and "have a number" and "need a number" deliberately wear the
   SAME handset in two colours, because they are one fact counted both ways.

   Only where there is an icon: Settings and the package upload draw this same
   tile as a plain figure and are right to, so the row layout is asked for by
   what is inside rather than imposed on every .tile in the app. */
.tile:has(.cardicon) { display:flex; align-items:center; gap:13px; }
.tile .cardicon { flex:none; width:34px; height:34px; border-radius:10px; }
.tile .cardicon .navicon { width:17px; height:17px; }
.tile .tfig { min-width:0; }
/* A tile carrying a mark needs the room the mark takes, or five of them wrap
   to two rows on a laptop - which is what the row was meant to save. */
.cards.wide { grid-template-columns:repeat(auto-fit, minmax(196px, 1fr)); }

/* The date stepper and the filters in one card, which is the mockup's shape
   and also the honest one: both answer "which sessions am I looking at". The
   bar keeps its own class and its id, so foldFilters() goes on folding it and
   goes on putting its Filters button in the .toolbar above - the walk back
   stops at the first .toolbar, which is now this card's own first row. */
.msgbar { padding:13px 15px; }
.msgbar .toolbar { margin:0; }
.msgbar .filterbar { margin-top:11px; }
.msgbar .filterbar.folded { margin-top:0; }
.msgbar .filterbar.folded.open { margin-top:11px; }
/* ONE DATE (20 October 2026): the day's button is the date picker too. The
   date input it opens from lies exactly over it, unseen and out of reach of
   a tap - the button takes every press - and the size of the button, so the
   picker opens under the day and nothing measures it as a tiny control. */
.msgdaypick { position:relative; display:inline-flex; }
.msgdaypick input[type="date"] {
  position:absolute; inset:0; width:100%; height:100%; margin:0; padding:0;
  border:0; opacity:0; pointer-events:none;
}
/* The day on screen, in the middle of its own stepper. */
#msgToday .navicon { width:14px; height:14px; color:var(--faint); }

/* WHAT IS ABOUT TO HAPPEN, AND THE BUTTON THAT DOES IT, on one band: the
   template a run goes as and the decision to send it are read in the same
   second. They were stacked, so the button sat a paragraph below the sentence
   saying how many families it reaches. */
.sendband {
  display:flex; flex-wrap:wrap; align-items:flex-end; gap:12px 18px;
  margin-top:12px;
}
.sendwords { flex:1 1 330px; min-width:0; }
.sendwords .tplpick { margin-top:0; }
/* The name is about twenty characters and the box was taking the whole band.
   Capped so the band reads as a sentence - sent as THIS, and the button that
   sends it - rather than as a form with a field in it. */
.sendwords .tplpick select { flex:0 1 240px; }
.sendgo { display:flex; align-items:center; gap:14px; margin-left:auto; }

.btn.tiny { min-height:0; padding:3px 9px; font-size:12px; }

.msgtext {
  margin:0; padding:13px 15px; white-space:pre-wrap; word-break:break-word;
  background:var(--sunken); border-radius:var(--radius);
  font-family:inherit; font-size:13.5px; line-height:1.55;
}

.panel > .cards { margin-bottom:0; }
.cards + .panel { margin-top:16px; }

/* -------------------------------------------------------------- history --- */

.seenby { display:flex; flex-wrap:wrap; gap:6px; align-items:center; margin-top:14px; }
.seenby .lbl {
  font-size:10px; font-family:"IBM Plex Mono", ui-monospace, monospace;
  letter-spacing:.11em; text-transform:uppercase; color:var(--faint);
}
.seenby .tpill {
  color:#fff; font-size:11px; font-weight:600; padding:2px 10px; border-radius:var(--radius-pill);
}

/* The time it was booked for, with what actually ran underneath it. */
.ran { display:block; font-size:10px; color:var(--faint); }
.notecell { max-width:280px; color:var(--muted); font-size:12px; word-break:break-word; }
table.rows td .badge { margin-left:4px; }

/* ----------------------------------------------------------------- team --- */

.readonly {
  margin:0; padding:8px 10px; background:var(--sunken); border-radius:var(--radius-sm);
  font-size:13px; color:var(--muted);
}
.never { color:var(--faint); }
.muted { color:var(--faint); font-size:12px; }

/* Roles as cards, not a dropdown: what each one means is the decision, and a
   select hides it behind a click. */
.rolepick { display:grid; grid-template-columns:repeat(auto-fit,minmax(200px,1fr)); gap:8px; }
.rolecard {
  display:flex; gap:9px; align-items:flex-start; cursor:pointer;
  border:1px solid var(--line); border-radius:var(--radius); padding:10px 12px;
  background:var(--ground);
}
.rolecard:has(input:checked) { border-color:var(--accent); background:var(--accent-bg); }
.rolecard:has(input:disabled) { opacity:.55; cursor:default; }
.rolecard input { width:auto; margin-top:3px; flex:none; }
.rolecard b { display:block; font-size:13px; }
.rolecard em { display:block; font-style:normal; font-size:11.5px; color:var(--muted); margin-top:2px; }

/* ------------------------------------------------- one person's access ---
   Reported with the dialog open: "it looks very messy and it's hard to
   understand". It was 92 tick boxes in one flat grid. The parts below are
   the index the eye uses - a card per question, a colour and an icon per
   group of permissions - and nothing they show or save is different. */

/* The coloured head belongs to this dialog alone, which is what :has says. */
.modal-box:has(#personForm) .modal-head {
  background:linear-gradient(120deg, var(--accent-bg), transparent 70%);
  border-bottom:1px solid var(--line);
}

.pident {
  display:flex; align-items:center; gap:12px; padding:12px 14px;
  border:1px solid var(--line); border-radius:var(--radius);
  background:linear-gradient(120deg, var(--accent-bg), var(--sunken));
}
.pavatar {
  flex:none; width:40px; height:40px; border-radius:50%;
  background:var(--accent); color:#fff; font-weight:650; font-size:14px;
  display:inline-flex; align-items:center; justify-content:center;
}
.pidwho b { display:block; font-size:15px; letter-spacing:-.01em; }
.pidwho .sub { font-size:11.5px; }
.pidmail { font-size:12px; color:var(--muted); }

.pbox {
  border:1px solid var(--line); border-radius:var(--radius);
  padding:12px 14px; background:var(--surface);
}
.pbox > h4 {
  display:flex; align-items:center; gap:8px; margin:0 0 10px;
  font-size:12px; font-weight:650; text-transform:uppercase; letter-spacing:.06em;
  color:var(--muted);
}
.pbox > h4 .btn.tiny { text-transform:none; letter-spacing:0; }
.pbicon { display:inline-flex; width:16px; height:16px; color:var(--accent); }
.pbicon svg { width:16px; height:16px; }
.pswitchrow { display:flex; align-items:center; gap:12px; }
.pswitchrow b { font-size:13px; }
.pswitchrow .hint { margin:2px 0 0; }

/* The one control here that decides whether the account works at all, drawn
   as the switch it is. The input underneath is unchanged. */
.switch { flex:none; display:inline-block; width:44px; height:25px; position:relative; }
.switch input { position:absolute; opacity:0; width:100%; height:100%; margin:0; cursor:pointer; }
.switch span {
  position:absolute; inset:0; border-radius:999px; background:var(--line);
  transition:background .15s ease; pointer-events:none;
}
.switch span::after {
  content:''; position:absolute; top:3px; left:3px; width:19px; height:19px;
  border-radius:50%; background:#fff; transition:transform .15s ease;
  box-shadow:0 1px 2px rgba(0,0,0,.35);
}
.switch input:checked + span { background:var(--accent); }
.switch input:checked + span::after { transform:translateX(19px); }
.switch input:focus-visible + span { box-shadow:var(--ring); }

.permgrid { display:grid; grid-template-columns:repeat(auto-fit,minmax(215px,1fr)); gap:14px; }
/* A GROUP, IN ITS OWN COLOUR. --a is set per section from AREA_LOOK, so a
   group this app has never heard of still draws - in the fallback grey. */
.permarea {
  /* The group's colour, overridden inline per section from AREA_LOOK. Defined
     here as well as there, so a group drawn without one still has a colour -
     and so the rules below are not dropped whole by an undefined var. */
  --a:var(--muted);
  border:1px solid var(--line); border-radius:var(--radius); overflow:hidden;
  background:var(--surface); align-self:start;
}
.permarea:has(.pabody:not([hidden])) { border-color:color-mix(in srgb, var(--a) 45%, var(--line)); }
.pahead {
  display:flex; align-items:center; gap:8px; width:100%; padding:9px 10px;
  border:0; cursor:pointer; font:inherit; text-align:left;
  color:var(--ink); background:color-mix(in srgb, var(--a) 10%, transparent);
}
.pahead:hover { background:color-mix(in srgb, var(--a) 18%, transparent); }
.pahead:focus-visible { outline:none; box-shadow:var(--ring) inset; }
.paicon { display:inline-flex; width:16px; height:16px; flex:none; color:var(--a); }
.paicon svg { width:16px; height:16px; }
.paname { font-size:12.5px; font-weight:650; }
.pan {
  margin-left:auto; font-size:10.5px; color:var(--muted); font-variant-numeric:tabular-nums;
  background:var(--sunken); border-radius:999px; padding:1px 7px;
}
.pachanged {
  font-size:10.5px; font-weight:650; color:#fff; white-space:nowrap;
  background:var(--a); border-radius:999px; padding:1px 7px;
}
.pahead .chev { font-size:10px; color:var(--muted); transition:transform .15s ease; }
.pahead[aria-expanded="true"] .chev { transform:rotate(180deg); }
.pabody { padding:4px 10px 10px; border-top:1px solid var(--line); }

.permarea h4 {
  margin:0 0 6px; font-size:10px; letter-spacing:.11em; text-transform:uppercase;
  color:var(--faint); font-family:"IBM Plex Mono", ui-monospace, monospace; font-weight:600;
}
.check.perm {
  display:flex; align-items:center; gap:7px; font-size:12.5px; color:var(--ink);
  font-weight:400; margin:0 0 5px; cursor:pointer;
}
.check.perm input { width:auto; flex:none; }
.check.perm .ovr {
  font-style:normal; font-size:9px; letter-spacing:.06em; text-transform:uppercase;
  color:var(--wait); background:var(--wait-bg); padding:1px 5px; border-radius:var(--radius-xs);
}

.steps { margin:12px 0 0; padding-left:22px; font-size:13.5px; line-height:1.7; }
.steps li { margin-bottom:7px; }

/* The filter bar folds at every width, not only on a phone.
   Nine controls above a grid whose whole job is to be looked at is a lot of
   screen on a laptop too, and having it fold on one width and not the other
   meant two mechanisms - which is what happened, and they both appeared. */
.foldbtn {
  display:inline-flex; align-items:center; gap:6px;
  appearance:none; cursor:pointer; font:inherit; font-size:12.5px;
  margin-top:9px; padding:7px 11px;
  border:1px solid var(--line); border-radius:var(--radius);
  background:var(--surface); color:var(--muted); text-align:left;
}
.foldbtn:hover { border-color:var(--accent); color:var(--ink); }
.foldbtn::after { content:" ▾"; color:var(--faint); }
.foldbtn[aria-expanded="true"]::after { content:" ▴"; }
/* BY CLASS, not by `.foldbtn + .filterbar`. That selector needed the button
   to be the bar's immediate sibling, and the button now sits inside the
   toolbar above it - on its own line it cost a whole row to say one word.
   The guarantee is unchanged: `folded` is added by the same code that creates
   the button, so a filter bar that never gets one is never hidden. */
.filterbar.folded { display:none; }
.filterbar.folded.open { display:flex; margin-top:9px; }
/* AND NO BUTTON FOR A BAR THAT HAS GONE UP WHOLE (25 October 2026). When
   toolbarFilters() fits every filter into the row above, it hides the bar it
   emptied; "Filters" would then open nothing - present, the right size and
   dead, the blank button icons.js's note describes. Packages, Approvals and
   the Change log at a wide window. */
.toolbar:has(~ .filterbar.folded[hidden]) > .foldbtn { display:none; }

/* In a toolbar it is one control among others, so it loses the margin that
   separated it from the bar above and sits at the same height as a button.
   The label grows with what is filtered ("Filters - Occupational Therapy,
   5:50"), and a long one would push + Booking off the row it was just given,
   so it is capped and ellipsised rather than allowed to wrap the bar. */
.toolbar .foldbtn {
  margin-top:0; padding:7px 11px; max-width:26ch;
  white-space:nowrap; overflow:hidden; text-overflow:ellipsis;
}

/* On a phone it takes the full width, because a row with one short button on
   it wastes the line it sits on.

   The rules above are why the bar is only ever hidden once a button exists to
   bring it back: foldFilters runs at page mount, so a filter bar drawn LATER -
   the History tab on Messages, for one - never gets a button, and hiding it
   unconditionally would leave its search and filters on screen-reader duty
   with no way to open them. A missing enhancement must not take the content
   with it. */
@media (max-width: 760px) {
  .foldbtn { display:flex; width:100%; padding:9px 12px; }
}

/* ------------------------------------ the toolbar's second half, on a phone -

   The Calendar's toolbar has eleven controls on it. On a desktop they are one
   row and nobody thinks about them. On a 394px handset they wrapped to six,
   and with the filter bar and the conflict and decision bars underneath, the
   page put about 450px above a 780px viewport - more than half the screen -
   before the first session appeared.

   `display:contents` is what makes this free above 760px: the wrapper stops
   generating a box at all, so its buttons are laid out by .toolbar exactly as
   they were when they were its own children. Nothing about the desktop bar
   moves, which is why this is a media query on one side only.

   Below it they fold, and `order` puts the opened row and its button at the
   END of the bar rather than in the middle of it - the wrapper sits where the
   buttons used to, which is before + Booking, and a row of secondary controls
   opening ABOVE the primary one reads as the bar having rearranged itself. */
.tbmore { display:contents; }
.tbmorebtn { display:none; }

/* MEASURED, not guessed. This used to be a 760px media query, on the reading
   that a desktop has room for eleven controls. Reception works at 1330px,
   where the bar wants 1,454px: it wrapped, and spent two extra rows on the
   refresh button and + Booking - two rows of the grid they read all day.
   fitToolbars() in ui.js adds `tight` when the row has actually run out of
   room, which also covers the controls that only exist for some permissions
   and the widths a browser's own date input happens to render at. */
.toolbar.tight .tbmorebtn { display:inline-flex; order:8; }
/* A COUNT FOLDED INTO MORE IS SAID ON IT (sayMoreCount in ui.js): the RS
   number, in RS's own blue, on the button that now holds RS - or More hides
   the one figure reception books against. Inline, not the corner dot RS
   itself wears, because the button's words sit where that dot would go. */
.tbmorebtn { align-items:center; gap:6px; }
.tbmorebtn .morecount {
  min-width:18px; height:18px; padding:0 5px; border-radius:9px;
  background:var(--accent); color:#fff;
  font-size:10.5px; font-weight:700; line-height:18px; text-align:center;
}
.tbmorebtn .morecount[data-kind="rs"] { background:var(--rs); }
.toolbar.tight .tbmorebtn::after { content:" \25BE"; color:var(--faint); }
.toolbar.tight .tbmorebtn[aria-expanded="true"]::after { content:" \25B4"; }
.toolbar.tight .tbmore {
  display:none; order:9; flex-basis:100%; gap:6px; flex-wrap:wrap;
}
.toolbar.tight .tbmore.open { display:flex; }

/* --------------------------------------------- one row, on a laptop ---
   (25 October 2026) fitRows() in main.js, while its switch on Settings,
   General is on: the page's Change history block sits IN the bar - its
   button in the row, its list once opened on a line of its own under it and
   under an open More - and a sentence the row had no room for is shed. Only
   while body.fitrow, so off nothing here applies; and the classes are only
   ever added by that code. */
body.fitrow #view .toolbar > .chgblock.inbar { display:contents; }
body.fitrow #view .toolbar > .chgblock.inbar > .chgbar .sub { display:none; }
/* Its symbol alone, as the Calendar's other secondary buttons are: the words
   were 141px of a 1280px laptop's row and the More button 88px short of it.
   They stay its accessible name, and its title says them on hover. */
body.fitrow #view .toolbar > .chgblock.inbar > .chgbar .btn {
  font-size:0; gap:0; padding-left:9px; padding-right:9px;
}
body.fitrow #view .toolbar > .chgblock.inbar > .chgbody {
  flex-basis:100%; order:10; margin-top:0;
}
body.fitrow #view .toolbar .rowshed { display:none; }
/* IN A PAGE'S BAND (Services & pricing, Policies) the row is the band's right
   half; while its history is open it takes the band's whole width, so the
   list is not read in a column a third of the screen wide. */
body.fitrow #view > .pagetop > .toolbar:has(> .chgblock.inbar > .chgbody:not([hidden])) {
  flex-basis:100%;
}


/* ------------------------------------------------ one row, on a phone ---
   (25 October 2026) A bar marked data-onerow keeps only its .keep items and
   a "..." button (main.js, oneRowBars); "..." (.rowopen) puts back the bar as
   it was. Only while body.onerow - the switch on Settings, General - and only
   inside the phone query, so off, or on a laptop or the tablet, nothing here
   applies. The page's Change history bar, while shut, folds with it and comes
   back with "...". The "..." wears a dot while what it hides is narrowing the
   page - a filter, or a child typed into a search box - and for nothing
   else: the conflicts flag and the reschedule count are on the clinic's
   Calendar all day (36 and 99+ the day this was tried on the phone), and a
   dot that never goes out says nothing. */
/* #view, because the button is also .btn.icon, whose inline-flex outranks a
   single class - and on 25 October 2026 a laptop's Calendar wore a "..." that
   did nothing, the first hour this was live. */
#view .onerowbtn { display:none; }
@media (max-width: 760px) {
  body.onerow #view > .toolbar[data-onerow] > .onerowbtn {
    display:inline-flex; order:20; margin-left:auto; position:relative;
  }
  body.onerow #view > .toolbar[data-onerow]:not(.rowopen) > :not(.keep):not(.onerowbtn) {
    display:none;
  }
  body.onerow #view > .toolbar[data-onerow]:not(.rowopen) { flex-wrap:nowrap; }
  body.onerow #view > .toolbar[data-onerow]:not(.rowopen) > .rangenav {
    flex:1 1 0; min-width:0;
  }
  body.onerow #view > .toolbar[data-onerow]:not(.rowopen) > .rangenav .range {
    min-width:0; overflow:hidden; text-overflow:ellipsis; white-space:nowrap;
  }
  body.onerow #view > .toolbar[data-onerow]:not(.rowopen) ~ .chgblock.pagechg:has(> .chgbody[hidden]) {
    display:none;
  }
  body.onerow #view > .toolbar[data-onerow]:not(.rowopen):has(
    > .foldbtn.narrowed, > input.search:not(:placeholder-shown)) > .onerowbtn::after {
    content:""; position:absolute; top:3px; right:3px; width:8px; height:8px;
    border-radius:50%; background:var(--wait);
  }
}

@media (max-width: 760px) {
  .tbmorebtn { display:inline-flex; order:8; }
  /* The literal characters, exactly as .foldbtn above writes them. A CSS hex
     escape did not survive being written by a script: the backslash went
     through a shell heredoc, one was eaten, and Python read what was left as
     an OCTAL escape - so the button said "More ␣BE" on a phone. */
  .tbmorebtn::after { content:" ▾"; color:var(--faint); }
  .tbmorebtn[aria-expanded="true"]::after { content:" ▴"; }
  .tbmore { display:none; order:9; flex-basis:100%; gap:6px; flex-wrap:wrap; }
  .tbmore.open { display:flex; }
}

/* --------------------------------------------------- chips, fuller form --- */

/* The outcome is a word, right-aligned, because it is what the eye goes to
   after the name. A tick meaning "attended" is indistinguishable at a glance
   from a tick meaning "confirmed", and staff should not have to learn which. */
/* ATTENDED, CANCELLED, NOT CONFIRMED. A tag like the rest, and sized with
   them - it is the first thing anybody looks for on a marked session. */
.outcome {
  font-size:11.5px; font-weight:700; letter-spacing:.06em; white-space:nowrap;
  padding:2px 7px; border-radius:var(--radius-xs); background:var(--sunken); color:var(--muted);
}
/* THE TWO CANCELLATIONS ARE DIFFERENT COLOURS BECAUSE THEY ARE DIFFERENT
   THINGS, and the clinic already had the colours: the Dashboard has drawn
   "Cancellations" amber and "Immediate cancellations" red since it was
   written. The chip disagreed with its own dashboard - CANCELLED was red,
   which made an ordinary call-off look like the serious one, and IMMEDIATE
   was the only tag on the whole chip painted as a solid block, which made it
   look like a different KIND of thing rather than a worse version of the one
   above it. Reception noticed, which is the test that matters: they are the
   people who have to tell the two apart at a glance all day.

   Amber for cancelled, red for immediate - as a tint behind their own colour
   at first, like every other tag here; then SOLID, in two hexes the clinic
   chose with the chips pictured: "cancelled should be also written in ED880C
   color", "IMMEDIATE C1 should be also written in F51902 color". White on
   both, which is what the pictures show and what reads on either. The ring
   round the chip (below, with .sess.tentative) wears the same two. */
.outcome.attended  { background:var(--ok-bg); color:var(--ok); }
.outcome.cancelled { background:var(--canc); color:#fff; }
.outcome.immediate { background:var(--imm); color:#fff; }
.outcome.scheduled { background:var(--accent-bg); color:var(--accent); }
/* The slot panel's three answers about a child the plan expects. NOT BOOKED
   was the default grey - the quietest thing on a block full of badges, for
   the one of the three that is a job to do. Amber is what this app already
   uses for "somebody has to look at this". ELSEWHERE TODAY is deliberately
   not amber: it is the answer that means nothing is wrong. */
.outcome.notbooked { background:var(--wait-bg); color:var(--wait); }
.outcome.moved     { background:var(--ind-bg);  color:var(--ind); }

/* The therapy code is on every single chip, which is precisely why it cannot
   be a solid block of colour: something that never varies carries no
   information by being loud, it only takes attention from the things that do. */
.badge.code {
  background:var(--ind-bg); color:var(--ind); border-color:transparent;
  font-family:"IBM Plex Mono", ui-monospace, monospace;
}

/* 14px rather than 10px, because the hit areas below grow 6px either side
   and two targets that touch hand the tap to whichever came later in the
   markup - which is the bug this is meant to fix, not cause. 14 - 6 - 6 = 2px
   of clearance. */
/* What is left of this row: only what a caller adds to it, which today is the
   Staff schedule's two support buttons. History, Edit and "+" moved up to the
   top line - see .chipicon below. */
.sess-acts { display:flex; align-items:center; gap:14px; margin-top:4px; }
/* Two of these sit under every chip, so on a full week there are eight
   hundred underlines on screen. The underline stays - it is what says these
   are things you can press - but drawn in the line colour rather than the
   text colour, so it is there when looked for and gone when not. */
/* GROW THE TARGET, NOT THE INK.
   A usability audit measured History and Edit at about 18x14px and called
   them "nearly impossible to use on touch devices without mis-clicking",
   which was right, and on a mouse too. The obvious fix - padding - would
   make every chip taller, and there are forty chips on a screen: the density
   is what lets reception read a week at a glance and is not ours to spend.
   So the pressable area is an invisible ::after, and the writing stays
   exactly the size it was. */
/* History and Edit, a quarter bigger. They were 10.5px, which is small for
   something pressed all day - and the transparent hit area below still does
   the work of making them a thumb-sized target without moving the text. */
.linkbtn {
  appearance:none; border:0; background:none; padding:0; cursor:pointer;
  position:relative;
  font:inherit; font-size:13px; color:var(--muted); text-decoration:underline;
  text-decoration-color:var(--line); text-underline-offset:2px;
}
.linkbtn::after { content:""; position:absolute; inset:-10px -6px; }
.linkbtn:hover { color:var(--accent); text-decoration-color:currentColor; }
.sess-top .grow { flex:1; }

/* HISTORY AND EDIT, on the top line, as a clock and a pencil.

   Sized to sit in the line the name is on without changing its height: 22px
   of button around a 15px glyph, against a name that is 12.5px on a 1.55
   line. Anything taller and every chip on the page grows by the difference,
   which is the cost the whole change was made to avoid.

   The pressable area is an invisible ::after taking them to 34x34, the same
   trick the underlined words used and for the same reason - the audit that
   measured those at 18x14 and called them "nearly impossible to use on touch
   devices" was right, and a glyph on its own would be smaller still.

   Muted rather than coloured. There are two on every chip and forty chips on
   a screen; eighty accent-coloured icons would read as eighty things asking
   to be pressed. They come up to full strength on hover and focus. */
.chipicon {
  appearance:none; border:0; background:none; padding:0; cursor:pointer;
  flex:none; position:relative;
  width:22px; height:22px;
  display:inline-flex; align-items:center; justify-content:center;
  color:var(--muted); border-radius:var(--radius-sm);
}
.chipicon svg { width:15px; height:15px; display:block; }
.chipicon::after { content:""; position:absolute; inset:-6px; }
.chipicon:hover { color:var(--accent); background:var(--accent-bg); }
.chipicon:focus-visible { outline:2px solid var(--accent); outline-offset:1px; }

/* WHAT THE THREE DOTS OPEN. Fixed to the viewport rather than to the chip:
   a chip lives inside .gridwrap, which scrolls and clips, and the chips whose
   menu most needs opening are the ones nearest the edge. Positioned in JS,
   which is the only place the room either side is known. */
.chipmenu-pop {
  /* It settles in (motion, at the foot of this file) - opacity and nothing
     else, because it measures itself the moment it is put on the page. */
  animation:tgpop .08s ease-out both;
  position:fixed; z-index:80; min-width:212px; max-width:92vw;
  padding:5px; border-radius:var(--radius);
  border:1px solid var(--line); background:var(--surface); color:var(--ink);
  box-shadow:var(--shadow-md);
}
.chipmenu-pop button {
  appearance:none; width:100%; border:0; background:none; cursor:pointer;
  font:inherit; color:inherit; text-align:left;
  display:flex; align-items:flex-start; gap:10px;
  /* 44px of height. This is the one place the three actions are reachable
     now, so it is not the place to save a pixel. */
  padding:9px 10px; border-radius:var(--radius-sm);
}
.chipmenu-pop button:hover,
.chipmenu-pop button:focus-visible {
  background:var(--accent-bg); color:var(--accent); outline:none;
}
.chipmenu-pop .mi { flex:none; display:flex; margin-top:1px; color:var(--muted); }
.chipmenu-pop .mi svg { width:16px; height:16px; display:block; }
.chipmenu-pop button:hover .mi,
.chipmenu-pop button:focus-visible .mi { color:inherit; }
.chipmenu-pop .mt { display:flex; flex-direction:column; gap:1px; min-width:0; }
.chipmenu-pop .mt b { font-weight:600; font-size:13px; line-height:1.25; }
/* The line that answers "what does this one do". It is the whole reason the
   menu replaced three symbols, so it keeps its contrast rather than fading
   to --muted the way a sub-line usually would. */
.chipmenu-pop .mt .sub { font-size:11.5px; line-height:1.3; opacity:.75; }

/* The "+" sits with them now rather than at the end of a row of its own, so
   the automatic left margin that used to push it there would push the two
   icons away from it instead. The .grow before them does that job. */
.sess-top .chipadd { margin-left:0; }

/* Booking another session for this child.
   It was a 10.5px underlined "+" between two words, which is a link-sized
   target for the action staff reach for most often on a chip - and the hardest
   thing on the page to hit on a phone. Now it is a round button with a real
   target, pushed to the end of the row so it is never mistaken for a third
   link.

   26px is a compromise with the change made alongside it. A real button makes
   the action row taller than the two links it sits with, and that height is
   paid once per chip on a page whose other complaint was that sessions were
   pushed off the screen. 26 is still above the 24px floor below which a finger
   starts missing, and costs ten pixels a chip rather than fifteen. */
.chipadd {
  appearance:none; cursor:pointer; font:inherit; line-height:1;
  margin-left:auto; flex:none;
  /* 30, not 26. The audit called 26x26 "significantly below standard
     accessibility guidelines", and it is: the pressable area below takes it
     to 40x40 without the button itself growing enough to change the chip. */
  min-width:30px; height:30px; padding:0; position:relative;
  display:inline-flex; align-items:center; justify-content:center;
  font-size:17px; font-weight:600;
  color:var(--accent); background:var(--accent-bg);
  border:1px solid transparent; border-radius:var(--radius-sm);
}
/* 40x40 of pressable area around a 30px button. */
.chipadd::after { content:""; position:absolute; inset:-5px; }
.chipadd:hover { border-color:var(--accent); }
.chipadd:active { transform:translateY(1px); }
.chipadd:focus-visible { outline:2px solid var(--accent); outline-offset:1px; }

/* Nothing uses this any more. It belonged to a second Filters button on the
   Calendar that duplicated foldFilters, and it carried a bug worth
   remembering: display on a class beats the hidden attribute's own
   display:none, so "hidden" never hid it and the button read "Filters0" at
   all times. An element toggled with .hidden needs [hidden]{display:none}
   somewhere with enough weight to win. */

/* ---------------------------------------------------------- month view --- */

.monthhead {
  display:grid; grid-template-columns:repeat(7,1fr); gap:5px; padding:10px 10px 0;
  font-size:10px; font-family:"IBM Plex Mono", ui-monospace, monospace;
  letter-spacing:.09em; text-transform:uppercase; color:var(--faint);
}
.monthgrid { display:grid; grid-template-columns:repeat(7,1fr); gap:5px; padding:6px 10px 10px; }
.mday {
  appearance:none; cursor:pointer; text-align:left; font:inherit;
  background:var(--raised); border:1px solid var(--line); border-radius:var(--radius-sm);
  padding:8px 9px; min-height:80px; color:var(--ink);
  display:flex; flex-direction:column; gap:1px;
}
.mday:hover:not(:disabled) { border-color:var(--accent); }
.mday:disabled { cursor:default; }
.mday.blank { background:none; border:0; min-height:0; }
.mday.off { background:none; border-style:dashed; color:var(--faint); }
.mday.shut { background:var(--bad-bg); border-color:var(--bad); }
.mday.today { border-color:var(--accent); box-shadow:inset 0 0 0 1px var(--accent); }
.mday .n { font-family:"IBM Plex Mono", ui-monospace, monospace; font-weight:600; font-size:14px; }
.mday .s { font-size:10.5px; color:var(--muted); }
.mday .s.bad { color:var(--bad); }
.mday .s.dim { color:var(--faint); }

/* --------------------------------------------------------- availability --- */

table.rows.avail th { font-size:10px; }
.availcell { vertical-align:top; }
.freeslot {
  display:inline-block; margin:0 3px 3px 0; padding:1px 6px; border-radius:var(--radius-xs);
  background:var(--ok-bg); color:var(--ok); font-size:10px;
  font-family:"IBM Plex Mono", ui-monospace, monospace;
}
.availcell .none { color:var(--faint); font-size:11px; }

/* ------------------------------------------------------------ side panel --- */

/* ------------------------------------------ the full-screen button ---
   Bottom right, like a video player's, on the two pages that are a grid.

   STACKED ABOVE THE WHATSAPP BUTTON rather than beside it. That corner was
   already taken - 52px of green at right:18px - and two circles side by side
   in the corner of a calendar read as a pair of things that belong together,
   which these do not. One above the other reads as a column of page tools,
   and it keeps the WhatsApp button where people have already learnt it is.

   Both offsets are the FAB's own geometry written out. Vertically, 18 bottom
   + 52 tall + 12 clear. Horizontally it is NOT 18: these are different widths
   - 52 and 40 - so matching the right edge leaves their centres 6px apart,
   which reads as one of them being crooked rather than as a column. 18 + 26
   is the FAB's centre; less half of this one is 24.

   If that button ever moves, this follows it or they collide. */
.btn.icon.fsbtn {
  position:fixed; right:24px; bottom:82px; z-index:80;
  width:40px; height:40px; padding:0; border-radius:50%;
  background:var(--surface); border:1px solid var(--line); color:var(--muted);
  box-shadow:var(--shadow-md);
  display:flex; align-items:center; justify-content:center;
  transition:transform .15s, opacity .15s;
}
.btn.icon.fsbtn:hover { color:var(--ink); border-color:var(--muted); }
.btn.icon.fsbtn .navicon { width:18px; height:18px; }

/* In full screen the furniture is gone and this is the only way back other
   than Escape, so it stops being quiet about it.

   INVERTED, NOT ACCENT. The accent is a teal green and this button sits 12px
   above a WhatsApp green circle - two greens in a column, at the exact moment
   the button matters most. The stylesheet two rules below says why that is
   wrong: green means WhatsApp here and nothing else, so it is found without
   being read. Ink on surface says "on" as loudly and belongs to nobody. */
.btn.icon.fsbtn[aria-pressed="true"] {
  background:var(--ink); border-color:var(--ink); color:var(--surface);
}

/* EVERY RULE HERE IS .btn.icon.fsbtn, not .fsbtn. The button wears `btn icon`
   for its focus ring and its sizing, and `.btn.icon { display:inline-flex }`
   sits further down this file - two classes beating one, later in the sheet,
   so a plain `.fsbtn { display:none }` lost silently and the button stayed on
   screen at 375px with the media query matching. Nothing reports that; the
   rule is simply outranked. The glyph rule needed it too: `.btn.icon .navicon`
   sizes it 15px, and a corner bracket that small stops reading as corners. */

/* A grid on a phone is scrolled, not surveyed, and that corner already holds
   the WhatsApp button above a fixed navigation bar. Safari on iPhone cannot
   do this at all - the button is not even drawn there - so hiding it under
   760px costs the phones nothing and keeps tablets, which can, working. */
@media (max-width: 760px) { .btn.icon.fsbtn { display:none; } }

/* ------------------------------------------- the floating WhatsApp button ---
   Bottom right, above everything except the panel it opens. It is the only
   green in the app on purpose: green means WhatsApp here and nothing else, so
   it is found without being read. Hard-coded rather than tokenised for the
   same reason the therapist colours are - it is somebody else's colour, not
   part of this theme, and it must not drift when the theme does. */
/* THE INSET IS ON THE BASE RULE, not only in the phone block below it.
   That block is @media (max-width: 760px) and a tablet is 800px, so it gets
   none of it - measured on a Lenovo Tab M11: the gesture bar is 60px and this
   button sat 18px off the bottom, which put 42px of a 52px button behind the
   bar. Ten pixels of it were reachable.
   On a phone the rule further down still wins and lifts it clear of the page
   bar as well; on a desktop the inset is 0 and this is the 18px it always
   was. */
.wafab {
  position:fixed; right:18px; bottom:calc(18px + env(safe-area-inset-bottom));
  z-index:80;
  width:52px; height:52px; border-radius:50%; border:0; cursor:pointer;
  background:#25D366; color:#fff;
  display:flex; align-items:center; justify-content:center;
  box-shadow:0 3px 14px rgba(0,0,0,.28);
  transition:transform .15s, box-shadow .15s, opacity .15s;
}
.wafab:hover { transform:translateY(-2px); box-shadow:0 6px 20px rgba(0,0,0,.34); }
.wafab:active { transform:translateY(0); }
.wafab .navicon { width:26px; height:26px; stroke-width:1.6; }

/* The count, on the button rather than only in the navigation bar. Red on
   green because it has to be seen AGAINST the button, and red is what an
   unread count is everywhere else in here - see .wadot, which this matches
   deliberately rather than inventing a second language for one number. */
.wafab .fabdot {
  position:absolute; top:-2px; right:-2px;
  display:inline-flex; align-items:center; justify-content:center;
  min-width:20px; height:20px; padding:0 5px; border-radius:var(--radius);
  background:var(--bad); color:#fff;
  font-size:11px; font-weight:700; line-height:1;
  box-shadow:0 0 0 2px var(--ground);
}

/* Something is waiting. Two slow pulses of the shadow, not a jiggle: this can
   sit on screen for an hour while somebody is on the telephone, and anything
   faster becomes the thing you stop seeing. */
@keyframes wafab-wait {
  0%, 100% { box-shadow:0 3px 14px rgba(0,0,0,.28), 0 0 0 0 rgba(37,211,102,.55); }
  50%      { box-shadow:0 3px 14px rgba(0,0,0,.28), 0 0 0 9px rgba(37,211,102,0); }
}
.wafab.waiting { animation:wafab-wait 2.4s ease-out infinite; }
@media (prefers-reduced-motion: reduce) { .wafab.waiting { animation:none; } }

/* ------------------------------------- something changed while you read ---
   Pages used to re-mount themselves when anybody anywhere changed a session:
   show() blanks the view to a spinner and rebuilds, so somebody reading
   Reschedules while the clinic worked had the page taken away and handed back
   every eight seconds. This is what replaced that.

   ALONG THE BOTTOM, not the top, and that was not the first answer. Top
   centre put it squarely on the page's own toolbar - on Reschedules it sat on
   the "Owed now" tab and made it unpressable, which is a worse thing to do to
   somebody than the refresh it replaced. There is no gutter to float in: the
   grid is stretched to the window.

   The bottom is the one place where what it covers can be moved out from
   under it, because the grid scrolls and a toolbar does not. It clears the
   WhatsApp button by being centred and capped, and on a phone it sits above
   the page bar, where the thumb already is.

   It does not animate in. A thing that slides into the corner of the eye is
   the same interruption the re-mount was, done more politely. */
.livenew {
  position:fixed; bottom:calc(18px + env(safe-area-inset-bottom)); left:50%;
  transform:translateX(-50%); z-index:70;
  display:inline-flex; align-items:center; gap:10px;
  max-width:min(92vw, 460px);
  padding:7px 8px 7px 14px; border-radius:var(--radius-pill);
  border:1px solid var(--line); background:var(--surface); color:var(--ink);
  box-shadow:var(--shadow-md); cursor:pointer;
  font:inherit; font-size:13px; text-align:left;
}
.livenew span { white-space:nowrap; overflow:hidden; text-overflow:ellipsis; }
.livenew b {
  flex:none; background:var(--accent); color:var(--accent-ink);
  border-radius:var(--radius-pill); padding:4px 12px; font-size:12.5px;
  font-weight:650;
}
.livenew:hover { border-color:var(--faint); }
.livenew:hover b { background:var(--accent-hover); }
.livenew:focus-visible { outline:2px solid var(--accent); outline-offset:2px; }

/* (The Packages grid's Save bar at the foot sat here until 22 October 2026;
   the package window's Save is the only one now.) On a phone the news chip
   sits above the page bar: the top of a phone screen is the one place a
   finger reaches least, and the bar along the bottom is where the thumb
   already is. */
@media (max-width: 760px) {
  .livenew {
    bottom:calc(64px + env(safe-area-inset-bottom));
    max-width:calc(100vw - 24px);
  }
}

/* ---------------------------------------------- tucking them out of the way ---
   Out of the way while somebody reads downwards; back the moment they scroll
   up. Mostly off the edge rather than gone - a button that vanishes reads as
   broken, and the unread count is the point of the WhatsApp one.

   NOT ONLY ON A PHONE, which is where it started. A desktop grid is stretched
   to the window now, so these two sit over the bottom-right corner of it -
   which on the calendar is a session chip with the "+" that books one. Same
   hazard, same answer. watchTuck() in main.js listens to the grid rather than
   the window there, because the window no longer scrolls.

   30px RATHER THAN A PERCENTAGE. It was translateX(58%) when only the
   WhatsApp button did this, and 58% of 52px is 30px - but these two buttons
   are 52 and 40 wide and the whole point of the layout above is that their
   CENTRES line up. A percentage would slide them by different amounts and
   break the column at exactly the moment both are moving. */
.wafab.tucked, .btn.icon.fsbtn.tucked { transform:translateX(30px); opacity:.55; }

/* Reaching for one brings it back before the pointer gets there, so a tucked
   button is never something you have to chase. */
.wafab.tucked:hover, .btn.icon.fsbtn.tucked:hover { transform:none; opacity:1; }

/* NEVER THE WAY OUT OF FULL SCREEN. With the furniture gone this button and
   the Escape key are the only ways back, and half of a faded circle in the
   corner is not something somebody finds while looking for the exit. Last of
   these rules on purpose: it has to beat the hover above it as well. */
.btn.icon.fsbtn[aria-pressed="true"].tucked { transform:none; opacity:1; }

/* Out of the way of the panel IT opened, which is not the same as any panel.
   On a phone that panel is the whole screen, so the button would otherwise
   float on top of the conversation it was pressed to reach; either way it has
   nothing left to do while the inbox is open.

   `.tone-wa` IS THE INBOX AND ONLY THE INBOX - the note on `tone` in ui.js
   says the green head is opt-in and that this is the one panel that asks for
   it. This used to read `.sidepanel`, which was a fair reading of "the panel
   it opened" while only one panel could be open at a time: a panel being up
   meant there could not be a second one anyway. Pinning made that false and
   the button is the ONLY way into the inbox, so opening the slot planner
   first left nothing to press. Reported the hour pinning shipped.

   The other half of the old comment - that it overlaps a right-hand panel's
   corner - is still true and is answered by duckFloaters() in ui.js, which
   steps the button left of whatever is sitting on it. */
body:has(.sidepanel.tone-wa:not(.minimized)) .wafab { display:none; }
/* ON THE REPLIES TAB THE INBOX IS ALREADY THE PAGE, so the button that opens
   it is the one place it is not drawn (13 October 2026: every other page has
   it). */
body:has(#view .inbox) .wafab { display:none; }
/* MINIMIZED: kept with everything in it, out of sight until the button
   brings it back. */
.sidepanel.minimized, .minichat.minimized { display:none !important; }
/* The button says a chat is waiting: a small bar, a window put away. */
.wafab .fabmin {
  position:absolute; left:50%; bottom:7px; transform:translateX(-50%);
  width:16px; height:3px; border-radius:2px; background:#fff;
}

/* Panels sit beside the grid rather than over it, because their whole job is to
   be read against it - who else is in this slot, what this child did last
   month. A dialog covering the cell you just clicked answers the question by
   hiding the thing you asked it about. */
.sidepanel {
  /* It settles in from its own side (motion, at the foot of this file). */
  animation:tgslide-r .12s cubic-bezier(.2, .8, .2, 1) both;
  position:fixed; top:64px; bottom:16px; width:min(430px, 46vw); z-index:90;
  background:var(--surface); border:1px solid var(--line); border-radius:var(--radius);
  box-shadow:var(--shadow); display:flex; flex-direction:column; overflow:hidden;
}
.sidepanel.left  { left:16px; animation-name:tgslide-l; }
.sidepanel.right { right:16px; }

/* ---- TWO AT ONCE ------------------------------------------------------
   While one panel is pinned the next opens on the other side, so neither may
   take more than half the window. The inbox asks for 660px because it is two
   columns and an ordinary panel for 430; side by side that is 1,090 plus
   margins, which overlaps on a laptop - at 1000px by 42px, with the newer
   panel drawn over the pinned one it was opened to sit beside.

   A CAP, not a width, so each keeps what it asked for wherever there is room.
   `:not(.free)` because a panel somebody has dragged or resized is theirs,
   which is the line the rest of this machinery draws. */
body:has(.sidepanel.kept) .sidepanel:not(.free) {
  max-width:calc(50vw - 24px);
}

.sidepanel .panel-head {
  display:flex; align-items:flex-start; gap:12px; padding:13px 16px;
  border-bottom:1px solid var(--line); flex:none;
}
/* The title takes the room, so the buttons after it sit in the order they
   are written. See the note on .pin below. */
.sidepanel .panel-head > div:first-child { flex:1 1 auto; min-width:0; }
/* A panel that carries a mark beside its name (panel()'s `mark`, the Alarms
   popup first): a tile like a card head's, and the name still takes the room
   between it and the buttons. */
.sidepanel .panel-head > .panelmark + div { flex:1 1 auto; min-width:0; align-self:center; }
.sidepanel .panel-head .panelmark {
  flex:none; display:inline-flex; align-items:center; justify-content:center;
  width:38px; height:38px; margin-right:10px; border-radius:var(--radius-sm);
  color:var(--a, var(--accent)); background:color-mix(in srgb, var(--a, var(--accent)) 14%, transparent);
  border:1px solid color-mix(in srgb, var(--a, var(--accent)) 30%, transparent);
}
.sidepanel .panel-head .panelmark svg { width:19px; height:19px; }
.sidepanel .panel-head h3 { margin:0; font-size:15px; font-weight:600; }
.sidepanel .panel-head .sub { font-size:11.5px; color:var(--muted); }
/* THE SAME FIX THE MODAL'S CROSS ALREADY HAS, and it is the same argument: a
   22px glyph with 3px either side is a 19x22 target, which is under half what a
   finger needs. `.modal-head .x` was given a real 32px box and the note beside
   it says why; this one was left as it was, and a side panel is what every chip
   on the calendar opens. Measured on a phone by tools/mobilecheck.py --panels,
   which had never looked at a panel until today. */
.sidepanel .panel-head .x {
  appearance:none; border:0; background:none; cursor:pointer; margin-left:auto;
  color:var(--muted); font-size:22px; line-height:1;
  width:32px; height:32px; border-radius:var(--radius-sm); flex:none;
  display:flex; align-items:center; justify-content:center;
}
.sidepanel .panel-head .x:hover { color:var(--ink); }

/* ---- THE PIN: this panel stays while the next one opens ----------------
   Asked for with both wanted on screen at once - planning a slot IS reading
   the messages beside it - and panel() has closed whatever was open since it
   was written.

   THE SAME 32px BOX AS THE CROSS beside it, for the reason the note above
   gives: a side panel is what every chip on the calendar opens, and these are
   read on a tablet. `margin-left:auto` lives on the cross, so this sits to the
   left of it and the pair stays together on the right of the head. */
.sidepanel .panel-head .pin {
  appearance:none; border:0; background:none; cursor:pointer; flex:none;
  color:var(--muted); width:32px; height:32px; border-radius:var(--radius-sm);
  display:flex; align-items:center; justify-content:center;
}
.sidepanel .panel-head .pin svg { width:15px; height:15px; }
.sidepanel .panel-head .pin:hover { color:var(--ink); }
.sidepanel .panel-head .pin:focus-visible {
  outline:2px solid var(--accent); outline-offset:-2px;
}
/* PRESSED IS A STATE, NOT A HOVER, so it is the accent filled rather than a
   darker grey - somebody has to be able to tell at a glance which of two
   panels is the one that will survive the next click. */
.sidepanel .panel-head .pin[aria-pressed="true"] {
  color:var(--accent); background:var(--accent-bg);
}
/* And the panel itself says so quietly, because the button is 32px in a head
   somebody is not looking at while they work the grid. */
.sidepanel.kept { border-color:color-mix(in srgb, var(--accent) 45%, var(--line)); }
.sidepanel .panel-body { overflow-y:auto; padding:14px 16px 18px; flex:1; }

/* The WhatsApp panel wears WhatsApp's green head. It is a copy of something
   people already use every day, and the bar is most of what makes it
   recognisable before a word is read - see `tone` in panel(). No other panel
   takes a tone, so nothing else in the app changes colour. */
.sidepanel.tone-wa .panel-head {
  background:var(--wa); border-bottom-color:var(--wa);
}
.sidepanel.tone-wa .panel-head h3,
.sidepanel.tone-wa .panel-head .sub,
.sidepanel.tone-wa .panel-head .x,
.sidepanel.tone-wa .panel-head .pin,
.sidepanel.tone-wa .panel-head .flip { color:#fff; }
.sidepanel.tone-wa .panel-head .x:hover,
.sidepanel.tone-wa .panel-head .pin:hover,
.sidepanel.tone-wa .panel-head .flip:hover { color:#fff; background:rgba(255,255,255,.16); }
/* PRESSED HAS TO READ ON GREEN TOO. The accent tint below is a pale blue
   panel colour and disappears on this head, which is the one panel most
   likely to be the pinned one. */
.sidepanel.tone-wa .panel-head .pin[aria-pressed="true"] {
  color:#fff; background:rgba(255,255,255,.28);
}
/* The inbox brings its own scrolling regions, so the panel body must not add
   a second one around them, and its padding would inset a design meant to
   run edge to edge. */
/* A COLUMN, because the day filter is a band across the panel above the two
   columns - see .wadaypick for why it cannot live inside the list column. */
.sidepanel.tone-wa .panel-body {
  padding:0; overflow:hidden; display:flex; flex-direction:column;
}
/* Wider by default than an ordinary panel, because this one is two columns.
   At the standard 430px the list is 130px of truncated names and the
   conversation has nowhere to put a bubble. Still resizable, and a size the
   reader has chosen still wins - this only sets where it starts. */
.sidepanel.tone-wa:not(.free) { width:min(660px, 58vw); }
.sidepanel.tone-wa .inbox {
  /* Proportional, not a fixed 150px. The panel is resizable now, so a column
     wide enough at 430px is a waste of a 900px window and a fixed wide one
     leaves no room for the conversation in a narrow one. The floor stops it
     collapsing to initials.

     align-items:stretch undoes the `start` the full-page .inbox sets. On a
     page that is right - the two cards are as tall as their contents. Inside a
     panel of fixed height it meant the conversation column took its content
     height and overflowed the bottom, which put the reply box and its Send
     button off the end of the panel where nothing could reach them. */
  flex:1; min-height:0; gap:0; margin:0; align-items:stretch;
  grid-template-columns:minmax(132px, 30%) minmax(0, 1fr);
}
.sidepanel.tone-wa .inbox-list,
.sidepanel.tone-wa .inbox-thread {
  border:0; border-radius:0; box-shadow:none; min-height:0;
  display:flex; flex-direction:column;
}
.sidepanel.tone-wa .inbox-list {
  max-height:none; border-right:1px solid var(--line);
}
/* min-height:0 as well as flex:1, or the 180px floor the full-page inbox
   wants stops this shrinking and pushes the reply box out of the panel. */
.sidepanel.tone-wa .wamsgs { max-height:none; min-height:0; }

/* ---- ONE FAMILY'S CHAT, small and beside its panel (openChat, main.js) ----
   Asked for with the slot panel open: "it should be smaller in size since it
   is showing the messages of one kid only and it should not open on top of
   'who planned here'". The thread alone - no list, no day filter - so 380px
   is enough, and main.js places it on the side of the panel with more room.
   z-index 91: over the page and the panels (90), under dialogs (100), so the
   thread's own "Name this chat" and reminder windows open over it. */
.minichat {
  position:fixed; top:64px; z-index:91; width:380px;
  height:min(640px, calc(100vh - 80px));
  display:flex; flex-direction:column; overflow:hidden;
  background:var(--surface); border:1px solid var(--line);
  border-radius:var(--radius); box-shadow:var(--shadow-md);
}
.minichat-head {
  display:flex; align-items:center; gap:8px; padding:10px 12px; flex:none;
  background:var(--wa); color:#fff; cursor:grab; user-select:none;
}
.minichat-head svg { width:18px; height:18px; }
.minichat-head .grow { flex:1; }
.minichat-head .minichat-all {
  color:#fff; background:rgba(255,255,255,.16); border-color:transparent;
}
.minichat-head .x {
  appearance:none; border:0; background:none; cursor:pointer; color:#fff;
  font-size:22px; line-height:1; width:32px; height:32px; flex:none;
  border-radius:var(--radius-sm); display:flex; align-items:center; justify-content:center;
}
.minichat-head .x:hover, .minichat-head .minichat-all:hover { background:rgba(255,255,255,.26); }
.minichat-body { flex:1; min-height:0; display:flex; flex-direction:column; }
.minichat .inbox.single {
  flex:1; min-height:0; margin:0; gap:0; display:flex; align-items:stretch;
}
.minichat .inbox-thread {
  flex:1; min-height:0; display:flex; flex-direction:column;
  border:0; border-radius:0; box-shadow:none;
}
.minichat .wamsgs { max-height:none; min-height:0; padding:10px 5%; }
/* The floating WhatsApp button would sit on the window's reply box. */
body:has(.minichat:not(.minimized)) .wafab { display:none; }
/* Minimize, beside the cross in both heads: the same 32px box. */
.sidepanel .panel-head .min, .minichat-head .min {
  appearance:none; border:0; background:none; cursor:pointer; flex:none;
  width:32px; height:32px; border-radius:var(--radius-sm); font-size:22px; line-height:1;
  display:flex; align-items:center; justify-content:center; color:#fff;
}
.sidepanel .panel-head .min:hover, .minichat-head .min:hover { background:rgba(255,255,255,.26); }

@media (max-width: 760px) {
  /* NO BESIDE ON A PHONE: the panel is a sheet across the bottom. So the
     sheet is shortened while the chat is open and the chat takes the top -
     both on screen, neither over the other.
     THE CHAT GETS THE LARGER SHARE, measured on the clinic's handset: at an
     even split its heads - the bar, who it is, the closed-window line, the
     reminder offer - took 333 of its 366px and left the messages a sliver.
     The sheet is still there to scroll, and back closes the chat first. */
  .minichat {
    left:8px !important; right:8px; width:auto !important; top:58px;
    height:auto; bottom:calc(40vh + 16px + env(safe-area-inset-bottom));
  }
  .minichat-head { cursor:default; padding:6px 10px; }
  .minichat .wamsgs { min-height:140px; }
  .minichat .waclosed { padding:8px 12px; }
  /* What still does not fit scrolls rather than being cut off: the reminder
     offer wraps to two lines on a phone and its second button sat below the
     window's edge, present and unreachable. */
  .minichat .inbox-thread { overflow-y:auto; }
  body:has(.minichat:not(.minimized)) .sidepanel { height:40vh; }
}

/* Dragged by its head and resized from its corner, because where a panel is
   convenient is not something this code can know - see makeMovable in ui.js.
   The head says so with the cursor rather than with a grip nobody would find:
   a title bar you can drag is a convention older than the web. */
.sidepanel .panel-head { cursor:grab; user-select:none; }
.sidepanel .panel-head h3, .sidepanel .panel-head .sub { cursor:inherit; }
/* Except the buttons in it, which are buttons. */
.sidepanel .panel-head button { cursor:pointer; }

/* Only once it has been moved. Docked, it spans top to bottom and there is no
   height to drag; `resize` needs a definite size, which free mode sets. */
/* The corner you pull to resize. Drawn here and driven by addGrip() in ui.js
   rather than by CSS `resize`, which did nothing at all on these panels - see
   the note on that function. Its own element, so it sits above whatever the
   panel is showing: the inbox runs edge to edge and would otherwise have its
   reply box exactly where the corner needs to be. */
.sidepanel.free .pgrip {
  position:absolute; right:0; bottom:0; width:20px; height:20px; z-index:6;
  cursor:nwse-resize; touch-action:none; opacity:.55; color:var(--muted);
  background:
    linear-gradient(135deg, transparent 46%, currentColor 46%, currentColor 58%, transparent 58%),
    linear-gradient(135deg, transparent 70%, currentColor 70%, currentColor 82%, transparent 82%);
}
.sidepanel.free .pgrip:hover { opacity:1; color:var(--accent); }
/* Not on a phone, where the panel is a sheet and there is nothing to resize. */
.sidepanel:not(.free) .pgrip { display:none; }
/* Nothing inside reacting to a pointer that is only passing through, and no
   transition fighting the drag. */
.sidepanel.sizing { transition:none; }
.sidepanel.sizing .panel-body { pointer-events:none; }
.sidepanel.dragging {
  cursor:grabbing; transition:none;
  /* Lifted while it is being carried, and nothing inside it reacting to a
     pointer that is only passing through. */
  box-shadow:0 14px 40px rgba(0,0,0,.34);
}
.sidepanel.dragging .panel-head { cursor:grabbing; }
.sidepanel.dragging .panel-body { pointer-events:none; }

/* The inbox was built for a full page, two columns wide. In a 430px panel it
   has to stack, and the list is capped so the thread below it is on screen
   rather than a scroll away - the thread is the thing being opened. */
.sidepanel .inbox { grid-template-columns:1fr; margin-top:0; }
.sidepanel .inbox-list { max-height:38vh; }

@media (max-width: 760px) {
  /* Narrow enough that a panel is a sheet, so there is nowhere to drag it to
     and makeMovable does not arm. The heading must stop offering: a grab
     cursor on something that cannot be grabbed is the interface lying. */
  .sidepanel .panel-head { cursor:default; user-select:auto; }
  /* The inbox's wider default is a desktop idea. Left alone it outranks the
     sheet width below on specificity - .sidepanel.tone-wa:not(.free) beats
     .sidepanel.right - and the panel came back as a 218px column down the
     middle of a 375px phone. */
  .sidepanel.tone-wa:not(.free) { width:auto; }
  /* One column on a phone, and the list is the first thing: pick a
     conversation, then read it. Side by side there is room for neither. */
  .sidepanel.tone-wa .inbox-list { border-right:0; }
  /* No room to sit beside anything; become a sheet from the bottom - clear of
     the gesture bar, which sits on top of the page once the app is installed
     full screen. Its own Save is the row that would be under it. */
  .sidepanel, .sidepanel.left, .sidepanel.right {
    left:8px; right:8px; width:auto; top:auto; height:72vh;
    bottom:calc(8px + env(safe-area-inset-bottom));
  }
  /* THE LIST OR THE THREAD, NEVER BOTH (toList in replies.js). They were
     stacked: the list capped at 30vh above, the thread in what was left - and
     measured on the clinic's phone on 18 October 2026 what was left was 266px,
     of which the head, the alert card and the reply box took all but 20 for
     the messages. "It opens but we cant see anything or cant scroll anything."
     Now the list fills the sheet until a chat is pressed, and the chat fills it
     until Back. A column rather than the grid, so whichever is showing simply
     takes the height. */
  .sidepanel.tone-wa .inbox { display:flex; flex-direction:column; }
  .sidepanel.tone-wa .inbox-list,
  .sidepanel.tone-wa .inbox-thread { flex:1 1 0; min-height:0; }
  .sidepanel.tone-wa .walist { max-height:none; flex:1 1 0; min-height:0; }
  .sidepanel.tone-wa .inbox:not(.reading) .inbox-thread,
  .sidepanel.tone-wa .inbox.reading .inbox-list,
  .sidepanel.tone-wa .panel-body:has(.inbox.reading) .wadaypick { display:none; }
  .sidepanel.tone-wa .waback { display:inline-flex; }
  /* A FLOOR, so the messages can never be a slit again: whatever the heads
     above them grow to - a long name, the alert card, the reminder offer -
     the thread scrolls as a whole rather than squeezing the conversation.
     The small chat window's own rule, for its own measured 20px. */
  .sidepanel.tone-wa .inbox-thread { overflow-y:auto; }
  .sidepanel.tone-wa .wamsgs { min-height:180px; }
  /* AND THE SCREEN, not a 72vh sheet: a conversation is the one thing here
     that is read down rather than glanced at, and the page behind it has
     nothing to say while it is open. Down from the bar, as the small chat
     window sits. */
  .sidepanel.tone-wa { top:58px; height:auto; }
}

/* ------------------------------------------------- panels: availability --- */

.availlist { display:flex; flex-direction:column; gap:7px; margin-bottom:14px; }
.availrow { display:grid; grid-template-columns:1fr auto auto; gap:6px; align-items:center; }
.availrow .tpill {
  color:#fff; font-size:11px; font-weight:600; padding:3px 10px; border-radius:var(--radius-pill);
  justify-self:start;
}
.availrow select, .availrow input { font-size:12px; padding:5px 8px; min-height:0; }
/* The time, as a slot or as typed: the list and the box share one cell of the
   row, so the box appearing does not push "asked on" out of its place. */
.availrow .slotpick { display:inline-flex; align-items:center; gap:4px; }
.availrow .slotpick .at { width:7.5em; }
/* "Out between" has a second time, on a line of its own under the first and
   ending where it ends, so nothing else in the row moves when it appears. */
.availrow .backpick {
  grid-column:1 / -1; justify-self:end;
  display:inline-flex; align-items:center; gap:4px;
}
.availrow .backpick .backat { width:7.5em; }
.availrow .backword { font-size:12px; color:var(--muted); }

/* Three more things per row, and only when there IS leave: when it was asked
   for, what that makes it, and how many children are already booked in. They
   are hidden rather than greyed while the therapist is Available, so a list of
   eleven people stays eleven lines until somebody marks one off. */
.availrow .asked {
  grid-column:1 / 2; display:flex; align-items:center; gap:5px 12px; flex-wrap:wrap;
  font-size:11px; color:var(--muted); white-space:nowrap;
}
/* Day off and asked on side by side, each its word and its box; on a narrow
   panel the second goes under the first rather than squeezing both. */
.availrow .asked label { display:inline-flex; align-items:center; gap:5px; flex:1 1 11.5rem; min-width:0; }
.availrow .asked input { flex:1; min-width:0; }
.availrow .noticetag {
  grid-column:2 / 3; justify-self:start; white-space:nowrap;
  font-size:10.5px; font-weight:600; padding:2px 8px; border-radius:var(--radius-pill);
  border:1px solid transparent;
}
.availrow .noticetag.planned { background:var(--ok-bg); color:var(--ok); border-color:var(--ok); }
.availrow .noticetag.short { background:var(--wait-bg); color:var(--wait); border-color:var(--wait); }
.availrow .noticetag.unrecorded { background:var(--sunken); color:var(--muted); border-color:var(--line); }
.availrow .availbooked {
  grid-column:3 / 4; justify-self:end; white-space:nowrap;
  font-size:10.5px; color:var(--wait);
}
.availrow .note { grid-column:1 / -1; }

.availwhen { display:flex; align-items:center; gap:8px; margin-bottom:12px; flex-wrap:wrap; }
/* Which day a row's leave is for, its own box in the row - beside the box it
   was once typed into by mistake. The word is bold, the date is not. */
.availrow .asked .offday { color:var(--ink); font-weight:600; }
.availrow .asked .offday input { font-weight:400; }
.availrow .availdel { grid-column:3 / 4; justify-self:end; }
/* Leave from today on, a pill each in the therapist's colour; the one for the
   day on screen is filled. Pressing one opens that day. */
.availsoon { display:flex; flex-wrap:wrap; align-items:center; gap:5px; margin:-4px 0 12px; }
.availsoon .soonhead { font-size:11px; color:var(--muted); margin-right:2px; }
.availsoon .soonpill {
  font:inherit; font-size:11px; color:var(--ink); cursor:pointer;
  padding:2px 9px; border-radius:var(--radius-pill);
  border:1px solid var(--a); background:color-mix(in srgb, var(--a) 14%, transparent);
}
.availsoon .soonpill b { font-weight:600; }
.availsoon .soonpill.here { background:var(--a); color:#fff; }

/* One day of leave, on Staff details. The colour is the same three-way split
   the availability panel uses, so planned means the same green in both
   places. Wrapped rather than scrolled: a month of leave for one person is a
   handful of these and they read as a sentence. */
.leavetag {
  display:inline-flex; align-items:center; gap:4px; white-space:nowrap;
  font-size:10.5px; font-weight:600; padding:2px 8px; margin:1px 0;
  border-radius:var(--radius-pill); border:1px solid transparent;
}
.leavetag i { font-style:normal; font-weight:400; opacity:.75; }
.leavetag.planned { background:var(--ok-bg); color:var(--ok); border-color:var(--ok); }
.leavetag.short { background:var(--wait-bg); color:var(--wait); border-color:var(--wait); }
.leavetag.unrecorded { background:var(--sunken); color:var(--muted); border-color:var(--line); }
.availwhen label { font-size:12px; font-weight:600; color:var(--muted); }
.availwhen .sub { font-size:11px; color:var(--faint); }

/* On a phone the panel is the width of the screen and the row above becomes
   three columns of nothing. One thing per line reads, and the list is scrolled
   rather than squinted at. */
@media (max-width:560px) {
  .availrow { grid-template-columns:1fr; }
  .availrow .asked, .availrow .noticetag, .availrow .availbooked { grid-column:1 / -1; }
  .availrow .availbooked { justify-self:start; }
  /* Remove leave is placed in the third column on a wide panel, and a third
     column named here would bring three columns back onto this one - its
     list squeezed to an arrow on every row with leave (found 20 October). */
  .availrow .availdel { grid-column:1 / -1; justify-self:start; }
}

/* ------------------------------------------------------ panels: history --- */

.histhead { display:flex; align-items:center; gap:6px; margin-bottom:12px; flex-wrap:wrap; }
.histhead .grow { flex:1; }
.histhead .sub { font-size:11.5px; color:var(--muted); }
.histtiles { display:grid; grid-template-columns:repeat(3,1fr); gap:6px; margin-bottom:14px; }
.htile {
  background:var(--raised); border:1px solid var(--line); border-radius:var(--radius-sm);
  padding:8px 10px; display:flex; flex-direction:column;
}
.htile.warn { border-color:var(--wait); }
.htile .n { font-size:19px; font-weight:600; line-height:1.1; }
.htile.warn .n { color:var(--wait); }
.htile .l {
  font-size:9px; letter-spacing:.09em; text-transform:uppercase; color:var(--faint);
  font-family:"IBM Plex Mono", ui-monospace, monospace; margin-top:2px;
}
table.rows.hist td { padding:5px 7px; font-size:12px; }
/* Any rows table: .dim is the quiet half of a cell wherever it appears.
   This was written twice, scoped to .hist and .pk-sess, so the next table
   to use the class silently got no styling at all. */
table.rows .dim { color:var(--faint); }
.linkbtn.more { margin-top:9px; font-size:12px; }

/* ----------------------------------------------------- panels: planners --- */

.plannedkid { border-bottom:1px solid var(--line-soft); padding:11px 0; }
/* THE CHILD THE PILL WAS PRESSED FOR. The panel lists everybody planned in
   the hour - ten of them on a full one - so the one somebody actually asked
   about has to be findable in it. Same --miss as the pill they came from, so
   the two are visibly one action; and it STAYS while the panel is open,
   because "which one did I press" is still the question once the form under
   it has been read. */
.plannedkid.spotlit {
  box-shadow:0 0 0 2px var(--miss); border-radius:var(--radius-sm);
  padding-inline:9px; scroll-margin:10px;
}
.plannedkid:last-child { border-bottom:0; }
.pk-head { display:flex; align-items:center; gap:5px; flex-wrap:wrap; margin-bottom:7px; }
.pk-head .grow { flex:1; }
.pk-head .tpill { color:#fff; font-size:10px; font-weight:600; padding:1px 8px; border-radius:var(--radius-pill); }

.planwhen { display:flex; flex-wrap:wrap; gap:5px; margin-bottom:14px; }
.whenpill {
  display:inline-flex; align-items:center; gap:5px; font-size:11px;
  background:var(--sunken); border-radius:var(--radius-pill); padding:3px 10px;
  font-family:"IBM Plex Mono", ui-monospace, monospace;
}
.whenpill i { width:8px; height:8px; border-radius:50%; }

.addform { display:flex; flex-wrap:wrap; gap:5px; align-items:center; }
.addform input, .addform select {
  border:1px solid var(--line); background:var(--ground); color:var(--ink);
  border-radius:var(--radius-sm); padding:5px 8px; font:inherit; font-size:12px; min-height:0;
}
.addform .n { width:56px; font-family:"IBM Plex Mono", ui-monospace, monospace; }
.addform .tm { width:88px; font-family:"IBM Plex Mono", ui-monospace, monospace; }
/* Who takes it. Capped rather than left to size itself: the widest therapist
   name would otherwise set the width of the control and push the number, the
   time and the Add button onto a second line of a 390px panel. The native
   select truncates, and the full name is in the open list. */
.addform .th { max-width:112px; }
.addform .note { flex:1 1 130px; }
.addform .btn { min-height:0; padding:5px 13px; font-size:12px; }
.addform .err { flex:1 1 100%; margin:4px 0 0; }

/* Clicking a child fades everyone else. Colour on one chip among four hundred
   is something you have to hunt for; the absence of colour everywhere else is
   not. The faded ones stay legible on purpose - the shape of the day is still
   worth seeing behind the answer. */
.focusbar {
  display:flex; align-items:center; gap:9px; margin-top:10px;
  background:var(--accent-bg); border:1px solid var(--accent);
  border-radius:var(--radius); padding:8px 13px; font-size:13px;
}
.focusbar .sub { color:var(--muted); font-size:12px; }
.focusbar .grow { flex:1; }

/* ------------------------------------------------------- editable grid --- */

/* Fields live in the row rather than behind a dialog, because this page is used
   to go DOWN a column - who has no phone number, which of these are unpaid -
   and a dialog per row turns one pass into eighty open-edit-close cycles. */
table.rows.edit td { padding:5px 7px; vertical-align:middle; }
table.rows.edit tbody tr { cursor:default; }
table.rows.edit tbody tr:hover td { background:transparent; }
table.rows.edit input, table.rows.edit select {
  border:1px solid var(--line); background:var(--ground); color:var(--ink);
  border-radius:var(--radius-sm); padding:5px 7px; font:inherit; font-size:12px;
  min-height:0; width:100%; min-width:92px;
}
table.rows.edit input[type="checkbox"] { width:auto; min-width:0; }
/* ON A TOUCH SCREEN A FIELD IS AS WIDE AS WHAT IT SAYS. The floor above is
   92px, which holds "Occupational Therapy" at the 12px set here - but a touch
   screen gets 16px in every field (the !important floor further down, which
   stops iOS zooming in on a tap), and at 16px that therapy reads "Occupatior"
   and a package name "Reyan OT P". Found on the clinic's phone, on Packages.

   SIZED TO THE VALUE rather than given a bigger floor, because the floor
   applies to every column: raising it for the one long word would widen
   fifteen columns of short ones. field-sizing makes each box exactly its own
   value, so "paid" stays narrow and the therapy gets what it needs. The table
   already scrolls sideways on a phone, so a wider column costs nothing else.

   CAPPED, because a comment can be a paragraph and would otherwise stretch
   its column across three screens. Text, not dates or numbers: a date box
   has its own width (see the Packages note on 8.5rem) and a number is
   never long. A browser without field-sizing - Safari, today - simply keeps
   the old width, which is what it had before. */
@media (pointer: coarse) {
  table.rows.edit select,
  table.rows.edit input[type="text"],
  table.rows.edit input:not([type]) {
    field-sizing:content; width:auto; max-width:16rem;
  }
}
table.rows.edit input:focus, table.rows.edit select:focus {
  outline:2px solid var(--accent); outline-offset:-1px;
}
table.rows.edit input:disabled, table.rows.edit select:disabled {
  opacity:.65; cursor:default;
}
table.rows.edit .kidcell { white-space:nowrap; }
table.rows.edit .mid { text-align:center; }
table.rows.edit .amt { min-width:80px; text-align:right;
  font-family:"IBM Plex Mono", ui-monospace, monospace; }
/* "covers 5 of 12" under Paid so far - what the figure will do to the payment
   reminder, said where it is typed (0273). */
table.rows.edit .paidsofar .sub { display:block; font-size:11px; white-space:nowrap; }

/* ------------------------------------------- the package window, as cards ---
   Asked for with a mockup on 15 October 2026: "make the package page somewhat
   like this design". A change of clothes only - packages.js still clones the
   row's own boxes in FORM_FIELDS' order and writes back into the row - so
   everything here is the window's look: a card per subject with a head, a mark
   on every label and inside every box, the payments as rows, and the Save in
   a band of its own at the foot. The app's own theme, not the mockup's navy,
   the same call every redesign before it made. */
.pkgwin { display:flex; flex-direction:column; gap:12px; }
.pkgwin .pwcard { margin:0; padding:14px 16px; }
.pkgwin .pwcard .cardhead { margin-bottom:12px; }
.pkgwin .form2 { gap:12px 14px; }
/* The mark on a label, in the accent - the mockup's small coloured glyph. */
.pkgwin label[for] > .navicon {
  width:13px; height:13px; margin-right:6px; vertical-align:-2px; color:var(--accent);
}
/* AND INSIDE THE BOX, muted, with the box padded to make room. A wrapper
   round the clone, never a change to it: every reader of #w_<field> finds the
   control it always did. Pointer-events off, or a press on the mark would land
   on the wrapper and not reach the select under it. */
.pkgwin .pwfield { position:relative; display:block; }
.pkgwin .pwfield > .navicon {
  position:absolute; left:10px; top:50%; transform:translateY(-50%);
  width:15px; height:15px; color:var(--muted); pointer-events:none;
}
.pkgwin .pwfield > input, .pkgwin .pwfield > select { padding-left:33px; }
/* FULL WIDTH OVER THE ROW'S OWN SIZE. The clone carries the grid's inline
   width - the name box is 9rem on the row - so Package name came out a third
   of its card. Measured on the deployed window; !important because an inline
   style outranks any selector. */
.pkgwin .pwfield > input, .pkgwin .pwfield > select { width:100% !important; }
/* Send sits on its label's line, as the mockup draws it beside the phone. */
.pkgwin [data-holds="send_messages"] { display:flex; align-items:center; gap:10px; padding-top:22px; }
.pkgwin [data-holds="send_messages"] label { margin:0; }
.pkgwin [data-holds="send_messages"] input { width:auto; }

/* THE PAYMENTS, one row each (0284), with a Correct at the end of each payment
   and refund (0285) and the one form that opens for it. */
#pkgWinPaid .ledger { list-style:none; margin:0 0 8px; padding:0; font-size:12.5px;
  display:flex; flex-direction:column; gap:6px; }
#pkgWinPaid .ledger li {
  display:flex; align-items:center; gap:9px; padding:7px 10px;
  border:1px solid var(--line); border-radius:var(--radius-sm); background:var(--sunken);
}
#pkgWinPaid .ledger li > .navicon { width:15px; height:15px; color:var(--muted); }
#pkgWinPaid .ledger .mono { flex:none; color:var(--muted); }
#pkgWinPaid .ledger .lwhat { flex:1 1 auto; min-width:0; }
#pkgWinPaid .ledger .sub { display:block; color:var(--muted); font-size:11.5px; }
#pkgWinPaid .ledger .btn.tiny { flex:none; margin-left:auto; }

/* The two choices side by side, as the mockup has them, and a choice whose
   form has unfolded takes the whole width - the form is two columns of its own. */
.pkgwin .pwopts { display:flex; flex-wrap:wrap; gap:10px 18px; }
/* 190px, measured: the window's card is about 460px inside, and two of 230
   plus the gap did not fit, so the ticks stacked. */
.pkgwin .pwopts > * { flex:1 1 190px; min-width:0; }
.pkgwin .pwopts > :has(.advform:not([hidden])),
.pkgwin .pwopts > :has(#w_endForm:not([hidden])) { flex-basis:100%; }
.pkgwin .pwopts .advform { margin-top:10px; }

/* The foot: what Save is about to write, and Save. */
/* PACKAGE AND BILLING, SIDE BY SIDE (22 October 2026): "When user opens the
   popup, it will have two tabs side by side one is package and other is
   billing". Two halves of one track, each a mark, a word and a line - the
   Billing one saying where the money stands - and the one showing lifted out
   of the track. Held at the top while the window scrolls, so the other tab is
   one press away from anywhere in a long package. A dot on Billing while a
   course of this child's is paid with no invoice yet. */
.pkgwin .pwtabs {
  position:sticky; top:0; z-index:3;
  display:grid; grid-template-columns:1fr 1fr; gap:6px; padding:4px;
  background:var(--sunken); border:1px solid var(--line); border-radius:var(--radius);
}
/* The window's body has 16px of padding over the track, and the form scrolled
   up through that gap above the stuck tabs - the Amount box showing over them.
   A strip in the window's own colour fills it. */
.pkgwin .pwtabs::before {
  content:''; position:absolute; left:-2px; right:-2px; bottom:100%; height:18px;
  background:var(--surface);
}
.pkgwin .pwtab {
  position:relative; display:flex; align-items:center; gap:10px; min-width:0;
  min-height:52px; padding:8px 12px; border:0; border-radius:9px;
  background:transparent; color:var(--muted); font:inherit; text-align:left; cursor:pointer;
}
.pkgwin .pwtab > .navicon { width:20px; height:20px; flex:none; }
.pkgwin .pwtab > span { display:flex; flex-direction:column; min-width:0; }
.pkgwin .pwtab b { font-size:14px; color:inherit; }
.pkgwin .pwtab small {
  font-size:12px; color:var(--faint);
  white-space:nowrap; overflow:hidden; text-overflow:ellipsis;
}
.pkgwin .pwtab:hover { color:var(--ink); }
.pkgwin .pwtab:focus-visible { outline:2px solid var(--accent); outline-offset:1px; }
.pkgwin .pwtab[aria-selected="true"] {
  background:var(--surface); color:var(--accent); box-shadow:var(--shadow-xs);
}
.pkgwin .pwtab[aria-selected="true"] small { color:var(--muted); }
.pkgwin .pwtab.needs::after {
  content:''; position:absolute; top:9px; right:10px; width:9px; height:9px;
  border-radius:50%; background:var(--miss); box-shadow:0 0 0 2px var(--surface);
}
.pkgwin .pwpane { display:flex; flex-direction:column; gap:12px; }
.pkgwin .pwpane[hidden] { display:none; }
/* PAYMENTS AND INVOICES, AND NEW INVOICE (22 October 2026). Each payment's
   invoice - its number, Print and Edit - sits in its line before Correct; a
   line for this package paid with no invoice yet is amber; + New invoice is
   under the list; and the New invoice card holds billing.js's own form. */
#pkgWinPaid .ledger .billref { display:inline-flex; align-items:center; gap:6px;
  flex:none; margin-left:auto; flex-wrap:wrap; }
#pkgWinPaid .ledger .billref:empty { display:none; }
#pkgWinPaid .ledger .billref .btn.tiny { margin-left:0; }
#pkgWinPaid .ledger .billref + .btn.tiny { margin-left:0; }
#pkgWinPaid .ledger .pwbillno { color:var(--accent); font-weight:700; }
#pkgWinPaid .ledger li { flex-wrap:wrap; }
#pkgWinPaid .ledger li.awaits { border-color:color-mix(in srgb, var(--miss) 45%, var(--line));
  background:color-mix(in srgb, var(--miss) 10%, var(--sunken)); }
#pkgWinPaid .ledger li.awaits > .navicon { color:var(--miss); }
.pkgwin .pwbilldo { display:inline-flex; gap:6px; }
.pkgwin .pwbilldo .btn { display:inline-flex; align-items:center; gap:5px; }
.pkgwin .pwbilldo .navicon { width:13px; height:13px; }
.pkgwin .pwnewinv { display:flex; align-items:center; gap:10px; margin:8px 0 0; }
.pkgwin .pwnewinv[hidden] { display:none; }
.pkgwin .pwnewinv .hint { margin:0; flex:1 1 auto; max-width:none; }
.pkgwin .pwnewinv .btn { margin-left:auto; flex:none; }
.pkgwin .pwinvsay { margin:0 0 10px; }
.pkgwin .billembed .billlines .gridwrap { max-width:100%; }
/* The window is half the width of the invoice dialog, so the therapy keeps
   the room it needs to be read and the two figures give it up - a box
   reading "Occupat" is a box nobody can check. */
.pkgwin .billembed .linetable td:first-child { min-width:170px; }
.pkgwin .billembed .linetable .lSessions { max-width:70px; }
.pkgwin .billembed .linetable .lPrice { max-width:96px; }
.pkgwin .billembed .linetable th { white-space:normal; }

.pkgwin .pwfoot { display:flex; align-items:center; gap:12px; }
.pkgwin .pwfoot .pwsay { flex:1 1 auto; min-width:0; }
.pkgwin .pwfoot .pwsay b { display:block; font-size:14px; }
.pkgwin .pwfoot .pwsay .hint { margin:2px 0 0; }
.pkgwin .pwfoot .btn { flex:none; display:inline-flex; align-items:center; gap:7px; }
.pkgwin .pwfoot .btn .navicon { width:15px; height:15px; }
@media (max-width: 520px) {
  .pkgwin .form2 { grid-template-columns:1fr; }
  .pkgwin .pwfoot { flex-wrap:wrap; }
  .pkgwin .pwfoot .btn { width:100%; justify-content:center; }
}

/* ---------------------------------------------- Kid details, as cards ---
   Asked for with the popup in a screenshot on 18 October 2026: "it should
   look good and easy to understand and with proper symbols". The package
   window's language (above) brought to the child's record, so the two read
   as one kind of window: a card a subject, a mark on every label and inside
   every box, the Save in a band of its own. A change of clothes - every id
   panels.js reads is where it was. */
.kidwin { display:flex; flex-direction:column; gap:12px; }
.kidwin .pwcard { margin:0; padding:14px 16px; }
.kidwin .pwcard .cardhead { margin-bottom:12px; }
.kidwin .pwcard .cardhead:last-child { margin-bottom:0; }
.kidwin .form2 { gap:12px 14px; }
.kidwin label[for] > .navicon {
  width:13px; height:13px; margin-right:6px; vertical-align:-2px; color:var(--accent);
}
.kidwin .pwfield { position:relative; display:block; }
.kidwin .pwfield > .navicon {
  position:absolute; left:10px; top:50%; transform:translateY(-50%);
  width:15px; height:15px; color:var(--muted); pointer-events:none;
}
.kidwin .pwfield > input, .kidwin .pwfield > select { width:100%; padding-left:33px; }
/* A LIST WEARS ITS ARROW. `.form2 select` is given `background:` whole a few
   hundred lines up, which takes the arrow away with it, so every list here -
   the Message wording lists among them (22 October 2026, "it will be a
   dropdown") - read as a text box. Put back for a list that can be opened. */
.kidwin .pwfield > select:not([multiple]):not(:disabled) {
  padding-right:27px;
  background-image:
    linear-gradient(45deg, transparent 50%, currentColor 50%),
    linear-gradient(135deg, currentColor 50%, transparent 50%);
  background-position:right 13px center, right 9.5px center;
  background-size:4.5px 4.5px, 4.5px 4.5px;
  background-repeat:no-repeat;
}
/* A date box draws its own picker at the right; the mark sits at the left. */
.kidwin .pwfield > input[type="date"] { padding-left:33px; }
/* The site as a place, not a code: a pin and the name, in the card's amber. */
.kidwin .kdsites { gap:8px; margin:0 0 6px; }
.kidwin .kdsite {
  display:inline-flex; align-items:center; gap:6px; padding:5px 11px 5px 9px;
  border-radius:var(--radius-pill); font-size:12.5px; font-weight:600;
  color:#b45309; background:color-mix(in srgb, #f59e0b 16%, transparent);
  border:1px solid color-mix(in srgb, #f59e0b 40%, transparent);
}
:root[data-theme="dark"] .kidwin .kdsite { color:#fbbf24; }
@media (prefers-color-scheme: dark) { :root:not([data-theme="light"]):not([data-theme="lavender"]):not([data-theme="blush"]) .kidwin .kdsite { color:#fbbf24; } }
.kidwin .kdsite .navicon { width:13px; height:13px; }
/* Both numbers: a band with the switch at its end, as on Your login - and
   Trial on New booking (22 October 2026), the same switch in the same band. */
.kidwin .kdswitch, #bookForm .kdswitch {
  display:flex; align-items:center; gap:12px; padding:10px 12px;
  border:1px solid var(--line); border-radius:var(--radius); background:var(--sunken);
}
.kidwin .kdswitch .cardicon, #bookForm .kdswitch .cardicon { width:32px; height:32px; border-radius:9px; }
.kidwin .kdswitch label, #bookForm .kdswitch label { margin:0; font-size:13px; cursor:pointer; }
.kidwin .kdswitch .hint, #bookForm .kdswitch .hint { margin:2px 0 0; }
/* The WhatsApp answer as a coloured word beside its label. */
.kidwin .kdconsent {
  display:inline-block; margin-left:8px; padding:1px 8px; border-radius:var(--radius-pill);
  font-size:11px; font-weight:600; vertical-align:1px;
  color:var(--a); background:color-mix(in srgb, var(--a) 15%, transparent);
  border:1px solid color-mix(in srgb, var(--a) 40%, transparent);
}
.kidwin .kdconsent:empty { display:none; }
/* The foot: what Save does, Cancel and Save. */
.kidwin .kdfoot { display:flex; align-items:center; gap:10px; flex-wrap:wrap; }
.kidwin .kdfoot .cardicon { width:32px; height:32px; border-radius:9px; }
.kidwin .kdfoot .hint { margin:0; }
.kidwin .kdfoot .btn { display:inline-flex; align-items:center; gap:7px; }
.kidwin .kdfoot .btn .navicon { width:15px; height:15px; }
.kidwin > .err { margin:0; }
.kidwin .kdpair { display:grid; grid-template-columns:1fr 1fr; gap:12px 14px; }
.kidwin .kdhist { padding:10px 16px; }
.kidwin .kdhist .chgblock { margin:0; }
@media (max-width: 760px) {
  .kidwin .kdpair { grid-template-columns:1fr; }
}

/* ------------------------------- every popup in one set of clothes ---
   Asked for on 18 October 2026, with Kid details just redrawn: "do this kind
   of redesigning in all the popups without dropping any utility or
   functionality". dressPopup() in ui.js marks each popup at run time -
   `.dressed` on the dialog, a symbol in front of each label's words, the
   symbol inside each box as the box's own background (--fimark, so nothing
   is wrapped and no control moves), a tile on each section heading - and
   these rules are its look: the package window's and Kid details', for every
   other dialog. A form that already draws its own cards (Kid details, the
   package window, the person window, Message therapy type) is left as it is. */
.modal.dressed label[for] > .navicon,
.sidepanel.dressed label[for] > .navicon {
  width:13px; height:13px; margin-right:6px; vertical-align:-2px; color:var(--accent);
}
.modal.dressed .fimark,
.sidepanel.dressed .fimark {
  padding-left:33px; background-image:var(--fimark, none); background-repeat:no-repeat;
  background-position:left 10px center; background-size:15px 15px;
}
.modal.dressed textarea.fimark,
.sidepanel.dressed textarea.fimark { background-position:left 10px top 10px; }
/* A select keeps its arrow: the mark is one more layer of the same image. */
.modal.dressed select.fimark:not([multiple]),
.sidepanel.dressed select.fimark:not([multiple]) {
  background-image:var(--fimark, none),
    linear-gradient(45deg, transparent 50%, currentColor 50%),
    linear-gradient(135deg, currentColor 50%, transparent 50%);
  background-position:left 10px center, right 13px center, right 9.5px center;
  background-size:15px 15px, 4.5px 4.5px, 4.5px 4.5px;
}
.modal.dressed select.fimark:disabled,
.sidepanel.dressed select.fimark:disabled {
  background-image:var(--fimark, none); background-position:left 10px center; background-size:15px 15px;
}
/* The form as a card, the way Kid details' are. */
.modal.dressed .modal-body > .form2:not(:has(.pwcard)):not(:has(.pbox)):not(:has(.panel)),
.modal.dressed .modal-body > form > .form2:not(:has(.pwcard)):not(:has(.pbox)):not(:has(.panel)),
.modal.dressed .modal-body > .form1:not(:has(.pwcard)):not(:has(.pbox)):not(:has(.panel)) {
  padding:14px 16px; border:1px solid var(--line); border-radius:var(--radius);
  background:var(--surface); box-shadow:var(--shadow-xs);
}
/* A section heading wears a tile, as a card head does. */
.modal.dressed .dressedhead, .sidepanel.dressed .dressedhead { display:flex; align-items:center; gap:10px; }
.modal.dressed .dressedhead > .cardicon,
.sidepanel.dressed .dressedhead > .cardicon { width:28px; height:28px; border-radius:8px; }
.modal.dressed .dressedhead > .cardicon svg,
.sidepanel.dressed .dressedhead > .cardicon svg { width:15px; height:15px; }
/* The buttons in a band of their own at the foot - a full row of them, not
   a button half-way down a form (Add a child's Match preview). */
.modal.dressed .form2 > .actions.span2:last-child,
.sidepanel.dressed .form2 > .actions.span2:last-child,
.modal.dressed .modal-body > form > .actions:last-child,
.modal.dressed .form1 > .actions:last-child {
  border-top:1px solid var(--line); padding-top:12px; margin-top:2px;
}
.modal.dressed .btn .navicon, .sidepanel.dressed .btn .navicon { width:15px; height:15px; }
@media (max-width: 760px) {
  .modal.dressed .modal-body > .form2:not(:has(.pwcard)):not(:has(.pbox)):not(:has(.panel)),
  .modal.dressed .modal-body > form > .form2:not(:has(.pwcard)):not(:has(.pbox)):not(:has(.panel)),
  .modal.dressed .modal-body > .form1:not(:has(.pwcard)):not(:has(.pbox)):not(:has(.panel)) {
    padding:12px;
  }
}
@media (max-width: 520px) {
  .kidwin .form2 { grid-template-columns:1fr; }
}
#pkgWinPaid .fixform { display:grid; gap:4px; margin:6px 0; padding:8px 10px;
  border:1px solid var(--line); border-radius:var(--radius-sm); background:var(--sunken); }
#pkgWinPaid .fixform .actions { display:flex; gap:6px; justify-content:flex-end; }
/* "This family has stopped coming", and the choice it asks for. */
.endwrap .endchoice { display:flex; flex-wrap:wrap; gap:4px 16px; margin:4px 0; }
.endwrap .endrefund { margin-top:4px; }
table.rows.edit .tiny { width:58px; min-width:0; margin-left:5px; }
table.rows.edit .lastcell { white-space:nowrap; }
table.rows.edit .progress { min-width:170px; }

/* A row with unsaved edits, so it is obvious what Save is about to write. */
table.rows.edit tr.touched td { background:var(--accent-bg); }
table.rows.edit tr.touched .kidcell { box-shadow:inset 3px 0 0 var(--accent); }

.count { font-size:11.5px; color:var(--faint); }
.unsaved {
  font-size:12px; color:var(--wait); background:var(--wait-bg);
  padding:4px 11px; border-radius:var(--radius-pill); font-weight:500;
}
.toolbar .check {
  display:flex; align-items:center; gap:7px; font-size:12.5px; color:var(--muted);
}
.toolbar .check input { width:auto; }

/* --------------------------------------------------------- history page --- */

.panel.corrections { padding-bottom:6px; }
.corr-head { display:flex; align-items:baseline; gap:12px; flex-wrap:wrap; margin-bottom:10px; }
.corr-head h2 {
  margin:0; font-size:11px; letter-spacing:.11em; text-transform:uppercase;
  color:var(--faint); font-family:"IBM Plex Mono", ui-monospace, monospace;
}
.corr-head .sub { font-size:12px; color:var(--muted); margin-left:auto; }

table.rows.corr td { vertical-align:middle; }
table.rows.corr td .sub { display:block; font-size:11.5px; color:var(--muted); font-weight:400; }
table.rows.corr input {
  width:62px; text-align:center; border:1px solid var(--line);
  background:var(--ground); color:var(--ink); border-radius:var(--radius-sm);
  padding:6px 4px; font:inherit; font-size:14px;
  font-family:"IBM Plex Mono", ui-monospace, monospace;
}
table.rows.corr input:focus { outline:2px solid var(--accent); outline-offset:-1px; }
/* The counted number under the corrected one: a box showing 9 on its own does
   not say whether that is what the sessions add up to or what somebody decided. */
table.rows.corr .of {
  display:block; font-size:10px; color:var(--faint); margin-top:3px;
  font-family:"IBM Plex Mono", ui-monospace, monospace;
}

.wholine {
  display:flex; align-items:center; gap:8px; flex-wrap:wrap;
  margin:18px 0 12px; font-size:16px;
}
.wholine > b { font-size:19px; font-weight:600; }
.wholine .tpill { color:#fff; font-size:11px; font-weight:600; padding:2px 10px; border-radius:var(--radius-pill); }
.wholine .facts {
  font-size:12.5px; color:var(--muted);
  font-family:"IBM Plex Mono", ui-monospace, monospace;
}
.wholine .facts b { color:var(--ink); font-weight:600; }

.histmonths { display:grid; grid-template-columns:repeat(auto-fit,minmax(290px,1fr)); gap:12px; }
.histmonth {
  background:var(--surface); border:1px solid var(--line);
  border-radius:var(--radius); padding:12px 14px 14px;
}
.hm-head { display:flex; align-items:baseline; gap:10px; margin-bottom:9px; }
.hm-head .sub { margin-left:auto; font-size:11.5px; color:var(--faint); }
.hm-week, .hm-grid { display:grid; grid-template-columns:repeat(7,1fr); gap:4px; }
.hm-week span {
  font-size:9px; letter-spacing:.08em; text-transform:uppercase; color:var(--faint);
  font-family:"IBM Plex Mono", ui-monospace, monospace; text-align:center; padding-bottom:3px;
}

/* A day the child was due. The colour is the outcome, so the pattern of a term
   is legible without reading a single date. */
.hd {
  position:relative;
  display:flex; flex-direction:column; align-items:center; justify-content:center;
  min-height:42px; border-radius:var(--radius-sm); border:2px solid transparent;
  font-family:"IBM Plex Mono", ui-monospace, monospace;
}

/* Who took the session, in her own colour, one stripe per session that day.
   Cover and swaps are the thing people scan a year for, and a name is too big
   to print ninety times - a colour is not. */
.hd .hflag {
  position:absolute; top:0; left:0; right:0; height:3px;
  display:flex; gap:1px; border-radius:var(--radius-xs) var(--radius-xs) 0 0; overflow:hidden;
}
.hd .hflag b { flex:1; background:var(--faint); }

/* THE MARK ITSELF, wherever it is drawn. Kid details positions it in the
   corner of a day; the slot panel sets it beside an outcome in a table row,
   which was reported as a session reading RS on its chip and nothing at all
   in the panel listing the same session. Two places, one mark - a third
   spelling is how the two come to disagree about what a reschedule looks
   like. The rule below refines this one for the calendar cell and is
   deliberately next to it. */
.rstag {
  font-size:9px; font-weight:800; font-style:normal; text-decoration:none;
  letter-spacing:.2px; color:var(--rs); background:var(--rs-bg);
  border-radius:3px; padding:0 3px; margin-left:4px; line-height:1.3;
  vertical-align:middle;
}

/* THE MARK HAS TO READ ON EVERY GROUND THE GRID PAINTS, which it did not have
   to when a day holding a make-up was always blue. Now the cell is coloured by
   what HAPPENED, so this blue sits on green, orange, red, grey and a dashed
   border - and 8px of --rs on --wait-bg is the one pairing that disappears.
   So it carries its own backing instead of borrowing the cell's. */
.hd .rstag {
  position:absolute; right:1px; bottom:1px;
  font-size:8px; font-weight:800; font-style:normal; text-decoration:none;
  letter-spacing:.2px; color:var(--rs);
  background:var(--surface); border-radius:2px; padding:0 2px; line-height:1.25;
  box-shadow:0 0 0 1px var(--rs-bg);
}
.hd.multi em { font-size:9px; letter-spacing:-.3px; }
.hd i { font-style:normal; font-size:11px; color:var(--muted); }
.hd em { font-style:normal; font-size:10px; font-weight:700; }
.hd.blank { border:0; }
.hd.none i { color:var(--faint); opacity:.55; }
.hd.done { border-color:var(--ok); background:var(--ok-bg); }
.hd.done i, .hd.done em { color:var(--ok); }
.hd.cx   { border-color:var(--wait); background:var(--wait-bg); }
.hd.cx i, .hd.cx em { color:var(--wait); }
.hd.ic   { border-color:var(--bad); background:var(--bad-bg); }
.hd.ic i, .hd.ic em { color:var(--bad); }
.hd.tent { border-style:dashed; border-color:var(--muted); }
.hd.open { border-color:var(--line); background:var(--raised); }
.hd.open i, .hd.open em { color:var(--muted); }

/* ------------------------------------------- planner: one child in full --- */

/* The whole point of this panel is deciding what to write for each child, so
   each one carries their own record rather than a status tag. */
.pk-counts {
  display:flex; align-items:center; gap:10px; margin-bottom:8px;
  font-size:11px; color:var(--faint);
  font-family:"IBM Plex Mono", ui-monospace, monospace;
}
.pk-counts b { color:var(--ink); font-size:12.5px; font-weight:600; margin-right:-6px; }
.pk-counts .span {
  margin-left:auto; background:var(--sunken); color:var(--muted);
  padding:1px 8px; border-radius:var(--radius-xs);
}

table.rows.pk-sess { margin-bottom:7px; }
table.rows.pk-sess td { padding:3px 6px; font-size:11.5px; border-bottom:0; }
table.rows.pk-sess tr:hover td { background:var(--raised); }

.plannedkid .hint { margin:0 0 8px; }

/* THE CONVERSATION, ACROSS THE FOOT OF THE BLOCK. Asked for with the live
   panel in a screenshot and an arrow at the empty space under the Add form:
   "it is defeating the purpose to use the existing space with maximum
   inoformation for the user ... move the watsapp chats to the horizontal rows
   below kids details".

   A 330px column beside the sessions was the first shape and the wrong one
   for this message. Measured over the 147 in the book: an outgoing reminder
   is 291 characters and TEN LINES OF ITS OWN, four of them blank, and
   incoming ones are a single line - so the column showed ONE message in
   360px while the space under the form sat empty. Full width and tightened
   (tighten() in shared.js draws a run of blank lines as one, taking nothing
   away but the air) the same band holds about three.

   IT SCROLLS AND OPENS AT THE NEWEST - "note that watsapp chats are having
   scrollable type so that user can see messages" - which scrollChats() in
   panels.js does after the write, because a scroll position is a property of
   a laid-out element.

   THE HEAD IS A BUTTON THAT FOLDS IT, and the band always opens: the clinic's
   own answer. Nothing is remembered, because a band that stayed shut on one
   device is a feature somebody loses by accident and reports as missing. */
.pkchat {
  display:flex; flex-direction:column; min-width:0; max-height:190px;
  margin-top:10px;
  border:1px solid var(--line); border-radius:var(--radius-sm); overflow:hidden;
}
/* THE HEAD IS A ROW OF TWO now: the fold, and the way into the real chat.
   It was one full-width <button>, and the second control cannot live inside
   it - a button inside a button is markup no browser agrees about - so the
   border that said "head" belongs to the row rather than to either of them.
   The fold still grows to fill whatever the second control leaves. */
.pkchat-head {
  display:flex; align-items:stretch; flex:none;
  border-bottom:1px solid var(--line); background:var(--wa-bar);
}
.pkchat-open {
  flex:none; align-self:center; margin-right:6px;
  font-size:11px; padding:3px 8px; color:var(--wa-ink);
}
.pkchat-open svg { width:13px; height:13px; }
.pkchat-top {
  display:flex; align-items:center; gap:7px; flex:1; min-width:0;
  padding:7px 10px; border:0;
  /* The chat window's own bar colour, so the head reads as WhatsApp's
     rather than as this panel's. --panel-2 was the first guess and is
     not a variable this app has; webcheck refuses an undefined one for
     the reason history_test records - CSS drops the whole declaration
     rather than just the colour. */
  font-size:12px; background:var(--wa-bar); color:var(--wa-ink);
  text-align:left; cursor:pointer; font:inherit;
}
.pkchat-top .grow { flex:1; }
.pkchat-mark {
  display:inline-flex; align-items:center; justify-content:center;
  width:22px; height:22px; border-radius:50%; background:#25D366; color:#fff;
}
.pkchat-mark svg { width:14px; height:14px; }
.pkchat-days {
  font-size:10px; letter-spacing:.04em; text-transform:uppercase;
  color:var(--muted); font-variant-numeric:tabular-nums;
}
.pkchat-caret { display:inline-flex; color:var(--muted); }
.pkchat-caret svg { width:14px; height:14px; transition:transform .15s var(--ease); }
/* FOLDED: the head keeps its place and the band gives up its height, so the
   block is shorter by exactly the conversation. */
.pkchat.shut { max-height:none; }
.pkchat.shut .wamsgs { display:none; }
.pkchat.shut .pkchat-head { border-bottom:0; }
.pkchat.shut .pkchat-caret svg { transform:rotate(-90deg); }

/* THE FREE THERAPIST'S PILL IS TWO CONTROLS when she owes a reschedule: the
   name opens her diary, as it always has, and RS:3 opens what she owes. A
   button cannot hold another button, so the pill is a span and both halves
   sit inside it - every rule .openpill had stays on the span, so a cell with
   nothing owed looks exactly as it did.

   Asked for in this shape: "RS:3 Nainsee inside the pill where RS means
   reschedule 3 means reschedule sessions count and Nainsee is the therapist
   name". */
.openpill-name, .openpill-rs {
  border:0; background:none; color:inherit; font:inherit; cursor:pointer;
  padding:0; line-height:inherit;
}
.openpill-name { padding:0 2px; }
/* DARKENED RATHER THAN COLOURED. The fill comes off the therapist's own row,
   so nothing here knows what it is sitting on - the Stats row's own count
   panel solves this the same way, with a translucent black that darkens any
   of the ten by the same amount. */
.openpill-rs {
  margin-right:4px; padding:0 4px; font-weight:700;
  font-variant-numeric:tabular-nums; letter-spacing:.02em;
  background:rgba(0, 0, 0, .28); border-radius:3px;
}
.openpill-rs:hover { background:rgba(0, 0, 0, .42); }
/* Nobody's: the ones a therapist who has left still owes. */
.openpill.none { background:var(--line); color:var(--muted); }
/* THE THERAPIST'S COLOUR, in the gap that was already in the header. Asked
   for so a child can be found while scrolling a panel of ten: everything
   else in that row is text at one size, and a band of colour is the only
   thing the eye finds without reading. Rounded and thin so it reads as a
   rule rather than as a control, and `--a` is unset when the child has no
   therapist - the bar is then invisible rather than an invented grey. */
.pk-bar {
  align-self:center; height:6px; min-width:18px; border-radius:3px;
  background:var(--a, transparent); margin:0 8px;
}

/* THE COPY MARK ON A CHIP. Quiet until wanted: a chip already carries a name,
   a time, a hash, a therapy code and up to five badges, and a sixth thing
   shouting would make the row unreadable. It comes up on hover and focus, and
   is always visible to a keyboard.

   THE CONFIRMATION IS THE MARK ITSELF, through a data attribute rather than a
   toast - a tiny answer for a tiny act, where the pointer already is.

   AND IT IS DRAWN IN THE MARK'S PLACE, NOT ABOVE IT. The first version floated
   the word over the button, and every chip is overflow:hidden - it has to be,
   or a long name and five badges run out of the cell - so the word was cut
   off by the chip's own edge: the lower half of "copied" on a Calendar chip,
   a two-pixel green sliver on Planning's one-line chip. Measured by rendering
   both chips with these stylesheets, not argued. It was found when Planning
   asked for the same mark "so that user ... gets the text as 'copied'" -
   which, on the Calendar, nobody ever had. In flow, inside the chip, there is
   nothing that can clip it; the name gives way for the second it is shown,
   which is what the name is already built to do. */
.sess-copy {
  border:0; background:none; padding:0 2px; cursor:pointer; line-height:1;
  color:inherit; opacity:.35; transition:opacity .12s var(--ease);
}
.sess-copy svg { width:11px; height:11px; display:block; }
.sess:hover .sess-copy, .sess-copy:focus-visible { opacity:.9; }
.sess-copy.done, .sess-copy.failed { opacity:1; }
.sess-copy.done svg, .sess-copy.failed svg { display:none; }
/* ::before, BECAUSE ::after IS THE PAD BELOW - and ::after is where this
   app's tap pads live, which is what tools/mobilecheck.py reads to credit
   one. */
.sess-copy.done::before, .sess-copy.failed::before {
  content:attr(data-said); display:inline-block; white-space:nowrap;
  font-size:9px; font-weight:700; letter-spacing:.03em; line-height:1.4;
  padding:1px 5px; border-radius:3px;
  background:var(--ok); color:#fff;
}
.sess-copy.failed::before { background:var(--bad); }
/* A THUMB-SIZED TARGET ON A PHONE, measured there: the mark is 15x11, and
   missing it lands on the chip, which narrows the whole grid to that child -
   the opposite of what was asked for. The real handset found it on Calendar
   and Staff; Planning draws the same mark.

   A PAD, NOT A BIGGER BUTTON, the .sess-name way: a transparent ::after
   takes the tap and no chip grows a pixel. It reaches up and down to the
   chip's own edge (which clips it, so it can never land on the chip above)
   and mostly RIGHTWARDS, onto the therapy badges - spans, nothing to steal
   from - and only 4px left, because the name's own pad is on that side.

   TOUCH ONLY. With a mouse the mark is easy to hit as it is, and a pad there
   would only take clicks meant for the chip. And on a touch screen there is
   no hover to bring the mark up from .35, so it starts at .6 - a control that
   can only be found by tapping blindly is not a control. */
@media (pointer: coarse) {
  .sess-copy { position:relative; opacity:.6; }
  .sess-copy::after {
    content:""; position:absolute; top:-9px; bottom:-9px; left:-4px; right:-10px;
  }
}

.plannedkid .linkbtn.more { display:block; margin:0 0 9px; font-size:11.5px; }
.addform .ended { width:78px; }
/* The day this panel was opened from, among the child's usual days. */
.whenpill.on { background:var(--accent-bg); color:var(--accent); font-weight:600; }
.vs {
  font-size:11px; letter-spacing:.09em; text-transform:uppercase; color:var(--faint);
  font-family:"IBM Plex Mono", ui-monospace, monospace;
}

/* The comparison pickers read as one control rather than two stray dates. */
.toolbar .dateinput { min-width:142px; }
#calSwap { padding:7px 11px; font-size:14px; line-height:1; }

/* A therapist taken from the plan rather than recorded. It is a good guess and
   must not look like a fact. */
td.guessed { color:var(--muted); }
.guess {
  margin-left:4px; font-size:9px; font-weight:700; color:var(--wait);
  background:var(--wait-bg); padding:0 4px; border-radius:var(--radius-xs); vertical-align:1px;
}
.panel.notice.thin { padding:11px 14px; }
.panel.notice.thin .lead { margin:0; font-size:12.5px; }
/* A package naming a different therapy from its therapist's. Legitimate, but
   worth seeing - it is usually a typo. */
td.mismatch input { border-color:var(--wait); }

/* Who gets the daily message. Reception opens this to answer "is everybody
   on?", which is a counting question, so the count sits in the bar and not at
   the bottom of a list of ninety. */
.whobar {
  display:flex; align-items:center; gap:8px; flex-wrap:wrap;
  padding-bottom:10px; margin-bottom:10px; border-bottom:1px solid var(--line);
}
.whobar .whocount { font-family:"IBM Plex Mono", ui-monospace, monospace; font-size:15px; }

.whogrid {
  display:grid; gap:2px 14px;
  grid-template-columns:repeat(auto-fill, minmax(215px, 1fr));
}
.whoitem {
  display:flex; align-items:center; gap:7px;
  padding:3px 5px; border-radius:var(--radius-sm); cursor:pointer; min-width:0;
}
.whoitem:hover { background:var(--raised); }
.whoitem input { margin:0; flex:none; }
.whoitem .whodot { width:9px; height:9px; border-radius:var(--radius-xs); flex:none; }
.whoitem .whoname {
  flex:1; min-width:0; overflow:hidden; text-overflow:ellipsis; white-space:nowrap;
}
.whoitem .whoth { font-size:11px; flex:none; max-width:82px;
  overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }

/* Left out, and saying so plainly - the tick alone is easy to miss in ninety. */
.whoitem.off .whoname { color:var(--muted); text-decoration:line-through; }
.whoitem.off .whodot { opacity:.3; }
.whoitem.off .whoth { opacity:.45; }

/* A heading between groups of panels, for a tab that holds two subjects -
   your own login, and everybody else's. */
.sechead {
  margin:18px 2px 8px; font-size:13px; font-weight:700;
  text-transform:uppercase; letter-spacing:.06em; color:var(--muted);
}
.sechead:first-child { margin-top:2px; }

/* The messages log: a failure has to read as one at a glance in 500 rows. */
.err-inline { color:var(--bad); }
.rows td.wrap {
  white-space:normal; max-width:520px; line-height:1.45;
  font-size:12px; color:var(--muted);
}
.rows tr.current > td { background:var(--ok-bg); }

/* ---- WhatsApp automation: a health banner, then three cards -------------- */

/* THE BANNER WEARS THE STATE IT IS REPORTING. It was a card in the ordinary
   card colour with a four-pixel stripe down one edge, which says "healthy"
   only to somebody already looking for the stripe - and this is the one line
   on the page read from across a desk. The whole panel is tinted in its own
   --a now, so healthy and not-configured are told apart before the sentence
   has been read, and one rule serves both. */
.ahero {
  --a:var(--ok);
  display:flex; align-items:center; gap:16px; flex-wrap:wrap;
  padding:18px 20px; margin-bottom:14px; border-radius:var(--radius);
  background:color-mix(in srgb, var(--a) 9%, var(--surface));
  border:1px solid color-mix(in srgb, var(--a) 32%, var(--line));
  border-left-width:4px; border-left-color:var(--a);
}
.ahero.warn { --a:var(--wait); }
.ahero h2 { margin:0 0 3px; font-size:16.5px; letter-spacing:-.01em; }
.ahero p  { margin:0; color:var(--muted); font-size:13px; max-width:62ch; }
.ahero .agrow { flex:1 1 320px; min-width:0; }

/* A FILLED DISC, not a tinted one. The mark is the first thing the eye lands
   on and it has one job: say which of two states this is, at a glance. */
.ahero .amark {
  flex:none; width:46px; height:46px; border-radius:50%;
  display:grid; place-items:center; color:#fff; background:var(--a);
  box-shadow:0 0 0 5px color-mix(in srgb, var(--a) 16%, transparent);
}
.ahero .amark .navicon { width:24px; height:24px; stroke-width:2.4; }

/* ------------------------------------------- choosing a rule, not a value -

   The auto-reply page asks three questions where each answer needs a sentence
   beside it: "outside desk hours" and "every message, at any hour" are not
   self-explaining, and the difference between them is a parent's phone buzzing
   while reception is sitting there. A <select> cannot carry that sentence, and
   a tooltip is not read by somebody deciding.

   So each option is a row: the radio, the name, and the consequence
   underneath. The whole row is the label, which also makes it a target a thumb
   can hit - the radio itself is 20px on a phone and these rows are stacked. */
.achoices { display:flex; flex-direction:column; gap:6px; margin-bottom:14px; }
/* The description under each choice, on its own line.
   It had none, so it ran straight on from the bold label it belongs to and
   the page read "The markThe centre's logo" - which looks like a missing
   space rather than a missing rule, and had survived because the eye supplies
   the break that the markup does not. .pinswitch solves the same shape the
   same way; this is that rule, here. */
.achoices .check .sub {
  display:block; margin-top:3px; font-size:12px; line-height:1.5;
  color:var(--muted);
}
.arow {
  display:flex; align-items:flex-start; gap:10px; cursor:pointer;
  padding:10px 12px; border:1px solid var(--line); border-radius:var(--radius);
  background:var(--surface);
}
.arow:hover { border-color:var(--faint); }
.arow.on { border-color:var(--accent); background:var(--accent-bg); }
.arow input { margin-top:2px; flex:none; }
.arow .atext { display:flex; flex-direction:column; gap:2px; }
.arow .atext b { font-size:13px; font-weight:600; }
.arow .atext .sub { font-size:11.5px; color:var(--muted); line-height:1.45; }

/* The message itself. A one-line box invites a one-line answer, and this is
   the only text in the app that goes to a family without anybody reading it
   first - it is worth seeing all of it while writing it. */
textarea.dateinput.wide {
  width:100%; min-height:84px; resize:vertical; line-height:1.5;
  margin-bottom:12px;
}

.astat {
  flex:none; display:flex; flex-direction:column; gap:4px;
  padding-left:20px; border-left:1px solid var(--line); min-width:112px;
}
/* The app's own figure-and-caption, the one .rsfig wears on the Reschedules
   tiles: a mono caption in small capitals under a figure that is the point. */
.astat .l {
  font-size:10px; font-family:"IBM Plex Mono", ui-monospace, monospace;
  letter-spacing:.09em; text-transform:uppercase; color:var(--faint);
}
.astat .v { font-size:17px; font-weight:700;
            font-family:"IBM Plex Mono", ui-monospace, monospace; }
.astat .v.good { color:var(--ok); }
.astat .v.bad  { color:var(--bad); }
.astat .v em   { font-style:normal; font-size:11px; font-weight:600; }
.astat .v em.bad { color:var(--bad); }

.acards {
  display:grid; gap:12px; margin-bottom:14px;
  grid-template-columns:repeat(auto-fit, minmax(290px, 1fr));
}
.acard {
  padding:16px 18px; border:1px solid var(--line);
  border-radius:var(--radius); background:var(--raised);
}
.acard h3 {
  margin:0 0 12px; font-size:15px; letter-spacing:-.01em;
  display:flex; align-items:center; gap:11px; flex-wrap:wrap;
}
/* EVERY FACT ON ITS OWN LINE, ruled. Four pairs in a bare grid run together
   into a block of text, and what somebody comes to this card for is ONE of
   them - the time it sends, or whether test mode is off. */
.acard .kv {
  margin:0; gap:0; grid-template-columns:minmax(0, auto) minmax(0, 1fr);
}
.acard .kv dt, .acard .kv dd {
  padding:8px 0; border-top:1px solid var(--line-soft); min-width:0;
}
.acard .kv dt { padding-right:18px; }
.acard .kv > dt:first-of-type,
.acard .kv > dt:first-of-type + dd { border-top:0; padding-top:0; }
.acard .hint { margin-top:12px; }

/* The sentence saying this page changes nothing, with the mark that says so
   beside it rather than a paragraph on its own in a card. */
.anote { display:flex; align-items:center; gap:13px; }
.anote .lead { margin:0; }

/* ---- Template versions -------------------------------------------------- */

/* THE TEMPLATES TAB'S TWO FILTERS (22 October 2026) sit in the empty half of
   the Change history row, as asked with it circled. The history block steps
   out of the way (display:contents) so its bar, the filters and its list are
   items of one row: the filters beside the button, and the list, once opened,
   on a line of its own under both. Without the history permission the row is
   the filters alone. */
.tplbar { display:flex; flex-wrap:wrap; align-items:center; gap:8px 14px; margin:4px 0 12px; }
.tplbar > .chgblock.pagechg { display:contents; }
.tplbar > .chgblock > .chgbody { flex-basis:100%; order:2; margin-top:0; }
.tplfilters { display:flex; flex-wrap:wrap; gap:8px; align-items:center; min-width:0; }
.tplfilters select { max-width:100%; min-width:0; }
.tcols { display:grid; gap:12px; grid-template-columns:minmax(0,1fr) minmax(0,1fr); }
@media (max-width: 900px) { .tcols { grid-template-columns:minmax(0,1fr); } }

.tver {
  padding:12px 14px; margin-bottom:10px;
  /* var(--bg), which does not exist, so this card has had no background at
     all since it was written - it inherited the panel behind it and looked
     deliberate. --raised is the token for a card sitting on a panel. */
  border:1px solid var(--line); border-radius:var(--radius); background:var(--raised);
}
.tver.using { border-color:var(--ok); }
/* A version's picture at the top (0345), small beside the words saying which. */
.tvpic { display:flex; align-items:center; gap:8px; }
.tvpic img { width:52px; height:52px; object-fit:contain; border-radius:6px;
  background:#fff; border:1px solid var(--line); flex:none; }
.tvhead { display:flex; align-items:center; gap:8px; flex-wrap:wrap; }
.tvhead .sub { font-family:"IBM Plex Mono", ui-monospace, monospace; font-size:11px; }
.tvsub { font-size:11px; color:var(--muted); margin-top:3px; }
.tvacts { display:flex; gap:6px; flex-wrap:wrap; margin-top:10px; }

/* A sample for each blank of a new template (0280), under its body: the
   blank's number marked as it is in the wording, and a box beside it. One
   row each, because the blanks are read in order and a reviewer will read
   them in order too. */
.tsamples { display:grid; gap:6px; margin-top:8px; }
.tsamples .hint { margin:0 0 2px; }
/* Two classes, because `.form2 label` (a class and an element) makes every
   label in the form a block - which put each blank above its box rather
   than beside it, measured on the live form. */
.tsamples .tsample { display:flex; align-items:center; gap:8px; margin:0; }
.tsamples .tsample .tplblank {
  flex:none; min-width:3.4em; font-weight:700; color:var(--wa-ink); text-align:center;
  border-radius:4px; padding:1px 4px; background:var(--wa-mine);
}
.tsamples .tsample input { flex:1 1 auto; min-width:0; }
.tvsamples { margin:6px 0 0; }

/* Two rows of counts: this package, then the whole record. The scope label is
   what stops them reading as one long line of numbers. */
.pk-counts .pk-scope {
  font-size:9.5px; text-transform:uppercase; letter-spacing:.07em;
  color:var(--muted); min-width:52px;
}
.pk-counts.all { margin-top:-3px; opacity:.72; }
.pk-counts.all b { font-weight:500; }

/* A required field left empty, once somebody has been in it. Outlined rather
   than filled: the point is to find it, not to shout. */
.needs {
  border-color:var(--bad) !important;
  box-shadow:0 0 0 2px color-mix(in srgb, var(--bad) 18%, transparent);
}

/* A divider inside a form, where the fields below answer a different question
   from the ones above. */
hr.rule { border:0; border-top:1px solid var(--line); margin:4px 0 2px; }

/* ---- Dashboard ---------------------------------------------------------- */

/* Four to a row, not as many as happen to fit.

   auto-fit with a 150px minimum gave whatever the window width allowed - at
   1280px that was seven, so the eighth figure sat alone on a second row with
   six empty columns beside it, and the strip read as a mistake rather than a
   layout. A fixed count per breakpoint keeps the rows even at every width the
   clinic actually uses. */
.dstrip {
  display:grid; gap:10px; margin-bottom:14px;
  grid-template-columns:repeat(4, minmax(0, 1fr));
}
@media (max-width: 1080px) { .dstrip { grid-template-columns:repeat(3, minmax(0, 1fr)); } }
@media (max-width: 820px)  { .dstrip { grid-template-columns:repeat(2, minmax(0, 1fr)); } }

/* ------------------------------------------- today, this week, this month -

   The same five figures asked three ways, so the period sits ON the strip
   rather than above it as a separate control - it belongs to these cards and
   to nothing else on the page.

   Five cards rather than the four the grid above assumes. They are all one
   number and a short label, so they take a narrower column happily, and
   splitting five across four columns would leave one card alone on a second
   row looking like an afterthought. */
.mstrip { margin-bottom:14px; }
.mstrip .dstrip { grid-template-columns:repeat(5, minmax(0, 1fr)); margin-bottom:0; }
.mstrip-when { margin-bottom:10px; }
@media (max-width: 1200px) {
  .mstrip .dstrip { grid-template-columns:repeat(3, minmax(0, 1fr)); }
}
@media (max-width: 820px) {
  .mstrip .dstrip { grid-template-columns:repeat(2, minmax(0, 1fr)); }
}

/* "367 sessions with no rate" under a money figure. Amber rather than red:
   it is a gap in the price list, not a fault - and the figure above it is
   still true of the sessions it could price. */
.warnnote { color:var(--wait); }

/* The colour was a 3px rule across the top of the card, which at a glance is
   the same shape as the underline marking the current page in the nav bar.
   It moves to the figure, which is the thing the colour is about: 78% is
   green because attendance is good, and the card it sits on has no opinion. */
.dbig {
  padding:15px 17px; border:1px solid var(--line);
  border-radius:var(--radius); background:var(--surface);
  box-shadow:var(--shadow-xs);
}
.dbig.good .n { color:var(--ok); }
.dbig.warn .n { color:var(--wait); }
.dbig .n {
  font-family:"IBM Plex Mono", ui-monospace, monospace;
  font-size:27px; font-weight:700; line-height:1.1; letter-spacing:-.03em;
}
.dbig .l { margin-top:3px; font-size:12px; font-weight:600; }
.dbig .s { margin-top:4px; font-size:11px; color:var(--faint); line-height:1.4; }

.dcols { display:grid; gap:12px; }

/* An attendance rate reads as a column of bars, not a column of numbers. */
.dbar {
  display:inline-block; vertical-align:middle;
  width:74px; height:7px; border-radius:var(--radius-xs);
  background:var(--sunken); overflow:hidden;
}
.dbar i { display:block; height:100%; background:var(--muted); }
.dbar.good i { background:var(--ok); }
.dbar.warn i { background:var(--wait); }
.dbarn {
  margin-left:7px; font-size:11px; color:var(--muted);
  font-family:"IBM Plex Mono", ui-monospace, monospace;
}

.rows tr.totalrow > td {
  border-top:2px solid var(--line); background:var(--sunken);
}
.rows tr.muted-row > td { color:var(--muted); }

.swatchrow { display:flex; gap:8px; align-items:center; }
.swatchrow input[type="color"] {
  width:44px; height:34px; padding:2px; cursor:pointer;
  border:1px solid var(--line); border-radius:var(--radius-sm); background:var(--raised);
}
.swatchrow input[type="text"] { flex:1; min-width:0; }

/* ---- Charts ------------------------------------------------------------- */

/* Two across at most. Four charts in a row on a wide screen is four narrow
   charts, and the axis labels are the first thing to go. */
.cgrid {
  display:grid; gap:12px; margin-bottom:14px;
  grid-template-columns:repeat(auto-fit, minmax(440px, 1fr));
}
@media (min-width: 1500px) { .cgrid { grid-template-columns:repeat(2, 1fr); } }

/* ------------------------------------------------- the payment dashboard ---
   Two panels side by side once there is room for them. The rings are small
   and squat beside a paragraph of explanation, so they fit where a third
   time-series chart would not. */
.cgrid.two { grid-template-columns:repeat(auto-fit, minmax(300px, 1fr)); }

/* A ring is square and should not stretch to the panel it is in - at full
   width it becomes a hoop with a hole you could drive through. */
.chart.pie { max-width:260px; margin:4px auto 2px; }
.chart.pie .pietotal {
  fill:var(--ink); font-size:20px; font-weight:650; text-anchor:middle;
  font-family:"IBM Plex Sans", system-ui, sans-serif;
}
.chart.pie .piesub {
  fill:var(--faint); font-size:11px; text-anchor:middle; letter-spacing:.06em;
  text-transform:uppercase;
}
/* The key under a ring is labels, not buttons - see keyFor in charts.js. */
.clegend.pkey { justify-content:center; }
.clegend.pkey span { font-size:12px; color:var(--muted); }
.clegend.pkey b { color:var(--ink); font-weight:600; }

/* Said once, under the tiles, because every figure on the page depends on it:
   this is what was SOLD, not what was collected. */
.moneynote { margin:10px 0 14px; max-width:78ch; }

/* The page is correct and empty, which looks exactly like the page being
   broken. Amber rather than red: nothing has gone wrong, there is simply
   nothing entered yet. */
.panel.warnpanel { border-color:var(--wait); background:var(--wait-bg); }
.panel.warnpanel h2 { color:var(--wait); }
.cpanel { margin-bottom:0; }

/* The viewBox does the scaling; height is fixed so a row of charts lines up. */
.chart { display:block; width:100%; height:auto; margin-top:6px; }
/* Gridlines are scaffolding, not data. Drawn in the line colour they were as
   dark as the bars' own edges and the chart read as a grid with bars in it. */
.chart .grid { stroke:var(--line-soft); stroke-width:1; }
.chart .grid.base { stroke:var(--line); }
.chart .ytick {
  fill:var(--faint); font-size:10.5px; text-anchor:end;
  font-family:"IBM Plex Mono", ui-monospace, monospace;
}
.chart .xtick { fill:var(--faint); font-size:10.5px; text-anchor:middle; }
/* The value over each bar was full-strength ink at 700: twelve of them across
   a chart, each heavier than the axis, so the numbers were read before the
   shape was - and the shape is the reason to draw a chart at all. */
.chart .vlabel {
  fill:var(--muted); font-size:11px; text-anchor:middle; font-weight:600;
  font-family:"IBM Plex Mono", ui-monospace, monospace;
}

.clegend {
  display:flex; flex-wrap:wrap; gap:4px 14px; margin-top:10px;
  font-size:11px; color:var(--muted);
}
.clegend span { display:inline-flex; align-items:center; gap:5px; }
.clegend i { width:9px; height:9px; border-radius:var(--radius-xs); }

/* A segmented control sitting inside a heading rather than a toolbar. */
.seg.inline { display:inline-flex; margin-left:10px; vertical-align:middle; }
label.inline { font-size:11px; color:var(--muted); margin:0 2px 0 6px; }

/* ---- Recently deleted -----------------------------------------------------
   The refusal is the part that matters. A restore that cannot happen has to
   say who is in the way by name, in the row it belongs to, rather than as a
   toast that is gone before somebody has read it. */
.binlist td.wrap b { font-weight:600; }
.binwhy > td { background:var(--bad-bg); padding:9px 12px; }
.binwhy .lead { margin:0 0 6px; color:var(--bad); }
.binconf { margin:0; padding-left:18px; display:grid; gap:4px; }
.binconf li { font-size:12.5px; line-height:1.5; color:var(--ink); }

/* ---- The lunch break, across the Calendar and the Planning grid --------
   One band rather than a cell per day, because it is the same break in every
   column. Deliberately quiet: it is a landmark somebody reads past on the way
   to a session, not a thing to look at. Nothing in it books anybody - there is
   nothing to book into lunch - but on the Calendar each day of it takes a
   note, because something does happen in that hour. */
.lunchband > td {
  padding:0; background:var(--sunken);
  border-top:1px solid var(--line); border-bottom:1px solid var(--line);
}
/* WITH A CELL PER DAY it must still read as ONE stripe across the week, which
   is the whole point of the band - so the cells carry no vertical borders
   between them and keep the sunken ground. The Calendar draws it this way
   because a note belongs to a date; Planning, whose columns are weekdays,
   keeps the single spanning cell above. */
.lunchband.withcells > td { border-left:0; border-right:0; }
.lunchband.withcells > td.lunchcell { padding:4px 6px; vertical-align:top; }
.lunchband.withcells > td.slotcell { padding:0; }
/* Quiet until it is wanted or there is something to say. This row is
   furniture, and a pencil on every day of it would compete with the sessions
   above and below for attention it does not deserve. */
.lunchband .notebtn { opacity:.35; }
.lunchband:hover .notebtn,
.lunchband .notebtn:focus-visible,
.lunchband .notebtn.has { opacity:1; }
.lunchband .cellnote { margin:0 0 3px; }
/* PINNED, the way every other row pins its slot label - and for the same
   reason. The band spans the whole grid, so on a phone (688px of table in
   375px of screen) its words were the first thing to scroll out of sight,
   leaving an unexplained empty stripe across the calendar. Sticking the
   contents rather than adding a heading cell keeps the band one continuous
   thing, which is the whole point of it, and does better than the slot rows:
   the label is readable wherever you have scrolled to. */
.lunchin {
  position:sticky; left:0;
  display:inline-flex; align-items:baseline; gap:8px;
  padding:4px 10px; white-space:nowrap;
}
.lunchband .lu {
  font-size:10.5px; font-weight:700; letter-spacing:.06em;
  text-transform:uppercase; color:var(--muted);
}
.lunchband .lut {
  font-family:"IBM Plex Mono", ui-monospace, monospace;
  font-size:11px; color:var(--ink);
}
.lunchband .sub { font-size:10px; color:var(--muted); }

/* Two times side by side on Settings, reading as one field rather than two. */
.lunchfields { display:flex; align-items:center; gap:8px; max-width:280px; }
.lunchfields input { flex:1; min-width:0; }
.lunchfields .to { font-size:12px; color:var(--muted); }

/* ---- One therapist's diary ---------------------------------------------
   Slots down, days across - the same shape as the calendar it opens from, so
   nobody has to learn a second grid. Deliberately plainer than the calendar:
   the question here is how full she is, and a cell has only three answers to
   give. */
.occgrid { font-size:12px; }

/* THE DIARY IS A TIMELINE NOW, not a slot grid, and the difference shows in
   one place: there is no left-hand column of times any more, because the
   times are not shared between the days. Tuesday's three o'clock and
   Wednesday's three o'clock are on different lines, and each block says when
   it is. That is the honest shape when ninety of the 394 plan lines run at a
   time their row does not mention - a shared axis can only be right if
   everything sits on it. */
.occgrid.diary td.occ { min-width:132px; }
.occgrid.diary thead th { text-align:left; }

/* Her shift, under the day. The column starts and ends at these rather than
   at the first and last row of a grid everybody shares, so they are the two
   numbers that say what the column covers. */
.occhours {
  display:block; font-size:10px; font-weight:500; color:var(--faint);
  letter-spacing:0; text-transform:none; margin-top:1px;
}

/* A booked stretch: the time it starts, then whoever is in it. More than one
   name means they overlap - two children whose sessions run into each other,
   which the timeline merges rather than pretending is two separate blocks. */
.occblock { display:flex; align-items:flex-start; gap:4px; margin-bottom:2px; }
.occblock .occat {
  flex:none; width:44px; font-size:10px; line-height:20px;
  color:var(--muted); font-variant-numeric:tabular-nums;
}
.occblock > .occchip, .occblock > button { flex:1 1 auto; min-width:0; }
/* Called off: the hour came back, so it is not part of the timeline the gaps
   are measured from - but it is still shown, because "she had four and two
   called off" is a different afternoon from "she had two". */
.occblock.off { opacity:.6; }
.occblock.off .occchip { text-decoration:line-through; }

/* The time on a free pill. It did not need one when every pill in a row began
   at the row's time; on a timeline it is the only thing saying WHEN. */
.pillat {
  font-size:10px; font-weight:600; opacity:.75; margin-right:5px;
  font-variant-numeric:tabular-nums;
}

.occgrid td.occ { padding:3px 4px; vertical-align:top; min-width:88px; }
.occgrid td.occ.shut { background:var(--sunken); }
/* Not-free is left blank on purpose. She is on leave, or had not started, or
   has left - none of which is occupancy and none of which is bookable, so a
   label would be three different explanations in one cell. */
.occgrid td.occ.away { background:repeating-linear-gradient(
  135deg, transparent, transparent 4px, var(--line) 4px, var(--line) 6px); }
/* Working at the OTHER branch, while a branch filter is on. Her hour is gone
   either way, but it is not leave, and the hatch above is what leave looks
   like - so this gets a cell of its own that says where she is instead. */
.occgrid td.occ.elsewhere { background:var(--sunken); }
.occelse { font-size:10px; line-height:1.35; color:var(--muted); padding:2px; }

.occchip {
  appearance:none; cursor:pointer; font:inherit; text-align:left; width:100%;
  display:flex; align-items:center; gap:5px;
  padding:3px 6px; margin-bottom:2px;
  border:1px solid var(--line);
  /* Hers, the same 6px band the calendar chip carries. It was var(--accent),
     which is green and already means something else in this app - so every
     chip in every diary came out the same green whoever it belonged to. */
  border-left:6px solid var(--kidcolour, var(--line));
  border-radius:var(--radius-xs); background:var(--surface); color:var(--ink);
}
.occchip:hover { border-color:var(--faint); background:var(--raised); }
.occchip b { font-size:11.5px; font-weight:600; }
/* The session number staff quote, in the mono the rest of the app numbers
   with. Plain rather than the calendar's plated .snum: the cell is 88px and a
   plate at that size is mostly plate. */
.occchip .n {
  font-family:"IBM Plex Mono", ui-monospace, monospace;
  font-size:10px; font-weight:700; color:var(--muted); flex:none;
}
.occchip .badge.g { flex:none; }
.occchip .code {
  font-family:"IBM Plex Mono", ui-monospace, monospace;
  font-size:9px; color:var(--muted); margin-left:auto;
}
/* Supporting somebody else, and jobs for the slot. Both are time she is
   spoken for without a chip of her own, which is exactly what was missing
   from a day that looked half empty. */
.occsup, .occtask {
  font-size:10.5px; line-height:1.35; padding:3px 6px; margin-bottom:2px;
  border-radius:var(--radius-xs); border:1px solid transparent;
}
.occsup { background:var(--rs-bg); color:var(--rs); }
/* Dashed, the same way the Staff schedule marks a partial one - she is in the
   room for part of it, so the slot is not closed and can still be booked. */
.occsup.partial { border-style:dashed; border-color:currentColor; background:none; }
.occsup .sub { font-size:9.5px; }
.occtask { background:var(--wait-bg); color:var(--wait); }
.occtask.done { text-decoration:line-through; opacity:.6; }

/* WHAT IS LEFT OF A SLOT SHE CANNOT FILL.

   A thirty-minute therapist on a forty-five-minute grid finishes a quarter of
   an hour before the next session time. That quarter hour is hers and is not
   long enough for a child, and it only appears for somebody whose session is
   shorter than the grid - everybody on the ordinary length has no remainder
   and never sees one of these.

   Deliberately NOT a pill. The pills beside it are her colour and are
   BOOKABLE; this is the opposite of a thing to click, so it takes none of
   that - no background of its own, a dashed edge to say the time is unclaimed
   rather than allocated, and the muted ink every other "nothing here" in this
   app uses. Making it look like a pill would put an unbookable button next to
   two bookable ones in the same colour. */
.occgap {
  display:block; margin-top:3px; padding:2px 6px;
  border:1px dashed var(--line); border-radius:var(--radius-xs);
  font-size:9.5px; line-height:1.4; color:var(--faint);
  text-align:center; cursor:default;
}

/* Struck through, the same way the calendar marks one, so a cancelled session
   reads as "was booked and is not" rather than as an ordinary booking. */
.occchip.st-cancelled b, .occchip.st-immediate b {
  text-decoration:line-through; color:var(--muted);
}

/* ---- Picking a therapist by colour, not by coloured text ---------------- */

.tpick { position:relative; }
/* THE ONE THAT WAS REPORTED, and it was fainter than its neighbours twice
   over. Every other field in a dialog is filled with --ground, a well slightly
   darker than the dialog; this one used --raised, which is slightly LIGHTER
   than the dialog - so on a form it read as a gap between two labels rather
   than as a control. With "unassigned" in it there was no text to give it away
   either.

   --ground to match the others, and the same --faint edge, so a therapist box
   now looks like the boxes above and below it rather than like nothing. */
.tpick-face {
  display:flex; align-items:center; gap:7px; width:100%;
  padding:8px 10px; border:1px solid var(--faint); border-radius:var(--radius-sm);
  background:var(--ground); color:var(--ink); font:inherit; cursor:pointer;
  text-align:left;
}
.tpick-face:hover { border-color:var(--muted); }
.tpick.off .tpick-face { opacity:.55; cursor:not-allowed; }
.tpick .caret { font-style:normal; font-size:10px; color:var(--muted); }

/* THE OPEN LIST IS ON THE BODY, fixed to the window at the face's own
   position - therapist.js moves it there while it is open and says why: a
   list positioned from inside a scrolling wrapper is clipped to the wrapper,
   and the plan dialog's rows table is one. So nothing here says WHERE; the
   left, width, top or bottom and the height it may have are measured when it
   opens and written inline. Above the dialog's z-index of 100, because a
   list on the body is outside the dialog's own stacking context and would
   otherwise open behind the form it belongs to. */
.tpick-list {
  position:fixed; z-index:150; box-sizing:border-box;
  max-height:250px; overflow:auto; padding:4px;
  border:1px solid var(--line); border-radius:var(--radius-sm);
  background:var(--raised); box-shadow:var(--shadow-md);
}

.tpick-opt {
  display:flex; align-items:center; gap:7px; width:100%;
  padding:6px 8px; border:0; border-radius:var(--radius-sm);
  background:none; color:var(--ink); font:inherit; cursor:pointer;
  text-align:left;
}
.tpick-opt:hover { background:var(--sunken); }
.tpick-opt[aria-selected="true"] { background:var(--ok-bg); }
.tpick .dot { width:11px; height:11px; border-radius:50%; flex:none; }
.tpick .dot.none { background:transparent; border:1px dashed var(--line); }
.tpick .code {
  padding:0 4px; border-radius:var(--radius-xs); font-size:9px; font-weight:700;
  background:var(--sunken); color:var(--muted);
}

/* Somebody who is off that day. Dimmed and labelled rather than removed: the
   database refuses the booking, so this is the warning that arrives BEFORE
   the form has been filled in, and a name that vanished would read as the app
   losing a therapist rather than as her being on leave. */
.tpick-opt.away { opacity:.55; }
.tpick-opt.away::after {
  content:'on leave'; margin-left:auto; font-size:9px; font-weight:700;
  letter-spacing:.03em; text-transform:uppercase; color:var(--wait);
  background:var(--wait-bg); padding:1px 6px; border-radius:var(--radius-pill);
}
.tpick.away > .tpick-face { border-color:var(--wait); background:var(--wait-bg); }

/* A date box narrow enough to sit in a table column. */
.dateinput.tiny { padding:3px 6px; font-size:12px; min-width:132px; }

/* A second toolbar inside the page, for a section with its own controls. */
.sub-toolbar { margin-top:4px; }

/* Anything in a chart that can be clicked to isolate one series. */
.chart .pick { cursor:pointer; }
.chart .pick:hover { opacity:.78; }
/* A REAL TAP TARGET, measured on a handset rather than emulated. At
   font-size 11 with 2px of padding this was 23px tall, and a finger misses
   more often than it lands under about 28. It is the control that filters
   the Dashboard's chart, and there are six of them in a row.
   Found by tools/mobilecheck.py --port against a phone over adb; the 375px
   emulation had reported the page clean. */
.clegend button.pick {
  border:1px solid transparent; border-radius:var(--radius-pill); padding:2px 9px 2px 5px;
  background:none; color:var(--muted); font:inherit; font-size:11px;
  cursor:pointer; display:inline-flex; align-items:center; gap:5px;
  min-height:28px;
}
.clegend button.pick:hover { border-color:var(--line); color:var(--ink); }
.clegend button.pick.on {
  border-color:var(--ink); color:var(--ink); font-weight:600;
}
.clegend button.pick.clear { color:var(--bad); }

/* ---- The master switch -------------------------------------------------- */

/* Deliberately not shaped like the panels around it. This is the one control
   on the page that silences a whole channel, and it should not read as one
   more setting among nine. */
.gate {
  display:flex; align-items:center; gap:18px; flex-wrap:wrap;
  padding:16px 20px; margin-bottom:14px;
  border:1px solid var(--line); border-left-width:5px; border-radius:var(--radius);
  background:var(--raised);
}
.gate.on  { border-left-color:var(--ok); }
.gate.off { border-left-color:var(--bad); background:var(--bad-bg); }

.gate-state { display:flex; align-items:center; gap:9px; flex:none; }
.gate-state b {
  font-family:"IBM Plex Mono", ui-monospace, monospace;
  font-size:19px; letter-spacing:.06em;
}
.gate.on  .gate-state b { color:var(--ok); }
.gate.off .gate-state b { color:var(--bad); }

.gate-lamp { width:13px; height:13px; border-radius:50%; flex:none; }
.gate.on .gate-lamp {
  background:var(--ok);
  box-shadow:0 0 0 4px color-mix(in srgb, var(--ok) 20%, transparent);
}
.gate.off .gate-lamp {
  background:var(--bad);
  box-shadow:0 0 0 4px color-mix(in srgb, var(--bad) 20%, transparent);
}

/* A MARK FOR A BANNER THAT SAYS ITS STATE IN A SENTENCE. The .gate-lamp above
   is a 13px dot and is right where it is used - in a row that also carries the
   word ON or OFF in monospace, where the dot is the colour and the word is the
   fact. Payment reminders says "Reminders are off" in prose instead, so the
   mark has to carry it across a desk: a filled disc in the state's own colour
   with a bell, struck through when nothing is going out. */
.gate-mark {
  --a:var(--muted);
  flex:none; width:46px; height:46px; border-radius:50%;
  display:grid; place-items:center; color:#fff; background:var(--a);
  box-shadow:0 0 0 5px color-mix(in srgb, var(--a) 16%, transparent);
}
.gate.on  .gate-mark { --a:var(--ok); }
.gate.off .gate-mark { --a:var(--bad); }
.gate-mark .navicon { width:22px; height:22px; stroke-width:2.1; }

.gate-say { flex:1 1 300px; min-width:0; }
.gate-say h2 { margin:0 0 3px; font-size:15px; }
.gate-say p { margin:0; font-size:12.5px; color:var(--muted); max-width:70ch; }


/* ONE SETTING, ONE CARD. Four settings down a two-column form, each a label
   over a paragraph over a box, is twelve things in a column and the eye has
   no way in; the paragraphs are the longest part and they are what makes the
   page worth reading. A card each gives every one a mark to find it by and
   keeps its explanation attached to its own box.

   It stays INSIDE the .form2 that already held the four - that grid is
   already two columns and already collapses to one on a phone, and the
   controls take their field styling from it. The cards are what changed. */
.fcard {
  padding:15px 17px; background:var(--raised);
  border:1px solid var(--line); border-radius:var(--radius);
}
.fcard .cardhead { margin-bottom:12px; }
.fcard .cardtitle .sub { line-height:1.55; }
/* Which payment details go to parents: three ticks on one line, wrapping on a
   phone, above the wording they depend on. */
.fcard .payticks { display:flex; flex-wrap:wrap; gap:6px 22px; margin:0 0 12px; }
.fcard .payticks .check { margin:0; }

/* An action that is not undone by pressing it again. */
.btn.danger {
  background:var(--bad); border-color:transparent; color:#fff; font-weight:600;
}
.btn.danger:hover:not(:disabled) { filter:brightness(1.08); }
.btn.danger:disabled { opacity:.45; cursor:not-allowed; }

/* THE QUIET HALF OF A COLOURED BUTTON. A solid red Delete is right once, at
   the end of a dialog, where it is the decision being taken; a column of six
   of them beside six Saves is a table that shouts at whoever opens it, and
   the eye stops telling the two apart. Tinted in its own colour instead -
   --a, the same way .tagpill and .cardicon take theirs - so the pair reads as
   two kinds of action rather than as an alarm. */
.btn.tint {
  --a:var(--accent);
  color:var(--a); font-weight:600;
  background:color-mix(in srgb, var(--a) 13%, transparent);
  border-color:color-mix(in srgb, var(--a) 34%, transparent);
}
.btn.tint:hover:not(:disabled) {
  background:color-mix(in srgb, var(--a) 20%, transparent);
  border-color:color-mix(in srgb, var(--a) 50%, transparent);
}
.btn.tint .navicon { width:14px; height:14px; }

.typed label { display:block; margin:16px 0 6px; font-size:12px; color:var(--muted); }
.typed label b { color:var(--ink); font-family:"IBM Plex Mono", ui-monospace, monospace; }
.typed input {
  width:100%; font-family:"IBM Plex Mono", ui-monospace, monospace;
  letter-spacing:.14em; text-transform:uppercase;
  border:1px solid var(--line); background:var(--ground); color:var(--ink);
  border-radius:var(--radius-sm); padding:10px 12px; font-size:15px;
}
.typed input:hover { border-color:var(--faint); }
.typed input:focus {
  outline:none; border-color:var(--accent); box-shadow:var(--ring);
  background-color:var(--surface);
}
.typed .actions { margin-top:18px; }
/* A tick inside a typed question - the reminder check's "Send again to the
   ones already sent" (16 October 2026). The rule above dresses the box the
   word is typed into, and would stretch a tick box to the dialog's width on a
   line of its own above its words. */
.typed label.check { display:flex; align-items:center; gap:8px; margin:12px 0 2px;
  font-size:13px; color:var(--ink); }
.typed label.check input { width:auto; padding:0; border:0; background:none;
  letter-spacing:normal; }

/* The same divider the filter bars use, for a toolbar that has groups. */
.toolbar .sep { width:1px; align-self:stretch; background:var(--line); margin:0 4px; }

/* The appearance button: a glyph, not a word, because it sits between a name
   and Sign out and both of those need the room more. */
.btn.ghost.themebtn {
  padding:6px 9px; font-size:15px; line-height:1; min-width:34px;
  /* 34x27 without this, which is the one control in the whole app a finger
     could not reliably hit - tools/mobilecheck.py found it a pixel short on
     the height while every other button passed. The glyph is 15px and the
     padding is symmetrical, so there is nothing here to make it taller except
     saying so. */
  min-height:30px;
}

/* The right-hand end of the bar, as one group.
   margin-left:auto rather than relying on the pages to grow into the gap:
   they are hidden below 760px, where the pages move to the bar along the
   bottom, and then nothing was pushing these to the right at all. */
.navend { display:flex; align-items:center; gap:10px; margin-left:auto; flex:none; }

/* You, as a circle with your initial in it.
   Same 34px box as the appearance button beside it, so the row reads as a set
   of controls - but filled and round, because it is the only one of them that
   is about a person rather than about a setting, and that difference is what
   makes it findable without a label. */
.btn.ghost.mebtn {
  width:34px; min-width:34px; height:34px; padding:0;
  display:inline-flex; align-items:center; justify-content:center;
  border-radius:50%;
  background:var(--accent); color:#fff;
  border:1px solid transparent;
  font-size:13px; font-weight:650; line-height:1; letter-spacing:.01em;
}
.btn.ghost.mebtn:hover { background:var(--accent); filter:brightness(1.08); }
.btn.ghost.mebtn[aria-expanded="true"] {
  box-shadow:0 0 0 3px var(--accent-bg);
}

/* The menu it opens. It is a .navmenu, so it inherits the placing, the
   bounded height and the item styling from the one the page groups use -
   two menu designs in one bar is one too many. */
.memenu { min-width:224px; }
.mehead {
  display:flex; flex-direction:column; gap:1px;
  padding:9px 10px 10px; margin-bottom:4px;
  border-bottom:1px solid var(--line);
}
.mehead > b {
  font-size:13px; font-weight:650; letter-spacing:.01em;
  text-transform:uppercase;
}
.mehead .sub { font-size:11.5px; word-break:break-all; }

/* Log out is the one row here that ends a session, so it is the one row that
   is not the ink colour. Coloured from the start rather than on hover: a
   destructive item that looks ordinary until the pointer is already on it has
   warned nobody. */
.navmenu-item.danger { color:var(--bad); }
.navmenu-item.danger:hover { background:var(--bad-bg); }
.navmenu-item .navicon { width:17px; height:17px; flex:none; }

/* ---- A count on a symbol, in the toolbar --------------------------------

   The conflict bar was a full-width strip above the grid that existed to say
   one number. The number is what anybody needs at a glance, so it sits on the
   flag; the strip opens from it and is not in the layout until it does. */
.warnflag { position:relative; overflow:visible; }
.warnflag .navicon { color:var(--bad); }
.cdot {
  position:absolute; top:-6px; right:-6px; min-width:17px; height:17px;
  padding:0 4px; border-radius:9px; background:var(--bad); color:#fff;
  font-size:10.5px; font-weight:700; line-height:17px; text-align:center;
  box-shadow:0 0 0 2px var(--surface);
}
#calConflicts.shut, #calQueue.shut { display:none; }

/* RS, in the reschedule blue this app has used for the queue since it was
   written - the bar's own left edge is the same colour. The letters are the
   button, so they are sized and tracked like the badge on a chip rather than
   left as ordinary button text. */
.rsflag {
  position:relative; overflow:visible;
  font-family:"IBM Plex Mono", ui-monospace, monospace;
  font-size:11.5px; font-weight:700; letter-spacing:.04em; color:var(--rs);
}
.rdot {
  position:absolute; top:-6px; right:-6px; min-width:17px; height:17px;
  padding:0 4px; border-radius:9px; background:var(--rs); color:#fff;
  font-family:system-ui, sans-serif;
  font-size:10.5px; font-weight:700; line-height:17px; text-align:center;
  box-shadow:0 0 0 2px var(--surface);
}

/* ---- The reschedule queue, above the grid ------------------------------- */

.queuebar {
  margin-bottom:10px; border:1px solid var(--line); border-left:4px solid var(--rs);
  border-radius:var(--radius); background:var(--raised); overflow:hidden;
}
.queuehead {
  display:flex; align-items:center; gap:9px; width:100%;
  padding:9px 13px; border:0; background:none; color:var(--ink);
  font:inherit; cursor:pointer; text-align:left;
}
.queuehead b { color:var(--rs); font-size:14px; }
.queuebar[data-open="true"] .chev { transform:rotate(180deg); }
.queuehead .chev { font-style:normal; color:var(--muted); transition:transform .15s; }

.queuelist { border-top:1px solid var(--line); padding:4px; }
/* The list's own "Find a child" (26 October 2026), the box and count of the
   pill's RS list, on a line of its own at the top of the opened list. */
.queuefind { display:flex; align-items:center; gap:10px; flex-wrap:wrap; padding:4px 5px 6px; }
.queuefind .searchbox { flex:1 1 220px; max-width:360px; }
.queuefind .searchbox > .search { width:100%; }
.queuenone { margin:4px 9px 8px; }
.queueitem {
  display:flex; align-items:center; gap:9px;
  padding:6px 9px; border-radius:var(--radius-sm); font-size:12.5px;
}

/* Which session this row is about. Two cancellations for one child on one day
   are two separate obligations, and without the date and the slot the rows
   read as the list having printed somebody twice - which is how it was
   reported. */
.queueitem .when {
  font-size:10.5px; font-weight:600; white-space:nowrap;
  background:var(--sunken); color:var(--muted); border:1px solid var(--line);
  padding:1px 7px; border-radius:var(--radius-pill);
}
.queueitem:hover { background:var(--sunken); }
.queueitem.held { opacity:.72; }

/* ---- The four decisions ------------------------------------------------- */

.decisions { display:grid; gap:8px; margin:6px 0 14px; }
.decision {
  display:flex; gap:10px; align-items:flex-start;
  padding:11px 13px; border:1px solid var(--line); border-radius:var(--radius-sm);
  cursor:pointer;
}
.decision:hover { border-color:var(--muted); }
.decision:has(input:checked) { border-color:var(--ind); background:var(--accent-bg); }
.decision input { margin-top:3px; flex:none; }
.decision b { display:block; font-size:13px; }
.decision em {
  display:block; margin-top:3px; font-style:normal;
  font-size:12px; color:var(--muted); line-height:1.5;
}

/* A cancellation nobody has answered yet, in the queue bar. */
.queuehead b.pending { color:var(--wait); }
/* Rejected: red, the same colour this fact wears as .pill.off everywhere
   else, and the row dimmed further than .held because there is nothing left
   to do with it - it is in the list to be found, not to be worked. */
.queuehead b.gone { color:var(--bad); }
.queueitem.gone { opacity:.6; }
.queueitem.gone:hover { opacity:1; }

/* What a child is owed, in the planner panels. Deliberately louder than the
   counts above it: those describe what has happened, this is a thing somebody
   has to do something about. */
.pk-owed { display:flex; gap:7px; flex-wrap:wrap; margin:-2px 0 8px; }
.owed {
  display:inline-flex; align-items:baseline; gap:5px;
  padding:2px 9px; border-radius:var(--radius-pill); font-size:11px;
  font-family:"IBM Plex Mono", ui-monospace, monospace;
}
.owed b { font-size:12.5px; }
.owed em { font-style:normal; opacity:.72; }
.owed.ready { background:var(--rs-bg); color:var(--rs); }
.owed.held { background:var(--wait-bg); color:var(--wait); }

/* ---- Building a child's week in one go ---------------------------------- */

.bulkadd {
  display:flex; align-items:center; gap:8px; flex-wrap:wrap;
  padding:10px; margin-bottom:10px;
  border:1px dashed var(--line); border-radius:var(--radius-sm); background:var(--sunken);
}
.bulkadd .bulkther { min-width:190px; }

.daypick { display:flex; gap:4px; flex-wrap:wrap; }
.daybox {
  display:inline-flex; align-items:center; gap:5px;
  padding:5px 9px; border:1px solid var(--line); border-radius:var(--radius-pill);
  font-size:12px; cursor:pointer; background:var(--raised);
}
.daybox:hover { border-color:var(--muted); }
.daybox:has(input:checked) {
  border-color:var(--ind); background:var(--accent-bg); font-weight:600;
}
.daybox input { margin:0; }

/* The rows are a form, so they get room the reading tables do not need. */
#pRows td { padding:4px 6px; vertical-align:middle; }
#pRows select { width:100%; }

/* The day already chosen above: ticked, and not up for debate. */
.daybox.fixed {
  border-color:var(--ind); background:var(--accent-bg);
  font-weight:600; cursor:default;
}
.daybox.fixed input { cursor:default; }

/* ---- The registration wizard -------------------------------------------- */

.wizsteps { display:flex; gap:16px; margin-bottom:12px; font-size:11px; }
.wizsteps b {
  color:var(--faint); font-weight:600;
  text-transform:uppercase; letter-spacing:.06em;
}
.wizsteps b.on { color:var(--ind); }

.wizcard {
  padding:14px 16px; margin-bottom:12px;
  border:1px solid var(--line); border-radius:var(--radius); background:var(--raised);
}
.wizcard h3 {
  margin:0 0 10px; font-size:14px;
  display:flex; align-items:center; gap:8px;
}
.wizcard h4.sechead { margin:16px 0 8px; }

/* A possible duplicate. Laid out so the name and the code are the first two
   things read, because that is what somebody is comparing. */
.wizmatch {
  display:flex; align-items:center; gap:12px;
  padding:10px 13px; margin-bottom:6px;
  border:1px solid var(--line); border-radius:var(--radius-sm); background:var(--raised);
}
.wizmatch .sub { font-size:11px; color:var(--muted); margin-left:6px; }
.wizmatch .grow > .sub { display:block; margin:3px 0 0; }

.daylist { display:grid; gap:6px; }
.dayrow {
  display:flex; align-items:center; gap:9px; flex-wrap:wrap;
  padding:8px 11px; border:1px solid var(--line); border-radius:var(--radius-sm);
}
.dayrow > b { min-width:82px; font-size:12px; }

/* THE TIME AND THE LENGTH on a wizard day row.

   The length is four characters wide because it is a number of minutes and
   nothing else; giving it the width of a therapist picker would say it was as
   important as one. It is a placeholder rather than a value on purpose - blank
   means the therapist's usual, which is right for almost every row, and a
   pre-filled 45 would be somebody else's answer typed into everybody's form.

   The reason for the clash mark being on the ROW rather than on one field: it
   is the combination that collides. The time, the length and the therapist are
   each fine on their own, and changing any of the three fixes it. */
.dayrow .rowtime { max-width:8.5rem; }
.dayrow .rowmins { max-width:5rem; text-align:right; }
.dayrow.clash { outline:2px solid var(--bad); outline-offset:3px; border-radius:var(--radius-sm); }
.dayrow .rowwhy { font-size:11px; color:var(--bad); white-space:nowrap; }

/* ---- Navigation with icons, and an overflow that never hides a page ----- */

.navlink { display:inline-flex; align-items:center; gap:7px; }
.navicon { width:16px; height:16px; flex:none; }

.navmore { position:relative; }

/* Fixed, and placed by placeMenu() rather than by this rule.
   It was absolute, hung off .navmore, and never once appeared: .navlinks sets
   overflow:hidden to stop links spilling off the bar, and an absolutely
   positioned box IS clipped by an overflow ancestor when its containing block
   sits inside that ancestor. Hanging it below the bar put every pixel of it
   outside the clip box, so the menu opened, measured correctly, and was cut
   away to nothing - clicking More looked like it did nothing at all.
   z-index does not help; clipping is not a stacking question. Nor does
   overflow-y:visible, which CSS forces back to auto when the other axis is
   hidden. Leaving the clip is the only way out, so top/right are set in JS
   against the button's own rectangle. */
.navmenu {
  position:fixed; z-index:60;
  min-width:190px; padding:5px;
  border:1px solid var(--line); border-radius:var(--radius);
  background:var(--raised); box-shadow:var(--shadow);
  /* It is placed 58px down and holds one row per page the person may open.
     On a phone that is thirteen rows today and fits with room to spare, but
     nothing about it was bounded: a fixed-position box has no scrollbar of its
     own and cannot be scrolled to, so the fourteenth page - or a shorter phone
     - would have put Settings below the bottom of the screen with no way to
     reach it. Bounded to the space under the button instead. */
  max-height:calc(100vh - 70px); overflow-y:auto;
}
.navmenu-item {
  display:flex; align-items:center; gap:9px; width:100%;
  padding:8px 10px; border:0; border-radius:var(--radius-sm);
  background:none; color:var(--ink); font:inherit; font-size:13px;
  cursor:pointer; text-align:left;
}
.navmenu-item:hover { background:var(--sunken); }

/* The brand mark beside the wordmark. No gap: the wordmark is two elements -
   "Thera" and a coloured "Grid" - and a flex gap would space them apart as if
   the product were called two things. */
.brand img { display:block; border-radius:var(--radius-sm); }
/* The mark on its own needs no gap after it. */
.brand img:only-child { margin-right:0; }

/* The centre's name in the bar, set by Settings, Branches. `.brand span` is
   the old wordmark's accent-coloured "Grid" and would have tinted the centre's
   name with it, so this says the colour out loud rather than inheriting a rule
   written for something else. */
.brand .bname { color:var(--ink); }

/* Both names ticked means "the full one where it fits". 1300px is not a round
   number picked for tidiness: the bar caps at 1340px and has 99px spare at
   1205px against a name that needs 219px, so the swap has to happen above the
   width where the pages would start folding into the More menu. */
.brand.swap .bname.short { display:none; }
@media (max-width: 1300px) {
  .brand.swap .bname.full { display:none; }
  .brand.swap .bname.short { display:inline; }
}

/* Settings, Branches: the header preview, and the centre's own mark.

   The preview reuses `.brand` rather than restyling a copy of it, so what is
   on screen is the real bar with the real rules. A preview drawn from its own
   markup is a preview of the preview, and starts lying the first time the bar
   changes and nobody remembers this exists. */
.hdrpreview { border:1px solid var(--line); border-radius:var(--radius);
  background:var(--raised); padding:10px 12px; margin:0 0 4px; }

.markrow { display:flex; align-items:flex-start; gap:14px; margin:8px 0 2px; }
.markdo { display:flex; flex-wrap:wrap; align-items:center; gap:8px; flex:1;
  min-width:0; }
.markdo .sub { flex-basis:100%; color:var(--muted); font-size:12px;
  line-height:1.5; }
/* The picker is a label pointing at a hidden file input, which is the only
   way to dress one as a button, so it has to be told to behave like one. */
.markdo label.btn { cursor:pointer; }

/* A logo is very often white, or transparent around the edges, and on a plain
   panel both of those look like nothing was loaded. The chequerboard is what
   tells an administrator the difference between a transparent background and
   a failed upload - and it is drawn from the theme's own tokens, so it does
   not go black-on-black at night. */
.markshow { flex:none; width:64px; height:64px; display:flex;
  align-items:center; justify-content:center;
  border:1px solid var(--line); border-radius:var(--radius);
  background-color:var(--surface);
  background-image:
    linear-gradient(45deg, var(--sunken) 25%, transparent 25%,
                    transparent 75%, var(--sunken) 75%),
    linear-gradient(45deg, var(--sunken) 25%, transparent 25%,
                    transparent 75%, var(--sunken) 75%);
  background-size:12px 12px;
  background-position:0 0, 6px 6px; }
.markshow img { display:block; border-radius:var(--radius-sm); }

.signin-mark { display:block; border-radius:var(--radius); flex:none; }

/* A toolbar button that is a symbol rather than a word. */
.btn.icon {
  display:inline-flex; align-items:center; justify-content:center;
  padding:6px 9px; min-width:34px;
}
.btn.icon .navicon { width:15px; height:15px; }

/* Below this the labels go and the icons carry the bar on their own.

   :not(.wadot) because the unread count is also a span inside .navlink, and
   this rule hid it on every phone - the one device where somebody is away from
   the desk and the badge is the only thing that would tell them a parent has
   written. An icon with no number beside it is exactly the silence the badge
   exists to break. */
@media (max-width: 860px) {
  /* The waiting count too, for the same reason as the unread dot: on a
     tablet the label goes and the icon stays, and a count hidden with the
     label is a count nobody at that tablet sees. */
  .navlink > span:not(.wadot):not(.navcount) { display:none; }
  .navlink { padding-left:9px; padding-right:9px; }
}

/* ---- Phones -------------------------------------------------------------
   Everything added since the first mobile pass. The rule throughout is that a
   row of controls becomes a column rather than a thing to scroll sideways:
   horizontal scrolling is right for a grid of sessions, where the shape is the
   information, and wrong for a form, where it just hides half the fields. */

@media (max-width: 760px) {
  /* Toolbars and filter bars wrap instead of overflowing. */
  .toolbar, .filterbar, .bulkadd, .whobar, .clegend { flex-wrap:wrap; }
  .toolbar .grow, .filterbar .grow { flex-basis:100%; height:0; }
  .toolbar .seg { flex-wrap:wrap; }

  /* A finger is not a mouse pointer. */
  .btn { min-height:36px; }
  .btn.tiny { min-height:30px; padding:5px 9px; }
  .pill-btn { min-height:32px; }

  /* The master switch reads as a column: lamp, sentence, button. The sentence
     is flex:1 1 300px so it fills the row on a wide screen - once the
     container is a column that 300px becomes a HEIGHT basis, and the card grew
     a hand-sized gap above its own button. */
  .gate { flex-direction:column; align-items:stretch; gap:10px; }
  .gate-say { flex:0 0 auto; }
  .gate .btn { width:100%; }

  /* The same trap anywhere else a row becomes a column. */
  .ahero { flex-direction:column; align-items:stretch; }
  .ahero .agrow { flex:0 0 auto; }
  .astat { padding-left:0; border-left:0; }

  /* Dashboard: two headline figures across, charts one per row. */
  .dstrip { grid-template-columns:repeat(auto-fit, minmax(140px, 1fr)); gap:8px; }
  .dbig { padding:11px 12px; }
  .dbig .n { font-size:21px; }
  .cgrid, .acards, .tcols { grid-template-columns:minmax(0, 1fr); }

  /* The registration wizard and the plan editor. */
  .wizcard { padding:12px; }
  .wizmatch { flex-direction:column; align-items:stretch; gap:8px; }
  .wizmatch .btn { width:100%; }
  .wizsteps { gap:10px; font-size:10px; }

  /* One session's settings, stacked, with the day as a heading rather than a
     column - three dropdowns side by side on a phone are three slivers. */
  .dayrow { flex-direction:column; align-items:stretch; gap:6px; }
  .dayrow > b { min-width:0; }
  .dayrow select, .dayrow .tpick { width:100%; }
  .bulkadd > * { flex:1 1 100%; }
  .bulkadd .daypick { flex:1 1 100%; }

  /* Decisions and queue entries. */
  .decision { padding:10px; }
  .queueitem { flex-wrap:wrap; }
  .queueitem .btn { width:100%; margin-top:4px; }

  /* Who gets the daily message: one column, so names are not truncated. */
  .whogrid { grid-template-columns:minmax(0, 1fr); }

  /* Modals use the whole screen: a 540px box inside a 375px phone with 16px
     padding either side leaves very little for the form itself. The width
     comes from max-width:none below - `align-items:stretch` was here too and
     did something else entirely: on a row-direction flex container it is the
     VERTICAL axis, so the box was forced to the full height of the screen
     while its contents carried on past the bottom of it. Editing a session on
     a phone, the last third of the form - Delete, Close and Save among it -
     rendered outside the box's own border, floating on the dim backdrop. */
  /* The bottom padding carries the gesture bar as well. Installed, the app
     is full screen and the phone's own bar sits ON TOP of the page - so the
     last row of a modal is under it, and the last row of a modal is where
     Save, Delete and Restore are. The pagebar already does this; the modal
     covers the pagebar (z-index 100 against 90) and so has to do it too. */
  /* AND THE STATUS BAR AT THE TOP (24 October 2026). The installed app is
     drawn edge to edge, under the phone's own status bar - 40px on the
     clinic's phone - and the app's top bar leaves room for it (app.css,
     .appnav). A popup did not: its head, the child's name and the three new
     window buttons, sat under the clock and the signal icons. */
  .modal { padding:calc(10px + env(safe-area-inset-top)) 8px calc(10px + env(safe-area-inset-bottom)); }
  .modal-box, .modal-box.wide, .modal-box.widest { max-width:none; }
  .modal-body { padding:12px 12px 14px; }
  .modal-head { padding-left:12px; padding-right:12px; }

  /* Two date inputs side by side do not fit; nor do the chart range controls. */
  .dateinput { min-width:0; }
  label.inline { flex-basis:100%; }
}

/* Narrower still: the smallest phones in use. */
@media (max-width: 420px) {
  .dstrip { grid-template-columns:repeat(2, minmax(0, 1fr)); }
  .seg .btn { padding-left:9px; padding-right:9px; font-size:12px; }
  .wizsteps b { font-size:9px; }
}

/* A proposed settings change, shown as what moves rather than as JSON. */
.diffs { display:flex; flex-wrap:wrap; gap:6px; margin:4px 0 0; flex-basis:100%; order:3; }
.dchip {
  font-size:11px; font-family:"IBM Plex Mono", ui-monospace, monospace;
  padding:3px 8px; border-radius:var(--radius-sm);
  background:var(--sunken); color:var(--muted);
}
.dchip b { color:var(--ink); font-weight:600; }

/* What each role can do: one row per permission, one column per role. The key
   is printed under the label because the label is prose and the key is what
   appears in a policy, an error message and this file. */
.rolegrid td:first-child { line-height:1.35; }
.rolegrid .sub { font-size:10px; opacity:.65; }
.rolegrid tr.areahead td {
  font-size:11px; font-family:"IBM Plex Mono", ui-monospace, monospace;
  letter-spacing:.07em; text-transform:uppercase; color:var(--faint);
  background:var(--sunken); padding-top:9px; padding-bottom:5px;
}
.rolegrid input[type="checkbox"] { width:16px; height:16px; cursor:pointer; }
.rolegrid input[type="checkbox"]:disabled { cursor:not-allowed; opacity:.45; }

/* The asterisk on a required import field. Red, because the row is rejected
   without it rather than merely incomplete. */
.req { color:var(--bad); font-weight:700; }

/* Help that is there when wanted and out of the way when not.
   <details> rather than a button and a class: it collapses without JavaScript,
   keyboard and screen readers already understand it, and the browser's own
   find-in-page opens it to show a match. */
details.help {
  border:1px solid var(--line); border-radius:var(--radius);
  background:var(--sunken); margin-top:10px;
}
details.help > summary {
  cursor:pointer; padding:10px 14px; font-size:13px; font-weight:600;
  color:var(--ink); list-style:none; display:flex; align-items:center; gap:8px;
}
details.help > summary::-webkit-details-marker { display:none; }
/* The triangle is drawn here so it can turn; the native one cannot be styled
   consistently across browsers. */
details.help > summary::before {
  content:"▸"; color:var(--faint); font-size:11px;
  transition:transform .15s ease;
}
details.help[open] > summary::before { transform:rotate(90deg); }
details.help > summary:hover { color:var(--accent); }
details.help > p {
  margin:0 14px 10px; font-size:12.5px; line-height:1.55; color:var(--muted);
}
details.help > pre {
  margin:0 14px 10px;
}

/* The price list. Read far more often than it is edited, so the numbers are
   given room and the inputs are sized to their content rather than stretched
   to the column. */
.rows.fees td { vertical-align:middle; }
.rows.fees input { padding:5px 7px; font-size:13px; }
.rows.fees input[type="number"] { text-align:right; font-family:"IBM Plex Mono", ui-monospace, monospace; }
.rows.fees .unit { color:var(--faint); font-size:11px; }

/* Reading is the common case, so the read view gets the typography: the
   amount heavier than the rate beside it, and no boxes drawn around numbers
   nobody is editing.

   THE MOCKUP PUTS EVERY VALUE AT THE LEFT OF ITS COLUMN, under the heading
   naming it, rather than against the far edge of the cell - so the figures
   are marked `fig` rather than `num` and keep the mono tabular digits where
   they are. `num` is still what the edit-mode button cell wears. */
.rows.fees td.fig {
  font-family:"IBM Plex Mono", ui-monospace, monospace;
  font-variant-numeric:tabular-nums;
}
.rows.fees:not(.editing) td.fig b { font-weight:650; }
.rows.fees:not(.editing) tbody tr:hover { background:var(--sunken); }
/* The rate is worked out rather than entered, so it is set apart from the
   figures somebody types: no box around it, and quieter than the total it
   comes from. There is no disagreement rule any more because there is no
   longer a second number to disagree with. */
.rows.fees .feerate { color:var(--muted); }

/* A hint that has to be read rather than skimmed past - it stands between
   somebody and sending the wrong number of messages to parents. */
.hint.warn {
  border-left:3px solid var(--bad); padding-left:10px;
  color:var(--ink);
}
/* The "Apply this package" button on the Add-a-child card: in the accent
   colour, off the sentence by a little, and never split across lines. */
.hint.warn .applyhold { margin-left:6px; white-space:nowrap; vertical-align:middle; }

/* ------------------------------------------------- staff schedule ---- */

/* The day in columns, read left to right. */
/* Four even columns rather than two around lunch: the break is not what
   anybody reads this for, and 4+5 looked lopsided on a screenshot. */
.quarters { display:grid; grid-template-columns:repeat(4, minmax(0, 1fr)); gap:12px; }
@media (max-width: 1200px) { .quarters { grid-template-columns:repeat(2, minmax(0,1fr)); } }
@media (max-width: 760px)  { .quarters { grid-template-columns:minmax(0,1fr); } }

/* While the picture is being taken, the day is laid out the way it is meant
   to be READ, whatever the device taking it. A phone collapses .quarters to a
   single column, and html2canvas photographs what is actually there - so a
   schedule shot from a phone came out as one column a mile long. Forced here
   rather than left to html2canvas's own width option, because that only
   changes how media queries resolve and does not stop a narrow parent
   squeezing the grid anyway. */
.shooting { width:1400px !important; max-width:none !important; }
.shooting .quarters {
  grid-template-columns:repeat(4, minmax(0, 1fr)) !important;
}
.shooting .slotcard { break-inside:avoid; }

/* A free therapist here is a button: clicking her gives her a job. Partial
   support keeps her in the list - ten minutes settling a child in leaves the
   rest of the hour - and is marked so it is clear why she is still offered. */
/* WHO IS FREE, AT A SIZE SOMEBODY CAN READ.

   These are the answer to the other question this page is opened with - who
   could take another child at this hour - and they were 9px of white text on
   whatever colour the spreadsheet gave that therapist. Half of those colours
   are pale, so on several of them the name was invisible and the row read as
   a strip of colour with nothing written on it.

   Two changes, and the second matters more than the first. The size goes to
   11px with room around it. And the ink stops being white: kidStyle() picks
   black or white against the same luminance test the chips use, so the name
   is legible on #00FF00 and on #7A4FA8 alike, and a pill can never disagree
   with a chip of the same colour about what shows on it. */
/* The Planning page's pills carry a name rather than the word "free", so they
   need room for it. The colour and the ink are the base rule's now. */
.openpill.named {
  font-size:11px; letter-spacing:0; padding:2px 9px; line-height:1.5;
}

/* THE TIME ON A PLAN ROW, and the row it lands in.

   Both sit UNDER the slot dropdown in the same table cell rather than beside
   it: the cell is one column of a five-column table inside a dialog, so
   anything placed next to the box takes its width from the box. Hidden until
   the dropdown asks for them, so the ordinary case - which is every line in
   this clinic today - looks exactly as it did.

   The row is said out loud because a child put down for 11:35 is DRAWN in the
   11:15 row, and somebody who does not know that reads the grid afterwards,
   sees 11:15, and reports that the time did not save. */
.rowtime { margin-top:4px; width:100%; }
.rowrow {
  display:block; margin-top:2px; font-size:11px; color:var(--muted);
  letter-spacing:.01em;
}
.rowrow.bad { color:var(--bad); }

/* The short code for a therapy the centre has not listed, and the sentence
   saying why it is being asked for. Side by side because the box is four
   characters wide and a line of its own would read as a new section. */
.newtherapy {
  display:flex; align-items:center; gap:10px; flex-wrap:wrap; margin-top:6px;
}
.newtherapy #pCodeBox { display:flex; align-items:center; gap:6px; }
.newtherapy label { margin:0; white-space:nowrap; }
.newtherapy input { max-width:6rem; text-transform:uppercase; }
.newtherapy .hint { margin:0; flex:1 1 12rem; }

.openpill.clickable { cursor:pointer; }
.openpill.clickable:hover { outline:2px solid var(--ink); outline-offset:1px; }
/* currentColor, not a page token: the pill is always a saturated therapist
   colour with white text, so the dash keys off the text and reads in both
   themes. This said var(--bg), which is not a token here - so the border
   was dropped entirely and a partly-booked therapist looked identical to a
   free one, which is the one thing the class exists to prevent. */
.openpill.partly { border:1px dashed currentColor; }

.slotcard {
  background:var(--surface); border:1px solid var(--line);
  border-radius:var(--radius); padding:10px 12px; margin-bottom:10px;
}
.slotcard.quiet { opacity:.6; }
.slothead { display:flex; align-items:center; gap:9px; }
.slothead .mono { font-size:14px; }

.floaters {
  display:flex; flex-wrap:wrap; align-items:center; gap:6px;
  margin-top:8px; padding:7px 9px;
  background:var(--sunken); border-radius:var(--radius-sm);
}

/* The chip here is the Calendar's, so it needs no styling of its own. What
   is added is the row of helpers under it, and the two buttons that put them
   there - kept visually with the chip's own History and Edit links rather
   than made into something new to learn. */
/* THE ROW OF HELPERS ONLY - a CHILD of the chip. It was `.sess .sup`, and the
   "+ Support" and "+ Partial" buttons carry the class `sup` as well, so every
   one of them took this row's dashed top line, its padding and its margin:
   the dashed strokes over each link, and about 18px of every chip on the page.
   Measured on the live page on 13 October 2026 - the link row was 31px tall
   for 13px of text - when the clinic asked for these chips to be smaller. */
.sess > .sup {
  display:flex; flex-wrap:wrap; align-items:center; gap:5px;
  margin-top:5px; padding-top:5px; border-top:1px dashed var(--line);
}
.linkbtn.sup { color:var(--accent); }

/* AND THE STAFF SCHEDULE'S CHIPS ARE COMPACT - asked for in one line on 13
   October 2026: "staff schedule chips are so big in size make them smaller as
   much as you can without losing any tag or utility". Scoped to .slotcard,
   which is this page's alone, because the Calendar draws the same chip and
   its size has been argued over row by row (see the notes above .sess-top).

   THE TIME AND THE TWO LINKS SHARE A LINE. The time line is half empty - a
   time, a name and the outcome - and "+ Support / + Partial" had a line of
   their own under it; side by side they still fit a column of this page with
   room over, and on a narrow one the links simply wrap back underneath, so
   nothing is ever cut. Every badge, the menu, the helpers' row and both links
   are where they were; what goes is padding, the half-again-size clock
   (15px, 13px here), and the empty line. */
.slotcard ul.sessions { gap:4px; }
.slotcard .sess {
  display:flex; flex-wrap:wrap; align-items:center; column-gap:12px;
  padding:4px 8px 4px 14px;
}
.slotcard .sess > .sess-top,
.slotcard .sess > .sess-note,
.slotcard .sess > .sup { flex:1 0 100%; }
.slotcard .sess > .sess-meta { flex:1 1 auto; min-width:0; margin-top:1px; font-size:13px; }
.slotcard .sess > .sess-acts { flex:none; margin-top:1px; gap:12px; }
.slotcard .sess > .sess-acts .linkbtn { font-size:12px; }

/* A named helper. The left border is her colour, the same one she has on every
   other page, so a day's staffing can be read by colour without reading names. */
.suptag {
  display:inline-flex; align-items:center; gap:5px;
  font-size:11.5px; padding:3px 7px; border-radius:var(--radius-pill);
  border:1px solid var(--line); border-left-width:3px; background:var(--surface);
}
.suptag em { font-style:normal; color:var(--faint); font-size:10px; }
.suptag.partial { border-style:dashed; }
.suptag .x {
  appearance:none; border:0; background:none; cursor:pointer;
  color:var(--faint); font-size:13px; line-height:1; padding:0 0 0 2px;
}
.suptag .x:hover { color:var(--bad); }

.picklist { display:grid; grid-template-columns:repeat(2, minmax(0,1fr)); gap:4px 14px; }
.picklist .check { display:flex; align-items:center; gap:7px; padding:4px 2px; }
.picklist .check.taken { opacity:.5; }

@media (max-width: 900px) {
  .picklist { grid-template-columns:minmax(0,1fr); }
}

/* Jobs given out for a slot - not sessions, so they sit under the chips and
   read as a list rather than as more appointments. */
.tasklist { list-style:none; margin:8px 0 0; padding:0; display:grid; gap:4px; }
.taskline {
  display:flex; align-items:center; gap:7px; font-size:12px;
  padding:5px 8px; border-radius:var(--radius-sm);
  /* IN THE THERAPIST'S OWN COLOUR (15 October 2026): --kidcolour and the
     ink that reads on it come from kidStyle() on the line, as on the
     free-therapist pills. A line with neither keeps the sunken ground. */
  background:var(--kidcolour, var(--sunken)); color:var(--kidink, var(--ink));
}
.taskline .tx { color:inherit; opacity:.88; }
.taskline.done .tx { text-decoration:line-through; opacity:.6; }
.taskline .x {
  appearance:none; border:0; background:none; cursor:pointer;
  color:inherit; opacity:.7; font-size:14px; line-height:1; margin-left:auto; padding:0;
}
.taskline .x:hover { color:var(--bad); opacity:1; }

/* ---- The receipt ---------------------------------------------------------

   Laid out from a printed PCDC invoice rather than invented: the clinic block
   centred at the top, a rule, the four facts about this bill, then Child and
   Parent side by side, then the lines. Bordered and black-on-white even in
   dark mode, because this is the one thing in the app that leaves the screen
   on paper and a parent's copy should not depend on a setting. */
.invoice {
  --ink-blue:#2E75B6;        /* the receipt's own blue, from the printed one */
  /* THE BAND IS A TINT OF THE INK, NOT A COLOUR OF ITS OWN. Both renderers
     set it inline from pale() in reportfiles.js - the accountant's own
     arithmetic, 28% of the ink on white - so the sheet on screen is the sheet
     in the monthly file. This is only the fallback for a receipt drawn with
     no colour set at all, and it is pale() of the blue above.

     It was #BDD7EE and fixed, so a clinic that chose purple got a purple
     receipt with a pale blue band across the middle of it. */
  --band:rgb(196,216,235);
  background:#FFFFFF; color:#000000; padding:26px 28px 30px;
  border:1px solid var(--line); border-radius:var(--radius);
  font-size:13px; line-height:1.5;
}

/* The clinic's name is the loudest thing on the page and blue, which is what
   makes it read as a document rather than as a table with borders switched on.
   Under it a solid blue rule, not a hairline. */
.inv-org {
  text-align:center; padding-bottom:12px;
  border-bottom:4px solid var(--ink-blue);
}
.inv-org h3 {
  margin:0 0 5px; font-size:21px; font-weight:700; letter-spacing:0;
  color:var(--ink-blue);
}
.inv-org p { margin:1px 0; font-size:12px; color:#000000; }

/* The centre's logo above its name. Bounded in HEIGHT rather than width: a
   logo is wide or tall and the one thing that must not vary between receipts
   is how much of the page the header eats. */
.inv-logo { display:block; margin:0 auto 8px; max-height:64px; max-width:60%; }

/* The seal, over the signature line, at the foot on the right - where the
   clinic's chop goes on paper. It does not stretch: a stamp printed at the
   wrong aspect is a stamp that looks forged. */
.inv-sign {
  margin-top:22px; display:flex; flex-direction:column; align-items:flex-end;
  page-break-inside:avoid;
}
/* BIGGER THAN IT FIRST WAS. A clinic stamp is not a logo - it carries the
   centre's name, its address and a GST number in small type, and at 82px
   those are three grey lines. This is about 3cm on an A4 receipt, which is
   what a rubber stamp actually measures. */
.inv-stamp { max-height:118px; max-width:280px; margin-bottom:2px; }
.inv-for {
  border-top:1px solid #555555; padding-top:4px; min-width:190px;
  text-align:center; font-size:11.5px; color:#000000;
}

/* Settings: choosing one. */
.stamprow { display:flex; gap:14px; align-items:flex-start; flex-wrap:wrap; }
.stamp-box {
  width:120px; height:120px; flex:none; display:flex;
  align-items:center; justify-content:center; text-align:center; padding:6px;
  background:var(--sunken); border:1px solid var(--line);
  border-radius:var(--radius-sm); font-size:11px;
}
.stamp-shot { max-width:100%; max-height:100%; }

/* Four facts in two columns: the month against the date, the HSN code against
   the invoice number - paired as the printed receipt pairs them, so the two
   numbers a parent quotes sit one above the other on the right. */
.inv-meta {
  display:grid; grid-template-columns:repeat(2, minmax(0,1fr));
  gap:7px 24px; padding:16px 2px 18px;
}
.inv-meta .right { text-align:right; }
.inv-meta .lbl { font-weight:700; }

/* Two boxes sharing a border, their headings on a pale blue band. */
.inv-parties {
  display:grid; grid-template-columns:repeat(2, minmax(0,1fr));
  border:1px solid var(--ink-blue);
}
.inv-party + .inv-party { border-left:1px solid var(--ink-blue); }
.inv-party h4 {
  margin:0; padding:6px 10px; font-size:12.5px; font-weight:700;
  background:var(--band); color:#000000;
  border-bottom:1px solid var(--ink-blue);
}
.inv-party p { margin:0; padding:5px 10px 0; }
.inv-party p:last-child { padding-bottom:8px; }

/* The lines. One border colour throughout and one weight, because the mixture
   of black table borders and grey box borders was the thing that made the
   first attempt look unfinished. */
.inv-lines {
  width:100%; border-collapse:collapse; margin-top:18px;
  border:1px solid var(--ink-blue);
}
.inv-lines th, .inv-lines td {
  border:1px solid var(--ink-blue); padding:9px 11px; text-align:left;
  vertical-align:middle;
}
.inv-lines th {
  background:var(--ink-blue); color:#FFFFFF; font-weight:700; font-size:12.5px;
}
.inv-lines .num { text-align:right; }
.inv-lines th.num { text-align:right; }
.inv-lines tfoot td { font-weight:700; }
.inv-lines tfoot .mid { text-align:center; }

/* A receipt for part of a package (0284): the table above is the whole
   package, and this is what the receipt records - the money received now,
   what has been paid so far and what is left. In the ink, bold, because it is
   the line a parent reads first. */
.inv-part { margin:10px 0 0; font-weight:700; color:var(--ink-blue); font-size:12.5px;
  white-space:pre-line; }
.inv-part .sub { font-weight:400; color:#555555; }

.inv-foot { margin:16px 0 0; font-size:11px; color:#555555; }

/* WHAT AN EDIT ADDS TO THE PAPER (0331), in the receipt's own ink: a field is
   a line like Name and Age, an added column is a column, an added row is a
   row, and the text under the table keeps its bold, colours, lists and tables
   - the same markup richtext.js gives the Reports page. */
.invx-field { margin:0; padding:5px 10px 0; }
.inv-party .invx-field:last-child { padding-bottom:8px; }
.invx-notes { margin:14px 0 0; font-size:12.5px; line-height:1.5; overflow-wrap:anywhere; }
.invx-notes ul, .invx-notes ol { margin:2px 0; padding-left:22px; }
.invx-notes table { border-collapse:collapse; width:100%; margin:6px 0; table-layout:fixed; }
.invx-notes th, .invx-notes td { border:1px solid var(--ink-blue); padding:4px 7px; vertical-align:top; }
.invx-notes th { background:var(--band); font-weight:700; text-align:left; }

/* ------------------------------------------------- the receipt editor ---
   The form on the left and the paper on the right, as the Reports page has
   them, the paper staying in view while the form scrolls. What the bill
   already says is drawn locked and grey; what was added sits in boxes. */
.rxwrap { display:grid; grid-template-columns:minmax(0, 1fr) minmax(320px, 0.9fr); gap:16px; align-items:start; }
.rxform { display:flex; flex-direction:column; gap:12px; min-width:0; }
.rxform .pwcard { margin:0; padding:14px 16px; }
.rxform .pwcard .cardhead { margin-bottom:10px; }
.rxlead { margin:0; }
.rxpreview { position:sticky; top:0; min-width:0; }
.rxpaperhead { display:flex; align-items:center; gap:6px; font-size:12px; font-weight:600; color:var(--muted);
  margin:0 0 6px; }
.rxpaperhead svg { width:14px; height:14px; }
.rxpaper { max-height:calc(100vh - 170px); overflow:auto; border-radius:var(--radius); }
.rxpaper .invoice { font-size:11.5px; padding:18px 18px 22px; }
.rxfixedrow { display:flex; flex-wrap:wrap; gap:6px; margin-bottom:8px; }
.rxfixed { display:inline-flex; align-items:center; gap:5px; padding:4px 9px; border-radius:999px;
  background:var(--sunken); color:var(--muted); font-size:12.5px; }
.rxfixed b { color:var(--ink); font-weight:600; }
.rxfixed svg { width:12px; height:12px; }
.rxpair { display:grid; grid-template-columns:minmax(0, 1fr) minmax(0, 1.4fr) auto; gap:6px; margin-bottom:6px; }
.rxpair .search, .rxgrid .search { width:100%; min-width:0; }
.rxadd { margin-top:2px; }
/* Under HSN Code and under Invoice No, side by side as the paper has them. */
.rxsides { display:grid; grid-template-columns:repeat(2, minmax(0, 1fr)); gap:14px; }
.rxsidehead { display:block; font-size:12px; font-weight:600; color:var(--muted); margin-bottom:6px; }
.rxsides .rxpair { grid-template-columns:minmax(0, 1fr) minmax(0, 1.2fr) auto; }
.rxdel { padding:4px 7px; }
.rxdel svg { width:14px; height:14px; }
/* In a dialog, not under the page's floating buttons: none of the room a
   page's grid keeps for them on a phone, and no height of its own. */
.rxform .rxgridwrap { max-width:100%; margin-top:0; padding-bottom:0; max-height:none; }
.rxgrid { border-collapse:collapse; width:100%; font-size:12.5px; }
.rxgrid th, .rxgrid td { border:1px solid var(--line); padding:4px 6px; text-align:left; vertical-align:middle; }
.rxgrid th { background:var(--sunken); font-weight:600; white-space:nowrap; }
.rxgrid th svg { width:12px; height:12px; margin-right:4px; vertical-align:-1px; }
.rxgrid .num { text-align:right; }
.rxgrid .rxlocked { color:var(--muted); }
.rxgrid td.rxadded { min-width:90px; }
.rxgrid .rxcolhead { display:flex; gap:4px; align-items:center; min-width:130px; }
.rxgrid .rxaddedrow td { background:color-mix(in srgb, var(--accent) 5%, transparent); }
.rxtableacts { justify-content:flex-start; margin-top:8px; }
.rxnotes .rpedit { min-height:120px; max-height:40vh; }
.rxfigs { display:grid; grid-template-columns:repeat(2, minmax(0, 1fr)); gap:10px; margin-bottom:10px; }
.rxfigs label { display:grid; gap:4px; font-size:12px; color:var(--muted); }
.rxwhere { display:grid; gap:6px; }
.rxwhere .check { display:grid; grid-template-columns:auto 1fr; column-gap:8px; align-items:start; }
.rxwhere .check .sub { grid-column:2; font-size:12px; }
.rxwhere .check.off { opacity:.6; }
.rxpeople { display:flex; flex-wrap:wrap; gap:6px 16px; }
.rxacts { margin-top:2px; }
.rxwaitsay { margin:0; }
/* A banner on the editor and over a receipt: an edit waiting, or refused. */
.rxstate { display:flex; gap:8px; align-items:flex-start; margin:0 0 10px; padding:9px 12px;
  border-radius:var(--radius-sm); font-size:13px; line-height:1.45; }
.rxstate svg { flex:none; width:16px; height:16px; margin-top:2px; }
.rxstate.wait { background:var(--wait-bg); color:var(--ink); border:1px solid color-mix(in srgb, var(--wait) 45%, transparent); }
.rxstate.bad { background:color-mix(in srgb, var(--bad) 10%, transparent); color:var(--ink);
  border:1px solid color-mix(in srgb, var(--bad) 40%, transparent); }
/* On a receipt's row: what became of its edit. */
.rxpill { display:inline-flex; align-items:center; gap:4px; margin-top:3px; padding:1px 8px; white-space:nowrap;
  border-radius:999px; font-size:11px; font-weight:600; --a:var(--accent);
  background:color-mix(in srgb, var(--a) 14%, transparent); color:color-mix(in srgb, var(--a) 70%, var(--ink)); }
.rxpill svg { width:11px; height:11px; }
.rxpill.pending { --a:var(--wait); }
.rxpill.rejected { --a:var(--bad); }
.rxcompare { margin:0 0 12px; }
.rxsetcard .pswitchrow + .pswitchrow { margin-top:12px; }
.rxsetwho { display:flex; gap:6px; align-items:flex-start; margin-top:12px; }
.rxsetwho svg { flex:none; width:14px; height:14px; margin-top:2px; }
@media (max-width: 860px) {
  .rxwrap { grid-template-columns:1fr; }
  .rxpreview { position:static; }
  .rxpaper { max-height:none; }
  .rxfigs { grid-template-columns:1fr; }
  .rxsides { grid-template-columns:1fr; }
}

/* Printing.
   Two things print from here - a receipt and a child's record - and each one
   wants everything else off the page. So each names itself on the body before
   calling print, and neither rule can reach a print it was not written for.
   The unscoped first version of this hid the whole app on every print, which
   made the record come out blank.

   The zero page margin removes the browser's own header, the date and page
   title it stamps at the top. Chrome drops both when there is no room for
   them, so each sheet carries its own padding instead of the paper's margin.
   Named pages because @page cannot be chosen by a body class. */
@page { size:A4; margin:12mm; }
@page receipt { size:A4; margin:0; }

@media print {
  body.printing-invoice > *:not(.modal) { display:none !important; }
  body.printing-invoice .modal {
    position:static; background:none; display:block;
    backdrop-filter:none; -webkit-backdrop-filter:none;
  }
  body.printing-invoice .modal-box {
    max-width:none; width:auto; box-shadow:none; border:0;
  }
  body.printing-invoice .modal-head,
  body.printing-invoice .noprint { display:none !important; }
  body.printing-invoice .modal-body { padding:0; overflow:visible; max-height:none; }
  body.printing-invoice .invoice {
    page:receipt; border:0; border-radius:0; padding:16mm 14mm;
    -webkit-print-color-adjust:exact; print-color-adjust:exact;
  }
  .inv-lines { page-break-inside:avoid; }
  /* Browsers drop background images when printing; these are <img> elements
     precisely so they do not, but the colour-adjust rule on .invoice above is
     what stops a printer washing the logo out to save ink. Named again here
     because a receipt without its stamp is the one thing a parent would
     notice. */
  body.printing-invoice .inv-logo,
  body.printing-invoice .inv-stamp {
    -webkit-print-color-adjust:exact; print-color-adjust:exact;
  }
}

/* The editable version of the same four columns, in the New bill dialog. */
.billlines .linetable { width:100%; }
.billlines .linetable input, .billlines .linetable select { width:100%; }
.billlines .linetable .lSessions { max-width:110px; }
.billlines .linetable .lPrice { max-width:130px; }
/* The therapy typed under "Other" (13 October 2026), under the list it was
   chosen from rather than beside it - the column is the narrowest place on
   the form to put a second box. */
.billlines .linetable .lTherapyOwn { margin-top:6px; }
.billlines .linetable tfoot td { border-top:2px solid var(--line); }
.billlines #bAddLine { margin-top:8px; }

/* A suggested session number the package has already been past. Red because
   it is the kind of mistake that is invisible once saved: two sessions
   carrying one number look perfectly ordinary apart from each other. */
.addform .num.outofseries, #bNum.outofseries {
  border-color:var(--bad); color:var(--bad); font-weight:600;
}
/* Said UNDER the box in the booking form rather than beside it. That grid has
   two columns and this is a sentence naming the number that would be repeated,
   not the two-word note that fits on the end of an add row. */
#bNumSay[data-warn] { color:var(--bad); }
.addform .seriesnote {
  font-size:10.5px; color:var(--bad); white-space:nowrap; align-self:center;
}

/* Picking the colour a receipt prints in. Named swatches rather than a bare
   colour well: "Blue" is what somebody says to a colleague, #2E75B6 is not. */
.swatches { display:flex; flex-wrap:wrap; gap:8px; margin:6px 0 12px; }
.swatch {
  display:inline-flex; align-items:center; gap:7px; cursor:pointer;
  appearance:none; font:inherit; font-size:12.5px;
  padding:6px 11px; border-radius:var(--radius-pill);
  border:1px solid var(--line); background:var(--surface); color:var(--ink);
}
.swatch:hover { border-color:var(--accent); }
.swatch[aria-pressed="true"] { border-color:var(--ink); border-width:2px; padding:5px 10px; }
.swatch .dot { width:13px; height:13px; border-radius:50%; flex:none; }
.swatch.custom input[type="color"] {
  width:22px; height:22px; padding:0; border:0; background:none; cursor:pointer;
}

/* The preview is the receipt at its own size, boxed so it reads as a sample
   of something rather than as the page itself. */
.previewbox {
  margin-top:10px; padding:14px; border-radius:var(--radius);
  background:var(--sunken); border:1px solid var(--line);
  max-height:520px; overflow:auto;
}
.previewbox .invoice { font-size:12px; }

/* Taking a child's record out: the state of the permission, and the way to
   use it, above the record it applies to. */
.exportbar {
  display:flex; flex-wrap:wrap; align-items:center; gap:9px;
  background:var(--surface); border:1px solid var(--line);
  border-radius:var(--radius); padding:9px 11px; margin-bottom:9px;
}

/* Where the money went, by category. A bar rather than a pie: the question is
   "what is the big one", and lengths answer it without a legend. */
.catbars { list-style:none; margin:0; padding:0; display:grid; gap:7px; }
.catbars li { display:flex; align-items:center; gap:10px; }
.catbars .cname { flex:0 0 140px; font-size:12.5px; }
.catbars .cbar { flex:1; height:9px; background:var(--sunken); border-radius:var(--radius-pill); }
.catbars .cbar i { display:block; height:100%; background:var(--accent); border-radius:var(--radius-pill); }
.catbars .cval { flex:0 0 110px; text-align:right; font-size:12.5px; }

@media (max-width: 700px) {
  .catbars .cname { flex-basis:96px; }
  .catbars .cval { flex-basis:88px; }
}

/* The report sheet, and nothing else on the paper.

   The first attempt printed the live page, which put the filter row on page
   one and left the record with no heading saying whose it was. It also set a
   named page on an element in the middle of the document, which made the
   browser break before it and emit a blank sheet first. Both gone: the export
   builds its own sheet, and that is the only thing print sees. */
.reportholder { position:absolute; left:-10000px; top:0; width:900px; }

@media print {
  body.printing-history > *:not(.reportholder) { display:none !important; }
  body.printing-history .reportholder {
    position:static; left:auto; width:auto;
  }
  body.printing-history .report {
    -webkit-print-color-adjust:exact; print-color-adjust:exact;
  }
  .rep-rows { break-inside:auto; }
  .rep-rows tr { break-inside:avoid; }
  .rep-rows thead { display:table-header-group; }
}

/* The sheet itself. Black on white whatever the app is wearing: it is a
   document that leaves the building, not a screen. */
.report {
  background:#FFFFFF; color:#000000; width:900px; max-width:100%;
  padding:18mm 14mm; font-size:12.5px; line-height:1.5;

  /* The month grids inside are the page's own, and the page's own draw from
     theme tokens - so on a dark screen they would come out dark, and the
     exported picture with them. Pinned to the light values here rather than
     rewritten: one block, and the grid stays the grid everybody already reads
     rather than a second version of it that drifts. */
  --surface:#FFFFFF; --raised:#FAFAFA; --sunken:#F3F4F6; --stripe:#FBFCFD;
  --ink:#000000; --muted:#444444; --faint:#777777;
  --line:#CCCCCC; --line-soft:#E5E5E5;
  --ok:#1B7F5A; --ok-bg:#E7F4EE;
  --bad:#C0392B; --bad-bg:#FBEBE9;
  --wait:#B7791F; --wait-bg:#FBF3E4;
  --rs:#0E7FB8;  --rs-bg:#E3F3FB;
  --radius:8px;
}

.rep-h {
  margin:18px 0 8px; font-size:12px; font-weight:700;
  text-transform:uppercase; letter-spacing:.07em; color:#2E75B6;
}

/* Two months across the sheet, which is what fits A4 without shrinking a day
   cell past reading. A month is kept whole across a page break. */
.report .histmonths { grid-template-columns:repeat(2, minmax(0, 1fr)); gap:10px; }
.report .histmonth { break-inside:avoid; border-color:#CCCCCC; padding:10px 11px 12px; }
.report .hd { min-height:34px; }
.report .hd i { font-size:10px; }
.report .hd em { font-size:9px; }

/* What the colours mean. The grid is unreadable to somebody seeing it for the
   first time otherwise, and a parent is exactly that. */
.rep-key {
  list-style:none; display:flex; flex-wrap:wrap; gap:6px 16px;
  margin:10px 0 0; padding:0; font-size:10.5px; color:#444444;
}
.rep-key li { display:flex; align-items:center; gap:6px; }
.rep-key .k {
  width:13px; height:13px; border-radius:var(--radius-xs); border:2px solid transparent;
}
.rep-key .k.done { border-color:#1B7F5A; background:#E7F4EE; }
.rep-key .k.cx   { border-color:#B7791F; background:#FBF3E4; }
.rep-key .k.ic   { border-color:#C0392B; background:#FBEBE9; }
/* The legend shows the MARK rather than a swatch, because RS is not a colour
   any more - a key offering a blue square for it would be describing a cell
   the grid never draws. Static, since there is no day cell to sit in. */
.rep-key .rstag.legendrs {
  position:static; margin-right:6px; font-size:9px;
  background:var(--surface); box-shadow:0 0 0 1px var(--rs-bg);
}
.rep-key .k.tent { border-color:#444444; border-style:dashed; }
.rep-key .k.open { border-color:#CCCCCC; background:#FAFAFA; }
.rep-head {
  display:flex; justify-content:space-between; align-items:flex-start; gap:20px;
  border-bottom:3px solid #2E75B6; padding-bottom:10px;
}
.rep-head h3 { margin:0 0 3px; font-size:18px; color:#2E75B6; }
.rep-head p { margin:1px 0; font-size:11.5px; color:#333333; }
.rep-when { text-align:right; }
.rep-when b { font-size:13px; }
.rep-when .sub { color:#666666; font-size:10.5px; }

.rep-who {
  display:grid; grid-template-columns:auto 1fr auto 1fr; gap:5px 12px;
  margin:14px 0 16px;
}
.rep-who dt { font-weight:700; color:#444444; }
.rep-who dd { margin:0; }

.rep-counts, .rep-rows { width:100%; border-collapse:collapse; }
.rep-counts { margin-bottom:16px; }
.rep-counts th, .rep-counts td, .rep-rows th, .rep-rows td {
  border:1px solid #BBBBBB; padding:6px 9px; text-align:left;
}
.rep-counts th, .rep-rows th { background:#EFEFEF; font-size:11.5px; }
.rep-counts .num, .rep-rows .num { text-align:right; }
.rep-counts td { font-size:13px; }
.rep-rows td { font-size:11.5px; }
.rep-none { padding:18px 0; color:#666666; }
.rep-foot {
  margin-top:14px; padding-top:8px; border-top:1px solid #DDDDDD;
  font-size:10.5px; color:#666666;
}

/* The session happening right now.
   A left edge and a tint rather than a border: chips already carry a coloured
   left edge for the therapist, and a second border around the whole thing
   reads as an error state. This has to say "here" without saying "wrong". */
.sess.running {
  background:var(--accent-bg);
  box-shadow:inset 0 0 0 1px var(--accent);
}
.sess.running .sess-name::after {
  content:"now";
  margin-left:6px; font-size:9.5px; font-weight:700; letter-spacing:.06em;
  text-transform:uppercase; color:var(--accent);
  border:1px solid var(--accent); border-radius:var(--radius-pill); padding:1px 5px;
  vertical-align:middle;
}

/* A link inside a sentence that goes somewhere else in the app. Sized with
   the prose around it rather than with the 10.5px chip links, because this one
   is read as part of what it is written in. */
.jump {
  color:var(--accent); text-decoration:underline;
  text-decoration-color:var(--line); text-underline-offset:2px;
  white-space:nowrap;
  /* AN INVISIBLE PAD, not a taller link. This sits inline in a sentence, so
     giving it a real 28px box would push the line apart everywhere it is
     used; the ::after is the pattern .notemore and the owed badges already
     use here. Measured at 115x15 on a handset - the height of the text and
     nothing else. */
  position:relative;
}
.jump::after { content:""; position:absolute; inset:-7px -4px; }
.jump:hover { text-decoration-color:currentColor; }

/* A moment's emphasis after jumping to it, because a smooth scroll ending on
   a chip that looks like every other chip leaves you hunting for what moved.
   The same is true of a settings box reached from a link on another page:
   Settings has eleven tabs and that tab has a dozen inputs. */
.dateinput.flash, input.flash, select.flash, textarea.flash {
  animation:fieldflash 2.2s ease-out 1;
}
@keyframes fieldflash {
  0%, 30% { box-shadow:0 0 0 2px var(--accent), 0 0 0 6px var(--accent-bg); }
  100%    { box-shadow:none; }
}

.sess.flash { animation:sessflash 2.2s ease-out 1; }
@keyframes sessflash {
  0%, 30% { box-shadow:inset 0 0 0 2px var(--accent), 0 0 0 4px var(--accent-bg); }
  100%    { box-shadow:inset 0 0 0 1px var(--accent); }
}

@media (prefers-reduced-motion: reduce) {
  .sess.flash, .dateinput.flash, input.flash, select.flash, textarea.flash {
    animation:none;
  }
}

/* --------------------------------------------- Birthday messages, as cards ---
   Asked for with a mockup on 9 October 2026 - "make Birthday messages page
   something like this" - under the condition the five redesigns before it
   carried: "you don't have to change any utility of functionality its just UI
   change". Every id, every handler and every call is where it was; the hour
   is still the SELECT it has always been, because the schedule it writes is
   an hour and a box taking 09:30 would be a box promising something the
   database cannot keep. */

/* A fact about the template, in a box of its own. The four were a <dl> - four
   labels down the left, four values down the right - which reads as a form
   somebody has to fill in rather than as four things that are already true.
   Across instead, each with its own mark, the way the mockup draws them. */
.factrow {
  display:flex; flex-wrap:wrap; gap:10px; margin:14px 0 0;
}
.factbox {
  flex:1 1 170px; min-width:0;
  display:flex; align-items:center; gap:11px; padding:10px 13px;
  background:var(--sunken); border:1px solid var(--line);
  border-radius:var(--radius);
}
.factbox .cardicon { flex:none; }
.factbox .fv { min-width:0; }
.factbox .fl {
  display:block; font-size:10px; font-family:"IBM Plex Mono", ui-monospace, monospace;
  letter-spacing:.1em; text-transform:uppercase; color:var(--faint);
}
.factbox .fn {
  display:block; margin-top:2px; font-size:13.5px; font-weight:600;
  overflow:hidden; text-overflow:ellipsis;
}

/* THE WORDING IN A BOX WITH ITS OWN NAME ON IT. It was a bare <pre> under a
   paragraph, and the button that copies it sat below in an actions row - so
   the thing being copied and the thing that copies it were a block apart. */
.tplwrap {
  margin-top:13px; border:1px solid var(--line); border-radius:var(--radius);
  background:var(--sunken); overflow:hidden;
}
/* It wraps: on a phone "Copy the wording" ran off the card's edge, cut by the
   box's overflow. */
.tplhead {
  display:flex; flex-wrap:wrap; align-items:center; gap:9px; padding:8px 13px;
  border-bottom:1px solid var(--line);
}
.tplhead .navicon { width:15px; height:15px; color:var(--faint); flex:none; }
.tplhead b { font-size:12.5px; font-weight:600; }
.tplhead .btn { margin-left:auto; }
/* The wording itself is the bubble a parent receives (13 October 2026:
   "make the birthday message wording a bubble too"); it was .tplbody, a
   monospace block. pre-wrap is the bubble's own, so the whitespace Meta
   compares is shown exactly as before. */
.tplwrap .wabubble { margin:12px 13px; }
/* THE BLANKS, MARKED. Four {{n}} in a paragraph of ordinary words are the
   only part of it that must be retyped into Meta exactly, and in one colour
   with everything else they are the part the eye slides over. Escaped FIRST
   and marked afterwards - the notemark_test order - so a brace typed into
   the wording cannot become markup. */
.tplblank { color:var(--accent); font-weight:600; }

/* A CARD BODY IN TWO HALVES: what is true on the left, what somebody can
   change on the right. Written here rather than per card - it was .bdsend,
   named for the birthday card, and the Database tab is the second to want
   exactly these four lines. */
.splitrow { display:flex; flex-wrap:wrap; align-items:flex-start; gap:16px 22px; }
/* Each side of it, where neither side is the wider one by nature. The
   Database tab names its two (.dbfacts and .dbform) because there what is
   TRUE and what can be CHANGED want different widths; this is the plain
   even split. */
/* The eight steps for setting the spreadsheet copy up. Read standing at a
   screen with a spreadsheet open beside it, so each step is a block with air
   round it rather than a line in a dense list - the reason the step and its
   explanation are a <b> over a .sub rather than one sentence. */
.shsteps { margin:10px 0 0; padding-left:22px; }
.shsteps li { margin:0 0 12px; line-height:1.45; }
.shsteps li .sub { color:var(--muted); }

.splitrow .half { flex:1 1 300px; min-width:0; }
.splitrow .half > h3:first-child { margin-top:0; }
/* A rule between the halves rather than a gap: they are one setting read in
   two parts, and a gap alone reads as two unrelated controls. */
.vrule { flex:none; align-self:stretch; width:1px; background:var(--line); }

/* Sending: what is switched on, when it goes, and the button that keeps it. */
.bdsend .bdcol { flex:1 1 290px; min-width:0; }
.bdsend .bdwhen { flex:0 1 280px; min-width:0; }
.bdsend .err { flex:1 1 100%; margin:0; }
.bdsend .actions { flex:none; margin-left:auto; align-self:center; }
.bdtoggle { display:flex; align-items:flex-start; gap:11px; cursor:pointer; }
.bdtoggle input {
  flex:none; width:19px; height:19px; margin:1px 0 0; accent-color:var(--accent);
}
.bdtoggle b { display:block; font-size:13.5px; }
.bdtoggle .hint { margin:5px 0 0; }

/* The template the wish goes as, and what fills each blank (0334): "user
   should have the flexibility to create new templates and use it". The
   picker is as wide as a template's name needs and no wider; the blanks are a
   grid of small choices, each under the {{n}} it fills, so the wording below
   can be read against them. */
.bdtpl .bdpick { max-width:520px; }
.bdtpl .bdpick > label, .bdblanks > b {
  display:block; margin:0 0 6px; font-size:12.5px; font-weight:600;
}
.bdtpl .bdpick .hint { margin:6px 0 0; }
.bdblanks { margin-top:14px; }
.bdgrid {
  display:grid; grid-template-columns:repeat(auto-fill, minmax(210px, 1fr)); gap:10px 14px;
}
.bdblank { min-width:0; }
.bdblank label { display:block; margin:0 0 4px; }
.bdtpl .actions { margin-top:14px; }
.bdtpl .err { margin:8px 0 0; }
/* WHY A WISH DID NOT GO (0335), under its pill in the thirty days' list:
   WhatsApp's own sentence, which can be long, held to a readable measure so it
   does not stretch the table. */
.bdwhy { margin:4px 0 0; max-width:22rem; white-space:normal; }

/* A control with its mark inside its own box: the clock beside the hour. */
.iconfield {
  display:flex; align-items:center; gap:9px; min-height:40px; padding:0 12px;
  background:var(--sunken); border:1px solid var(--line);
  border-radius:var(--radius);
}
.iconfield .navicon { width:15px; height:15px; color:var(--faint); flex:none; }
.iconfield select, .iconfield input {
  flex:1; min-width:0; margin:0; padding:0; border:0; background:none;
  font:inherit; font-size:14px; color:var(--ink);
}
.iconfield select:focus-visible, .iconfield input:focus-visible {
  outline:2px solid var(--accent); outline-offset:3px;
}

/* NOTHING TO SHOW, SAID AS SOMETHING. An empty list was a grey sentence where
   a table would have been, which reads as a page that failed to load rather
   than as a month with no birthdays in it. */
.emptybox {
  display:flex; align-items:center; gap:11px; padding:13px 15px;
  background:var(--sunken); border:1px dashed var(--line);
  border-radius:var(--radius); color:var(--muted); font-size:13px;
}
.emptybox .navicon { width:19px; height:19px; color:var(--faint); flex:none; }
.cardhead .emptybox { flex:1 1 260px; }

@media (max-width: 760px) {
  .vrule { display:none; }
  .bdsend .actions { margin-left:0; }
}

/* ------------------------------------------------- the Database tab ---
   Asked for with a mockup on 9 October 2026 - "make settings page -->
   database page something like this" - under the condition the seven
   redesigns before it carried: "you don't have to change any utility of
   functionality its just UI change". Every id the handlers read - cronForm,
   cronAt, cronOn, binForm, binDays, bkForm, bkLocal, bkDrive, bkKeep,
   bkPlain, snapForm, snapLabel, data-snaprestore, data-snapdrop - is where
   it was, and so is the ORDER of .hint inside #binForm: the projection
   paragraph is found by querySelector('.hint') and moving another one above
   it would leave the figure updating the wrong sentence. */

/* WHAT IS TRUE, one fact a line with its own mark. These were <dl class="kv">
   - a label column and a value column - which is right for a dense card of
   account details and wrong here, where three facts are the answer to "is the
   backup actually working" and each wants to be landed on separately. */
.dbfacts { flex:1 1 270px; min-width:0; }
.dbfact {
  display:flex; align-items:center; gap:11px; padding:10px 0;
  border-top:1px solid var(--line-soft); font-size:13px;
}
.dbfact:first-child { border-top:0; padding-top:0; }
.dbfact > .navicon { flex:none; width:15px; height:15px; color:var(--faint); }
.dbfact .dl { flex:0 0 112px; min-width:0; color:var(--faint); }
.dbfact .dv {
  min-width:0; font-family:"IBM Plex Mono", ui-monospace, monospace;
  font-size:12.5px;
}
.dbform { flex:1 1 320px; min-width:0; }
/* The form's own hint sits under the field it is about, not under the card. */
.dbform .hint { margin:0; }
/* A path box says what it holds. Not a button: a web page cannot open a
   folder picker on the clinic computer, and a control that looks pressable
   and does nothing is the blank-button fault icons.js's note describes. */
.pathfield { display:flex; align-items:center; gap:0; position:relative; }
.pathfield input { flex:1; min-width:0; padding-right:38px; }
.pathfield .navicon {
  position:absolute; right:12px; width:16px; height:16px;
  color:var(--faint); pointer-events:none;
}

/* Looking at an older version of the plan. Deliberately unlike the rest of
   the page: a grid of the past that looks exactly like the grid of the
   present is the kind of thing somebody books against. */
.asofbar {
  display:flex; flex-wrap:wrap; align-items:center; gap:9px;
  background:var(--wait-bg); color:var(--wait);
  border:1px solid var(--wait); border-radius:var(--radius);
  padding:9px 12px; margin-bottom:9px;
}
.asofbar .sub { color:var(--wait); opacity:.85; }

/* A therapy set on the session rather than taken from the therapist. Marked,
   because "SE" beside Prachi is ordinary and "OT" beside Prachi is a decision
   somebody made and may need to explain. */
.badge.code.swapped { outline:1px dashed currentColor; outline-offset:1px; }

/* The parts of a chip, as a list of switches. Two columns so eleven of them
   read as a set rather than as a wall. */
.chipparts {
  display:grid; grid-template-columns:repeat(2, minmax(0, 1fr));
  gap:4px 18px; margin-top:6px;
}
.chipparts .check { padding:3px 2px; }
.chipparts .sub { margin-left:6px; font-family:"IBM Plex Mono", ui-monospace, monospace; }
@media (max-width: 700px) { .chipparts { grid-template-columns:minmax(0,1fr); } }

/* ------------------------------------------------- typing on a touchscreen -

   iOS Safari zooms the whole page in when you focus a control whose text is
   under 16px, and it does not zoom back out afterwards. Every date, search and
   filter control in the app is 12px or 13px - sized for a dense desktop
   toolbar - so on an iPhone tapping any one of them left the page magnified
   and shifted sideways, with the grid you were reading now off the edge.

   16px is the threshold Safari uses, so this is exactly enough and no more.
   Keyed on the pointer rather than the width because a tablet held in landscape
   is over 760px wide and zooms just the same; a mouse never triggers it, so a
   desktop is untouched. `font-size` only - padding and width stay as they are,
   which keeps the toolbars laid out the way they already were.

   It lives at the end of pages.css, and names the classes as well as the
   elements, because .dateinput and .search set their own size a few hundred
   lines up: a bare `input` selector loses to a class no matter which file it
   is in, and the rule would have looked right and done nothing.

   IT DID EXACTLY THAT ANYWAY. Naming three classes covered three classes.
   Eleven other rules set a form control under 16px - `.form2 input` at 14px
   most of all, which is the standard two-column form and therefore nearly
   every dialog in the app - and each one has a class in it, so each one won.
   The comment above described the trap and the rule below walked into it.

   SO IT IS !important NOW, and that is a decision rather than a shortcut.
   Enumerating the eleven would work today and lose again the first time
   somebody adds a twelfth, in a rule that will look completely reasonable and
   have no way of knowing. This is not a style preference competing with other
   style preferences: it is a floor, on touch devices only, below which the
   browser does something the designer did not ask for and cannot undo. A
   floor that can be overridden by being more specific is not a floor.

   tools/webcheck.py now fails on a new sub-16px control rule, so the next one
   is caught while it is being written rather than on somebody's phone. */
@media (pointer: coarse) {
  input, select, textarea, .dateinput, .search, .dateinput.tiny {
    font-size:16px !important;
  }
}

/* --------------------------------------------------- reaching a small link -

   The History and Edit links under each chip are 10.5px text: 16px tall and,
   for "Edit", 18px wide. With a mouse that is a comfortable target. With a
   thumb it is a coin toss between the two, and the calendar has eight hundred
   of them on screen at once - so the miss is not rare, and it opens the wrong
   thing rather than nothing.

   The touchable area grows; the text does not move. A transparent ::after over
   the button is what the finger actually hits, which keeps the chip the exact
   size it was - grow the link itself and every row of the grid gets taller,
   and a phone is the last place to spend vertical space.

   The numbers are measured, not chosen: .sess-acts sets a 10px gap, so 4px
   either side leaves 2px between two hit areas and they can never overlap
   (an overlap would hand the tap to whichever came later in the markup, which
   is the bug this is meant to fix). Vertically the nearest link in the same
   column is 86px away, so 8px is free. */
@media (pointer: coarse) {
  /* A finger, so wider still. Vertically the nearest link in the same column
     is 86px away, which is what limits this rather than taste. */
  .linkbtn::after { inset:-13px -6px; }
}
/* -------------------------------------------- whose row is this, on a phone -

   Every list in the app is a wide table in a horizontal scroller: Packages is
   1638px of table inside 349px of phone. The header row was already pinned, so
   you keep the column names as you scroll down - but nothing was pinned
   sideways, so scrolling right to read "sessions used" or "amount" took the
   child's name off the left-hand edge and left a row of numbers belonging to
   nobody.

   THE RULE THAT DOES THAT USED TO BE HERE, AND AGAIN 1,600 LINES BELOW, and
   the two did not agree: this one at 760px drew the seam in --line-soft and
   hovered to --raised, the other at 560px drew --line, hovered to --sunken,
   and also knew about the highlighted row, the striped row, the totals row and
   the stock ledgers' two-cell variant. Under 560 both applied and the later
   one won; between 561 and 760 only this one did, so one table had two
   appearances depending on the width of the window. Nothing looked wrong at
   either end, which is how it sat there.

   There is one rule now, near the other pinning rules, and it is keyed on
   whether the row actually FITS rather than on a breakpoint - which is what
   fixed the case both copies missed, a 2.5-screen table on the clinic's
   tablet with no pin at all. Search for `.gridwrap.wide`. */

/* ------------------------------------------------------- replies, inbox ---

   Two panes on a desktop and one column on a phone, which is what every
   messaging app settles on for the same reason: the list is how you choose and
   the thread is what you read, and on a narrow screen doing both at once
   leaves room for neither. */
.inbox {
  display:grid; grid-template-columns:290px 1fr; gap:12px; margin-top:10px;
  align-items:start;
}
.inbox-list, .inbox-thread {
  background:var(--surface); border:1px solid var(--line);
  border-radius:var(--radius); box-shadow:var(--shadow-xs); overflow:hidden;
}
.inbox-list { max-height:calc(100vh - 230px); overflow-y:auto; }

/* The row: avatar, then name and preview, with the time and any unread count
   down the right. Grid rather than flex so the preview can span both text
   columns and be clipped to one line - a parent who writes a paragraph must
   not push every other conversation off the list. */
.waitem {
  display:grid; grid-template-columns:auto 1fr auto; gap:1px 10px; width:100%;
  padding:10px 12px; border:0; border-bottom:1px solid var(--line-soft);
  background:none; color:var(--ink); font:inherit; text-align:left; cursor:pointer;
  align-items:center;
}
.waitem:hover { background:var(--sunken); }
.waitem.on { background:var(--wa-soft); }
.waitem .waav { grid-row:1 / span 2; }
.waname { font-weight:600; font-size:13px; }
.wawhen { font-size:11px; color:var(--faint); text-align:right; }
.walast {
  grid-column:2; font-size:12px; color:var(--muted);
  overflow:hidden; text-overflow:ellipsis; white-space:nowrap;
}
.wacount {
  grid-column:3; justify-self:end; min-width:18px; height:18px; padding:0 5px;
  display:inline-flex; align-items:center; justify-content:center;
  font-size:10.5px; font-weight:700; border-radius:var(--radius-pill);
  background:var(--wa); color:#fff;
}

/* Initials in a circle. The hue comes from the name in JS; only the shape and
   the text colour are decided here, so every avatar is legible whatever hue it
   lands on. */
/* THE CIRCLE READS THE CHIP'S OWN TWO VARIABLES. kidStyle() answers
   `--kidcolour` and `--kidink` - it does not set background and color - so a
   circle given that style and no rule to consume it draws transparent with
   white text, which is what shipped the first time and what a hand-written
   CSS mock cheerfully certified. The fallbacks keep a circle with neither
   variable looking like a circle. */
.waav {
  width:34px; height:34px; flex:none; border-radius:50%;
  display:inline-flex; align-items:center; justify-content:center;
  background:var(--kidcolour, var(--muted));
  color:var(--kidink, #fff);
  font-size:12px; font-weight:700; letter-spacing:.02em;
}

.wasearch { padding:9px 10px; border-bottom:1px solid var(--line); }
/* The search box and New chat on one line (13 October 2026). The box takes
   whatever the button leaves; the button never shrinks below its words. */
.wasearch:has(#waNew) { display:flex; align-items:center; gap:6px; }
.wasearch #waNew { flex:none; white-space:nowrap; }
.wasearch #waNew svg { width:13px; height:13px; }
.wasearch input {
  width:100%; border:1px solid var(--line); background:var(--ground);
  color:var(--ink); border-radius:var(--radius-pill); padding:7px 13px; font:inherit;
  font-size:12.5px;
}
.wasearch input:focus {
  outline:none; border-color:var(--wa); background-color:var(--surface);
}
/* ---- NARROWING THE INBOX TO A DAY AND AN HOUR ----------------------
   Asked for with this panel pinned beside the slot planner.

   A BAND ACROSS THE PANEL, not a row inside the list column, and that is a
   measurement rather than a preference. The column is minmax(132px, 30%) and
   the panel's own resize floor is MIN_W 280, so it offers 112px of content at
   its narrowest and 178px at the default 660 - against a native date control
   whose min-content width with this padding is 130px on its own. Rendered in
   the column at 198px the date read "10-10-202" with the calendar icon over
   the last digit; at 132px it was the ICON ALONE.

   AND A DATE INPUT REPORTS NO OVERFLOW: scrollWidth stays equal to
   clientWidth however far it is squeezed, because the internal fields simply
   clip. So `min-width:0` on one is the silent kind of wrong - nothing throws,
   nothing scrolls, and it goes on looking like a working control with its
   digits gone. The floor is written down rather than left to flex, and the
   row wraps when even that will not fit.

   NOT `.waday`, which is the date SEPARATOR inside a conversation - the
   "Yesterday" pill between two days of messages, seventy lines below. Two
   rules about a day in one panel is the `addbtn` collision: a second rule
   further down the file silently restyling the first. This one PICKS a day;
   that one announces one. The first attempt shipped the rename into the
   stylesheet and left the markup saying `waday`, so the filter was drawn by
   the separator's rule - a centred pill with no flex row in it. webcheck
   cannot see that: it asks whether a class is defined twice, not whether a
   class a template writes has any rule at all. */
/* The payment previews were .paypreview, a grey box of their own; they are
   .wabubble now with every other message of ours (13 October 2026: "make
   the payment and day messages previews bubbles too"), and their blanks are
   marked by `.wabubble .tplblank`. */

.wadaypick {
  flex:none; display:flex; align-items:center; flex-wrap:wrap; gap:6px;
  padding:9px 10px; border-bottom:1px solid var(--line);
}
/* ITS OWN SIZE, AND THE SLACK GOES AFTER IT. Grown to fill, these came out
   319px and 281px wide on a 660px panel - a quarter of it spent saying
   "10-10-2026". 150 is the date box's comfortable width and 130 the floor it
   may be squeezed to; past that the row wraps rather than clipping. */
.wadaypick input[type="date"] {
  flex:0 0 auto; width:150px; min-width:130px; padding:5px 9px;
  /* AND IT LOOKS LIKE ITS NEIGHBOURS. This rule sized the box and never
     dressed it, so it was the browser's own square date field between two
     pill selects - the one bare field in the whole app on the clinic's
     phone, found by asking every page which field had square corners. */
  border:1px solid var(--line); background:var(--ground); color:var(--ink);
  font:inherit; font-size:12px; border-radius:var(--radius-pill);
}
/* TWO SELECTS NOW - the hour and the site - so each keeps a floor and the
   row wraps rather than squeezing either into an ellipsis. */
.wadaypick select {
  flex:0 1 auto; min-width:92px; max-width:100%; border:1px solid var(--line);
  background:var(--ground); color:var(--ink); font:inherit; font-size:12px;
  border-radius:var(--radius-pill); padding:5px 8px;
}
.wadaypick select:disabled { opacity:.5; }
.wadaypick .linkbtn { flex:none; font-size:11.5px; }
/* What the list has been narrowed to, in the order the controls sit in. */
.wanarrow {
  margin:0; padding:7px 13px 5px; font-size:11px;
  border-bottom:1px solid var(--line-soft);
}
.walist { overflow-y:auto; }
.wanone { margin:0; padding:14px 13px; }

/* The conversation heading: who, their number, and the state of the reply
   window - which is the one fact that decides whether the box below can be
   used at all, so it is up here and not hidden at the bottom. */
.wahead {
  display:flex; align-items:center; gap:10px; flex-wrap:wrap;
  padding:10px 12px; border-bottom:1px solid var(--line); background:var(--wa-bar);
}
.wahead .wawho { display:flex; flex-direction:column; min-width:0; }
/* BACK TO EVERY CHAT (toList in replies.js). Hidden wherever the list sits
   beside the thread; the phone rules below show it where the list has given
   way. Android's back asks whether it is on screen, so this one rule decides
   both. The chevron turned to point back, as .pkchat-caret turns it. */
.waback {
  display:none; appearance:none; border:0; background:none; cursor:pointer;
  width:36px; height:36px; margin-left:-6px; flex:none; border-radius:50%;
  align-items:center; justify-content:center; color:var(--wa-ink);
}
.waback:hover, .waback:focus-visible { background:rgba(127, 127, 127, .16); }
.waback svg { width:22px; height:22px; transform:rotate(90deg); }
.wahead .sub { font-family:"IBM Plex Mono", ui-monospace, monospace; font-size:11.5px; }

/* Whether the telephone alert is still getting through - the one thing in this
   panel that can be wrong and unnoticed. The window closes 24 hours after the
   clinic's own handset last wrote in, and an alerter that has stopped looks
   exactly like a quiet morning.

   A card inside the conversation now, rather than a strip above the whole
   page. It is about whether the telephone will buzz for THIS message, and it
   is read at the moment somebody is looking at one. */
.wabanner {
  display:flex; align-items:flex-start; gap:9px; margin:10px 12px 0;
  padding:9px 11px; border-radius:var(--radius); font-size:12px; line-height:1.45;
  border:1px solid var(--line);
}
.wabanner .wabell { font-size:15px; line-height:1.2; flex:none; }
.wabanner .wasaid { flex:1; min-width:0; }
.wabanner .sub { display:block; margin-top:2px; }
.wabanner .x {
  appearance:none; border:0; background:none; cursor:pointer; flex:none;
  color:inherit; opacity:.6; font-size:17px; line-height:1; padding:0 2px;
}
.wabanner .x:hover { opacity:1; }
.wabanner.ok   { background:var(--wa-soft); border-color:var(--wa-line); }
.wabanner.warn { background:var(--wait-bg); color:var(--wait); border-color:currentColor; }
.wabanner.bad, .wabanner.off { background:var(--bad-bg); color:var(--bad); border-color:currentColor; }

/* THE WALLPAPER. Asked for in one line - "i want app's watsapp should look
   like real watsapp chat" - and the wallpaper is most of what makes a thread
   read as one: WhatsApp draws a faint sheet of doodles over its paper colour
   in both themes. A tile of them here, one SVG per theme (--wa-wall, with
   the stroke's opacity baked in), painted on the box itself so the messages
   scroll OVER it rather than carrying it along - which is what the real one
   does. Narrow bubbles sit well in from either edge, as there. */
.wamsgs {
  display:flex; flex-direction:column; gap:2px; padding:12px 8%;
  flex:1; min-height:180px; overflow-y:auto;
  background:var(--wa-wall) 0 0 / 170px 170px repeat, var(--wa-paper);
}
/* THE SAME COLUMN, INSIDE A BAND. The slot panel draws one of these per
   child and may list ten, so what has to go is the 180px floor that makes it
   a SCREEN: it takes the height the band gives it and scrolls inside that,
   which is what the clinic asked for in one word - "note that watsapp chat is
   scroallable". The wallpaper, the bubbles, the tails and the ticks are
   untouched, which is the whole point of reusing it.

   AND THE BUBBLE TAKES THE BAND. 62 characters was the measure while the
   template's own line breaks decided the height and a wider bubble bought
   nothing; now that an outgoing message FLOWS (tighten() in shared.js), width
   is exactly what turns eleven lines into two, which is what was asked for -
   "make the watsapp messaged take less lines ... 1 or 2 full message will be
   visible". A long measure is a real cost and it is paid knowingly: this is
   four formulaic sentences read at a glance, not prose to be read down. */
.wamsgs.mini {
  flex:1 1 auto; min-height:0; overflow-y:auto;
  padding:10px 12px;
}
.wamsgs.mini .wabub { font-size:12px; max-width:94%; }

/* THE WAY FURTHER BACK, at the top of the thread where the conversation runs
   out. align-self rather than margin:auto because this column is a flex one -
   a bubble is as wide as its words and this must not be stretched to the full
   width like one. flex:none so it keeps its height when the thread is long
   enough to need every pixel. */
.waolder { align-self:center; flex:none; margin-bottom:6px; }

/* The date between days. Without it a thread is a column of clock times and
   "08:23" three screens up is not obviously last Tuesday. */
.waday { align-self:center; margin:10px 0 6px; }
.waday span {
  display:inline-block; padding:5px 12px; border-radius:7.5px;
  background:var(--wa-day); color:var(--wa-day-ink);
  box-shadow:0 1px .5px rgba(11, 20, 26, .13);
  font-size:12.5px; text-transform:uppercase; letter-spacing:.02em;
}

/* THE BUBBLE, as WhatsApp draws it: 7.5px corners, a flat half-pixel
   shadow, 14px text in the thread's own ink, and the time and ticks INSIDE
   the bubble at the bottom right - floated after the words, so they sit on
   the last line when there is room and drop under it when there is not,
   which is exactly what the real one does. It was a flex column with the
   time on a line of its own, which is a chat shape, not WhatsApp's.
   flow-root so the float is contained without clipping the tail outside. */
.wabub, .wabubble {
  position:relative; display:flow-root;
  padding:6px 7px 8px 9px; border-radius:7.5px;
  font-size:14.2px; line-height:19px; color:var(--wa-ink);
  box-shadow:0 1px .5px rgba(11, 20, 26, .13);
}
.wabub { max-width:65%; }
/* A MESSAGE OF OURS OUTSIDE A THREAD - a template, a preview, an automatic
   reply - IS THIS BUBBLE, not a copy of it: "make them look the same, inbox's
   exact bubble style for the previews too" (13 October 2026). It is named
   beside .wabub here and in the green and tail rules below, so a preview
   cannot drift from what it previews. Its own: the line breaks (a template's
   are part of its wording, and Meta compares them), a measure, and 8px on
   the right for the tail, which hangs outside. */
.wabubble {
  white-space:pre-wrap; overflow-wrap:anywhere;
  max-width:min(62ch, calc(100% - 8px)); margin:6px 8px 4px 0;
}
/* Theirs on the left, ours on the right in the WhatsApp green - the
   arrangement everybody already knows, so nobody has to learn which side is
   which. */
.wabub.in  { align-self:flex-start; background:var(--wa-theirs); }
.wabub.out { align-self:flex-end; }
/* .wabubble takes ours' green and the first-of-a-run's tail, not its side. */
.wabub.out, .wabubble { background:var(--wa-mine); }
/* THE TAIL, on the first bubble of a run from one side: a run is one voice,
   and WhatsApp points only its first bubble at the speaker. A pseudo-element
   that INHERITS the bubble's background and is cut to shape by a mask, so one
   shape serves both sides in both themes. */
.wabub.tail.in  { border-top-left-radius:0; }
.wabub.tail.out, .wabubble { border-top-right-radius:0; }
.wabub.tail::before, .wabubble::before {
  content:""; position:absolute; top:0; width:8px; height:13px; background:inherit;
}
.wabub.tail.in::before  { left:-8px;  -webkit-mask:url("data:image/svg+xml;utf8,%3Csvg%20xmlns='http://www.w3.org/2000/svg'%20width='8'%20height='13'%20viewBox='0%200%208%2013'%3E%20%3Cpath%20d='M8%200v13C8%206%204%202%200%200z'%20fill='%23000'/%3E%3C/svg%3E") no-repeat; mask:url("data:image/svg+xml;utf8,%3Csvg%20xmlns='http://www.w3.org/2000/svg'%20width='8'%20height='13'%20viewBox='0%200%208%2013'%3E%20%3Cpath%20d='M8%200v13C8%206%204%202%200%200z'%20fill='%23000'/%3E%3C/svg%3E") no-repeat; }
.wabub.tail.out::before, .wabubble::before { right:-8px; -webkit-mask:url("data:image/svg+xml;utf8,%3Csvg%20xmlns='http://www.w3.org/2000/svg'%20width='8'%20height='13'%20viewBox='0%200%208%2013'%3E%20%3Cpath%20d='M0%200v13C0%206%204%202%208%200z'%20fill='%23000'/%3E%3C/svg%3E") no-repeat; mask:url("data:image/svg+xml;utf8,%3Csvg%20xmlns='http://www.w3.org/2000/svg'%20width='8'%20height='13'%20viewBox='0%200%208%2013'%3E%20%3Cpath%20d='M0%200v13C0%206%204%202%208%200z'%20fill='%23000'/%3E%3C/svg%3E") no-repeat; }
.wabub + .wabub.tail { margin-top:10px; }

/* .wabubble on a page: the message templates, the previews and the
   automatic replies on the Messages page, asked for on 13 October 2026 ("keep
   all other watsapp message the same way"). Its look is the rules above; see
   the note on .wabubble there. */
.tver .wabubble { margin-top:9px; }
/* THE SAME BUBBLE, TYPED INTO - the closed-desk reply's wording, and a new
   template's body. A box and a preview of it would be the words twice; the
   bubble is the box. The ring says it takes typing, since a bubble does not
   look like a field.

   EVERY LOOK IS SAID AGAIN HERE, not left to .wabubble. The template body
   sits in a .form2, and `.form2 textarea` (a class and an element) outranks
   `.wabubble` (a class) - so its grey ground, mono face and square corners
   would win and the box would not be a bubble at all. Two classes outrank
   it, which is what this rule is. */
.wabubble.edit {
  display:block; width:100%; max-width:62ch; min-height:5.5em; resize:vertical;
  background:var(--wa-mine); color:var(--wa-ink);
  border:1px solid var(--wa); border-radius:7.5px; padding:6px 7px 8px 9px;
  font:inherit; font-size:14.2px; line-height:19px; outline-offset:2px;
  box-shadow:0 1px .5px rgba(11, 20, 26, .13);
}
.wabubble.edit::placeholder { color:var(--wa-meta); }
/* A blank in a template, inside the bubble. The accent the birthday card
   marks them in is a different green fighting this one, so here it is the
   bubble's own ink, bold, on a tint of WhatsApp's green. */
.wabubble .tplblank {
  color:inherit; font-weight:700; border-radius:4px; padding:0 3px;
  background:color-mix(in srgb, var(--wa) 22%, transparent);
}
.watext { white-space:pre-wrap; word-break:break-word; }
.wameta {
  float:right; display:inline-flex; align-items:center; gap:3px;
  font-size:11px; line-height:15px; color:var(--wa-meta);
  margin:5px 0 -5px 8px; position:relative; top:4px;
}
/* Ticks, not words. "· delivered" spent a quarter of the bubble on something
   two characters say, and everybody already reads these. */
.watick { font-size:11px; letter-spacing:-3px; padding-right:3px; }
.watick.read { color:var(--wa-tick); }
.watick.bad { color:var(--bad); letter-spacing:0; padding-right:0; }

/* A PHOTO, A DOCUMENT OR A VOICE NOTE AS ITSELF (13 October 2026) - drawn
   empty by chatBubble and filled by showChatFiles in shared.js. The empty
   frame keeps a photo's shape while it is fetched, so the thread does not
   jump when it arrives; the word inside it is what stays if it cannot be
   fetched. Sized the way WhatsApp sizes one: a photo fills the bubble's width
   up to a cap, never past it, and is pressed to open full size. */
.wafile {
  display:flex; align-items:center; gap:6px; font:inherit; color:inherit;
  border:0; background:rgba(0, 0, 0, .06); border-radius:6px; padding:8px 10px;
  margin:0 0 4px; cursor:pointer; text-align:left; max-width:100%;
}
.wafile svg { width:16px; height:16px; flex:none; }
.wafile.pic { width:240px; height:160px; justify-content:center; color:var(--wa-meta); }
.wafile.pic.ready { width:auto; height:auto; padding:0; background:none; }
.wafile.pic img { display:block; max-width:min(260px, 100%); max-height:320px; border-radius:6px; }
.wafile.doc { min-width:180px; }
.wafile-go { margin-left:auto; padding-left:12px; font-size:12px; font-weight:600; color:var(--wa-tick); }
.wafile.voice { cursor:default; }
.wafile.voice.ready { padding:0; background:none; }
.wafile.voice audio { display:block; width:240px; max-width:100%; height:36px; }
.wafile.gone { cursor:default; color:var(--wa-meta); font-style:italic; }
.wacap { display:block; }
/* A REACTION ON ITS MESSAGE (0275), a round chip hanging off the bubble's
   lower corner the way WhatsApp draws one - on the side the bubble opens to,
   so it never sits under the tail. The bubble keeps room for it below, or the
   next bubble would cover it. The chip's ring is the thread's own paper, which
   is what makes it read as sitting ON the bubble in both themes. */
.wabub.reacted { margin-bottom:14px; }
.wareact {
  position:absolute; bottom:-14px; display:inline-flex; align-items:center;
  justify-content:center; min-width:24px; height:22px; padding:0 4px;
  border-radius:11px; background:var(--wa-theirs); border:2px solid var(--wa-paper);
  font-size:13px; line-height:1; box-shadow:0 1px .5px rgba(11, 20, 26, .13);
}
.wabub.out .wareact { right:10px; }
.wabub.in .wareact { left:10px; }
/* "In the chat" on a template card (Messages, Templates), first among the
   card's actions because it is the one somebody changes most. */
.tvoffer { display:inline-flex; align-items:center; gap:6px; margin:0 8px 0 0;
  font-size:12.5px; color:var(--ink); font-weight:500; cursor:pointer; }
.tvoffer input { width:auto; margin:0; }
.tvoffer input:disabled + * { opacity:.7; }

/* The quieter half of a line about a message the app could not show - what
   it probably was, and what to do. */
.wasub { color:var(--wa-meta); font-size:12.5px; }
/* A MESSAGE'S ACTIONS - Reply (0276) and, on a photo or a document, Forward -
   outside the bubble on the side away from the speaker, where WhatsApp puts
   its own, so they never cover the words, the picture or the time. Shown
   under the pointer and after a tap; a thread of forty messages is not forty
   rows of buttons. Each a disc in the bubble's own colour with the meta ink,
   which is what keeps it visible on the dark paper as well as the light -
   the first Forward was a translucent black that vanished on the dark one. */
.waacts {
  position:absolute; top:50%; transform:translateY(-50%);
  display:flex; flex-direction:column; gap:4px;
  opacity:0; transition:opacity .12s var(--ease);
}
.wabub:hover .waacts, .wabub.acting .waacts, .waacts:focus-within { opacity:1; }
.waacts button {
  width:30px; height:30px; display:inline-flex; align-items:center; justify-content:center;
  padding:0; border:0; border-radius:50%; cursor:pointer;
  background:var(--wa-theirs); color:var(--wa-meta);
  box-shadow:0 1px 2px rgba(11, 20, 26, .3);
}
.waacts button:hover, .waacts button:focus-visible { color:var(--wa-ink); }
.waacts svg { width:16px; height:16px; }
.wabub.in .waacts { right:-38px; }
.wabub.out .waacts { left:-38px; }
/* On a touch screen there is no hover to find them by, so they are always
   there, quieter. */
@media (hover: none) {
  .waacts { opacity:.75; }
}

/* A MESSAGE'S MENU (13 October 2026), WhatsApp's own: a chevron in the
   bubble's top corner under the pointer and after a tap, opening the quick
   reactions over Reply, Copy, Download, Forward and Select messages. The
   chevron takes the bubble's own colour so it never sits on the words as a
   grey square. */
.wamenu-btn {
  position:absolute; top:3px; right:3px; width:24px; height:22px; padding:0;
  display:inline-flex; align-items:center; justify-content:center;
  border:0; border-radius:50%; background:inherit; color:var(--wa-meta);
  cursor:pointer; opacity:0; transition:opacity .12s var(--ease);
}
.wamenu-btn svg { width:16px; height:16px; }
.wabub:hover .wamenu-btn, .wabub.acting .wamenu-btn, .wamenu-btn:focus-visible { opacity:1; }
@media (hover: none) {
  .wamenu-btn { opacity:.6; }
}
.wamenu {
  position:fixed; z-index:103; min-width:200px; padding:6px;
  display:flex; flex-direction:column;
  background:var(--surface); color:var(--ink); border:1px solid var(--line);
  border-radius:12px; box-shadow:0 8px 24px rgba(0, 0, 0, .25);
}
.wamenu > button {
  display:flex; align-items:center; gap:10px; padding:8px 10px;
  border:0; border-radius:8px; background:none; color:inherit;
  font:inherit; font-size:13.5px; text-align:left; cursor:pointer;
}
.wamenu > button:hover, .wamenu > button:focus-visible { background:var(--ground); }
.wamenu > button svg { width:17px; height:17px; color:var(--muted); }
.wamenu-react {
  display:flex; gap:2px; padding:2px 2px 6px; margin-bottom:4px;
  border-bottom:1px solid var(--line);
}
.wamenu-react button {
  width:34px; height:34px; padding:0; border:0; border-radius:50%;
  background:none; font-size:20px; line-height:1; cursor:pointer;
}
.wamenu-react button:hover, .wamenu-react button:focus-visible { background:var(--ground); }
.wamenu-react button.on { background:var(--ground); box-shadow:inset 0 0 0 2px var(--wa-tick); }

/* SELECTING MESSAGES to forward several: each one a press to pick, the
   picked ones ringed, and the reply box giving way to a bar with the count,
   Forward, and a way out. The menu and the Forward disc stand down while it
   is on, so a press is only ever a pick. */
.waselecting .wabub { cursor:pointer; }
.waselecting .wabub.picked { box-shadow:0 0 0 3px var(--wa-tick); }
.waselecting .wamenu-btn, .waselecting .waacts { display:none; }
.wapickbar {
  display:flex; align-items:center; gap:10px; padding:8px 12px;
  background:var(--wa-bar); color:var(--wa-ink); border-top:1px solid var(--line);
}
.wapickbar .btn svg { width:14px; height:14px; }
.wapickx {
  width:32px; height:32px; padding:0; border:0; border-radius:50%;
  background:none; color:var(--wa-meta); font-size:22px; line-height:1; cursor:pointer;
}
.wapickx:hover { color:var(--wa-ink); }
.inbox-thread.picking .wareply, .inbox-thread.picking .wareplybar,
.inbox-thread.picking .waremind, .inbox-thread.picking .waclosed { display:none; }

/* THE MESSAGE BEING ANSWERED (0276), as WhatsApp heads a reply: a tinted
   block with a bar down its edge, who said it in the bar's colour and the
   start of it on one line. Inside a bubble it can be pressed to find the
   message; over the box it is the reply being written. */
.waquote {
  display:block; margin:0 0 4px; padding:5px 8px 5px 9px; border-radius:6px;
  border-left:4px solid var(--wa-tick); background:rgba(11, 20, 26, .07);
  font-size:12.5px; line-height:17px; cursor:pointer; max-width:100%;
}
.waquote b { display:block; color:var(--wa-tick); font-weight:600; }
.waquote span {
  display:block; color:var(--wa-meta); white-space:nowrap; overflow:hidden; text-overflow:ellipsis;
}
.wareplybar {
  display:flex; align-items:center; gap:8px; padding:6px 10px 0;
  background:var(--wa-bar);
}
.wareplybar .waquote { flex:1 1 auto; min-width:0; margin:0; cursor:default; }
.wareplyx {
  flex:none; width:28px; height:28px; border:0; border-radius:50%; padding:0;
  background:none; color:var(--wa-meta); font-size:20px; line-height:1; cursor:pointer;
}
.wareplyx:hover { color:var(--wa-ink); }
/* The message a quote was pressed for, lit for a moment as it comes into view. */
.wabub.waflash { animation:waflash 1.4s var(--ease); }
@keyframes waflash { 0%, 60% { box-shadow:0 0 0 3px var(--wa-tick); } 100% { box-shadow:0 1px .5px rgba(11, 20, 26, .13); } }
/* Who a file is forwarded to: the chats that can take one, a line each. */
.wafwdlist {
  max-height:220px; overflow-y:auto; margin:6px 0 4px;
  border:1px solid var(--line); border-radius:var(--radius-sm);
}
/* Two classes deep, because .form1 label (one class and an element) would
   otherwise make each line a 12px muted field label. */
.wafwdlist .wafwdto {
  display:flex; align-items:center; gap:8px; padding:8px 10px; margin:0;
  font-size:13px; color:var(--ink); font-weight:400; cursor:pointer;
  border-bottom:1px solid var(--line);
}
.wafwdlist .wafwdto:last-child { border-bottom:0; }
.wafwdlist .wafwdto input { width:auto; flex:none; margin:0; }
.wafwdlist .wafwdto .sub { color:var(--muted); font-size:12px; }
/* Staff messages (0306) draw their numbers with the same lines. A number with
   no message in 24 hours is greyed, with the reason beside it, and can still
   be ticked - the message goes if that phone writes before it is due. On the
   page the list is read, not ticked. */
.wafwdlist .wafwdto.shut { opacity:.55; }
.wafwdlist .wafwdto.shut:has(input:checked) { opacity:.8; }
.wafwdlist.smnums .wafwdto { cursor:default; }
.wafwdlist .wafwdto .pill { margin-left:4px; vertical-align:1px; }
/* In the panel's band, where a bubble is a line or two, a photo is a thumbnail. */
.wamsgs.mini .wafile.pic { width:160px; height:100px; }
.wamsgs.mini .wafile.pic.ready { width:auto; height:auto; }
.wamsgs.mini .wafile.pic img { max-width:160px; max-height:120px; }
.wamsgs.mini .wafile.voice audio { width:200px; height:32px; }

/* Full size, over everything: above the dialogs (100) and the leave question
   (101), because a photo is opened FROM one and has to sit on top of it. */
.walight {
  position:fixed; inset:0; z-index:102; display:flex; align-items:center; justify-content:center;
  background:rgba(0, 0, 0, .86); padding:16px; cursor:zoom-out;
}
.walight img { max-width:100%; max-height:100%; object-fit:contain; border-radius:4px; }
.walight-x {
  position:absolute; top:10px; right:12px; width:40px; height:40px; border:0; border-radius:50%;
  background:rgba(255, 255, 255, .14); color:#fff; font-size:26px; line-height:1; cursor:pointer;
}

.wareply {
  display:flex; align-items:center; gap:8px; padding:10px 12px; position:relative;
  border-top:1px solid var(--line); background:var(--wa-bar);
}
.wareply input {
  flex:1; min-width:0; border:1px solid transparent; background:var(--wa-field);
  color:var(--wa-ink); border-radius:8px; padding:9px 12px;
  font:inherit; font-size:15px;
}
.wareply input:hover { border-color:var(--line); }
.wareply input:focus {
  outline:none; border-color:var(--wa); box-shadow:var(--ring);
  background-color:var(--surface);
}
/* Round and green, the way a send button is drawn in every messaging app -
   and the only control in the row that commits anything. */
.wasend {
  flex:none; width:36px; height:36px; border-radius:50%; border:0; cursor:pointer;
  background:var(--wa); color:#fff; font-size:14px; line-height:1;
  display:inline-flex; align-items:center; justify-content:center;
}
.wasend:hover { filter:brightness(1.07); }
.wasend:disabled { opacity:.5; cursor:default; }
.waemo {
  flex:none; width:32px; height:32px; border-radius:50%; border:0; cursor:pointer;
  background:none; font-size:17px; line-height:1; padding:0;
  display:inline-flex; align-items:center; justify-content:center;
}
.waemo:hover { background:var(--sunken); }
/* The paperclip and the QR beside the emoji: drawn marks in the text colour,
   the size of the emoji glyph so the three read as one group of tools. */
.waemo .navicon { width:19px; height:19px; color:var(--muted); }
.waemo:hover .navicon { color:var(--ink); }
/* A photo, or the payment QR, as it will go - in the window that sends it. */
.wamediaprev {
  display:block; max-width:min(100%, 240px); max-height:240px; margin:0 auto 10px;
  border-radius:var(--radius-sm); border:1px solid var(--line); background:#fff;
}

/* The emoji tray, above the box it types into rather than below it - there is
   nothing below it but the edge of the panel. */
.waemotray {
  position:absolute; left:10px; right:10px; bottom:100%; z-index:5;
  max-height:220px; overflow-y:auto; margin-bottom:6px; padding:8px 10px;
  background:var(--surface); border:1px solid var(--line);
  border-radius:var(--radius); box-shadow:var(--shadow);
}
.waemogroup + .waemogroup { margin-top:8px; }
.waemogroup h4 {
  margin:0 0 4px; font-size:10px; font-weight:600; color:var(--muted);
  text-transform:uppercase; letter-spacing:.06em;
}
.waemorow { display:flex; flex-wrap:wrap; gap:2px; }
.waemoji {
  border:0; background:none; cursor:pointer; padding:3px; border-radius:var(--radius-sm);
  font-size:19px; line-height:1.15;
}
.waemoji:hover, .waemoji:focus-visible { background:var(--sunken); outline:none; }

.waclosed { margin:0; padding:13px 14px; border-top:1px solid var(--line); }
/* A template reminder, offered under the thread whether the reply window is
   open or shut - the one message WhatsApp lets the clinic start with. */
.waremind {
  display:flex; align-items:center; gap:8px; flex-wrap:wrap; padding:8px 12px;
  border-top:1px solid var(--line); background:var(--wa-bar); font-size:13px;
}
.waremind select { flex:1; min-width:12rem; }
.waremind-none { margin:0; padding:8px 14px; border-top:1px solid var(--line); }
/* THE REMINDER CHECK (16 October 2026): what a reminder is held for, in
   words. The pills are the app's own .pill.off - and .pill.warn for "sent
   already" in the question at Send - in the body face rather than the
   pill's monospace, because these are sentences, and allowed to wrap. */
.waflags { margin:0; flex-basis:100%; display:flex; flex-wrap:wrap; gap:4px; }
.rcflags { display:flex; flex-wrap:wrap; gap:4px; margin-top:4px; }
.rcflags .pill, .waflags .pill, .rcask .pill { font-family:inherit; white-space:normal; }
/* The row's own edge says it before the pills are read. Inset, so the table
   does not move by a pixel when a reminder is cleared. */
tr.rcbad > td:first-child { box-shadow:inset 3px 0 0 var(--bad); }
.rcname { color:inherit; text-decoration:underline dotted; text-underline-offset:3px; }
/* The count above Send, with "Show only these" beside it - a band of its
   own between the card's sentence and the Send band, ruled above and below,
   with a mark in the state's colour (the clinic's mockup, 17 October 2026).
   Amber, not red: held is "look at this", and red is for what went wrong. */
.rcsum {
  display:flex; flex-wrap:wrap; align-items:center; gap:8px 12px;
  margin:4px 0 12px; padding:10px 0;
  border-top:1px solid var(--line); border-bottom:1px solid var(--line);
}
.rcsum .cardicon.sm { border-radius:9px; }
.rcsum .rcsumtext { display:inline-flex; flex-wrap:wrap; align-items:center; gap:4px 6px; }
.rcsum.bad .rcsumtext > b { color:#f59e0b; }
.rcsum .rcsumsub { color:var(--muted); }
p.rcsum { border:0; padding:0; }
/* The list in the typed question: every flagged reminder, by name. */
.rcask { margin:6px 0 10px; padding-left:18px; max-height:40vh; overflow:auto; }
.rcask li { margin:3px 0; }
/* HELD FOR CHECKING, under Ready to send: one line per reminder held back -
   who, when, why, and the way to that family's chat. A red edge and a faint
   red wash, so the card reads as the one that needs somebody, at a glance. */
.rcheld {
  border-color:color-mix(in srgb, var(--bad) 55%, var(--line));
  background:linear-gradient(180deg, color-mix(in srgb, var(--bad) 9%, var(--surface)),
                                     color-mix(in srgb, var(--bad) 3%, var(--surface)));
}
.rcheld .cardicon, .rcready .cardhead .cardicon { border-radius:50%; }
/* CHECK AGAIN in the card's head (20 October 2026). On a phone it goes under
   the sentence rather than squeezing it into a column beside itself - the
   Reports cards' own rule. */
.rcheld .cardhead { flex-wrap:wrap; }
.rcheld .cardhead .cardtitle { flex:1 1 220px; min-width:0; }
.rcheld .cardhead .btn { flex:none; }
.rchelds { list-style:none; margin:0; padding:0; }
.rchelds li {
  display:flex; flex-wrap:wrap; align-items:center; gap:8px 12px;
  padding:10px 0; border-top:1px solid var(--line);
}
.rchelds .rcflags { margin-top:0; flex:1 1 16rem; }
/* The reason, outlined in red rather than filled: it is read, not pressed. */
.rchelds .rcflags .pill.off {
  background:color-mix(in srgb, var(--bad) 10%, transparent);
  border:1px solid color-mix(in srgb, var(--bad) 55%, transparent);
  padding:3px 10px;
}
/* Who it is about: a round mark, so a long list scans by its first column. */
.rcavatar {
  flex:none; width:32px; height:32px; border-radius:50%;
  display:inline-flex; align-items:center; justify-content:center;
  color:#a78bfa; background:color-mix(in srgb, #8b5cf6 22%, transparent);
}
.rcavatar svg { width:16px; height:16px; }
.rcwho { display:flex; flex-wrap:wrap; align-items:center; gap:4px 10px; }
.rcwho .sub { color:var(--muted); font-size:12px; }
.rcwho .rcsep { width:1px; height:14px; background:color-mix(in srgb, var(--muted) 55%, transparent); }
.rcwho .rcdot { color:var(--faint); }
/* The day and the time together, so a held line says which day it is for. */
.rcwho .rcwhen { font-variant-numeric:tabular-nums; white-space:nowrap; color:var(--muted); font-size:12px; }
/* What the parent reads, beside the app's name for the therapy (19 October
   2026) - only where the family's word differs from it. */
.rcwho .rcsays { white-space:nowrap; color:var(--muted); font-size:12px; font-style:italic; }
/* A WARNING IN HINDI, on a line of its own under the English (20 October
   2026) - where the session's branch has it on (hindiAt, flagWords). */
.hiline { display:block; margin-top:2px; font-family:inherit; }
/* A pill of two languages is two lines or more: rounded rather than a capsule,
   or the ends of its lines run past its curve. */
.pill:has(> .hiline) { border-radius:10px; }
/* The card's own sentence in Hindi, under the English one and in line with it. */
.rcheld .rchi { margin:-6px 0 8px 50px; color:var(--muted); font-size:12.5px; }
@media (max-width: 760px) { .rcheld .rchi { margin-left:0; } }

/* The way to the family's chat, in WhatsApp's own green - at the end of the
   line with Preview and Force send beside it (20 October 2026). */
.rchelds .rcacts { margin-left:auto; display:flex; flex-wrap:wrap; gap:6px; }
.rchelds .rcchatbtn svg { width:16px; height:16px; color:#25D366; }
.rchelds .rcprevbtn svg { width:16px; height:16px; }
.rchelds .rcforcebtn { color:var(--bad); border-color:color-mix(in srgb, var(--bad) 45%, var(--line)); }
.rchelds .rcforcebtn svg { width:16px; height:16px; }
/* A held reminder's message, under its line, as the parent would read it -
   left open while the chat window is up beside it, to compare the two. */
.rchelds .rcprev { flex-basis:100%; padding-left:44px; }
.rchelds .rcprev .hint { margin:0 0 4px; }
.rchelds .rcprev .wabubble { max-width:62ch; }
@media (max-width: 760px) { .rchelds .rcprev { padding-left:0; } }
.rcforce #rcForceWhy { margin:4px 0; }
.rcforce .wabubble { margin:6px 0 10px; }
/* On a phone the mark stays beside the name rather than on a line alone:
   the name block takes the rest of the first line, and the reason and the
   chat wrap under it. */
@media (max-width: 760px) { .rchelds .rcwho { flex:1 1 0; min-width:0; } }

/* THE RUN'S CONFIRM (17 October 2026): what each reminder will say, a line
   each - the child, the session, the time, and the therapy in the family's
   own word, with the app's name for it quieter beside it. A held reminder's
   line in askHeld says the session and therapy after the day. */
.rcrunwrap { max-height:min(52vh, 420px); margin:6px 0 4px; }
.rcrun { width:100%; border-collapse:collapse; font-size:13px; }
.rcrun th { text-align:left; font-weight:600; color:var(--muted); font-size:12px;
  padding:6px 10px; border-bottom:1px solid var(--line); position:sticky; top:0;
  background:var(--surface); }
.rcrun td { padding:7px 10px; border-bottom:1px solid var(--line-soft); vertical-align:top; }
.rcrun td .hint { font-size:12px; }
.rcrunhead { margin:14px 0 0; }
/* The Message therapy type popup on Planning (planning.js, openMessageWords),
   in its mockup's clothes (17 October 2026): the filters in a card, the count
   in a pill, the therapy a pill in its own colour, the table in a card with a
   tinted head, and a box in the last column that looks like a box. */
.mwpop .mwfilters { display:flex; flex-wrap:wrap; align-items:center; gap:10px;
  padding:10px 12px; margin:0 0 12px; }
.mwpop .mwfilters .searchbox { flex:1 1 220px; }
.mwpop .mwfilters .searchbox > .search { width:100%; }
.mwcount { padding:5px 12px; border:1px solid var(--line); border-radius:var(--radius-pill);
  font-size:12.5px; color:var(--ink); white-space:nowrap; font-variant-numeric:tabular-nums; }
.mwwrap { max-height:min(60vh, 560px); border:1px solid var(--line); border-radius:var(--radius);
  margin:0 0 4px; }
.mwtable th { background:var(--surface); }
.mwtable td { vertical-align:middle; padding:6px 10px; }
.mwtable tbody tr:hover td { background:color-mix(in srgb, var(--ink) 4%, transparent); }
/* The app's own field (.search, which every bare box here wears), held a
   little lower so a row is not a box's height and a half. */
.mwtable .mwword { width:100%; min-width:9rem; min-height:32px; padding:5px 10px; }
/* The wording box is a list (22 October 2026): room for its arrow. */
.mwtable select.mwword { padding-right:27px; }
.mwtable .tagpill { font-size:11px; }
/* THE TWO HIGHLIGHTS (19 October 2026): a wording that will not be sent, in
   the app's red, and one that names another therapy, in its amber - an edge on
   the row's first cell and a wash, and the reason in words under what the
   reminder says. Red wins where a row is both. */
.mwtable tr.mwodd td { background:color-mix(in srgb, var(--wait) 9%, transparent); }
.mwtable tr.mwodd td:first-child { box-shadow:inset 3px 0 0 var(--wait); }
.mwtable tr.mwstop td { background:color-mix(in srgb, var(--bad) 8%, transparent); }
.mwtable tr.mwstop td:first-child { box-shadow:inset 3px 0 0 var(--bad); }
.mwwhy { display:block; margin-top:3px; font-size:12px; line-height:1.4; }
.mwwhy.stop { color:var(--bad); }
.mwwhy.odd { color:var(--wait); }
.mwtrouble { display:flex; flex-wrap:wrap; align-items:center; gap:8px; margin:0 0 10px; }
.mwtrouble .mwwhy { margin:0; }
.btn.mwflag { font-size:12.5px; padding:4px 12px; }
.btn.mwflag.stop { color:var(--bad); border-color:color-mix(in srgb, var(--bad) 45%, transparent);
  background:var(--bad-bg); }
.btn.mwflag.odd { color:var(--wait); border-color:color-mix(in srgb, var(--wait) 45%, transparent);
  background:var(--wait-bg); }
/* The Calendar's "Owed with" list (calendar.js, openRsQueue) in the same
   clothes (23 October 2026): the Book button at the row's end, and a child
   who cannot take this hour drawn quieter with the reason under the name -
   marked, never hidden, the clinic's own choice. */
.rsqtable td.actions { text-align:right; white-space:nowrap; }
.rsqtable tr.dim td { color:var(--muted); }
.rsqtable tr.dim .tagpill { opacity:.75; }
.rsqwhy { display:block; margin-top:2px; font-size:12px; color:var(--wait); }
/* Who has taken a child, and how often (shared.js, seenWithHtml; 23 October
   2026), in the RS list and on the Reschedules page: a pill each - a dot in
   the therapist's own colour, the name, the count in a plate - the assigned
   therapist first, outlined and labelled. The text keeps the page's ink:
   some staff colours are too pale to read words in. */
.seenwith { display:flex; flex-wrap:wrap; gap:4px 6px; }
.seenpill { display:inline-flex; align-items:center; gap:5px; white-space:nowrap;
  padding:2px 3px 2px 8px; border:1px solid var(--line); border-radius:var(--radius-pill);
  font-size:12px; line-height:1.5; background:var(--surface); }
.seenpill.mine { border-color:color-mix(in srgb, var(--accent) 55%, var(--line));
  background:color-mix(in srgb, var(--accent) 8%, var(--surface)); }
.seendot { width:8px; height:8px; border-radius:50%; background:var(--a); flex:none; }
.seenpill b { font-weight:600; }
/* Somebody who has left is not drawn at all (seenWith, 24 October 2026). */
.seenrole { font-size:10.5px; color:var(--muted); }
.seenn { min-width:20px; padding:0 6px; border-radius:var(--radius-pill); text-align:center;
  background:color-mix(in srgb, var(--ink) 9%, transparent); font-weight:650;
  font-variant-numeric:tabular-nums; }
/* A pill that opens the sessions it counted (openTaken, 23 October 2026) is a
   button in the pill's own clothes: the page's type and ink, not the
   browser's grey face. On a touch screen a pad brings it to a finger's
   height without the row of pills growing; the 4px gap between rows of pills
   is shared, 2px each way, so two pads never overlap. */
button.seenpill { font:inherit; font-size:12px; color:inherit; cursor:pointer; position:relative; }
button.seenpill:hover { border-color:color-mix(in srgb, var(--accent) 60%, var(--line)); }
button.seenpill:focus-visible { outline:2px solid var(--accent); outline-offset:1px; }
@media (pointer: coarse) {
  button.seenpill::after { content:''; position:absolute; inset:-2px -3px; }
}
/* The sessions a pill counted (shared.js, openTaken): a line per month over
   its sessions, the pieces of one make-up named under its date. */
.tktable td { vertical-align:top; }
.tktable tr.tkmonth td { font-weight:650; font-size:12px; color:var(--muted);
  background:color-mix(in srgb, var(--ink) 4%, transparent); }
.tkpieces { display:block; margin-top:2px; font-size:11.5px; white-space:normal; }
.tkrs { font-size:10px; font-weight:700; letter-spacing:.03em; padding:0 4px;
  border-radius:var(--radius-pill); color:var(--rs, var(--accent));
  background:color-mix(in srgb, var(--ink) 8%, transparent); }
/* On a phone the day and the therapy wrap, so the four columns fit the
   screen rather than the therapy being swiped to: measured at 375px, 74px
   past the edge before, none after. */
@media (max-width: 760px) {
  .tktable td.nowrap, .tktable .tagpill { white-space:normal; }
}
/* The count on the button: the nav's own amber badge, inside the button. */
.btn.wabtn .navcount { margin-left:6px; }

/* A dialog head with a mark and a line under its title (ui.js, modal's
   `mark`): the card head's shape, in the title bar. */
.modal-head.marked { align-items:flex-start; gap:12px; }
.modal-head.marked h3 { display:flex; flex-direction:column; gap:3px; flex:1; }
.modal-head.marked h3 .sub { font-size:12.5px; font-weight:400; color:var(--muted);
  line-height:1.45; }

/* The WhatsApp mark on a button that is about WhatsApp, in the app's own
   WhatsApp green (--wa), which the inbox and its chat already wear. */
.btn.wabtn .navicon { color:var(--wa); }
.rcwho .rcwhat { font-size:12px; color:var(--ink); }

/* A CHANGE ASKED ABOUT WHERE IT IS MADE (ui.js, askHere) - the therapy a
   parent's message names, on the session dialog, the booking dialog, Kid
   details and the clinic's words on Settings. Amber, the app's "look at this"
   colour, and inside the form beside the box rather than over it. */
.chgask {
  margin:8px 0 4px; padding:10px 12px; border-radius:var(--radius-sm);
  border:1px solid color-mix(in srgb, var(--wait) 55%, transparent);
  background:var(--wait-bg); grid-column:1 / -1;
}
.chgask-q p { margin:0 0 6px; }
.chgask-q ul { margin:4px 0 6px; padding-left:18px; }
.chgask .actions { display:flex; align-items:center; gap:8px; flex-wrap:wrap; margin-top:8px; }
.chgask .actions .grow { flex:1; }
/* A save refused while it waits brings it into view and nudges it. */
.chgask.nudge { animation:chgnudge .5s ease-in-out 2; }
@keyframes chgnudge {
  0%, 100% { transform:translateX(0); }
  25% { transform:translateX(-4px); }
  75% { transform:translateX(4px); }
}
@media (prefers-reduced-motion: reduce) { .chgask.nudge { animation:none; outline:2px solid var(--wait); } }

/* READY TO SEND, in the same clothes: the figure that matters in the
   sentence is the count, and the WhatsApp mark is the thing this card is
   about to use - bright and lit rather than a faint watermark here. */
.rcready { border-color:color-mix(in srgb, #3b82f6 35%, var(--line)); }
.rcready .rcgo { color:color-mix(in srgb, #3b82f6 70%, var(--ink)); }
.rcready .cardmark {
  top:16px; right:20px; width:46px; height:46px; opacity:1; color:#25D366;
  filter:drop-shadow(0 0 10px color-mix(in srgb, #25D366 55%, transparent));
}
.rcready .cardmark .navicon { stroke-width:1.6; }
.rcready::after {
  content:''; position:absolute; top:-60px; right:-60px; width:220px; height:220px;
  pointer-events:none;
  background:radial-gradient(circle, color-mix(in srgb, #25D366 16%, transparent), transparent 65%);
}
/* The question at Send: each held reminder with its family's last messages. */
.rcholds { max-height:52vh; overflow:auto; margin:4px 0 12px; display:grid; gap:10px; }
.rchold { border:1px solid var(--line); border-radius:var(--radius-sm); padding:8px 10px; }
.rcchat { max-height:170px; margin-top:8px; }
/* ONE TICK PER HELD REMINDER (21 October 2026): "confirm" sends the ticked
   ones and no other. A row of its own under the chat, the whole line a
   target; one that went already says so in the warning colour. A ticked
   one's card takes the colour too, so a list of ten shows which will go. */
.rcpick { display:flex; align-items:center; gap:8px; margin-top:8px; padding:6px 8px;
  border-radius:var(--radius-sm); background:var(--sunken); cursor:pointer;
  font-size:13px; min-height:32px; }
.rcpick input { width:18px; height:18px; margin:0; flex:none; }
.rcpick.again { color:var(--wait); }
.rchold:has(.rcpick input:checked) { border-color:var(--wait); background:var(--wait-bg); }

/* THE WARNING ON CALENDAR AND PLANNING: today's held reminders, counted
   (main.js, paintHeld). The WhatsApp button's shape, beside it, in the
   app's warning orange rather than WhatsApp's green - it is about a message
   that has NOT gone - with a triangle on it and the count. Two motions,
   because it was asked to catch the eye: a ring that pulses out, and every
   few seconds a small shake. Neither while the reader has asked for less
   motion. */
.heldfab {
  position:fixed; right:82px; bottom:calc(18px + env(safe-area-inset-bottom));
  z-index:80;
  width:52px; height:52px; border-radius:50%; border:0; cursor:pointer;
  background:#ea580c; color:#fff;
  display:flex; align-items:center; justify-content:center;
  box-shadow:0 3px 14px rgba(0,0,0,.28);
  animation:heldfab-ring 1.8s ease-out infinite, heldfab-shake 4s ease-in-out infinite;
}
.heldfab .navicon { width:26px; height:26px; stroke-width:1.6; }
.heldfab .heldwarn {
  position:absolute; left:-4px; top:-4px;
  width:22px; height:22px; border-radius:50%;
  display:flex; align-items:center; justify-content:center;
  background:#facc15; color:#7c2d12; box-shadow:0 0 0 2px var(--ground);
}
.heldfab .heldwarn .navicon { width:14px; height:14px; stroke-width:2.2; }
.heldfab .heldcount {
  position:absolute; top:-4px; right:-4px;
  display:inline-flex; align-items:center; justify-content:center;
  min-width:21px; height:21px; padding:0 5px; border-radius:var(--radius);
  background:var(--bad); color:#fff;
  font-size:11px; font-weight:700; line-height:1;
  box-shadow:0 0 0 2px var(--ground);
}
@keyframes heldfab-ring {
  0%   { box-shadow:0 3px 14px rgba(0,0,0,.28), 0 0 0 0 rgba(234,88,12,.6); }
  100% { box-shadow:0 3px 14px rgba(0,0,0,.28), 0 0 0 14px rgba(234,88,12,0); }
}
@keyframes heldfab-shake {
  0%, 84%, 100% { transform:rotate(0); }
  87% { transform:rotate(-12deg); }
  90% { transform:rotate(10deg); }
  93% { transform:rotate(-7deg); }
  96% { transform:rotate(4deg); }
}
@media (prefers-reduced-motion: reduce) { .heldfab { animation:none; } }
@media (max-width: 760px) { .heldfab { bottom:calc(20px + env(safe-area-inset-bottom)); } }
/* Naming the machine from the lock screen (0240): a label, a box and a
   button, stacked, because the card is narrow and this is not a form. */
.ndname { margin:14px 0 6px; text-align:left; }
.ndname label { display:block; font-size:12px; color:var(--muted); margin-bottom:4px; }
.ndname input { width:100%; margin-bottom:8px; }
/* The editable reminder's preview was .wapreview, a grey box; it is the
   .wabubble every message of ours is drawn as (13 October 2026). */

@media (max-width: 860px) {
  .inbox { grid-template-columns:1fr; }
  .inbox-list { max-height:none; }
  .wamsgs { max-height:none; padding:10px 12px; }
  /* THE MESSAGES PAGE'S OWN INBOX, the list OR the thread (toList in
     replies.js). One column meant the thread under the whole list: measured
     on the clinic's phone on 18 October 2026, 5,451px of chats and the thread
     starting below them, so pressing a chat changed nothing anybody could see.
     The chat now opens over the page, the whole screen, as a phone's
     messaging app opens one; Back (or Android's back) closes it onto the list,
     still where it was scrolled to. Under the panels (90), the small chat
     (91) and dialogs (100), over the page's own bar (30). */
  #view .inbox:not(.reading) .inbox-thread { display:none; }
  #view .inbox.reading .inbox-thread {
    position:fixed; inset:0; z-index:85; border:0; border-radius:0; box-shadow:none;
    display:flex; flex-direction:column; overflow-y:auto;
    padding:env(safe-area-inset-top) 0 env(safe-area-inset-bottom);
  }
  #view .inbox.reading .wamsgs { flex:1 1 0; min-height:180px; overscroll-behavior:contain; }
  #view .inbox .waback { display:inline-flex; }
}

/* ------------------------------------------- connecting the reply webhook ---

   Four steps in three different places, and when it is not working they all
   look identical from inside the app: no messages. So each is stated on its
   own line with its own fix. */
.hooksteps { display:flex; flex-direction:column; gap:10px; margin:14px 0 18px; }
.hookstep { display:flex; align-items:flex-start; gap:11px; }
.hookstep .pill { flex:none; margin-top:1px; }
.hookstep p { margin:3px 0 0; }
.hookstep code {
  font-family:"IBM Plex Mono", ui-monospace, monospace; font-size:11.5px;
  background:var(--sunken); border-radius:var(--radius-xs); padding:1px 5px;
}
/* The commands, where somebody wiring this up can select and copy them.
   A block rather than inline code: these are two full lines with a Windows
   path in them, and inline they wrapped mid-path. */
.hookcmds { font-size:13.5px; font-weight:650; margin:20px 0 4px; }
pre.cmd {
  margin:6px 0 12px; padding:10px 12px; overflow-x:auto;
  background:var(--sunken); border:1px solid var(--line);
  border-radius:var(--radius-sm);
}
pre.cmd code {
  font-family:"IBM Plex Mono", ui-monospace, monospace; font-size:12px;
  color:var(--ink); white-space:pre; background:none; padding:0;
}

.hookurl { display:flex; gap:8px; margin-top:6px; }
.hookurl input {
  flex:1; min-width:0; font-family:"IBM Plex Mono", ui-monospace, monospace;
  font-size:12.5px; border:1px solid var(--line); background:var(--sunken);
  color:var(--ink); border-radius:var(--radius-sm); padding:9px 12px;
}

/* ---------------------------------------------------------- reschedules ---

   The grid is for typing into: most of what goes in it is history, entered a
   year at a time, and the columns are sized so a whole row is reachable
   without the horizontal scroll moving under the cursor. */
.rsgrid td input[type="text"] { min-width:170px; }
.rsgrid td select { min-width:130px; }
/* The date cell holds a field AND the link that opens it on the calendar, and
   the shared rule for this grid puts every input at width:100% - which on a
   cell with two things in it means the second one has nowhere to go. Both get
   an explicit size here instead. */
.rsgrid td.datecell { min-width:212px; }
.rsgrid td.datecell input.dateinput {
  width:132px; min-width:132px; display:inline-block; vertical-align:middle;
}
.rsgrid td.datecell .linkbtn { margin-left:8px; vertical-align:middle; }

/* The "i" on a column heading that needs one.

   Headings here are 11px uppercase in --faint, which is a colour chosen to
   recede. An icon inheriting that would be a smudge, so the button carries
   --muted and lifts to --accent on hover: present enough to be found when
   somebody is puzzled, quiet enough not to compete with the word beside it.

   The ::after is a 10px invisible pad on every side. The glyph is 13px, which
   is under the 24px a finger can reliably hit, and this page is used on a
   tablet - a target too small to press is the same as no button. It costs no
   layout because it is absolutely positioned. */
.hintbtn {
  appearance:none; border:0; background:none; padding:0; margin-left:5px;
  position:relative; cursor:pointer; color:var(--muted);
  vertical-align:-2px; line-height:0;
}
.hintbtn .navicon { width:13px; height:13px; }
.hintbtn::after { content:""; position:absolute; inset:-10px; }
.hintbtn:hover, .hintbtn:focus-visible { color:var(--accent); }
/* Open, it stays lit - so on a tablet, where there is no hover, it is still
   obvious which of six i's on a form the window belongs to. */
.hintbtn[aria-expanded="true"] { color:var(--accent); }

/* The small window an i opens.

   WIDTH IS THE WHOLE DESIGN. 30em of prose at this size is about nine words a
   line, which is where a short explanation reads as one thought rather than a
   paragraph. Wider and it becomes the wall of text it was put here to replace.

   position:fixed because it is placed against the button's own screen
   coordinates and must not be clipped by a scrolling panel, a dialog, or the
   settings body's own overflow - all three of which it opens inside.

   The z-index sits above a modal on purpose: a field inside a dialog has an i
   too, and a window that opened behind the dialog would be invisible. */
.infobox {
  position:fixed; z-index:120; max-width:min(30em, calc(100vw - 20px));
  background:var(--surface); color:var(--ink);
  border:1px solid var(--line); border-radius:var(--radius-sm);
  box-shadow:0 8px 28px rgba(0,0,0,.16);
  padding:11px 14px; font-size:12.5px; line-height:1.55;
}
.infobox b { font-weight:600; }
.infobox p { margin:0 0 7px; }
.infobox p:last-child { margin-bottom:0; }
.infobox code {
  font-family:"IBM Plex Mono", ui-monospace, monospace; font-size:11.5px;
  background:var(--sunken); border-radius:3px; padding:1px 4px;
}
/* The pointer. Drawn as a rotated square rather than a border triangle so it
   carries the same edge and the same shadow as the box it belongs to. */
.infobox::before {
  content:""; position:absolute; width:9px; height:9px; rotate:45deg;
  left:calc(50% - 5px); top:-5px;
  background:var(--surface); border:1px solid var(--line);
  border-right:0; border-bottom:0;
}
.infobox.above::before {
  top:auto; bottom:-5px; border:1px solid var(--line);
  border-left:0; border-top:0;
}

/* A hint that is now one short line does not need the space a paragraph had. */
.hintrow { display:flex; align-items:baseline; gap:2px; flex-wrap:wrap; }

/* The worked example inside that dialog. Set apart from the prose around it
   because it is the part people actually read - the sentences either side
   explain, and this one shows. */
.hintstory {
  margin:12px 0; padding:11px 13px; border-radius:var(--radius-sm);
  background:var(--sunken); border-left:3px solid var(--accent);
}
.hintstory p { margin:0; line-height:1.55; }
.hintlist { margin:10px 0 0; padding-left:20px; }
.hintlist li { margin:5px 0; line-height:1.5; }

/* A reschedule that was itself rescheduled.

   The clinic asked for these to stand out, and they are right to: a session on
   its third date is not a worse row, it is a different conversation - some
   arrangement is not working for that family, and in a list of dates that is
   invisible. Drawn as a tint rather than a border so it reads down the whole
   row, and left of centre in weight so the page does not look like an error
   report when several of them land together. */
/* THE ROW THE FORM JUST MADE, marked for two seconds and then not.
   "+ Add a reschedule" used to slip a blank row into a table of eighty-six and
   it was reported as doing nothing visible; the form that replaced it would
   leave the same question if it closed without saying where the row went. The
   shape is tr.again's - a tinted band and a bar down the left edge - because
   that is how this table already points at a row.

   --ok, not --accent: this says "that worked", not "look at this". And it
   fades rather than latching, since a mark that stays becomes part of the
   table and stops meaning anything.

   The transition is on the ROW's own colours, so a mark that is removed goes
   away gently rather than snapping back mid-read. */
tr.justadded > td {
  background:var(--ok-bg);
  transition:background .5s ease-out;
}
tr.justadded > td:first-child { box-shadow:inset 3px 0 0 var(--ok); }
@media (prefers-reduced-motion: reduce) {
  tr.justadded > td { transition:none; }
}

tr.again > td { background:var(--bad-bg); }
.gridwrap table.rows tbody tr.again:hover td { background:var(--bad-bg); filter:brightness(.98); }
tr.again > td:first-child { box-shadow:inset 3px 0 0 var(--bad); }

/* ------------------------------------------- offered and not taken --------

   Make-up sessions this child was booked for and then called off. Asked for so
   reception can say "we offered you this day and it was cancelled again" -
   which nothing in the app was keeping anywhere a person could see.

   IT SITS LAST AND IT IS RULED OFF. Reported the day it shipped: "your UI
   choice is confusing because this can show a long list". It was under the
   owed pills, which is where the eye goes first, and it is the one block on
   this panel that is NOT work to do - so every row added to it pushed what
   somebody opened the panel for further down. A line across the top says the
   work has ended and the history has started.

   A TABLE, sharing table.rows with the list above it rather than inventing a
   second way to draw five facts: four of them are read by POSITION here, which
   a line of prose cannot do. The red pill is the app's own .pill.off, the
   colour this fact wears everywhere else. */
.offered { margin-top:14px; padding-top:11px; border-top:1px solid var(--line); }
.offered h4 {
  margin:0; font-size:12px; font-weight:700; letter-spacing:.04em;
  text-transform:uppercase; color:var(--faint);
}
.offered .hint { margin:3px 0 7px; }
.offered .tscroll { margin-top:0; }
/* Ten at a time, and the button says how many are behind it. Full width so it
   reads as the end of the table rather than as part of the last row. */
.offered .more { display:block; width:100%; margin-top:7px; text-align:center; }
/* WHAT HAPPENED, AND WHAT HAS BEEN DECIDED SINCE - stacked rather than side by
   side, because they are the same column read downwards: the second is what
   became of the first. A row where they disagree - cancelled, and nobody
   asked - is the row this block exists to surface, and 163 of the 167 in the
   book read that way today. */
.cyclecell { line-height:1.9; }
.cyclecell .pill { display:block; width:fit-content; }
.cyclecell .pill + .pill { margin-top:3px; }

/* The RS count on the history panel is a way in now, not a number to read. */
button.htile {
  appearance:none; font:inherit; cursor:pointer; text-align:inherit;
  border:1px solid var(--line);
}
button.htile:hover { border-color:var(--accent); background:var(--sunken); }

/* --------------------------------------------------- grouped navigation ---

   A group is a button and a menu, the same pair More has always been - so it
   reuses .navmenu rather than growing a second kind of dropdown. */
.navgroup { display:flex; position:relative; }
.navlink.drop::after {
  content:""; margin-left:5px; width:0; height:0; flex:none;
  border-left:3.5px solid transparent; border-right:3.5px solid transparent;
  border-top:4px solid currentColor; opacity:.55;
}
.navlink.drop[aria-expanded="true"]::after { transform:rotate(180deg); opacity:1; }
.navlink.drop[aria-expanded="true"] { color:var(--ink); background:var(--sunken); }
/* The caret is not a label, and below 860px the labels go. Without this the
   group buttons would be an icon and an arrow in the space an icon had. */
@media (max-width: 860px) {
  .navlink.drop::after { display:none; }
}

/* ------------------------------------------------------------------ help ---

   A workflow is a picture with its awkward rules under it. Collapsible,
   because somebody who came for one of them should not scroll past the
   others - and the first stays open, because somebody who came to find out
   what help exists should see one rather than a list of closed boxes. */
.wf {
  background:var(--surface); border:1px solid var(--line);
  border-radius:var(--radius); box-shadow:var(--shadow-xs);
  margin-top:12px; overflow:hidden;
}
.wf > summary {
  display:flex; align-items:baseline; gap:12px; flex-wrap:wrap;
  padding:15px 18px; cursor:pointer; list-style:none;
}
/* Safari draws its own triangle through ::-webkit-details-marker, which the
   standard `list-style:none` does not reach. */
.wf > summary::-webkit-details-marker { display:none; }
.wf > summary::after {
  content:"▾"; margin-left:auto; font-size:16px; line-height:1;
  color:var(--accent); background:var(--accent-bg);
  width:30px; height:26px; border-radius:var(--radius-sm); flex:none;
  display:inline-flex; align-items:center; justify-content:center;
}
.wf[open] > summary::after { content:"▴"; }
.wf > summary:hover::after { background:var(--accent); color:var(--accent-ink); }
.wf > summary:hover { background:var(--sunken); }
.wf-title { font-size:17px; font-weight:650; letter-spacing:-.015em; }
.wf-body { padding:0 18px 18px; border-top:1px solid var(--line-soft); }

/* ------------------------------------------- a fold INSIDE a panel ---------

   The third collapsible thing in this app, and the first that is not a box of
   its own. .wf above and details.foldpanel below are both cards - surface,
   border, radius, shadow, a 17px title - because both are top-level things on
   a page. This one is a legend sitting inside the Exceptions panel, under the
   table it explains, and a card there would be a card drawn inside a card.

   So it takes the AFFORDANCE and leaves the chrome: the same accent ▾ chip at
   the same 30x26, the same flip to ▴, the same hover, the same Safari marker
   fix. Somebody who has opened a workflow on Help already knows what this is.
   What it drops is the background, the border and the shadow - it is part of
   the panel around it, not a thing on top of it.

   The title is h3-sized rather than 17px for the same reason: it sits under
   an h2 that owns the panel, and matching that h2 would make the footnote
   look like a second subject. That was the bug in the version before this
   one, where it WAS a second panel with its own h2. */
.notefold { margin:4px 0 2px; }
.notefold > summary {
  display:flex; align-items:center; gap:10px;
  padding:8px 0; cursor:pointer; list-style:none;
}
.notefold > summary::-webkit-details-marker { display:none; }
.notefold > summary::after {
  content:"▾"; margin-left:auto; font-size:16px; line-height:1;
  color:var(--accent); background:var(--accent-bg);
  width:30px; height:26px; border-radius:var(--radius-sm); flex:none;
  display:inline-flex; align-items:center; justify-content:center;
}
.notefold[open] > summary::after { content:"▴"; }
.notefold > summary:hover::after { background:var(--accent); color:var(--accent-ink); }
.notefold > summary:hover .notefold-title { color:var(--accent); }
.notefold-title { font-size:15px; font-weight:650; letter-spacing:-.01em; }
/* The paragraphs inside carry their own top margin, so the body adds none of
   its own above them - only the rule that separates it from the summary. */
.notefold-body { padding-top:2px; border-top:1px solid var(--line-soft); }
.notefold-body > .lead:first-child { margin-top:12px; }

/* The diagram scrolls rather than shrinking. Squeezed onto a phone it would
   be a picture of a picture; at its own size it can be read by panning, which
   is what people do with a diagram anyway.

   AND IT DOES NOT GROW EITHER, which this said and did not do. `width:100%`
   with no ceiling scaled the picture to the card, so on a 1820px window a
   diagram drawn at 980 across was shown at 1,734 - headings at 26.5px, body
   text at 22, and 1,433px tall, which is more than a window holds. A flow
   chart you can only read one stage at a time is not a flow chart. The
   ceiling is set from help.js, where the number it is drawn against lives;
   see FLOW_W there. Centred, because a 980 picture pinned to the left of a
   1,700px card looks like a card half full. */
.wf-svg { overflow-x:auto; margin:14px 0 4px; }
.wf-svg svg {
  display:block; width:100%; min-width:720px; height:auto; margin-inline:auto;
}

.wf-rules { margin:14px 0 0; padding-left:20px; display:grid; gap:9px; }
.wf-rules li { font-size:13.5px; line-height:1.55; color:var(--muted); }
.wf-rules b { color:var(--ink); font-weight:600; }

/* ----------------------------------------------- a day beside a decision ---

   Somebody answering approvals is judging a session they cannot see. This is
   the day it happened on, read-only, on the side. */
.daybar {
  display:flex; align-items:center; gap:7px; flex-wrap:wrap;
  padding-bottom:12px; margin-bottom:4px; border-bottom:1px solid var(--line-soft);
}
.daybar .dateinput.tiny { min-width:126px; }

.dayslot { display:flex; gap:11px; padding:11px 0; border-bottom:1px solid var(--line-soft); }
.dayslot:last-child { border-bottom:0; }
.dayslot-at {
  flex:none; width:58px; font-family:"IBM Plex Mono", ui-monospace, monospace;
  font-size:12px; font-weight:600; color:var(--muted);
  display:flex; flex-direction:column; gap:2px;
}
.dayslot-at .sub { font-size:10.5px; font-weight:400; color:var(--faint); }
.dayslot .sessions { flex:1; min-width:0; }
/* The chips are evidence here, not controls: nothing on them opens an editor,
   so the pointer should not promise that it does. */
.dayslot .sess { cursor:default; }
.dayslot .sess:hover { transform:none; box-shadow:var(--shadow-xs); }

/* Moving the panel out of the way of the thing being read. */
.sidepanel .panel-head .flip {
  appearance:none; border:0; background:none; cursor:pointer; margin-left:auto;
  color:var(--faint); font-size:15px; line-height:1;
  /* A real box rather than padding round a glyph, for the reason beside the
     close cross above: 15px with 2px and 4px of padding is 21x19, and this one
     sits immediately beside the button that shuts the panel. Missing it by a
     few pixels moves the panel instead of closing it. */
  width:32px; height:32px; border-radius:var(--radius-sm); flex:none;
  display:flex; align-items:center; justify-content:center;
}
.sidepanel .panel-head .flip:hover { color:var(--ink); background:var(--sunken); }
/* The close cross took the auto margin before there was anything beside it. */
.sidepanel .panel-head .flip + .x { margin-left:0; }

/* --------------------------------------------- a record beside a decision ---

   Three numbers that partition the past. The count that matters is the one in
   ink; the two that qualify it are smaller and coloured by what they mean, so
   a column of these can be read down for the rows that stand out. */
.tally { white-space:nowrap; font-size:12.5px; color:var(--muted); }
.tally b { color:var(--ink); font-weight:600; }
.tally i { font-style:normal; font-weight:600; }
.tally i.c { color:var(--wait); }
.tally i.ic { color:var(--bad); }
.rows td .tally + .sub { display:block; margin-top:2px; font-size:11px; }

/* ------------------------------------------------------- a panel that folds ---

   The role matrix is seventy-five permissions by four roles, sitting above the
   list of people. Folded away, the people are where somebody expects them.

   Same shape as the workflows on Help rather than a second kind of fold: one
   collapsible thing in an app should look like the other one. */
details.foldpanel { padding:0; }
details.foldpanel > summary {
  display:flex; align-items:baseline; gap:12px; flex-wrap:wrap;
  padding:16px 19px; cursor:pointer; list-style:none;
}
/* Safari draws its own triangle through a pseudo-element that list-style
   does not reach. */
details.foldpanel > summary::-webkit-details-marker { display:none; }
details.foldpanel > summary::after {
  content:"▾"; margin-left:auto; font-size:16px; line-height:1;
  color:var(--accent); background:var(--accent-bg);
  width:30px; height:26px; border-radius:var(--radius-sm); flex:none;
  display:inline-flex; align-items:center; justify-content:center;
}
details.foldpanel[open] > summary::after { content:"▴"; }
details.foldpanel > summary:hover::after { background:var(--accent); color:var(--accent-ink); }
details.foldpanel > summary:hover { background:var(--sunken); }
.foldtitle { font-size:17px; font-weight:650; letter-spacing:-.015em; }
.foldbody { padding:0 19px 18px; border-top:1px solid var(--line-soft); }
.foldbody > .lead:first-child { margin-top:14px; }

/* ------------------------------------------- folding an area of the matrix ---

   A <details> cannot wrap table rows without breaking the table, and the
   columns lining up is the one thing this grid is for. So each area is its own
   <tbody> and its heading toggles a class on it. */
.rolegrid tr.areahead td { padding:0; }
.areatoggle {
  display:flex; align-items:center; gap:8px; width:100%;
  appearance:none; border:0; background:none; cursor:pointer;
  font:inherit; font-size:11px;
  font-family:"IBM Plex Mono", ui-monospace, monospace;
  letter-spacing:.07em; text-transform:uppercase; color:var(--faint);
  padding:9px 13px 7px; text-align:left;
}
.areatoggle:hover { color:var(--ink); background:var(--line-soft); }
.areatoggle .n {
  font-size:10px; letter-spacing:0; opacity:.8;
  background:var(--surface); border-radius:var(--radius-pill); padding:1px 7px;
}
/* Big enough to aim at and coloured like something you press. A 10px grey
   caret on a grey heading is a decoration; people fold the section by clicking
   the words and never learn there is a control there at all. */
.areatoggle::after {
  content:"▴"; margin-left:auto; font-size:15px; line-height:1;
  color:var(--accent); background:var(--accent-bg);
  width:26px; height:22px; border-radius:var(--radius-sm); flex:none;
  display:inline-flex; align-items:center; justify-content:center;
}
.areatoggle[aria-expanded="false"]::after { content:"▾"; }
.areatoggle:hover::after { background:var(--accent); color:var(--accent-ink); }
.areagroup.shut > tr:not(.areahead) { display:none; }

/* ------------------------------------------- one language for the page ---
   The role grid names the same sixteen groups of permissions the person
   dialog does, and the same four roles. Asked for after the dialog was
   redrawn: "use the same theme on of whole user & access page". Each group
   carries its colour on the tbody and each role column its own, and --a has
   a value here as well as inline, because an undefined var makes CSS drop
   the whole declaration rather than just the colour. */
.areagroup { --a:var(--muted); }
.areatoggle .paicon { color:var(--a); flex:none; }
.rolegrid tr.areahead td { border-left:3px solid var(--a); }
.areatoggle:hover { background:color-mix(in srgb, var(--a) 12%, var(--line-soft)); }

.rolecol { --a:var(--muted); }
.rcicon {
  display:inline-flex; width:14px; height:14px; color:var(--a);
  vertical-align:-2px; margin-right:5px;
}
.rcicon svg { width:14px; height:14px; }

/* A role, wherever one is named - the People table, Your login, and the
   cards in the person dialog, whose shape this follows. */
.rolepill {
  --a:var(--muted);
  display:inline-flex; align-items:center; gap:5px; white-space:nowrap;
  font-size:11px; font-weight:650; padding:2px 9px 2px 7px;
  border-radius:var(--radius-pill); color:var(--a);
  background:color-mix(in srgb, var(--a) 14%, transparent);
  border:1px solid color-mix(in srgb, var(--a) 34%, transparent);
}
.rolepill svg { width:13px; height:13px; flex:none; }

/* The lead and the fold-everything button share a line. */
.areabar { display:flex; align-items:flex-start; gap:14px; }
.areabar .lead { flex:1; }

/* A rule between groups of fields in a two-column form. */
.form2 hr { border:0; border-top:1px solid var(--line); margin:2px 0; }

/* ------------------------------------------------- ticking a box by thumb -

   A checkbox is 13px. The role matrix is nothing BUT checkboxes - four roles
   across, seventy-five permissions down - and on a phone that is a 13px target
   with another one 60px away, where a miss does not do nothing, it grants or
   revokes the wrong permission and says so only if somebody re-reads the row.

   The `input { font-size:16px }` rule above does not help: font-size is what
   stops iOS zooming into a text field, and a checkbox has no text to size.

   20px rather than the 44px a guideline would ask for. The box sits in a table
   cell with its own padding, which carries the real target well past 30px, and
   a 44px box would make a seventy-five row grid roughly a metre tall. Buttons
   marked tiny get the same treatment for the same reason - they are the Fold
   all and Show everyone controls, not decoration. */
@media (pointer: coarse) {
  /* !important, and the reason is worth the line. FOUR rules in this
     stylesheet set `width:auto` on a tick box inside a label - .form1,
     .form2, .checkrow and .toolbar each have their own - and all four are
     (0,2,1) or better against this rule's (0,1,1). So this landed its height
     and lost its width everywhere but the plainest form, and the box came out
     13x20: not obviously small, just SQUASHED, which is the shape that
     survives a review.
     Enumerating the four was tried first and is the wrong fix - it leaves the
     fifth one to be found by somebody's thumb. A minimum size for a finger is
     not something a density rule should be able to undercut, which is exactly
     what !important is for. */
  input[type="checkbox"], input[type="radio"] { width:20px !important; height:20px !important; }
  .btn.tiny { min-height:36px; padding-left:12px; padding-right:12px; }
  /* The account menu and the More menu. Four rows 34px apart, and one of them
     is Log out - which does not ask, and costs whoever hit it their place in
     whatever they were doing plus a password. That is the wrong row to have a
     thumb's width away from Refresh app. 44px here rather than the 36 given
     to tiny buttons above, because these are a stacked list with nothing
     between the rows, so the target and the gap are the same thing. */
  .navmenu-item { min-height:44px; padding-top:0; padding-bottom:0; }

  /* The view switcher had opted out of all of this without meaning to.
     `.seg:has(.btn[aria-pressed]) .btn` sets min-height:30px to sit inside
     its recessed track, and that selector outranks the plain `.btn` rule
     that gives every other button 36px on a phone - so Month / Week /
     Compare / One day, the most-used control on the most-used page, was the
     SMALLEST target on the screen. Same track, same look, 36px of it. */
  .seg:has(.btn[aria-pressed]) .btn { min-height:36px; }
}

/* ------------------------------------------- what a thumb, measured, misses -

   The three rules above this cover the controls somebody thought about. These
   are what was left, found by mounting every page at 375px on a touch profile
   and measuring what a finger is actually offered. Each has its own cause, so
   each gets its own rule rather than one blanket minimum.

   1. THE TICK BOXES WERE 13px WIDE AND 20px TALL - fixed at the rule itself,
      a few lines up, where the reasoning now sits.

   2. FORM CONTROLS KEPT A HEIGHT CHOSEN FOR A MOUSE. The font-size:16px rule
      deliberately changes nothing but the text - its comment says so, to keep
      toolbars laid out as they were - and that is right for controls this
      stylesheet already sizes for a desktop. It leaves two kinds behind: the
      ones nothing ever sized, which stay as tall as the browser drew them
      (19px on Unplaced, 24px on Services), and the ones deliberately sized
      SMALL by a min-height:0 for density (30px in the Packages edit rows).
      222 of them across twelve pages.

   3. A BUTTON WHOSE LABEL IS ONE CHARACTER. `.btn` sets min-height and no
      min-width, so the pager arrows - a single chevron - came out 24px wide
      and 36 tall. They sit next to each other, so the miss is not a near-miss:
      it pages the wrong way.

   All keyed on the pointer, never on the width. A tablet in landscape is over
   1000px wide and is the device this was written for. */
@media (pointer: coarse) {
  /* (2) The floor, for every control this stylesheet never sized - and for
     the ones it deliberately sized SMALL. A dozen rules set min-height:0 to
     pack controls into dense rows (.availrow, .addform, table.rows.edit and
     the rest), every one of them more specific than a bare element selector,
     and every one of them written for a mouse. Same argument as the tick box
     above: density is a preference, a target is a floor.
     min-height, so it can only ever grow something - nothing already above
     36px moves. Checkboxes and radios are excluded because they get their own
     square 20px above; sweeping them in here would stretch them into
     rectangles. */
  select,
  textarea,
  input:not([type="checkbox"]):not([type="radio"]):not([type="range"]) {
    min-height:36px !important;
  }

  /* (3) A chevron is about 7px of ink, so a pager arrow came out a tall
     sliver. Widened with PADDING rather than a min-width, and the difference
     is not cosmetic.

     min-width:36px was the first attempt and it shipped a worse bug than it
     fixed. An explicit min-width REPLACES the automatic min-width:auto that
     every flex item gets, and min-width:auto is the only thing stopping a
     white-space:nowrap button being squeezed below its own label. So in any
     tight flex row the button shrank and the text ran out of the box: on the
     Unplaced page "Accept all 50 on this page" was drawn 139px wide over 143px
     of text, with the A sliced off by the green pill's edge. Measured on a
     phone, where it happened; invisible on a desktop, where the rows are not
     tight.

     Padding does the same job from the other side: it counts towards
     min-content, so the button grows and the floor stays. Measured on the
     same handset - the real pager arrows go 36x36 to 37x36, and the Accept
     all button goes back to 182px and stops overflowing.

     `.seg .btn` is named as well because it sets its own 0 12px a few hundred
     lines up and would otherwise win on specificity - which is how the view
     switcher opted out of the touch rules once already, a few lines above
     this. */
  .btn, .seg .btn { padding-left:16px; padding-right:16px; }
}

/* ------------------------------------------- who is free, under a thumb ----

   The open-slots view answers "who can take this child now", and its answer is
   284 name pills on one phone screen. Each was 40x16 with 9px text: the
   smallest target in the app, on the view whose entire purpose is to be tapped.

   NOT the transparent ::after used for the History and Edit links. That works
   there because the nearest neighbour is 86px away; these sit 3px apart, and
   an ::after big enough to matter would overlap the pill beside it - which
   hands the tap to whichever comes later in the markup. That is precisely the
   bug the ::after was introduced to fix, so repeating it here would undo it.
   Measured before choosing: 3px vertically and 3px horizontally.

   So the pill itself grows. 28px rather than 36: these are packed in a grid
   cell rather than standing alone, 36 would add nearly three hundred pixels to
   a phone's calendar, and 28px with a 3px gap is a target a thumb hits. The
   text goes to 11px because 9px is below what anybody reads at arm's length,
   and it stops the pill being all padding. */
@media (pointer: coarse) {
  .openpill {
    min-height:28px; padding:3px 10px; font-size:11px;
    display:inline-flex; align-items:center;
  }
  /* AND ITS BUTTONS FILL IT. The pill became two buttons - RS:3 and the
     name - when it learned to say what a therapist owes, and a button cannot
     hold another button, so the pill went from being the target to being a
     span AROUND the targets. It stayed 28px tall here and its buttons were
     17: only the middle of the pill did anything, and the clinic's phone
     measured all five on one day's grid at 17px.

     Pads reaching the pill's own edges, so the pill is a target again
     without growing. Each part takes the side of the pill that is only its
     own - RS the left padding, the name the right - and 2px of the gap
     between them each, so the two never overlap and the gap is never dead. */
  .openpill-name, .openpill-rs { position:relative; }
  .openpill-name::after, .openpill-rs::after {
    content:""; position:absolute; top:-6px; bottom:-6px;
  }
  .openpill-rs::after { left:-8px; right:-2px; }
  .openpill-name::after { left:-2px; right:-8px; }
  .openpill-name:first-child::after { left:-8px; }
}

/* A package that has counted more attended sessions than it sells.
   Always a fact about the data rather than the family - twenty-five of twenty
   is not something a child can do - so it reads as a warning to correct, not
   as a number to act on. The Packages page has said this for a while with a
   red progress bar; the Approvals page did not, which is where it was finally
   noticed by somebody trying to make a decision from it. */
.overflag {
  display:inline-block; margin-left:6px; padding:0 5px; border-radius:var(--radius-xs);
  background:var(--bad-bg); color:var(--bad);
  font-size:10px; font-weight:800; letter-spacing:.3px; white-space:nowrap;
  cursor:help;
}

/* The reason a package has counted past its size, on the Unplaced page.
   Prose in a table cell, so it needs a measure and a line height or it reads
   as a paragraph that fell into a spreadsheet. Scoped to the table rather
   than given a bare class: .why already belongs to the conflict bar and the
   clash chip, and a third top-level rule for it would be the collision this
   stylesheet's own check exists to catch. */
/* The Decided tab's Decision cell: what was decided, then the state of the
   obligation and the policy's note, then who decided and when - three lines,
   read in that order. */
.decidedcell .decided, .decidedcell .decstate, .decidedcell .decwhen { display:block; }
.decidedcell .decstate, .decidedcell .decwhen { margin-top:3px; }
table.rows td.why {
  color:var(--muted); font-size:12px; line-height:1.45;
  max-width:44ch; white-space:normal;
}
table.rows td.why b { color:var(--ink); }

/* The quiet half of a cell on the packages-over-size list.
   Its own class rather than .sub, which has no base rule in this app at all -
   it is defined per context (.toolbar .sub, .signin .sub) and inside a table
   cell resolves to nothing, so the therapist's name and the package size ran
   straight into the text beside them. Styling .sub globally instead would
   have reached into the Approvals table, which uses it in its own way. */
.overmeta { color:var(--muted); font-size:11.5px; }
.overmeta.line { display:block; margin-top:2px; }

/* A row that is finished but still worth seeing. On the owed reschedules it
   is an obligation that has been paid off: kept in the list so somebody can
   confirm it settled rather than wondering where it went, and quietened so
   the ones still outstanding are what the eye lands on. */
tr.dimrow > td { opacity:.55; }
tr.dimrow:hover > td { opacity:1; }

/* Nobody has decided about this one yet, which is not a warning and not a
   readiness - it is an absence. Neutral on purpose, beside .owed.ready and
   .owed.held which both mean somebody has answered. */
.owed.pending { background:var(--sunken); color:var(--muted); }
/* Rejected: the number stands and nothing is owed. Red, matching the
   .pill.off this same fact wears on the Reschedules page, and last in the row
   because it is the only one of the four that is not work still to do. It is
   shown at all so that booking a reschedule for a session somebody already
   decided against is a choice, not an accident. */
.owed.forfeit { background:var(--bad-bg); color:var(--bad); }
/* Room above the hand-entered ledger in the reschedules panel. */
.pk-owed.rslive { margin:0 0 12px; }

/* The standing slots, one block per course.
   A single run of pills is unreadable as soon as a child is on two therapies:
   ten of them, interleaved, told apart only by a two-letter code somewhere in
   the middle. Grouped, each block answers one question - when is this child
   here for behaviour therapy - and the days run in week order inside it. */
.planblock { margin-bottom:9px; }
.planblock:last-child { margin-bottom:0; }
.pbhead {
  display:flex; align-items:center; gap:6px; margin-bottom:4px;
  font-size:11.5px; font-weight:600; color:var(--muted);
}
.pbhead .wcode {
  font-size:9.5px; font-weight:800; letter-spacing:.3px;
  padding:1px 5px; border-radius:var(--radius-xs);
  background:var(--accent-bg); color:var(--accent);
}

/* A field the form will refuse to save without. Shown while there is still
   time to fill it in rather than after the save is turned away - a required
   field that looks optional until you are stopped is a rule people resent
   instead of following. */
input.needed, textarea.needed {
  border-color:var(--wait);
  box-shadow:0 0 0 3px color-mix(in srgb, var(--wait) 18%, transparent);
}

/* A pill that is also a link - "awaiting", which now leads to the page where
   somebody can release it. .btn has been used on an anchor for a while; .pill
   had not, and a link's underline through a pill reads as damage. */
a.pill { text-decoration:none; }
a.pill:hover { filter:brightness(.95); }

/* ------------------------------------------------- when nothing answers ---
   Read in a bad moment by somebody with a parent waiting at the desk. A
   symbol, four words and one sentence: what is wrong, and whether their work
   survived. The first version had a heading, a paragraph and three bullets,
   which is an explanation rather than an answer. */
.failure {
  max-width:34ch; margin:14vh auto 0; text-align:center;
}
.failure h2 { margin:16px 0 4px; font-size:19px; letter-spacing:-.3px; }
.failure p { margin:0; color:var(--muted); font-size:13.5px; line-height:1.5; }
/* The drawing. Sized in one place so the whole scene scales together. */
.failart { width:170px; height:auto; color:var(--muted); }
.failart .fa-far { color:var(--faint); opacity:.75; }

/* The robot breathes. Two pixels over three seconds - enough that the screen
   is not a still photograph, little enough that somebody reading the sentence
   underneath is not being waved at. */
@keyframes fa-bob { 50% { transform:translateY(-2.5px); } }
.failart .fa-bot { animation:fa-bob 3.2s ease-in-out infinite; }

/* Traffic setting out and dying at the gap, staggered so it reads as three
   attempts rather than one blinking decoration. This is what the screen is
   actually doing: it retries on its own. */
@keyframes fa-send { 0%, 70%, 100% { opacity:.15; } 25% { opacity:1; } }
.failart .fa-dash { animation:fa-send 2.4s ease-in-out infinite; }
.failart .fa-d2 { animation-delay:.28s; }

/* The break, and the antenna, and one eye that has given up. Different
   durations on purpose: in step they read as one pulse, out of step they read
   as a thing that is faulty. */
.failart .fa-break { color:var(--bad); }
@keyframes fa-flick { 0%, 100% { opacity:1; } 45% { opacity:.25; } }
.failart .fa-break { animation:fa-flick 1.7s ease-in-out infinite; }
.failart .fa-spark { animation:fa-flick 2.9s ease-in-out infinite; }
.failart .fa-eye { fill:currentColor; stroke:none; }
.failart .fa-dud { animation:fa-flick 1.1s steps(1, end) infinite; }

/* A refusal is not broken machinery, so it does not twitch. One slow breath,
   in the colour of a thing that was turned down. */
.failart .fa-refuse { color:var(--bad); }
@keyframes fa-breathe { 50% { opacity:.55; } }
.failart .fa-refuse { animation:fa-breathe 2.6s ease-in-out infinite; }

/* With motion turned off the drawing still has to say the same thing, so
   everything settles at full strength rather than wherever it stopped. */
@media (prefers-reduced-motion: reduce) {
  .failart * { animation:none !important; opacity:1 !important; }
  .failart .fa-dash { opacity:.55 !important; }
}

.failact {
  display:flex; align-items:center; justify-content:center; gap:10px;
  margin-top:18px;
}
.failact .sub { color:var(--faint); font-size:12px; font-variant-numeric:tabular-nums; }
.faildetail { margin-top:18px; font-size:11.5px; color:var(--faint); }
.faildetail summary { cursor:pointer; }
.faildetail code {
  display:block; margin-top:6px; padding:7px 9px; border-radius:var(--radius-sm);
  background:var(--sunken); font-size:11px; text-align:left; word-break:break-word;
}

/* Losing it mid-task. Fixed to the BOTTOM: the top is where the nav and the
   toolbars are, and pushing those down moves the control somebody is already
   reaching for. */
.offlinebar {
  position:fixed; left:0; right:0; bottom:0; z-index:60;
  display:flex; align-items:center; justify-content:center; gap:8px;
  padding:9px 16px; font-size:13px;
  background:var(--bad); color:#fff;
  box-shadow:0 -2px 12px rgba(16,24,32,.18);
}
.offlinebar .navicon { width:16px; height:16px; }

/* A label and the control it names, kept together.
   The filter bar wraps, and its labels are siblings of their controls - so on
   a narrower screen "State" ended one line and its dropdown began the next,
   which reads as a stray word. Pairing them means a bar wraps between filters
   instead of through one. */
.filterbar .fpair, .toolbar .fpair {
  display:inline-flex; align-items:center; gap:6px; white-space:nowrap;
  /* AND IT MUST NOT OUTGROW THE SCREEN. A <select> is as wide as its widest
     OPTION, whatever box it is in - and the Reschedules Decision filter offers
     the four cancellation decisions in full ("Approve - Reschedule (keep
     numbering)"), which measures 480px. The pair around it came to 537px on a
     375px phone: found by tools/mobilecheck.py against the clinic's tablet,
     where the two were the only faults in sixteen pages.

     The body never scrolled sideways, so nothing looked broken - the control
     simply ran past the edge of the bar with its tail unreachable. min-width:0
     is what lets a flex item shrink below its content at all; without it the
     max-width above is ignored and the select keeps its widest option. The
     text is then clipped by the browser, which is the right trade: the chosen
     option is readable and the list opens in full. */
  max-width:100%;
}
.filterbar .fpair select, .filterbar .fpair input,
.toolbar .fpair select, .toolbar .fpair input { min-width:0; max-width:100%; }

/* ------------------------------------------------- staff messages ---
   Two-line cells. `.sub` is inline by default in a table row, which ran the
   date and the time together as "20 September 202604:22 PM" and put the
   reason a message failed on the same line as its pill. Scoped to this table
   rather than made general: every other page's rows were written expecting
   the inline behaviour. */
.rows.smsg td .sub { display:block; font-size:11.5px; margin-top:2px; }
.rows.smsg td.when { white-space:nowrap; }

/* ------------------------------------------------- the page bar (phones) ---
   Every page, along the bottom, scrolling sideways. Replaces the "..." menu
   that a narrow top bar used to fold all sixteen pages into - see
   renderPageBar() in main.js for why a flat scrolling row beats a menu here.

   Above 760px it does not exist at all: the top bar has the room, and two
   navigations on one screen is one too many. */
/* ------------------------------------------------- the pages, on a phone ---
   A drawer from the left, opened by the burger in the top bar.

   It replaced a bar along the bottom that scrolled sideways through every
   page. See renderDrawer() in main.js for why that stopped working: twenty-one
   pages at 66px is about 1,400px of scrolling on a 375px screen.

   The handle and the panel exist only below 760px. Above it the top bar shows
   every page with its own dropdowns, and a second way to the same places is a
   second thing to keep in step. */
.burger { display:none; }
.drawer, .drawerback { display:none; }

@media (max-width: 760px) {
  .burger {
    display:inline-flex; align-items:center; justify-content:center;
    padding:6px 8px; min-width:36px; line-height:1;
  }
  .burger .navicon { width:20px; height:20px; }

  /* THE BACKDROP IS WHAT SAYS THE PAGE BEHIND IS NOT LISTENING, and it is
     also the biggest tap target for getting out. Faded rather than snapped,
     or the whole screen blinks on every open. */
  .drawerback {
    display:block; position:fixed; inset:0; z-index:95;
    background:rgba(0, 0, 0, .38);
    visibility:hidden; opacity:0;
    transition:opacity .18s ease, visibility 0s linear .18s;
  }
  .drawerback.open {
    visibility:visible; opacity:1;
    transition:opacity .18s ease, visibility 0s;
  }

  /* FROM UNDER THE TOP BAR, not over it. The brand and the account button
     stay put and the burger stays where the thumb left it, so the way out is
     exactly where the way in was.

     `height` off the bar rather than `inset:0`, and the safe-area insets on
     the sides, because a phone held sideways takes a bite out of the left
     edge with the notch - which is the edge this slides from. */
  .drawer {
    display:flex; flex-direction:column; gap:2px;
    position:fixed; z-index:96;
    /* UNDER THE BAR, EXACTLY. .appnav-inner is min-height 52px and
       .appnav adds the status-bar inset on top of it, so a flat 52px
       here would slide the panel up behind the bar by the height of the
       notch - covering its own handle on the phones that have one. */
    top:calc(52px + env(safe-area-inset-top)); bottom:0;
    left:0; width:min(84vw, 310px);
    padding:10px calc(10px + env(safe-area-inset-right))
            calc(14px + env(safe-area-inset-bottom))
            calc(10px + env(safe-area-inset-left));
    background:var(--surface); border-right:1px solid var(--line);
    box-shadow:6px 0 24px rgba(0, 0, 0, .14);
    overflow-y:auto; overscroll-behavior:contain;
    /* VISIBILITY, NOT THE `hidden` ATTRIBUTE, and this is the whole reason
       the panel needs no JavaScript beyond one class.

       It was written the usual way first - remove `hidden`, then add the
       class on the next animation frame, because a transition cannot run
       from a state the element was never painted in. On a real phone that
       broke: ANDROID DOES NOT RUN requestAnimationFrame IN A TAB IT
       CONSIDERS HIDDEN, and the callback simply never arrived. The drawer
       was then un-hidden, still translated off the left edge, with the page
       behind it locked - an app that had stopped responding with nothing on
       screen to say why.

       visibility is animatable in the sense that matters: delayed to the end
       of the transition on the way out, so the panel stays visible while it
       slides away, and immediate on the way in. It also does what `hidden`
       was there for - a hidden-by-visibility element is out of the tab order
       and out of the accessibility tree. */
    visibility:hidden;
    transform:translateX(-102%);
    transition:transform .2s ease, visibility 0s linear .2s;
  }
  .drawer.open {
    visibility:visible; transform:translateX(0);
    transition:transform .2s ease, visibility 0s;
  }

  /* A group, and the word above it. The heading is quiet and the items are
     not: the heading is there to be skimmed past once, and then never read
     again by somebody who knows where Finance is. */
  .drawgroup { padding:2px 0; }
  .drawgroup + .drawgroup {
    margin-top:6px; padding-top:8px; border-top:1px solid var(--line-soft);
  }
  .drawhead {
    display:flex; align-items:center; gap:8px;
    margin:0 0 2px; padding:4px 10px;
    font-size:10.5px; font-weight:700; letter-spacing:.07em;
    text-transform:uppercase; color:var(--faint);
  }
  .drawhead .navicon { width:14px; height:14px; }

  /* 44px tall, which is the smallest thing a finger hits reliably and is what
     tools/mobilecheck.py measures against. */
  .drawitem {
    display:flex; align-items:center; gap:11px; width:100%;
    min-height:44px; padding:8px 10px;
    border:0; border-radius:var(--radius-sm); background:none;
    color:var(--ink); font:inherit; font-size:14px; text-align:left;
    cursor:pointer;
  }
  .drawitem .navicon { width:18px; height:18px; flex:none; color:var(--muted); }
  .drawitem:active { background:var(--sunken); }
  .drawitem.current {
    background:var(--accent-bg); color:var(--accent); font-weight:600;
  }
  .drawitem.current .navicon { color:var(--accent); }

  /* The page behind holds still while the drawer is over it. Two scrolling
     things under one finger is how a drawer ends up moving the calendar
     underneath itself. */
  body.drawered { overflow:hidden; }

  /* ROOM AT THE FOOT OF A PAGE. This was 134px for the bottom bar plus the
     WhatsApp button floating above it. There is no bar now, so what is left
     is the button itself: it is 52px and sits 20px up, and without clearance
     the last row of any page sits under it - on the Calendar that is a row of
     sessions, and a tap aimed at a chip's "+" opened WhatsApp instead.
     AND THE PHONE'S OWN BAR UNDER BOTH: the button already stands on the
     safe-area inset, so the room has to as well, or the last row sits under
     the button by the 48px of the three-button bar (25 October 2026). */
  .wrap { padding-bottom:calc(84px + env(safe-area-inset-bottom)); }

  /* Back down to the corner, now that nothing is along the bottom. */
  .wafab { bottom:calc(20px + env(safe-area-inset-bottom)); }

  /* AND THE LAST ROW HAS TO CLEAR IT. Padding on .wrap is not enough and
     never was: a grid stretched to the window scrolls INSIDE its own box, so
     the page has no more scrolling left to do and the padding below it is
     never reached. Measured at the end of the scroll on Packages and on
     Reschedules - the last row ran to y=744 with the button starting at
     y=687, so its bottom 57px sat behind it, and on Packages that is where
     the row's own controls are.

     The clearance therefore goes INSIDE the scroller, which is the thing
     that actually runs out. 78px is the button's 52 plus the 20 it floats
     up by, plus a little. */
  .gridwrap { padding-bottom:78px; }

  /* The pages are in the drawer; the top bar keeps the brand, the bell, the
     theme and the account button. */
  .appnav-inner .navlinks { display:none; }
}

/* The way back in when the PIN is forgotten. Quiet, because it is the answer
   to a rare question - but present on the screen rather than hidden behind a
   link, because somebody looking at it has already failed twice and is
   deciding whether the app is broken. */
.signin .lockout {
  margin-top:10px; width:100%; font-size:12px; color:var(--muted);
}
#lock .card .err { margin-top:10px; }
#lock input#pin {
  font-size:20px; letter-spacing:.32em; text-align:center;
}

/* ------------------------------------------------------ notifications ---
   The bell and its list. Quiet on purpose: the WhatsApp button is loud
   because a parent is waiting for an answer, and if both shout neither is
   read. */
.btn.ghost.bellbtn {
  position:relative; padding:6px 9px; line-height:1; min-width:34px;
  display:inline-flex; align-items:center; justify-content:center;
}
.btn.ghost.bellbtn .navicon { width:18px; height:18px; }

/* ---------------------------------------------------- find a child ---
   The bell's own clothes, because it is the same kind of thing: a quiet glyph
   in the bar (findkid.js, paintFind in main.js). The popup it opens is a
   search box and a list, a line per child; the child picked shows its
   buttons under its name. */
.btn.ghost.findbtn {
  position:relative; padding:6px 9px; line-height:1; min-width:34px; min-height:30px;
  display:inline-flex; align-items:center; justify-content:center;
}
.btn.ghost.findbtn .navicon { width:18px; height:18px; }
.findkid { display:flex; flex-direction:column; gap:8px; min-width:min(560px, 100%); }
.findkid .search { width:100%; max-width:none; }
.findkid #fkSaid:empty { display:none; }
.fklist { list-style:none; margin:0; padding:0; display:flex; flex-direction:column; gap:4px; }
.fkrow {
  display:flex; flex-direction:column;
  border:1px solid var(--line); border-radius:var(--radius); background:var(--surface);
}
.fkrow.open { border-color:var(--accent); }
.fkwho {
  display:flex; align-items:baseline; gap:8px; flex-wrap:wrap; width:100%;
  min-height:40px; padding:8px 10px; text-align:left;
  background:none; border:0; color:inherit; font:inherit; cursor:pointer;
}
.fkwho b { overflow:hidden; text-overflow:ellipsis; white-space:nowrap; min-width:0; }
.fkacts { display:flex; flex-wrap:wrap; gap:6px; padding:2px 10px 10px; }
.belldot {
  position:absolute; top:1px; right:1px; min-width:15px; height:15px;
  padding:0 3px; border-radius:var(--radius-pill); background:var(--bad); color:#fff;
  font-size:9.5px; line-height:15px; font-weight:700; text-align:center;
}

.notiflist { list-style:none; margin:0; padding:0; }
.notiflist li { border-bottom:1px solid var(--line-soft); }
.notiflist li:last-child { border-bottom:0; }
.notiflist button {
  display:block; width:100%; text-align:left; cursor:pointer;
  border:0; background:none; font:inherit; color:var(--ink);
  padding:11px 13px; border-left:3px solid transparent;
}
.notiflist button:hover { background:var(--sunken); }
.notiflist .sub { display:block; color:var(--muted); font-size:12px; margin-top:2px; }
.notiflist .when { display:block; color:var(--faint); font-size:11px; margin-top:3px; }
/* Unread is a stripe, not a colour wash: the list is read top to bottom and
   a block of highlighted rows is harder to scan than a plain one. */
.notiflist li.fresh button { border-left-color:var(--accent); }
.notiflist li.fresh b { font-weight:700; }
/* What the number counted (BELL_COUNTS): the bell's own red on the stripe
   and a word, so the line the badge was about can be found in the list. */
.notiflist li.needs button { border-left-color:var(--bad); }
.needsyou {
  display:inline-block; margin-left:4px; padding:0 6px; border-radius:var(--radius-pill);
  background:var(--bad); color:#fff; font-size:10px; font-weight:700; line-height:16px;
  vertical-align:1px;
}
/* "Choose what reaches you", under the list. */
.notifprefs { border-top:1px solid var(--line-soft); padding:10px 13px 14px; }
.notifprefs summary { cursor:pointer; font-weight:600; color:var(--accent); padding:4px 0; }
.notifprefs .hint { margin:6px 0 8px; }
.notifkinds { list-style:none; margin:0; padding:0; }
.notifkinds li { display:flex; align-items:center; gap:10px; padding:7px 0;
  border-bottom:1px solid var(--line-soft); font-size:13px; }
.notifkinds li:last-child { border-bottom:0; }
.notifkinds .sub { color:var(--muted); font-size:11.5px; margin-left:4px; }

/* The grid on Settings, Notifications. */
.rows.notifgrid td.wrap { max-width:none; font-size:13px; color:var(--ink); }
.rows.notifgrid td.wrap .sub {
  display:block; font-size:12px; color:var(--muted); margin-top:2px;
}
.rows.notifgrid th.num, .rows.notifgrid td.num { text-align:center; width:104px; }

/* "none" and "default" under each role. Quiet, because they are the rarely
   used half of the column and the checkboxes are the point. */
.rows.notifgrid .colacts {
  display:flex; gap:8px; justify-content:center; margin-top:4px;
  font-weight:400; letter-spacing:0;
}
.rows.notifgrid .colacts button {
  border:0; background:none; padding:0; cursor:pointer;
  font:inherit; font-size:10.5px; text-transform:none;
  color:var(--faint); text-decoration:underline;
}
.rows.notifgrid .colacts button:hover { color:var(--accent); }
.rows.notifgrid input[type="checkbox"] { width:17px; height:17px; cursor:pointer; }

/* The short-form legend on Help. The first column holds the mark exactly as
   it appears on a card, at the size it appears, so the reader is matching a
   shape rather than reading a description of one. */
.rows.legend td:first-child { width:120px; white-space:nowrap; }
.rows.legend td:last-child { white-space:nowrap; font-size:11.5px; }
.rows.legend td.wrap { max-width:none; color:var(--ink); font-size:13px; }

/* ------------------------------------------- every chip in full colour ---
   A preference, held in this browser, toggled from the Calendar toolbar.

   THE COLOUR IS EXACT. The first version washed the chip with 12% of the
   therapist's colour, which was readable and pretty and wrong: the clinic
   asked for the colour assigned to that therapist, and 12% of #CC4125 is not
   #CC4125 - it is a pink nobody chose. A tint also collapses the distance
   between two therapists who were given deliberately different colours, which
   is the entire reason for having them.

   So the chip is painted the therapist's colour and the writing on it is
   flipped to whatever can be read there, per chip, computed from that
   colour's luminance in kidStyle() - see web/js/chip.js for why the
   threshold is 0.179 and not a number somebody liked.

   Only the writing is flipped. The session number, the badges, the outcome
   word and the + are each already a colour on a solid tint of ITSELF, so
   each reads on whatever ends up behind it and none of them needs touching.
   Repainting them was the first attempt and it broke the session number,
   whose own --snum-ink then had to fight a --kidink chosen for the chip
   rather than for the plate: on a yellow chip that was black on a dark grey
   box. They get one thing - an edge - so a plate never dissolves into a chip
   that happens to be near its own background.

   The stripe stays. 4px, painted the ink colour rather than --kidcolour,
   because a stripe the same colour as the chip it sits on is not a stripe.
   The clinic has already had that marker removed once in a restyle and asked
   for it back; it does not quietly disappear here either.

   Contrast was measured, not judged. Every piece of writing on a chip was
   read back from the rendered page against its own background, in both
   themes, across six deliberately awful colours - a mid purple, a pale
   yellow, a deep navy, an off-white, a mid green and a mid red. The mid
   tones are the hard ones and the numbers say so: nothing on this chip
   clears 5:1 on #7C4DFF, and that is the best any ink can do there.

   The fill itself is a plain var(), so there is no color-mix fallback to
   worry about. The three places that do mix - the border, the stripe and the
   plate edges - degrade to a flat colour, which is duller and still works. */
body.fullcolour .sess {
  background:var(--kidcolour, var(--surface));
  /* Not the chip colour itself: a therapist can pick #F5F5F5, and a white
     chip with a white border on a white page is a chip with no edge at all.
     Nudged 22% towards its own ink instead, which is a visible outline on
     every colour including the two that match the page. */
  border-color:color-mix(in srgb, var(--kidink, var(--line)) 22%, var(--kidcolour, var(--line)));
  color:var(--kidink, var(--ink));
}
body.fullcolour .sess::before {
  width:7px;
  background:color-mix(in srgb, var(--kidink, #000) 55%, var(--kidcolour, transparent));
}
/* No `filter` here, which the first version used to lift the chip on hover.
   `filter` is one property, not a list that accumulates, so it replaced the
   `saturate(.35)` that greys a chip filtered out of view - hovering an
   excluded chip made it look included. It also does nothing at either end of
   the range: brightness on a white chip has no room to go up and on a black
   one nothing to brighten. The border is the feedback, and it is visible on
   every colour because it is drawn in that chip's own ink. */
body.fullcolour .sess:hover { border-color:var(--kidink, var(--faint)); }
/* AND THE DOTTED BORDER SURVIVES THE COLOUR. `.sess.tentative` above draws a
   pencilled-in chip with a dotted edge, and this mode's rule - the same
   specificity, later in the file - painted the border back solid in a colour
   one nudge from the fill, so the edge was neither dotted nor visible.
   Reported with the chip in hand: "Not Confirmed session chip should be in
   dotted boundaries". Dotted here too - and AMBER, the same --wait the NOT
   CONFIRMED badge wears, asked for by name once it had been drawn in the
   chip's own ink: "for not confirmed chips made the dotted line of orange
   color". One colour for the state wherever the chip is, rather than a ring
   that is black on one chip and white on the next. */
body.fullcolour .sess.tentative {
  border-style:dashed; border-color:var(--tent);
  outline-color:var(--tent);
}
/* And the two cancellations' solid rings, over this mode's own border colour
   for the same reason. */
body.fullcolour .sess.st-cancelled { border-color:var(--canc); outline-color:var(--canc); }
body.fullcolour .sess.st-immediate { border-color:var(--imm); outline-color:var(--imm); }
/* The writing that has no background of its own: the name, the supporting
   lines, and History and Edit - whether they are drawn as words or as the
   clock and pencil glyphs that replaced them.

   THE GLYPHS WERE MISSED WHEN THEY MOVED. This rule was written when History
   and Edit were underlined words on the bottom line wearing .linkbtn, and it
   named that class. They are .chipicon on the top line now, and .chipicon is
   `color:var(--muted)` - a colour chosen to sit on the PAGE, which has no
   relationship at all to a chip painted a therapist's colour.

   It reads as a dark-theme fault and is not one. --muted flips with the theme
   and the chip's colour does not, so in one theme the glyph lands near the
   ink the chip actually needs and in the other it lands on the opposite side
   of it. Measured against all eleven colours in use here: ten of them fail AA
   in the dark theme and ten in the light one, worst 1.16:1 and 1.03:1. The
   magenta it was reported on is 1.28:1 dark and 1.87:1 light - bad in both,
   and only obvious in one. The "+" beside them was fine throughout because it
   is a plate on a tint of itself and was never drawn in --muted.

   All at full strength, which is the one place this differs from the plain
   chip. There the secondary lines are --muted, a step back from --ink, and
   the obvious translation was the ink at 82%. Measured against the real
   chips, that put History, Edit and the 10:30 line at 3.7:1 on a mid purple
   and 3.7:1 on a mid red - under AA, on the smallest text on the chip.

   There is no headroom to spend: on a colour of middling luminance the BEST
   available contrast is 4.58:1, so anything held back from full ink fails.
   The hierarchy is carried by size and weight instead, which is most of what
   was carrying it already - 12.5px semibold against 10px mono. */
body.fullcolour .sess .sess-name,
body.fullcolour .sess .sess-meta,
body.fullcolour .sess .sess-note,
body.fullcolour .sess .chipicon,
body.fullcolour .sess .linkbtn { color:var(--kidink, var(--ink)); }
body.fullcolour .sess .linkbtn:hover { text-decoration-color:currentColor; }

/* AND THE STATES, which reach for --accent on a plain chip. The accent is one
   teal green against a page that is grey; on a chip it is one more colour
   against a colour somebody chose, and on the purple and magenta in this book
   it is neither visible nor obviously a hover. So the glyph stays the chip's
   own ink and the FEEDBACK is a plate mixed from that ink - which is the same
   thing every badge on this chip does, and works on every colour because it
   is built from the one colour known to read there. */
body.fullcolour .sess .chipicon:hover {
  color:var(--kidink, var(--ink));
  background:color-mix(in srgb, var(--kidink, transparent) 16%, transparent);
}
body.fullcolour .sess .chipicon:focus-visible {
  outline-color:var(--kidink, var(--accent));
}

/* Everything that is already a small plate - the session number, the badges,
   the outcome word, the + button - is LEFT ALONE, including its own ink. Each
   was built as a colour on a solid tint of itself and reads on whatever is
   behind it; repainting them was the first attempt and it broke the session
   number, whose --snum-ink then had to fight a --kidink chosen for the chip
   rather than for the plate.

   They get one thing: an edge, so that a plate never dissolves into a chip
   that happens to be near its own background colour. A therapist can pick
   #F5F5F5 in a light theme and #161C22 in a dark one, and then every badge on
   that chip is invisible without this. */
body.fullcolour .sess .snum,
body.fullcolour .sess .badge,
body.fullcolour .sess .outcome,
body.fullcolour .sess .chipadd {
  box-shadow:0 0 0 1px color-mix(in srgb, var(--kidink, transparent) 22%, transparent);
}

/* ------------------------------------------ the calendar toolbar, on a phone -

   Measured on a 390px screen: the toolbar was 253px tall on an 840px
   viewport, and with the app bar and the conflicts banner above it that is
   47% of the screen gone before a single session is visible. It was not
   dense controls doing it - it was two of them each taking a whole row and
   leaving most of it empty.

   The date box asked for 154px and the range arrows for 258px, which is
   412px between them: twenty-six pixels too wide to share a 386px row, so
   they took one each. Most of that was the label saying "21 Sep – 26
   September 2026" - the same month written twice, in two different
   spellings. fmtSpan() in ui.js now says "21–26 Sep 2026", and the two
   controls that do the same job (pick a week) sit on the same line.

   560px rather than 760, because a tablet has the width to spare and this is
   the kind of tightening that only pays where it is needed. */
@media (max-width: 560px) {
  .toolbar { gap:6px; padding:6px; }
  /* Fixed, not flexible: a date box given room to grow takes the whole row
     and puts the arrows back where they started. */
  .toolbar .dateinput { flex:0 0 118px; min-width:0; }
  .toolbar .rangenav { flex:1 1 auto; min-width:0; }
}

/* ------------------------------------------ 118px stopped being enough -----

   The rule above squeezes a date box to 118px so it can share a row with the
   range arrows, and its own comment records what it cost: "the date box asked
   for 154px". That was a fair trade when the text in it was 12px.

   It is not one now. The iOS-zoom rule raises every control to 16px on a
   touch screen, and at 16px a date needs 146px - measured on the phone rather
   than guessed: "30/09/2026" is 96px of IBM Plex Mono, plus 22px of padding
   and border, plus the 28px picker indicator Chrome draws inside the field.
   118px leaves 28px too little, which is exactly the indicator, so the date
   was cut to "01/09/2" and the branch filter to "All bran". The Dashboard's
   whole top row - which branch, from when, to when - was three controls none
   of which could be read.

   Truncating a date is worse than wrapping a row. A row that wraps is still
   two usable controls; a date box reading "01/09/2" is not a control at all,
   and it is the one reception checks before believing any figure under it.

   The trade it was making is not even available any more, and that was
   measured too: on a 406px phone the range arrows are 369px wide, so they
   already have a row to themselves and the date is not sharing with anything.
   Applying this changed the calendar toolbar's height not at all - 194px
   before and after, the same seven rows - and the dashboard's the same, 218px
   and 214px. It costs nothing here and buys back both controls.

   Keyed on the pointer, so a narrow window on a laptop - 12px text, where
   118px is still plenty - keeps the tighter layout it was written for. */
@media (pointer: coarse) {
  .toolbar .dateinput { flex:0 0 max-content; min-width:max-content; max-width:100%; }
}

/* ------------------------------------- one child's last seven days, as chips -

   Opened from the NAME on a chip. The controls sit on one line above the
   list: a from, a to, and three spans that count BACK from the To date so
   that widening the window never drags it to this week.

   The list below it is a grid of day cells, which is what the calendar this
   window opens from is. It began as `.dayslot` borrowed from the day panel -
   a date on the left, a column of chips on the right - and that is right for
   the panel, which is a 430px strip where one column is all that fits. In an
   860px dialog it drew one file of chips down the left with half the window
   empty beside it, and six days of a quiet week needed a scroll.

   `.dayslot` is NOT restyled to fix that: the day panel still wants its label
   on the left, and one rule reaching two layouts from opposite ends of this
   file is how `.addbtn` silently restyled three pages. These are their own
   classes. */
.kdbar {
  display:flex; flex-wrap:wrap; align-items:center; gap:8px;
  padding:0 0 12px; margin-bottom:4px; border-bottom:1px solid var(--line-soft);
}
.kdbar label { font-size:12px; color:var(--muted); }
.kdbar .dateinput { min-width:0; }
.kdbar .grow { flex:1; }

/* The dialog is a fixed height so the controls do not move as the range
   changes - a toolbar that jumps up the screen when a quiet week is chosen is
   a toolbar you have to find again. */
#kdList { max-height:min(60vh, 560px); overflow-y:auto; }

/* A STRIP, NOT A GRID. Days run left to right in time and the row never
   wraps - it scrolls sideways instead.

   A wrapping grid was tried and is the wrong shape for a sequence of dates.
   Wrapping means the reader travels right along one line and then jumps back
   to the far left to carry on, and with the newest day first that also ran
   time backwards, so the dates went 22, 21, 19 and then down a line to 18.
   Both faults go away in one line read in one direction.

   scroll-snap so a flick lands on a day rather than halfway through one, and
   overscroll-behavior-x:contain so scrolling past the oldest day does not
   start dragging the page - or, on a trackpad, trigger the browser's back
   gesture, which would throw the whole window away mid-read. */
.kddays {
  display:flex; gap:10px; align-items:flex-start;
  overflow-x:auto; overscroll-behavior-x:contain;
  scroll-snap-type:x proximity;
  padding-bottom:8px;
}
/* 255px, and the number comes from the therapist's name rather than from the
   number of columns it happens to give. `.sess-who` is the one thing in a
   chip that ellipsises, and at 196px - which did fit four days - it cut
   Akanksha to "Akank.". The longest therapist name in the book is 9
   characters; measured against a 10-character one, 255 is the width where
   none of them clip, so the next hire does not break it either.

   Four days still fit, because the dialog took the extra width instead: see
   .modal-box.widest above. Density was the wrong thing to buy with a
   therapist's name.

   flex:none, or a day with one chip would be squeezed narrower than a day
   with three and the columns would stop lining up. */
.kdday {
  flex:none; width:255px; scroll-snap-align:end;
  border:1px solid var(--line-soft); border-radius:var(--radius-sm);
  background:var(--ground); padding:8px;
}
.kdday-at {
  display:flex; align-items:baseline; gap:6px; flex-wrap:wrap;
  margin-bottom:7px; padding-bottom:6px;
  border-bottom:1px solid var(--line-soft);
  font-family:"IBM Plex Mono", ui-monospace, monospace;
  font-size:12px; font-weight:600; color:var(--muted);
}
.kdday-at .sub { font-size:10.5px; font-weight:400; color:var(--faint); }
/* The chips are evidence here, not controls: nothing on them opens an editor,
   so the pointer should not promise that it does. Same reasoning as the day
   panel's copy of this, and the same two lines. */
.kdday .sess { cursor:default; }
.kdday .sess:hover { transform:none; box-shadow:var(--shadow-xs); }

@media (max-width: 560px) {
  /* The spans wrap to their own line rather than squeezing the date boxes,
     which on a phone are the harder of the two to hit. */
  .kdbar .grow { flex-basis:100%; height:0; }

  /* ONE DAY, FILLING THE WIDTH. At a fixed 255px a phone shows one whole day
     and 94px of the one before it - a strip of cut-off chips down the left
     edge with a floating ATTENDED in it, which reads as the window being
     broken rather than as there being more to scroll to. A day that fills the
     strip lands on a clean edge every time, and scroll-snap then moves
     between them one whole day at a time, which is the gesture a thumb makes
     anyway. */
  .kdday { width:100%; scroll-snap-align:center; }
  .kddays { scroll-snap-type:x mandatory; }
}

/* ------------------------------------------------ "today", inside a window -

   The grid has marked today since it was written. The windows opened FROM
   the grid had nothing - the last seven days, a course's whole history, the
   day a booking is for - and a list of dates with no anchor in it is a list
   of numbers. Reception's first act on opening one was to work out where
   they were.

   Small and quiet: it is a reference point, not an alarm. The row it sits on
   gets a tinted edge so the eye can find it while scrolling without reading
   any of the dates. */
.todaytag {
  display:inline-block; margin-left:6px; padding:0 5px;
  border-radius:var(--radius-xs); background:var(--accent-bg); color:var(--accent);
  font-size:9.5px; font-weight:800; letter-spacing:.4px; text-transform:uppercase;
  vertical-align:middle; white-space:nowrap;
}
tr.istoday > td { background:var(--accent-bg); }
tr.istoday > td:first-child { box-shadow:inset 3px 0 0 var(--accent); }
/* Today is now a CELL rather than a row across the window, so the tinted
   left edge that marked the row has nothing to run down. The whole cell takes
   the tint and the accent border instead, which is how the grid behind this
   dialog has marked today since it was written (td.daycell.today). */
.kdday.istoday { background:var(--accent-bg); border-color:var(--accent); }
.kdday.istoday .kdday-at { color:var(--accent); border-bottom-color:var(--accent); }

/* The chips in that window were stretching to the full width of the dialog -
   743px measured - which left 558px of nothing between the child's name and
   the tag saying what became of the session. That was held off with a
   max-width of 420px on the list, which treated the symptom: the chips
   stopped stretching and the 400px beside them stayed empty.

   The strip above is the actual fix and it makes the cap unreachable: a day
   is a fixed 196px at every window width, so a chip inside one is 178px and
   can never approach 420. The cap is deleted rather than left as a rule that
   reads as though it were load-bearing. */

/* The grid while a refresh is in flight.

   It used to be replaced by a spinner, which emptied the page and lost the
   scroll position - see refresh() in calendar.js. The old grid now stays put
   and dims instead, which is both honest (this IS last moment's data) and
   about 230ms long, so it reads as a blink rather than a wait.

   Still clickable on purpose. Blocking it would be tidier in theory and in
   practice would mean a stuck overlay on any request that never answers,
   which is the failure this replaced. */
#calGrid.busy { opacity:.6; transition:opacity .1s ease-out; }
@media (prefers-reduced-motion: reduce) { #calGrid.busy { transition:none; } }

/* The second line under the toolbar, when filters are hiding sessions.

   It shares .focusbar with "Showing only <child>" because the two say the
   same kind of thing - the grid is not showing you everything - and can be
   on screen together. The tint is different so that two bars stacked read as
   two facts rather than one repeated. */
.focusbar.filterbar-note {
  background:var(--sunken); border-color:var(--line);
}

/* Which branches a therapist works at, on the Staff details form.
   A row of tick boxes rather than a multi-select: there are two of them, and
   a two-item multi-select is a control nobody can operate without being told
   to hold ctrl. */
.branchpick { display:flex; flex-wrap:wrap; gap:8px 18px; margin:2px 0 6px; }

/* The PIN switch, above the form that sets the secret.
   Set apart from the form because it is a different question: the form
   chooses the PIN, this decides whether anything asks for it. Side by side
   with no separation they read as one control with two halves. */
/* A day rate has nothing to count down, so its cell carries words instead of
   a bar. It keeps the column's width and alignment - a row that suddenly went
   narrow would read as missing data rather than as a different arrangement. */
.progress.daily { line-height:1.35; }
.progress.daily .count { font-weight:600; }
.progress.daily .sub { display:block; font-size:10.5px; color:var(--muted); }

/* The Start cell carries its own "ask the calendar" button and the one line
   of answer it gives back. The answer is said out loud even when nothing
   moved - a button with no visible effect reads as broken, and "the calendar
   agrees with you" is a useful thing to be told. */
/* THE BUTTON WAS BEING PAINTED OVER BY THE NEXT COLUMN. Reported from the
   desk as "some extra text (from) near status column, which is looking ugly",
   and that is exactly what it was: `from calendar` overflowing a nowrap cell.
   Measured - the cell is 203px, its content needs 271px, and the button starts
   at x=1203 where the Status cell starts at x=1201, so the Status background
   covered all of it but the first three letters.

   A date input carries a native minimum width and was taking 189px of the 203
   on its own, which is far more than "03-09-2026" and a picker need. Holding
   it to 8.5rem leaves the button its room on the same line, so no row grows
   taller - which matters on a grid of ninety-five. */
.startcell { white-space:nowrap; }
/* The table's own `table.rows.edit input { width:100%; min-width:92px }` is
   (0,2,2) and out-specifies `.startcell input[type=date]` at (0,2,1), so the
   first version of this rule was in the stylesheet, matched, and lost - the
   box stayed 189px and the button stayed under the Status column. Found by
   asking the browser which rules it had, which is the only thing that can see
   a rule that is present and beaten. */
table.rows.edit td.startcell input[type="date"] { width:8.5rem; min-width:0; }
/* Paid on, the column before it: the same date box at the same width, for
   the same reason. It wore `.tiny` first, which the grid sizes at 58px for a
   two-digit number - enough to show "14-0" of a date and nothing else. */
.paidcell { white-space:nowrap; }
table.rows.edit td.paidcell input[type="date"] { width:8.5rem; min-width:0; }
/* The day an upcoming course will start, by the calendar's reckoning: a line
   of its own under the box. Drawn inline it once ran into the "from calendar"
   button on a cell that does not wrap; the button is gone (0232), the line is
   still its own. */
.startcell .ahead { display:block; font-size:10.5px; color:var(--muted); margin-top:2px; white-space:normal; }

/* Part payment, on the new-invoice dialog. Only drawn when the package row
   said the course was NOT paid in full, because the arithmetic it offers is
   meaningless otherwise. */
.partpay { background:var(--sunken); border:1px solid var(--line);
  border-radius:var(--radius-sm); padding:10px 12px; }
.partpay label { font-size:12px; font-weight:600; color:var(--muted); }
.partpay input { max-width:180px; }
.partpay .hint { margin:6px 0 0; }
/* Another course on the same receipt (0330): its own amount, ruled off from
   the one above, and the choice of a further one under them. */
.partpay .morepay, .partpay .morerow { margin-top:12px; padding-top:10px;
  border-top:1px dashed var(--line); }
.partpay .morerow select { max-width:100%; }

/* PAID, WITH NO INVOICE YET (20 October 2026): the child's other courses on
   the New invoice (offerAlso, .alsopaid) and every one of them in the package
   window (invoiceBlock, .invwait). Amber, the colour of a job still to do,
   with each course a row whose buttons sit at its end and drop under it on a
   phone. */
.alsopaid, .invwait { background:var(--wait-bg); border:1px solid var(--wait);
  border-radius:var(--radius-sm); padding:10px 12px; }
.invwait { margin-bottom:10px; }
.alsohead, .invwhead { display:flex; align-items:center; gap:7px; margin:0 0 6px; font-size:13px; }
.alsohead .navicon, .invwhead .navicon { width:16px; height:16px; color:var(--wait); }
.alsolist, .invwlist { list-style:none; margin:0; padding:0; display:flex; flex-direction:column; gap:6px; }
.alsolist li, .invwlist li { display:flex; flex-wrap:wrap; align-items:center; gap:6px 10px;
  padding:7px 10px; border-radius:var(--radius-sm); background:var(--surface);
  border:1px solid var(--line); font-size:12.5px; }
.alsolist .alsowhat, .invwlist .invwwhat { flex:1 1 220px; min-width:0; }
.alsolist .sub, .invwlist .sub { display:block; color:var(--muted); font-size:11.5px; }
.alsolist .alsodo { display:flex; flex-wrap:wrap; gap:6px; margin-left:auto; }
.invwlist .btn.tiny { margin-left:auto; }
/* "Its own invoice next" pressed: the row says it waits its turn. */
.alsolist li.queued { border-color:var(--ok); }
.alsolist .btn.on { background:var(--ok-bg); border-color:var(--ok); color:var(--ok); }
.invwait .hint { margin:6px 0 0; }

/* Kid details, before a child is chosen. A panel rather than a sentence in an
   empty box: the page has to say what it is for and put the one control that
   starts it where somebody is already looking. It has been two other things -
   a line of grey text with the only search field in the far corner, and a
   wall of two hundred names, which is a list rather than a design. */
.kidstart {
  background:var(--surface); border:1px solid var(--line);
  border-radius:var(--radius); padding:22px 24px 24px; box-shadow:var(--shadow-sm);
  max-width:620px;
}
.kidstart h2 { margin:0 0 4px; font-size:18px; }
.kidstart .lead { margin:0 0 14px; }
.kidstart .hint { margin:10px 0 0; }
/* ONE NAME BOX ON A PHONE, until a child is chosen (25 October 2026). The
   page's bar has its own "Type a child's name" (#hKid), which is how one
   child is swapped for another once a record is open; before that, on a
   phone, it sat one line above this panel's own box with the same
   placeholder, and two boxes asking one question read as a mistake. It
   comes back the moment a child is chosen and this panel goes. */
@media (max-width: 760px) {
  #view:has(.kidstart) #hKid { display:none; }
}
.search.big { width:100%; max-width:360px; font-size:15px; padding:10px 12px; }

/* The two jobs, side by side and the same size wherever they appear - in the
   opening panel greyed out, and on the child's card once there is one. */
.kidacts { display:flex; gap:8px; flex-wrap:wrap; margin-top:14px; }
.kidcard .kidacts { margin-top:0; }

.kidcardhead {
  display:flex; align-items:center; gap:10px; flex-wrap:wrap;
  margin-bottom:14px;
}
.kidcardhead > b { font-size:20px; font-weight:600; }
.kidcardhead .grow { flex:1; }

/* The facts themselves, two columns on a desk and one on a phone. */
.kidkv { margin:0; display:grid; grid-template-columns:auto 1fr; gap:6px 14px; }
.kidkv dt { color:var(--muted); font-size:12.5px; white-space:nowrap; }
.kidkv dd { margin:0; font-size:13.5px; }
@media (max-width:560px) {
  .kidkv { grid-template-columns:1fr; gap:2px; }
  .kidkv dd { margin-bottom:8px; }
}

/* The two messages as they will actually arrive. A <pre> on purpose: the
   layout IS the message, and reflowing it in a preview would show something
   the phone will not. */
.digestpre {
  white-space:pre-wrap; word-break:break-word; margin:0 0 12px;
  background:var(--sunken); border:1px solid var(--line);
  border-radius:var(--radius-sm); padding:12px 14px;
  font-size:12.5px; line-height:1.55; max-height:40vh; overflow:auto;
}

/* ------------------------------------------------------------- devices --- */

/* A one-time code (0368), in the person's window where an administrator
   reads it out to the person standing at the new device - often across a
   room or over a telephone, so large and spaced, and inline in its sentence. */
.devcode {
  display:inline-block;
  font-family:"IBM Plex Mono", ui-monospace, monospace;
  font-size:22px; font-weight:600; letter-spacing:.16em;
  margin:2px 4px; padding:4px 10px;
  background:var(--sunken); border:1px solid var(--line);
  border-radius:var(--radius-sm); color:var(--ink);
}
/* The limit and the hours on one row, wrapping on a phone. */
.pdevrules { display:flex; flex-wrap:wrap; gap:8px 14px; align-items:flex-end; }
.pdevrules > div { display:flex; flex-direction:column; gap:4px; min-width:7rem; }
.pdays { margin:6px 0 10px; }
/* Each registered device: its id, when, and Remove at the end. */
.pdevlist { list-style:none; margin:8px 0; padding:0; display:grid; gap:6px; }
.pdevlist li { display:flex; align-items:center; gap:10px; justify-content:space-between;
  padding:6px 10px; border:1px solid var(--line); border-radius:var(--radius-sm); }
.pdevlist li > span { display:flex; flex-direction:column; gap:2px; min-width:0; }
.pdevlist .mono { overflow-wrap:anywhere; }
.pcode { margin-top:8px; }
/* How long a code lets them in after hours (0372): the label over a select,
   and the time box beside it when "until a time" is chosen. */
.pgrant { display:flex; flex-wrap:wrap; gap:6px 10px; align-items:center; margin-bottom:8px; }
.pgrant label { flex-basis:100%; font-size:12px; color:var(--muted); }
.pgrant select, .pgrant input { width:auto; min-width:12rem; }

#nodevice h2 { margin:14px 0 6px; font-size:17px; }
/* THE LOCK OVER AN OPEN PAGE (0369): fixed over everything - the page, a
   dialog half filled in, a side panel - so nothing under it can be used, and
   all of it is still there when the code has been typed. Under the toasts
   (200 is theirs and they say what happened), over every dialog and panel. */
#nodevice.livelock { position:fixed; inset:0; z-index:199; overflow:auto; }

/* The user agent under the machine's name. Small and quiet: it is a hint for
   telling two unnamed machines apart, not something to read. */
.rows .agent { font-size:11px; margin-top:3px; }
.rows .dlabel { width:100%; min-width:130px; }
.rows .dwhen { font-size:12px; white-space:nowrap; }

.pinswitch {
  margin:2px 0 14px; padding:11px 13px;
  background:var(--sunken); border:1px solid var(--line);
  border-radius:var(--radius-sm);
}
.pinswitch .sub { display:block; margin-top:3px; color:var(--muted);
  font-size:12px; line-height:1.5; }

/* ------------------------------------------------- Your login, as cards ---
   Asked for with a mockup after the team half of Users & access was redrawn:
   "apply attached theme to User & Access page". Five panels that each opened
   with a bold line and a paragraph now open the same way - a coloured tile,
   a title, a sentence - so the eye finds one by its colour rather than by
   reading all five. --a is the card's colour, set inline and given a value
   here as well, because an undefined var drops the whole declaration. */
.pagehead { margin:2px 2px 12px; }
.pagehead .eyebrow {
  display:block; font-size:10.5px; font-weight:700; letter-spacing:.1em;
  text-transform:uppercase; color:var(--muted);
}
.pagehead h2 { margin:2px 0 2px; font-size:21px; letter-spacing:-.02em; }
.pagehead .sub { font-size:12.5px; color:var(--muted); }

/* The band at the top of a page: the page's name on the left, the controls
   belonging to the WHOLE page on the right. It was .rshead, which was exactly
   these six lines under a name that said Reschedules. */
.pagetop {
  display:flex; flex-wrap:wrap; align-items:flex-start; gap:14px;
  margin:2px 2px 14px;
}
.pagetop .pagehead { margin:0; flex:1 1 260px; }
/* The page's own name, larger than a tab heading: this is the first
   thing on the page rather than a label over one section of it. */
.pagetop .pagehead h2 { font-size:24px; }
/* The controls line up with EACH OTHER along their bottom edge rather than
   with the heading's top, so a button and a captioned box sit on one line
   however tall the caption makes the box. */
/* IT FITS BESIDE THE TITLE OR IT WRAPS WHOLE, and is never squeezed between
   the two. The title block is flex:1 1 260px, so it GROWS into any spare
   width; when the controls then needed more than was left they were squashed
   inside their own group - a search box, a button and the Filters toggle on
   two lines with half a screen of title beside them.

   THE SHRINKING WAS INSIDE, which is the part worth writing down. A flex
   container's own base size is read from its content, and an <input> in one
   is shrinkable by default: the box gave up width, the group therefore
   MEASURED smaller than it needs, and the title took the difference. Setting
   flex on the group alone fixed nothing, because the group was honestly
   reporting a smaller size. Rigid children are what make the measurement
   true. Measured on Policies at 1120px: the group wants 449 and was being
   handed 378.

   max-width keeps it inside the band on a phone, where it wraps internally
   as before. */
.headside {
  display:flex; flex-wrap:wrap; align-items:flex-end; gap:10px;
  flex:0 0 auto; max-width:100%; margin-left:auto;
}
.headside > * { flex:none; }
.headside .search { width:240px; max-width:100%; }
/* A SEARCH BOX WITH ITS MAGNIFIER INSIDE IT (Billing, 14 October 2026, from
   the page's mockup). The mark is drawn over the box's left edge and lets the
   pointer through to the box, so pressing it is pressing the box; the text
   starts after it. The label is what the band lays out, so it carries the
   width limit the box used to. */
.searchbox { position:relative; display:inline-flex; align-items:center; max-width:100%; }
.searchbox > .navicon {
  position:absolute; left:11px; width:16px; height:16px;
  color:var(--faint); pointer-events:none;
}
.searchbox > .search { padding-left:34px; }

/* THE BILLING ROWS, from the same mockup. The invoice number reads as the
   row's name rather than as code; the actions sit centred under their own
   heading, each button with its mark. */
.invno { font-weight:600; white-space:nowrap; font-variant-numeric:tabular-nums; }
/* Under `table.rows thead th`, which sets every heading's alignment. */
table.rows thead th.billacts, td.billacts { text-align:center; }
td.billacts .btn .navicon { width:14px; height:14px; }
/* AND IT CAN BE A .toolbar AS WELL, for exactly one reason: foldFilters()
   walks back from a .filterbar to the nearest .toolbar to put its Filters
   button in, and with none it takes a whole line of its own above the page -
   which on Policies is a row of chrome for one word. It is not a toolbar to
   LOOK at, so the card it would otherwise draw is stood down here. */
.headside.toolbar {
  background:none; border:0; box-shadow:none; padding:0; margin:0 0 0 auto;
  align-items:flex-end;
}
/* The bar it opens lives in the band too, on a line of its own under the
   controls - it is hidden until the button is pressed, so it costs nothing
   while it is shut. */
.pagetop > .filterbar { flex:1 1 100%; margin-top:2px; }
/* A control with its caption above it. The mockup draws the caption inside
   the control's own box; caption-above is the shape this app already uses on
   the Reschedules filter bar, and it keeps the NATIVE select - which still
   has to open a list under a thumb on the clinic's tablet. */
.capsel { display:inline-flex; flex-direction:column; gap:5px; min-width:0; }
.capsel > span {
  font-size:10px; font-weight:700; letter-spacing:.1em;
  text-transform:uppercase; color:var(--faint);
}
/* An empty note takes no room at all; a note takes a line above the cards. */
.fnote:not(:empty) { display:block; margin:-4px 2px 12px; }

.cardhead { display:flex; align-items:flex-start; gap:12px; margin-bottom:12px; }
.cardicon {
  --a:var(--accent);
  flex:none; width:38px; height:38px; border-radius:11px;
  display:inline-flex; align-items:center; justify-content:center;
  color:var(--a); background:color-mix(in srgb, var(--a) 15%, transparent);
  border:1px solid color-mix(in srgb, var(--a) 30%, transparent);
}
.cardicon svg { width:19px; height:19px; }
/* The role grid's summary is a row, not a card head, so its tile is smaller. */
.cardicon.sm { width:28px; height:28px; border-radius:8px; }

/* ------------------------------------------------- General, as cards ---
   TWELVE COLUMNS, NOT auto-fit, and the difference is the whole of why the
   first version came back as "poorly designed ... the one i sent you was
   compact". auto-fit gives every card the same width and a row of its OWN the
   moment one is marked wide - so the session times, whose content is nine
   90px boxes, took 1,354px and 642 of them tall with the right two thirds
   empty, the third column was empty on both short rows, and the page measured
   1,804px: six bands down a screen and a half. Measured on the built markup
   rather than argued about.

   Each card now says how many of the twelve it wants. The grid is still ONE
   form with ONE save - nothing about what it reads or writes moved. */
.gengrid {
  display:grid; grid-template-columns:repeat(12, minmax(0, 1fr));
  gap:14px; align-items:start;
}
.gengrid .panel { margin:0; }
/* One column on a phone: a card at a third of 375px is not a card. The error
   line, the buttons and the widest card stay full width at every size. */
.gengrid > * { grid-column:1 / -1; }
@media (min-width: 780px) {
  .gengrid > .g4 { grid-column:span 6; }
  .gengrid > .g8 { grid-column:span 6; }
}
@media (min-width: 1150px) {
  .gengrid > .g4 { grid-column:span 4; }
  .gengrid > .g8 { grid-column:span 8; }
  /* A list of five switches across the whole page is two columns and a lot of
     nothing. Inside the min-width block on purpose: the phone rule that drops
     .chipparts to one column is less specific than this and would lose. */
  .gengrid > .gfull .chipparts { grid-template-columns:repeat(3, minmax(0, 1fr)); }
}

/* A COLUMN INSIDE ONE CELL, which is the part that cannot be done with the
   page grid alone: a grid cell is stretched to its row, so three short cards
   beside one tall one can only ever be three tall rows. Stacked here they
   keep their own heights and the session times sit beside all three. */
.gcol { display:flex; flex-direction:column; gap:14px; min-width:0; }
.gpair {
  display:grid; grid-template-columns:repeat(auto-fit,minmax(230px,1fr));
  gap:14px; align-items:start;
}
/* The branch strip sits in the card's head, where the mockup put it. */
.cardhead .pills { margin:0; flex:none; }
/* AND DROPS BELOW THE TITLE WHEN THERE IS NO ROOM. A head is a row, and a row
   that cannot fit crushes whatever is allowed to shrink - the title, which is
   the one thing that must not. Three branch pills beside a title in a card a
   third of the page wide turned "Session times *" into three stacked words. */
.gengrid .cardhead { flex-wrap:wrap; }
.gengrid .cardhead .cardtitle { flex:1 1 170px; }
.gengrid .cardhead .pills { margin-top:2px; }

/* A FIELD ON THIS PAGE LOOKED LIKE A BROWSER DEFAULT, and that is the other
   half of what was reported. These inputs sit straight in a card rather than
   in a .form2, so nothing had ever styled them: the centre's name was 177px
   of the browser's own Arial in a 442px card, beside session times drawn in
   the app's mono. Same shape as .form2's fields, and full width, because the
   card is the column.

   A CLASS RATHER THAN `.gengrid input`, deliberately. The session times are
   text inputs too and are mono on purpose; a selector that caught them would
   win on specificity and quietly take that away. */
.gengrid .gfield {
  width:100%; border:1px solid var(--faint); background:var(--ground);
  color:var(--ink); border-radius:var(--radius-sm); padding:8px 10px;
  font:inherit; font-size:14px;
}
.gengrid textarea.gfield { resize:vertical; }
.gengrid .gfield:focus { outline:2px solid var(--accent); outline-offset:-1px; }
/* Only the one label that still has a `for`; the tick boxes are .check. */
.gengrid label[for] {
  display:block; font-size:12px; font-weight:500; color:var(--muted);
  margin-bottom:4px;
}

/* A working day as something to press, not a line of ticks in a row. */
.daychips { gap:8px; }
.daychips .check {
  padding:8px 12px; border:1px solid var(--line); border-radius:var(--radius-sm);
  background:var(--sunken);
}
.daychips .check:has(input:checked) {
  border-color:color-mix(in srgb, var(--ok) 45%, var(--line));
  background:color-mix(in srgb, var(--ok) 10%, var(--sunken));
}
.cardicon.sm svg { width:15px; height:15px; }
/* THE DIRECT CHILD, not every b in the block. The price list's section head
   says "Packages in this section are <b>500 a session</b>", and an unscoped
   rule made that price a 14.5px BLOCK in the middle of the sentence - the
   figure broke onto a line of its own, in the title's own size, reading as a
   second heading. Nothing was wrong with the sentence. */
.cardtitle > b { display:block; font-size:14.5px; letter-spacing:-.01em; }
.cardtitle .sub { display:block; margin-top:2px; font-size:12.5px; color:var(--muted); line-height:1.5; }
.cardp > .cardhead + .form2 { margin-top:2px; }

/* ---------------------------------- the other nine Settings tabs, as cards ---
   Asked for on 25 October 2026 - "below pages under settings page needs
   redesign as per other pages": Branches, Defaults, Exceptions, WhatsApp
   settings, Notifications, Spreadsheet, Hosting usage, Billing, Onboarding.
   A change of clothes, General's and Database's: every id, handler and saved
   value is where it was (settingscards_test.mjs holds each tab to the ids it
   drew before).

   The tab's name, said once at the top, with room on its right for what
   belongs to the whole tab (Hosting usage's Measure again). */
.sethead {
  display:flex; flex-wrap:wrap; align-items:flex-end; gap:10px 16px;
  margin:2px 2px 12px;
}
.sethead .pagehead { margin:0; flex:1 1 260px; min-width:0; }
.setheadside {
  display:flex; flex-wrap:wrap; align-items:center; gap:10px; margin-left:auto;
}
.setheadside > * { flex:none; }

/* TWELVE COLUMNS, General's grid, without General's field styling - these
   tabs keep their .form2 and .form1 forms. A card says how wide it is (s4,
   s6, s8); anything that does not is the whole row.

   A .setslot is a container a render function draws its cards into (the
   Billing tab's four). It is display:contents, so its cards are cells of
   this grid as if they were its own - which is why every width rule below
   names both depths. An empty cell is not drawn at all: a half nobody may
   see, or a step of Onboarding not reached yet, would otherwise leave a gap. */
.setgrid {
  display:grid; grid-template-columns:repeat(12, minmax(0, 1fr));
  gap:14px; align-items:start;
}
.setgrid > *, .setgrid > .setslot > * { grid-column:1 / -1; min-width:0; }
.setgrid > .setslot { display:contents; }
.setgrid > :empty { display:none; }
.setgrid .panel, .setgrid > .gate { margin:0; }
@media (min-width: 900px) {
  .setgrid > .s4, .setgrid > .setslot > .s4,
  .setgrid > .s6, .setgrid > .setslot > .s6,
  .setgrid > .s8, .setgrid > .setslot > .s8 { grid-column:span 6; }
}
@media (min-width: 1150px) {
  .setgrid > .s4, .setgrid > .setslot > .s4 { grid-column:span 4; }
  .setgrid > .s8, .setgrid > .setslot > .s8 { grid-column:span 8; }
}

/* A head carries a button or an empty-list box beside its title, and drops
   them under it when the card is too narrow - the title is the one thing
   that must not be squeezed. */
.setgrid .cardhead { flex-wrap:wrap; }
.setgrid .cardhead .cardtitle { flex:1 1 200px; min-width:0; }
.setgrid .cardhead > .btn { flex:none; }
/* A fact's name here can be "Issued from this system"; the Database tab's
   112px folds it onto three lines. */
.setgrid .dbfact .dl { flex-basis:150px; }
/* A value can be one long word: the Spreadsheet tab's two Google sheet ids
   are 44 characters with nowhere to break, and on a phone they held the
   page at 521px wide. They break anywhere now, inside their own column. */
.setgrid .dbfact .dv { overflow-wrap:anywhere; }

/* AN ON/OFF SETTING AS A SWITCH, its words beside it. Inside a .form2 or
   .form1, `.form2 label` would make the switch a block with a margin under
   it; the switch keeps its own shape here. */
.setswitch { align-items:flex-start; }
.form2 .setswitch > label.switch, .form1 .setswitch > label.switch {
  display:inline-block; margin:1px 0 0;
}
.setswitch .switch { margin-top:1px; }
.setswitches { display:flex; flex-direction:column; gap:14px; }
/* The test number, under the switch it belongs to, lined up with its words. */
.setswitchfield { margin:-6px 0 0 56px; }

/* A list whose rows open their own dialog says so by the pointer. */
.setrows tbody tr[data-branch], .setrows tbody tr[data-closure] { cursor:pointer; }

/* ONBOARDING'S FOUR STEPS (26 October 2026): staff, children, packages and
   the weekly plan, in the order they name each other, four across where they
   fit and one under another on a phone. Each says what is on file now. */
.obsteps {
  display:grid; gap:10px;
  grid-template-columns:repeat(auto-fit, minmax(210px, 1fr));
}
.obstep {
  display:flex; flex-direction:column; gap:6px; min-width:0;
  padding:12px; border:1px solid var(--line); border-radius:var(--radius-sm);
  background:var(--surface);
}
.obstephead { display:flex; align-items:center; gap:8px; }
.obstephead .cardicon { width:26px; height:26px; border-radius:8px; }
.obstephead .cardicon svg { width:15px; height:15px; }
.obnum {
  display:inline-grid; place-items:center; flex:none;
  width:20px; height:20px; border-radius:50%;
  background:var(--sunken); color:var(--muted); font-size:11.5px; font-weight:700;
}
.obhave { font-size:12.5px; font-weight:600; }
.obstep > .btn, .obstep > .actions { margin-top:auto; }
.obstep > .actions { flex-wrap:wrap; gap:6px; }
.obstep > .btn { align-self:flex-start; }
.obswatches { display:flex; flex-wrap:wrap; gap:4px; }
.obswatches .dot { width:12px; height:12px; border-radius:50%; flex:none; }
/* What a row of the check will do and why, on lines of their own. */
.obnote { display:block; margin-top:2px; }
/* THE WHOLE ORDER (26 October 2026): what to set up before the files and what
   follows them, a line each - its mark, what it is and what is on file, and
   the way to it. Nothing on file yet is said in the waiting colour. */
.oblines { display:flex; flex-direction:column; gap:6px; margin-bottom:6px; }
.obline {
  display:grid; grid-template-columns:auto minmax(0, 1fr) auto; align-items:center; gap:10px;
  padding:8px 10px; border:1px solid var(--line); border-radius:var(--radius-sm);
  background:var(--surface);
}
.obline .cardicon { width:26px; height:26px; border-radius:8px; }
.obline .cardicon svg { width:15px; height:15px; }
.obwhat { display:flex; flex-direction:column; gap:1px; min-width:0; }
.obline > .btn { white-space:nowrap; text-decoration:none; }
.obhave.none { color:var(--wait); }
/* What each column takes, under the sample buttons. */
.obguide { margin-top:10px; }
.obguide > summary { cursor:pointer; font-size:13px; font-weight:600; }
.obguide .gridwrap { margin-top:8px; }
.obguide .hint { margin:8px 0 0; }
/* The therapy file alone, in a window over Services & pricing. */
.obsheet { display:flex; flex-direction:column; gap:12px; min-width:0; }
/* THERAPIST COLOURS, chosen on the tab: a name each, opening its palette. */
.obcolours { display:flex; flex-direction:column; gap:6px; }
.obcol { border:1px solid var(--line); border-radius:var(--radius-sm); background:var(--surface); }
.obcol.moved { border-color:var(--accent); }
.obcol > summary {
  display:flex; align-items:center; gap:10px; padding:8px 10px; cursor:pointer; list-style:none;
}
.obcol > summary::-webkit-details-marker { display:none; }
.obswatch {
  width:22px; height:22px; border-radius:50%; flex:none;
  box-shadow:inset 0 0 0 1px rgb(0 0 0 / .18);
}
.obcolname { display:flex; flex-direction:column; min-width:0; }
.obcolsay { margin-left:auto; font-size:12px; color:var(--accent); text-align:right; }
.obcolsay.warn { color:var(--wait); }
.obpalette {
  display:flex; flex-wrap:wrap; align-items:center; gap:6px; padding:0 10px 10px 42px;
}
.obchip {
  --c:var(--faint); /* each chip sets its own colour inline */
  position:relative; width:26px; height:26px; padding:0; border:0; border-radius:50%;
  background:var(--c); cursor:pointer; box-shadow:inset 0 0 0 1px rgb(0 0 0 / .18);
}
.obchip:disabled { cursor:default; }
.obchip.on { outline:2px solid var(--ink); outline-offset:2px; }
/* Worn by somebody here now: a dot in the middle says so, and it can still be
   chosen - said, not refused. */
.obchip.worn::after {
  content:''; position:absolute; inset:8px; border-radius:50%; background:var(--surface);
}
.obother { display:inline-flex; align-items:center; gap:6px; font-size:12px; color:var(--muted); }
/* Staff details' free colours (28 October 2026): Onboarding's swatches, the
   ones nobody here wears, in a row under the colour boxes. */
.freecolours { display:flex; flex-wrap:wrap; gap:6px; }
.freecolours:empty { display:none; }
/* A finger needs more than 26px: the app's tap-target floor is 28. */
@media (pointer: coarse) {
  .freecolours { gap:8px; }
  .freecolours .obchip { width:32px; height:32px; }
}
.obother input[type="color"] { width:30px; height:26px; padding:0; border:0; background:none; cursor:pointer; }
@media (max-width: 760px) {
  .obline { grid-template-columns:auto minmax(0, 1fr); }
  .obline > .btn { grid-column:1 / -1; justify-self:start; }
  .obpalette { padding-left:10px; }
  .obcol > summary { flex-wrap:wrap; }
  .obcolsay { margin-left:32px; text-align:left; }
}
.setname { display:inline-flex; align-items:center; gap:7px; }
.setname .navicon { width:14px; height:14px; color:var(--faint); flex:none; }
.setsub { display:block; margin-top:1px; font-size:11.5px; }
/* Two forms in one card (Billing's numbering): the second is a section. */
.setsection { margin-top:14px; }
.setfoot .err { margin:0 0 8px; }
.setsubhead {
  margin:18px 0 8px; font-size:12px; letter-spacing:.09em;
  text-transform:uppercase; color:var(--faint);
}
.stampdo { display:flex; flex-wrap:wrap; align-items:center; gap:8px; }
.stampdo .btn .navicon, .setheadside .btn .navicon { width:14px; height:14px; }
/* A role named in a column heading, in its own colour (Notifications). The
   heading's capitals and the figure column's mono would turn "Senior
   therapist" into a shout in a typewriter. */
.rows.notifgrid thead th .rolepill {
  text-transform:none; letter-spacing:0;
  font-family:"IBM Plex Sans", system-ui, -apple-system, Segoe UI, sans-serif;
}

/* Four facts in two columns, each a quiet icon, its name and the answer. */
.detailgrid {
  display:grid; grid-template-columns:repeat(auto-fit,minmax(260px,1fr));
  gap:2px 22px; padding:10px 12px;
  background:var(--sunken); border-radius:var(--radius-sm);
}
.detailrow { display:flex; align-items:center; gap:9px; padding:7px 0; min-width:0; }
.dicon { display:inline-flex; width:15px; height:15px; flex:none; color:var(--muted); }
.dicon svg { width:15px; height:15px; }
.dlabel { font-size:12.5px; color:var(--muted); flex:none; min-width:52px; }
.dval { font-weight:600; font-size:13px; overflow-wrap:anywhere; }
.permlist { display:flex; flex-wrap:wrap; gap:5px; margin:0; }

/* The bands that say "this is on or off" are switches now, which is what
   they always were - a tick box says "include this", and none of these do.
   The input and its id are untouched; only its drawing changes. */
.pinswitch .check input[type="checkbox"] {
  appearance:none; -webkit-appearance:none;
  flex:none; width:44px; height:25px; margin:0; position:relative; border:0;
  border-radius:var(--radius-pill); background:var(--line); cursor:pointer;
  transition:background .15s ease;
}
.pinswitch .check input[type="checkbox"]::after {
  content:''; position:absolute; top:3px; left:3px; width:19px; height:19px;
  border-radius:50%; background:#fff; transition:transform .15s ease;
  box-shadow:0 1px 2px rgba(0,0,0,.35);
}
.pinswitch .check input[type="checkbox"]:checked { background:var(--ok); }
.pinswitch .check input[type="checkbox"]:checked::after { transform:translateX(19px); }
.pinswitch .check input[type="checkbox"]:disabled { opacity:.5; cursor:default; }
.pinswitch:has(input:checked) {
  border-color:color-mix(in srgb, var(--ok) 40%, var(--line));
  background:color-mix(in srgb, var(--ok) 8%, var(--sunken));
}
.btn.danger svg { width:14px; height:14px; margin-right:5px; vertical-align:-2px; }

/* The "this makes up that one" row in a session dialog. A select and a button
   that have to sit together on a phone as well, where the list of dates is
   the wide half and the button must not be squeezed to nothing. */
.tiewrap { display:flex; flex-wrap:wrap; align-items:center; gap:8px; }
.tiewrap select { flex:1 1 200px; min-width:0; }
/* The minutes box beside it: wide enough for three digits and no wider, so
   the select keeps the room on a dialog this narrow. */
.tiewrap .num { flex:none; width:72px; }
.tiewrap .btn { flex:none; }
/* The two boxes saying which session a make-up makes up - the day it was lost
   and its number - sit under the pill in the session dialog's "Makes up" row.
   .handrow is the booking dialog's shape for the same pair, reused so the two
   dialogs ask the same question in the same furniture. */
.kv .rsrow { margin:6px 0 2px; }
/* Money still to come, on a chip. The warning colour the cancellation count
   and the owed-and-flagged badge already use for "look before you act". */
/* SOLID, NOT A TINT, AND A FIFTH BIGGER. As text on a tint of itself the bell
   was "not recognizable" at twelve pixels - the clinic's words - and a mark
   that has to be found among #7, OT, P20 and the therapist's colour behind
   it needs to be the one thing on the chip that is not that colour. So the
   warning amber is the ground, near-black is the ink (4.6:1 on the light
   amber, more on the dark), a halo of the tint keeps it off whatever chip
   colour it sits on, and the bell is fourteen pixels with a heavier stroke. */
.badge.paydue {
  background:var(--wait); color:#141A21; border-color:var(--wait);
  box-shadow:0 0 0 2px var(--wait-bg);
  display:inline-flex; align-items:center; gap:3px; padding:2px 7px 2px 5px;
  letter-spacing:0; font-weight:800; font-size:13.5px; line-height:1;
}
.badge.paydue svg { width:14px; height:14px; display:block; flex:none; stroke-width:2.6; }

/* The copy of the screen held in front of a live rebuild. It is a picture:
   nothing in it may be clicked, because clicking it would do nothing and
   somebody would press twice. It is only up for as long as the page takes to
   come back - a few hundred milliseconds - and is removed even if that
   fails. */
.heldscreen { pointer-events:none; user-select:none; }

/* THE NUMBERS THAT ANSWER TO OTHER NUMBERS, on the purchase and withdrawal
   dialogs: the total amount, what is in the cupboard now, and what there will
   be afterwards. They are recomputed on every keystroke and none of them is
   sent anywhere.

   A panel rather than three disabled inputs, which is what this started as. A
   disabled input still looks like somewhere to type, so somebody clicks the
   total and finds it dead - and the one thing a computed figure must say is
   that it is not a field. */
.sumstrip {
  display:flex; flex-wrap:wrap; gap:8px 22px;
  padding:10px 13px; border-radius:var(--radius-sm);
  background:var(--sunken); border:1px solid var(--line);
}
.sumstrip > span { display:flex; flex-direction:column; gap:1px; min-width:0; }
.sumstrip b { font-size:17px; font-family:"IBM Plex Mono", ui-monospace, monospace; }
.sumstrip b.err { color:var(--bad); }
.sumstrip .sub { font-size:11px; color:var(--muted); }

/* ONE PURCHASE, SEVERAL ITEMS (0333). Each item on a bill is a card of its
   own - name, category, unit, units, cost, and an expiry when it is medical -
   with its own three figures under it, and the bill's total in a strip after
   the last card. A card rather than a table row because a medical line grows a
   box the others do not have, and because the dialog is read on a phone, where
   six columns of boxes in a row cannot be typed into.

   `.bad` is the line a refusal names, so the sentence above the buttons and
   the card it is about can be matched by eye on a bill of fifteen lines. */
.stklines { display:flex; flex-direction:column; gap:10px; margin:6px 0 8px; }
.stkline {
  min-width:0; padding:10px 12px 12px; border-radius:var(--radius-sm);
  border:1px solid var(--line); background:var(--surface);
}
.stkline.bad { border-color:var(--bad); box-shadow:inset 0 0 0 1px var(--bad); }
.stkline-head { display:flex; align-items:baseline; gap:8px; margin-bottom:8px; }
.stkline-head .grow { flex:1; }
.stkline-head .sub { font-size:12px; color:var(--muted); min-width:0; }
.stkline-grid {
  display:grid; grid-template-columns:repeat(3, minmax(0, 1fr)); gap:10px 12px;
  margin-bottom:10px;
}
.stkline-grid .wide { grid-column:span 2; }
.stkbill b { font-size:19px; }
/* THE FILES OF ONE BILL (0370), a row each with its cross, above the button
   that adds more - so a second press is seen to add rather than replace. */
.stkbills { display:flex; flex-direction:column; gap:6px; margin:4px 0 8px; }
.stkbills:empty { display:none; }
.stkbill1 { display:flex; align-items:center; gap:10px; padding:6px 6px 6px 10px;
  border:1px solid var(--line); border-radius:var(--radius-sm); background:var(--sunken); }
.stkbill1 .mono { flex:1; min-width:0; overflow-wrap:anywhere; }
.stkbill1 [data-billgo] { font-size:18px; line-height:1; padding:4px 8px; }
/* Take a photo (on a touch screen) beside Add photos or files, wrapping on a
   narrow phone rather than squeezing. */
.stkbillbtns { display:flex; flex-wrap:wrap; gap:8px; }

/* THE STOCK PAGE IN ITS MOCKUP'S CLOTHES (21 October 2026): four tiles, a
   band for what is out of stock, and one card holding the tabs, the filters
   and the list. Every colour is a category's own (CAT_LOOK in stock.js) or one
   of the app's state colours - not the mockup's navy, the app's theme. */
.stktiles { margin-bottom:14px; }
.stktiles .tile .tfig .l { margin:0 0 3px; }
.stktiles .tile .tfig .sub { font-size:12px; color:var(--muted); margin-top:3px; }
.stktiles .tile.stkoutfig .n { color:var(--bad); }

/* What is out. Amber, the app's --wait: red on this page is a row with none
   left. IN THE PAGE'S BAND, between its name and its buttons, CENTRED in the
   space they leave: the name keeps its own width instead of growing into it,
   and the space is shared by two auto margins, the warning's left and the
   buttons' own - so where the row wraps, each keeps to the right as the
   buttons always have. The height of the buttons beside it, and level with
   them. It is nothing while nothing is out. */
.pagetop > #stkBand { flex:0 1 auto; min-width:0; margin-left:auto; }
.pagetop > #stkBand:empty { display:none; }
.pagetop:has(> #stkBand:not(:empty)) > .pagehead { flex:0 1 auto; }
.stkband {
  display:flex; align-items:center; gap:9px; min-height:36px;
  padding:3px 3px 3px 6px; border-radius:var(--radius-sm);
  border:1px solid color-mix(in srgb, var(--wait) 45%, transparent);
  background:var(--wait-bg);
}
.stkbandmark {
  flex:none; display:flex; align-items:center; justify-content:center;
  width:24px; height:24px; border-radius:50%;
  color:var(--accent-ink); background:var(--wait);
}
.stkbandmark .navicon { width:14px; height:14px; }
.stkbandtext { color:var(--ink); font-size:13px; min-width:0; }
.stkband .btn { flex:none; min-height:28px; padding:0 10px; font-size:12.5px; box-shadow:none; }

/* The card. The tabs are the page's .seg on a row of their own, and the
   page's history beside them is display:contents, so its button is one of the
   row's own and its list, opened, takes a line under the tabs. */
.stkcard { margin-top:0; padding:14px 16px; }
.stkhead {
  display:flex; flex-wrap:wrap; align-items:center; gap:10px;
  padding-bottom:12px; margin-bottom:12px; border-bottom:1px solid var(--line);
}
.stkhead .seg .btn { display:inline-flex; align-items:center; gap:6px; }
.stkhead .seg .btn .navicon { width:15px; height:15px; }
.stkhead #stkHist { display:contents; }
.stkhead #stkHist > .chgbody { flex:1 1 100%; order:9; margin-top:4px; }
.stkhead #stkHist .chgbar .sub { display:none; }
/* Beside the tabs, taking what the row leaves; under them, whole, when that is
   less than the four controls need. The search box is what gives way. */
.stkfilters {
  flex:1 1 680px; min-width:0;
  display:flex; flex-wrap:nowrap; align-items:center; gap:9px;
}
.stkfilters .searchbox { flex:1 1 160px; min-width:140px; }
.stkfilters > select, .stkfilters > .btn { flex:none; }
.stkfilters .searchbox > .search { width:100%; min-width:0; }
.stkcard #stkBody > .panel.empty { margin-top:0; box-shadow:none; }

/* A row: the item's mark beside its name, its unit or note under the name. */
.stkitem { display:inline-flex; align-items:center; gap:10px; min-width:0; }
.stkitem > span:last-child { display:flex; flex-direction:column; min-width:0; }
.stkitem .sub { font-size:12px; color:var(--muted); font-weight:400; }
.stkmark {
  --a:var(--muted);
  flex:none; display:inline-flex; align-items:center; justify-content:center;
  width:30px; height:30px; border-radius:9px; color:var(--a);
  background:color-mix(in srgb, var(--a) 14%, transparent);
}
.stkmark .navicon { width:16px; height:16px; }
.tagpill.stkout { --a:var(--bad); margin-left:6px; }
/* The figure in the table's own mono, its "Out of stock" in the words' face. */
.stkavail > b { font-family:"IBM Plex Mono", ui-monospace, monospace; font-size:12px; }
/* A COUNT OF NOUGHT, IN RED (21 October 2026), on Stock and the Dashboard's
   Stocks tab. It wore `.err` - the error MESSAGE's class, box and all - so
   tools/smoke.py read every item out of stock as an error on the page (ten of
   them the day the cupboard was filled), and the nought sat in a padded pink
   box sized for a sentence. Its own class: the colour and nothing else. `.err`
   stays for messages, which is what smoke.py looks for. */
.outnum { color:var(--bad); }
/* A day is one thing to read, never three lines of a pill. */
.stkrows .pill { white-space:nowrap; }
/* ON A PHONE THE LIST IS WHAT THE PAGE IS FOR, and the furniture above it
   took the whole first screen: the tiles keep their figures and lose their
   second lines, and the three filters share their rows instead of a row
   each. */
@media (max-width: 760px) {
  .stktiles { gap:8px; margin-bottom:10px; }
  .stktiles .tile { padding:10px 12px; gap:10px; }
  .stktiles .tile .tfig .sub { display:none; }
  .stktiles .tile .n { font-size:21px; }
  /* A line of its own under the name, the buttons under it. */
  .pagetop > #stkBand { flex:1 1 100%; margin:0; }
  .stkbandtext { flex:1 1 auto; }
  .stkband .btn { min-height:32px; }
  .stkcard { padding:10px; }
  .stkhead { padding-bottom:10px; margin-bottom:10px; }
  /* Under the tabs and the history button, not between them. */
  .stkfilters { flex-basis:100%; order:5; gap:7px; flex-wrap:wrap; }
  .stkfilters .searchbox { flex:1 1 100%; }
  .stkfilters > select { flex:1 1 40%; min-width:0; }
}
/* BELOW ITS MINIMUM (0351): amber, the band's own colour - red is a row with
   none left. The minimum itself is a quiet line under the figure. */
.tagpill.stklow { --a:var(--wait); margin-left:6px; }
.stkavail .stkmin { display:block; font-size:11.5px; margin-top:2px; }

/* PURCHASE REQUESTS (0351): the tab's own count, and a card per request -
   what, how many, where and its state on one line, why under it, and the
   buttons this person may press at its foot. An open one has a coloured edge. */
.stkreqn {
  display:inline-flex; align-items:center; justify-content:center; min-width:18px;
  height:18px; padding:0 5px; border-radius:9px; font-size:11px; font-weight:700;
  color:var(--accent-ink); background:var(--wait);
}
/* THE CARDS SHARE A ROW (24 October 2026): "it is so un organized and hard to
   understand make it organized and use the space of the page". Each card was
   the page's full width with its items hugging the left, so two cards a row
   on a wide screen, one where two would not fit, and a card of many items
   (.long, more than LONG_CARD in stock.js) the whole row - a tall card beside
   a short one leaves a hole the height of the difference. auto-fit, so a
   single card is never half the width with nothing beside it. */
.stkreqs {
  display:grid; gap:12px; align-items:start;
  grid-template-columns:repeat(auto-fit, minmax(min(100%, 720px), 1fr));
}
.stkreq {
  min-width:0; padding:12px 14px; border:1px solid var(--line); border-radius:var(--radius);
  background:var(--surface);
}
.stkreq.long { grid-column:1 / -1; }
.stkreq.open { border-left:3px solid var(--wait); }
/* THE HEAD: what the order is on the left - its number, state, items and
   site; what it comes to; who asked and who approved, each with the day - and
   the buttons at its right, going under it when the card is too narrow. */
.stkreqhead { display:flex; flex-wrap:wrap; align-items:flex-start; gap:10px 16px; }
.stkreqfacts { flex:1 1 300px; min-width:0; display:flex; flex-direction:column; gap:6px; }
.stkreqtitle { display:flex; flex-wrap:wrap; align-items:center; gap:8px; }
.stkreqsum { margin:0; display:flex; flex-wrap:wrap; gap:2px 14px; font-size:12.5px; color:var(--muted); }
.stkreqsum b { color:var(--a); font-size:14px; font-variant-numeric:tabular-nums; }
.stkreqwhos { margin:0; display:flex; flex-wrap:wrap; gap:3px 16px; font-size:12px; color:var(--muted); }
.stkreqwho { display:inline-flex; align-items:center; gap:5px; }
.stkreqwho b { color:var(--ink); font-weight:600; }
.stkreqwho .navicon { flex:none; width:13px; height:13px; }
.stkreqacts { flex:0 1 auto; margin:0 0 0 auto; flex-wrap:wrap; justify-content:flex-end; gap:8px; }
.stkreqacts .search { flex:1 1 220px; min-width:0; }
.stkreqwhy, .stkreqnote { margin:8px 0 0; font-size:13px; }
.stkreqnote {
  padding:6px 10px; border-left:3px solid var(--line); border-radius:var(--radius-xs);
  background:var(--sunken);
}
/* SEVERAL ITEMS A REQUEST (0352): the order's number leads the card once it
   is one. Its items are a TABLE (24 October 2026): asked, approved, bought and
   still to buy a column each, right-aligned so the figures line up from one
   item to the next, the approver's reason a column of its own rather than a
   line hanging under the item. The ones turned down come last under their own
   divider, muted and struck through. */
.stkpo { font-family:"IBM Plex Mono", ui-monospace, monospace; font-size:13px; }
.stkreqtable {
  margin-top:10px; border:1px solid var(--line); border-radius:var(--radius-sm); overflow:hidden;
}
.stkreqitems { width:100%; border-collapse:collapse; table-layout:fixed; font-size:13px; }
.stkreqitems col.c-asked, .stkreqitems col.c-approved, .stkreqitems col.c-bought,
.stkreqitems col.c-left { width:5.5rem; }
.stkreqitems col.c-why { width:26%; }
.stkreqitems th, .stkreqitems td { padding:7px 12px; text-align:left; vertical-align:top; }
.stkreqitems thead th {
  font-size:11.5px; font-weight:600; color:var(--muted); white-space:nowrap;
  background:var(--sunken); border-bottom:1px solid var(--line);
}
.stkreqitems tbody td { border-top:1px solid var(--line-soft); }
.stkreqitems tbody:first-of-type tr:first-child td { border-top:0; }
.stkreqitems .c-asked, .stkreqitems .c-approved, .stkreqitems .c-bought,
.stkreqitems .c-left { text-align:right; white-space:nowrap; font-variant-numeric:tabular-nums; }
.stkreqitems .c-why { color:var(--muted); overflow-wrap:anywhere; }
.stkreqname { font-weight:600; overflow-wrap:anywhere; }
.stkreqyes { color:var(--ok); }
.stkreqleft { color:var(--wait); }
.stkreqgot { display:inline-flex; vertical-align:middle; color:var(--ok); }
.stkreqgot .navicon { width:15px; height:15px; }
.stkreqline.no td { color:var(--muted); }
.stkreqline.no .stkreqname { font-weight:500; text-decoration:line-through; }
.stkreqdiv th {
  padding:8px 12px 4px; font-size:11.5px; font-weight:600; color:var(--muted);
  border-top:1px dashed var(--line);
}
/* ON A PHONE a row of six columns does not fit, so each item is ONE LINE:
   its name, then each figure with its column's name (data-l) beside it, and
   the reason under those. It was a block of three lines - the name, then the
   column names, then the figures - so a request of 27 items was 27 tall
   blocks to scroll past on the Approvals page (25 October 2026). A waiting
   request has one figure, Asked, and fits beside the name; an order's four
   wrap under it when the name is long. */
@media (max-width: 760px) {
  .stkreq { padding:10px; }
  .stkreqacts { flex:1 1 100%; margin:0; justify-content:stretch; }
  .stkreqacts .btn { flex:1 1 auto; justify-content:center; }
  .stkreqitems, .stkreqitems tbody { display:block; }
  .stkreqitems colgroup, .stkreqitems thead { display:none; }
  .stkreqitems tr {
    display:flex; flex-wrap:wrap; align-items:baseline; gap:2px 12px;
    padding:7px 10px; border-top:1px solid var(--line-soft);
  }
  .stkreqitems tbody:first-of-type tr:first-child { border-top:0; }
  /* tbody td, or the line between two rows outranks it and draws inside one. */
  .stkreqitems tbody td, .stkreqitems th { display:block; padding:0; border:0; text-align:left; }
  .stkreqitems td.c-item { flex:1 1 40%; min-width:0; }
  .stkreqitems td.c-why, .stkreqdiv th { flex:1 1 100%; }
  .stkreqitems td:not(.c-item)::before {
    content:attr(data-l); display:inline; margin-right:5px;
    font-size:10.5px; font-weight:600; color:var(--muted);
  }
  .stkreqitems td.c-why::before { content:attr(data-l) ": "; display:inline; }
  .stkreqitems td.c-why:empty, .stkreqline.no td.c-approved, .stkreqline.no td.c-bought,
  .stkreqline.no td.c-left { display:none; }
  .stkreqitems .c-asked, .stkreqitems .c-approved, .stkreqitems .c-bought,
  .stkreqitems .c-left { text-align:left; }
}
/* The Ask dialog's items: the name across, how many beside it. */
.askgrid { grid-template-columns:minmax(0, 3fr) minmax(0, 1fr); }
.askgrid .wide { grid-column:auto; }
/* The decision, item by item. A turned-down item is quieter. */
.sdall { justify-content:flex-start; gap:8px; }
.sdlines { display:flex; flex-direction:column; gap:8px; }
.sdline {
  padding:10px 12px; border:1px solid var(--line); border-radius:var(--radius);
  background:var(--surface);
}
.sdline.no { opacity:.75; }
.sdline.no [data-sdunits] { opacity:.45; }
.sdhead { display:flex; align-items:baseline; gap:8px; margin-bottom:6px; }
.sdhead .sub { font-size:12px; }
.sdgrid { display:grid; grid-template-columns:repeat(2, minmax(0, 1fr)); gap:8px 12px; }
.sdgrid .wide { grid-column:1 / -1; }

/* The to-buy list in the purchase dialog: an approved request a tick. */
.stktobuy {
  padding:10px 12px; border:1px solid color-mix(in srgb, var(--ok) 40%, transparent);
  border-radius:var(--radius); background:var(--ok-bg);
}
.stktobuyhead { display:flex; align-items:center; gap:6px; margin-bottom:6px; }
.stktobuyhead .navicon { width:15px; height:15px; }
.stktobuyrow { display:flex; flex-wrap:wrap; align-items:baseline; gap:6px; padding:3px 0; }
.stktobuyrow .sub { font-size:12px; }
/* An order a group (0352): its number and who asked, then its items. */
.stktobuyorder + .stktobuyorder { margin-top:6px; padding-top:6px;
  border-top:1px solid color-mix(in srgb, var(--ok) 30%, transparent); }
.stktobuypo { display:flex; flex-wrap:wrap; align-items:baseline; gap:6px; }
.stktobuypo .sub { font-size:12px; }
.stktobuyorder .stktobuyrow { padding-left:14px; }
/* A line off an order buys the order's item: its name is shown, not typed. */
.stkline [data-f="name"][readonly] { background:var(--sunken); cursor:default; }

td.stkacts { width:1%; text-align:right; }
.stkacts .btn.icon.stkdots {
  min-width:30px; padding:4px 6px; border-color:transparent; background:none;
}
.stkacts .btn.icon.stkdots:hover,
.stkacts .btn.icon.stkdots[aria-expanded="true"] {
  border-color:var(--line); background:var(--sunken);
}
/* minmax(0, 1fr), never a bare 1fr: a box carrying its mark (dressPopup) has a
   wider min-content than one without, and a bare 1fr lets that widen its own
   column - seen on a 375px phone, where Category took more than twice the
   width of the unit box beside it. */
@media (max-width: 760px) {
  .stkline-grid { grid-template-columns:repeat(2, minmax(0, 1fr)); }
  /* The date box too: half a phone is narrower than "dd-mm-yyyy" and its
     calendar button, and the date was cut off. */
  .stkline-grid .wide, .stkline-grid [data-expwrap] { grid-column:1 / -1; }
}

/* A bill, at whatever size the dialog is. Capped by height as well as width:
   a photograph of a till receipt is much taller than it is wide, and without
   this one fills the screen and the close cross is off the bottom of it. */
.billshot {
  display:block; max-width:100%; max-height:70vh; margin:0 auto;
  border-radius:var(--radius-sm); border:1px solid var(--line);
}

/* A row on the Stocks tab whose usage is at least double its own recent past.
   Tinted rather than outlined: the table is read down a column, and a border
   on one row breaks the grid lines either side of it so the eye stops at the
   wrong thing. The number itself carries the warning colour; this only says
   which row to look at. */
.rows tr.oddrow > td { background:var(--wait-bg); }

/* Which site or sites a child is seen at, on their own record. A row of pills
   rather than a dropdown, because it is not typed in - booking a child at a
   site is what records it - and because two sites is an ordinary answer here
   that a single-select could only ever misrepresent. */
.seenat { display:flex; flex-wrap:wrap; gap:6px; margin:0 0 4px; }

/* ---- a wide table on a phone: keep the name in sight ------------------- */
/* MEASURED, NOT GUESSED. Packages is 16 columns and 1,900px wide; on a 375px
   phone that is 5.1 screens, and the child's name - the only thing that says
   which row you are reading - scrolls off after the first swipe. Approvals is
   2.5 screens, billing and reschedules 2.

   tools/mobilecheck.py passes all of them and is right to: the BODY does not
   scroll sideways, nothing hangs off the edge, and every control is big
   enough. The table scrolls inside its own wrapper, which is the correct
   behaviour and is also what hides this - the four things that check are all
   about the page, and this is about the row.

   So the first cell is pinned to the left edge while the rest moves under it.

   WHEN IT DOES NOT FIT, not under some width. This was
   `@media (max-width: 560px)` and the reason written beside it was "phone
   widths only: on a desktop the whole row is visible and a pinned cell would
   only add a seam" - a reason about a DESKTOP guarded by a breakpoint for a
   PHONE, and the clinic works in the gap between them. At 768px the Packages
   table is 16 columns and two and a half screens wide, the row is nowhere
   near visible, and the pin was off: ninety rows of numbers with nothing
   saying whose they are, on the tablet on the desk.

   Width was never the question. Whether the row fits depends on the columns
   the page drew, the font and the window, and only the browser knows. So
   fitGrids measures the wrapper it is already measuring for height and sets
   `.wide` on it, and these rules key off that.

   The seam is still avoided, and more exactly than before: a table that fits
   gets no class at any width - including a phone in landscape holding a
   narrow table, which the breakpoint pinned regardless.

   AN OPAQUE BACKGROUND IS NOT DECORATION. A sticky cell with a transparent
   one has the scrolling cells slide visibly underneath it, which is worse than
   no pinning at all - so the fill is stated here, and again for the states
   that change it, because a hovered or tinted row must not go see-through as
   it passes behind.

   The header's own corner cell needs a higher z-index than both: it is pinned
   in two directions at once and has to sit above the column it heads and the
   headings it sits beside. */
/* :not([colspan]) LEAVES THE EMPTY-STATE ROW ALONE. "Nothing here yet" spans
   the whole table, and a cell that wide pinned to the left edge slides its own
   sentence along under the columns as you scroll. It came from the older copy
   of this rule, which is otherwise gone - see the note above. */
.gridwrap.wide table.rows th:first-child,
.gridwrap.wide table.rows td:first-child:not([colspan]) {
  position:sticky; left:0; z-index:1;
  background:var(--surface);
  box-shadow:1px 0 0 var(--line);
}
.gridwrap.wide table.rows thead th:first-child { z-index:3; background:var(--sunken); }
.gridwrap.wide table.rows tbody tr:hover td:first-child { background:var(--sunken); }
.gridwrap.wide table.rows tbody tr.current > td:first-child { background:var(--ok-bg); }
.gridwrap.wide table.rows tbody tr.oddrow > td:first-child { background:var(--wait-bg); }
.gridwrap.wide table.rows tbody tr.totalrow > td:first-child { background:var(--sunken); }

/* A TABLE THAT OPENS WITH A ROW NUMBER needs two cells pinned, not one:
   "1" tells nobody which row they are reading. The stock ledgers are the
   only ones shaped that way - the clinic asked for s.no first - so they say
   so on the table rather than every table guessing. */
/* THE SECOND CELL NEEDS A KNOWN OFFSET, and `left` cannot be a
   percentage of something it is not inside - so the row number is given a
   fixed width and the name is pinned exactly that far in. 42px holds four
   digits at this font size, which is more rows than a stock ledger on a
   phone will ever show at once. */
.gridwrap.wide table.rows[data-stick="2"] th:first-child,
.gridwrap.wide table.rows[data-stick="2"] td:first-child {
  width:42px; min-width:42px; max-width:42px;
  padding-left:8px; padding-right:4px;
}
.gridwrap.wide table.rows[data-stick="2"] th:nth-child(2),
.gridwrap.wide table.rows[data-stick="2"] td:nth-child(2) {
  position:sticky; left:42px; z-index:1;
  background:var(--surface);
  box-shadow:1px 0 0 var(--line);
}
.gridwrap.wide table.rows[data-stick="2"] thead th:nth-child(2) {
  z-index:3; background:var(--sunken);
}
.gridwrap.wide table.rows[data-stick="2"] tbody tr:hover td:nth-child(2) {
  background:var(--sunken);
}
.gridwrap.wide table.rows[data-stick="2"] tbody tr.oddrow > td:nth-child(2) {
  background:var(--wait-bg);
}
/* The first cell carries no shadow when a second one follows it - two
   hairlines an inch apart read as an empty column. */
.gridwrap.wide table.rows[data-stick="2"] th:first-child,
.gridwrap.wide table.rows[data-stick="2"] td:first-child { box-shadow:none; }
/* ------------------------------- the controls above a grid, on a phone ---
   Three things, all measured on the clinic's own handset at 394x759 and all
   below 760px only - a desktop has room and none of this applies there.

   1. THE WIDTH WAS NOT BEING USED. The bar wraps, and seven of its rows ended
      well short of the right edge: the date row 107px short, the search 125,
      the view buttons 116, "+ New booking" and its two flags 161 - and "Now"
      sat on a line of its own with 305px of nothing beside it. Reported with
      that empty column circled.

      The cause of the worst of it is `.grow`, the flex spacer that pushes
      things right. In a bar that does not wrap it does exactly that; in one
      that does, it is a line break - it eats the whole remainder of the row
      it lands on. So it is off here, and the two controls that should take
      the space are told to.

   2. IT NEVER WENT AWAY. 281px of a 759px screen - 37% - and the page cannot
      scroll it off, because fitGrids stretches the grid to the window and the
      GRID is what scrolls. So the bar sat there for the whole of every day.
      It now slides up while somebody is reading downwards and comes straight
      back when the direction changes, which is the moment they have stopped
      reading and started looking - the same rule, from the same scroll
      watcher, that the floating buttons have always used.

   3. THE TEXT IS SMALLER. Buttons and labels only: `input` and `select` are
      pinned at 16px by the coarse-pointer rule further up this file, and that
      is not a style choice - below 16px iOS zooms the whole page on focus and
      leaves it zoomed, with the grid off the edge. tools/mobilecheck.py fails
      the build over it. */
@media (max-width: 760px) {
  /* ---- 0. the calendar grid, measured on a real handset ----------------
     Reported from the phone: with a filter on and the grid scrolled, "it
     works weirdly and i cant see any chips properly". Two faults, both of
     them only visible at this width, and both measured rather than guessed.

     THE TIME COLUMN WAS 159px OF A 381px GRID. `.slotcell` is nowrap and the
     widest thing ever put in it is the LUNCH label - "Lunch 2:00 - 2:30 30
     min" on one unbreakable line - so one row at lunchtime set the width of
     the column all day. The day column was left 222px of the 300 it needs,
     so every chip was clipped on the right: the outcome badge, the last word
     of a note, "CANCELLED" showing as "CANCE". Letting the label wrap gives
     the column back to the times it is for, and the chips stop being cut.

     THE TIME ITSELF SCROLLED OUT OF VIEW. A slot row here is 480 to 635px
     tall - a day of eight chips in one cell - against a 589px window, so
     once you are inside a row the label is above the fold and there is
     nothing on screen saying which slot you are reading. The cell was sticky
     LEFT only, which is right for scrolling sideways and does nothing for a
     row taller than the screen. Sticky in both axes pins it under the header
     for as long as its own row is on screen, which is what a table cell's
     sticky containing block gives for nothing: the row releases it on the
     way past. --headh is the measured header height, set by fitGrids. */
  table.cal tbody td.slotcell, table.cal tbody th.slotcell {
    top:var(--headh, 35px);
  }
  .lunchband .lunchin {
    flex-wrap:wrap; white-space:normal; gap:2px 8px; padding:4px 7px;
  }

  /* ---- 0b. one time wide, and a day to a screen (20 October 2026) ----
     Reported from the phone with Planning open: "slot section is too wide
     ... kid names are not full ... user have only 30% screen which is of
     use". Measured on the clinic's handset (406px wide): the time column was
     164px - 43% of the grid - because a MERGED row's label names every time
     it holds, "11:00 / 11:15 / 11:45", on one nowrap line; the lunch label
     above was the same fault once. The day column was left 220px, and every
     one of forty names on screen was cut to 31-44px beside the copy mark,
     the therapy, the time and the package.

     EACH TIME ON ITS OWN LINE: rowLabelHtml() (shared.js) gives each " / "
     an element of its own, which becomes a line break with the slash hidden
     - so the column is one time wide, about 50px, and still says every time
     the row holds. The cell stays nowrap, so a time is never split. That
     rule is no longer here: on 21 October the laptop asked for it too, and it
     sits beside td.slotcell's own rule at the top of this file.

     A DAY TO A SCREEN: each day column takes the width beside the time column
     - the Calendar's One day view already did - so a chip has room for the
     whole name. Week, Compare and Planning's six days are swiped sideways,
     a day at a time, each headed by its day. Measured on the handset after:
     time column 54px, day 323px, no name of forty cut (twenty were). A
     month is a different grid (.mday) and is untouched. */
  td.slotcell, th.slotcell { padding:6px 5px; font-size:12px; }
  /* AND THE DAY IS THE GRID'S WIDTH LESS THE TIME COLUMN, NOT THE SCREEN'S
     (24 October 2026). It was calc(100vw - 84px): the screen less a time
     column of 54px and the page's gutters, as measured on 20 October. The
     next day the Calendar's lunch label stacked into this column and its
     "2 - 2:30" line, kept whole, made the column 67px on any day with a
     lunch row - so on the clinic's phone the grid was 390px in a 370px frame
     and every chip lost its right edge, "BOOKED" and the "..." cut off. A
     number written here is a guess at three things this rule cannot see.

     So the time column is a WIDTH, 56px - "Time", "Stats" and every time fit
     it, and the lunch label gives up its side padding and may break its
     range at the thin spaces ("2 -" over "2:30"); a time itself still never
     splits - and the day is the GRID's own width less that column: 100cqi,
     the grid being the container (its inline size is its parent's, never
     its content's, so nothing here can loop). Less 12px more: the grid's own
     scrollbar is the app's 11px (app.css, ::-webkit-scrollbar), which cqi
     counts and the table cannot use, and the column's 1px border. A grid
     with no container ancestor reads cqi as the screen, which is the old
     rule's answer. */
  .gridwrap:has(> table.cal) { container-type:inline-size; }
  table.cal td.slotcell, table.cal th.slotcell { width:56px; min-width:56px; }
  .lunchband.withcells > td.slotcell .lunchin { padding-left:1px; padding-right:1px; }
  .lunchband.withcells > td.slotcell .lut { white-space:normal; }
  td.daycell, table.cal.plan td.daycell { min-width:calc(100cqi - 56px - 12px); }
  /* AND THE CHIP TAKES THE DAY. The 220px cap on a Planning chip is a
     desktop measurement - every name fitted at 220 there - and on a phone
     the same chip carries the "..." menu and larger badges, so the name
     was left 31px of it inside a 323px cell. Uncapped, the chip fills the
     cell and the name gets the difference. */
  .sess.plan.one { max-width:none; }

  /* ---- 1. use the room ---- */
  .toolbar .grow { display:none; }
  .toolbar .search, .toolbar #calSearch { flex:1 1 100%; min-width:0; }
  .toolbar #calDate { flex:1 1 auto; min-width:0; }
  /* AND THE REST OF EACH ROW GOES TO THE CONTROL THAT CAN USE IT.
     With the break removed the rows pack, but three of them still
     stopped 115, 151 and 182px short - so the one real control on
     each stretches to the edge. The icon buttons beside them are left
     alone: a flag stretched to 150px is a bigger target for something
     nobody is aiming at, and it would read as the main action. */
  .toolbar #calView, .toolbar #calBook, .toolbar .foldbtn {
    flex:1 1 auto; min-width:0;
  }
  /* THE FILTERS BUTTON WAS ALREADY MEANT TO DO THIS. There is a rule
     further up saying `.foldbtn { width:100% }` on a phone, with a
     comment explaining that a row with one short button on it wastes
     the line - and it has never taken effect, because
     `.toolbar .foldbtn { max-width:26ch }` outranks it and pinned the
     button at 174px. The cap is right on a desktop, where a long
     filter summary would shove + New booking off its row; here there
     is a row to fill and nothing to shove.

     It shares that row with More rather than taking one of its own,
     which is better than either: two full-width buttons would be two
     rows where these are one. */
  .toolbar .foldbtn { max-width:none; width:auto; }
  /* THE DATE ROW IS THE ONE THAT WAS CIRCLED. `.rangenav` is `flex:none`
     and `inline-flex`, so it took the whole row and then held its three
     children at their natural width - the arrows and the dates stopped
     at x=212 of 375, and the 163px to the right of them was the empty
     column in the screenshot.

     The label takes the middle and the two arrows go to the ends. That
     uses the room, and it puts the widest possible gap between the two
     controls whose whole risk is hitting the wrong one - a week
     backwards when you meant forwards looks exactly like the app
     losing your place. */
  .toolbar .rangenav { display:flex; flex:1 1 100%; }
  .toolbar .rangenav .range { flex:1 1 auto; text-align:center; }

  /* ---- 3. a little quieter ---- */
  .toolbar .btn, .filterbar .btn { font-size:12px; }
  .toolbar .btn.tbmorebtn, .foldbtn { font-size:12px; }
  .filterbar label, .toolbar label.inline { font-size:9px; }
  .pill-btn { font-size:10.5px; }
  .filterbar .sep { display:none; }

  /* ---- 2. out of the way while reading ----
     max-height rather than display:none, so it animates and so the grid
     underneath has something to grow into. `visibility` follows at the end,
     which keeps the buttons out of the tab order once they are gone without
     making them vanish halfway through the slide. */
  /* AND WHAT CAME AFTER IT (20 October 2026): the Change history bar (0312)
     and the strip that mirrors the grid's sideways scrollbar sat between the
     toolbar and the grid and never left - 79px of a 904px phone, measured,
     while everything above them folded away. They fold with it now. The
     history bar only while it is SHUT: open, it holds a list somebody is
     reading, and the 60vh cap would clip it. Its line of explanation goes
     on a phone - the button says what it is. */
  #view > .toolbar, #view > .filterbar, #view > .topscroll,
  #view > .chgblock.pagechg:has(> .chgbody[hidden]) {
    transition:max-height .18s ease, opacity .18s ease,
               margin .18s ease, padding .18s ease, visibility 0s;
    max-height:60vh; overflow:hidden;
  }
  /* BUT THE STRIP STILL SCROLLS. `overflow:hidden` above outranks the strip's
     own overflow-x:auto, so from cb1f1ce until the next commit the strip
     could not be dragged on a phone - mobilecheck found its spacer, as wide
     as the grid, no longer inside anything that scrolls. Its own
     overflow-y:hidden is what lets max-height fold it, so that is all it
     needs from the rule above. */
  #view > .topscroll { overflow-x:auto; overflow-y:hidden; }
  .chgblock.pagechg .chgbar .sub { display:none; }
  /* Planning's own line of explanation, beside its title: "Weekly plan" and
     the Message therapy type button then share a row instead of taking two. */
  #planTools > .sub { display:none; }
  /* A BAR THAT FOLLOWS THE READER (24 October 2026), on a page that scrolls
     itself - the Staff schedule's, seven screens long, whose day, date and
     branch went off the top with the page. It sticks under the app bar
     (--navh, main.js followNav) and, while somebody reads down, SLIDES out of
     sight rather than folding: folding a page's own bar is what took the
     height from under the reader on a page that scrolls (main.js, tuckAway).
     Back on the Calendar's rule. Below the app bar's 30, so it slides behind
     it. */
  #view > .toolbar.followbar {
    position:sticky; top:var(--navh, 0px); z-index:25;
    transition:transform .18s ease, visibility 0s;
  }
  body.barhid #view > .toolbar.followbar {
    transform:translateY(calc(-100% - var(--navh, 0px))); visibility:hidden;
    transition:transform .18s ease, visibility 0s linear .18s;
  }
  body.headtuck #view > .topscroll,
  body.headtuck #view > .chgblock.pagechg:has(> .chgbody[hidden]),
  body.headtuck #view > .toolbar, body.headtuck #view > .filterbar {
    max-height:0; opacity:0; margin-top:0; margin-bottom:0;
    /* Its own padding survives a max-height of zero and left 14px of empty
       bar behind - small, and exactly the sort of thing that reads as the
       collapse not having worked. */
    padding-top:0; padding-bottom:0; border-top-width:0; border-bottom-width:0;
    visibility:hidden;
    transition:max-height .18s ease, opacity .18s ease,
               margin .18s ease, padding .18s ease, visibility 0s linear .18s;
  }
}

/* "Is this the same child?" - the sessions that name already has on that day.
   Shown rather than described, because the whole difficulty is that the NAME
   cannot settle it: what settles it is the therapist, at her time, in her own
   colour, which is how the calendar identifies her everywhere else. */
.samekid { list-style:none; margin:0 0 12px; padding:0; display:grid; gap:8px; }
.samekid li {
  display:flex; align-items:flex-start; gap:10px;
  padding:10px 12px; border:1px solid var(--line);
  border-radius:var(--radius-sm); background:var(--sunken);
}
/* The therapist's own colour, the same swatch the therapist picker uses. A
   ring round it so a pale colour still reads as a deliberate mark on the
   sunken fill rather than as a smudge. */
.samekid .dot {
  width:12px; height:12px; border-radius:50%; flex:none; margin-top:3px;
  box-shadow:0 0 0 1px var(--line);
}
.samekid-what { min-width:0; }
.samekid-what .sub { margin-top:3px; }

/* "Check this is the right child" - asked INSIDE the booking dialog, in place
   of the form rather than on top of it. See warnBeforeBooking in calendar.js
   for why a second modal was wrong; the form is hidden, not closed, so going
   back returns it with everything still typed into it. */
.askover { display:block; }
.askhead { margin:0 0 4px; font-size:15px; }

/* THREE WARNINGS OF DIFFERENT WEIGHTS, and the colour is the whole of how
   they are told apart at a glance. Red is the same therapy already in the
   child's weekly plan, orange is already on the calendar that day, and the
   plain one is "this is a weekly child - consider a surname".

   A bar down the left rather than a filled block: three filled blocks in a
   column is a wall of colour where nothing stands out, which is the opposite
   of what a severity is for. The tint is carried by the bar and a very light
   ground, so the red one is found first and the text stays readable on both
   themes - these are the same --bad and --wait pairs every other state in the
   app uses. */
.warnlines { list-style:none; margin:0 0 12px; padding:0; display:grid; gap:7px; }
.warnline {
  padding:9px 12px 9px 13px; border-radius:var(--radius-sm);
  border:1px solid var(--line); border-left-width:4px;
  background:var(--sunken); font-size:13px; line-height:1.5;
}
.warnline.red    { border-left-color:var(--bad);  background:var(--bad-bg); }
.warnline.orange { border-left-color:var(--wait); background:var(--wait-bg); }
.warnline.plain  { border-left-color:var(--faint); }
.warnline.red b    { color:var(--bad); }
.warnline.orange b { color:var(--wait); }

/* The question the session dialog asks when the therapist being chosen works a
   different length from the one the session was booked with. It sits under the
   Session length box rather than in a modal of its own, because modal() begins
   by closing whatever is already open and would take the half-filled form with
   it - the same lesson the same-name warning learned. */
/* Session length spans both columns of .form2 so the pairs after it - Status
   with Type, Individual with Therapy - stay where they are. The box itself
   should not stretch with the row. */
.minsrow input { max-width:150px; }
.minsask { margin-top:7px; }
.minsbtns { display:flex; gap:8px; margin-top:8px; flex-wrap:wrap; }
.minsbtns button { font-size:12px; padding:4px 11px; white-space:nowrap; }

/* "This session is already given": the day it really happened, and the
   sessions that child has on it. A list rather than a typed time, because
   reception does not remember the minute and a time that misses by five looks
   exactly like a session that is not there. */
.givenrow input[type="date"] { max-width:190px; }
.givenlist { margin-top:8px; display:grid; gap:6px; }
.givenpick {
  display:flex; align-items:flex-start; gap:9px; padding:8px 10px;
  border:1px solid var(--line); border-radius:var(--radius-sm);
  background:var(--sunken); font-size:13px; cursor:pointer;
}
.givenpick:hover { border-color:var(--accent); }
.givenpick input { width:auto; margin-top:2px; }
.givenpick i { color:var(--faint); font-style:normal; }
.givenadd { display:flex; align-items:flex-end; gap:9px; flex-wrap:wrap; margin-top:6px; }
.givenadd input { max-width:150px; }
/* Added in the picker and not written yet - Save writes it (wireGiven). */
.givenpick.givennew { border-style:dashed; border-color:var(--wait); }

/* A button in the session dialog whose answer waits for Save: it stays
   pressed, and the line under it says what Save will do and how to take it
   back. The leave block's line is the same thing with an Undo in it. */
.btn.pending { box-shadow:inset 0 0 0 2px var(--wait); }
.pendline {
  margin:5px 0 0; padding:5px 9px; border-radius:var(--radius-sm);
  background:var(--wait-bg); color:var(--ink); font-size:12.5px; line-height:1.45;
}
.pendline .btn { margin-left:6px; }

/* The two reschedule panels in the booking dialog. They reuse .givenlist and
   .givenpick from the session dialog rather than growing a second look for the
   same job - one picker, two forms. */
.bookresch { border-top:1px solid var(--line); padding-top:10px; margin-top:2px; }
.bookresch > label { display:block; margin-bottom:6px; }
.handrow { display:flex; gap:10px; flex-wrap:wrap; align-items:flex-end; }
.handrow > div { display:grid; gap:4px; }
.handrow > .grow { flex:1; min-width:180px; }
.handrow input[type="number"] { max-width:130px; }
/* THE BY-HAND ROW LINES UP BY ITS LABELS. A .handrow sits its columns on their
   bottom edge, and the session number's column is the only one carrying a
   hint under its box - so that box rode up and the day and "Why it moved"
   dropped to the hint's level, three boxes at two heights. Reported with the
   unplanned booking open on 13 October 2026: "make these boxes aligned". By
   the top, three one-line labels put three boxes on one line, and the hint
   stays under the box it is about. Only here: #bPartRow's sentence is meant
   to sit level with its box. */
#bHandRow { align-items:flex-start; }
/* The whole session or part of it, for a debt nobody has started - two
   answers on one line, under the list they are about (13 October 2026).
   AN ANSWER, NOT A FIELD LABEL, so it wears .checkrow .check's clothes: the
   form's own `.form1 input { width:100% }` reaches these radios too, and the
   first one took 90px and folded "The whole session - 45 min" into a column
   of single words beside it. */
.lenpick { display:flex; flex-wrap:wrap; gap:6px 18px; margin:10px 0 2px; }
.lenpick label {
  display:inline-flex; align-items:center; gap:6px; cursor:pointer;
  font-size:13px; color:var(--ink); font-weight:400; margin:0;
}
.lenpick input { width:auto; flex:none; margin:0; }
.givenadd select { max-width:170px; }
.givenadd > div { display:grid; gap:4px; }
/* The refusal sits under the row it belongs to rather than floating away as a
   toast - the clash rule's sentence names a child, a window and the first
   minute that would work, and it is worth nothing if it vanishes. */
#esGivenErr { margin-top:8px; }

/* The two questions in "Not this booking". Boxed and legended rather than run
   together, because they are genuinely separate answers - what the family is
   owed, and what becomes of the session already on the calendar - and the
   commonest mistake would be reading the second as a consequence of the
   first. */
.unbookset {
  border:1px solid var(--line); border-radius:var(--radius-sm);
  padding:8px 10px 10px; margin:10px 0 0;
}
.unbookset legend {
  padding:0 5px; font-size:12px; font-weight:700; color:var(--faint);
  text-transform:uppercase; letter-spacing:.04em;
}
.radioline {
  display:flex; align-items:flex-start; gap:9px; padding:5px 0;
  font-size:13px; cursor:pointer;
}
.radioline input { width:auto; margin-top:3px; flex:none; }

/* The note box while a cancellation is being recorded: it is the reason then,
   and required. Marked rather than starred, because the placeholder already
   says what it wants and a second marker would be saying it twice. */
.addform .note.needed {
  border-color:var(--wait); background:var(--wait-bg);
}

/* The slots a focused child is in, on the bar above the plan - each one a way
   straight to that chip. Small and monospaced, because they are read as a
   column of times rather than as a sentence, and they wrap: a child on six
   days would otherwise push "Show everyone" off the end of the bar. */
.planjumps { display:flex; flex-wrap:wrap; gap:4px; margin-left:10px; }
.planjumps .jump {
  appearance:none; cursor:pointer;
  font-family:"IBM Plex Mono", ui-monospace, monospace; font-size:11px;
  padding:2px 7px; border-radius:999px;
  border:1px solid var(--accent); background:var(--ground); color:var(--accent);
}
.planjumps .jump:hover, .planjumps .jump:focus-visible {
  background:var(--accent); color:var(--ground);
}

/* A BEAT OF ATTENTION AFTER THE SCROLL, because the chip landed in a cell
   holding eight others and the eye has to be told which one. It fades out on
   its own - a mark that stayed would have to be cleared by something, and the
   grid redraws itself every few seconds while the clinic works. */
.sess.justhere {
  outline:2px solid var(--accent); outline-offset:2px;
  animation:justhere 1.6s ease-out;
}
@keyframes justhere {
  0%, 55% { outline-color:var(--accent); }
  100%    { outline-color:transparent; }
}
@media (prefers-reduced-motion: reduce) {
  .sess.justhere { animation:none; }
}

/* A child who buys sessions one at a time. Told apart from "no package",
   which means nobody has written down what they bought - a gap to chase rather
   than an arrangement. Quiet, because it is a fact about the child and not a
   warning. */
.badge.perses { color:var(--muted); border-style:dashed; }

/* Taking the make-up flag off a session that is not one. Under the hint that
   explains what the flag is, and set apart from it, because it is the one
   control in that block that changes something rather than describing it. */
.tieno { margin-top:7px; display:flex; align-items:baseline; gap:8px; flex-wrap:wrap; }
.tieno .hint { margin:0; flex:1 1 200px; }

/* The course has been used up, so the number restarts. Said on its own line
   because the box below is about to disagree with every row above it. Quiet -
   it is an explanation, not a warning: nothing is wrong and nothing needs
   deciding. */
.renews {
  margin:7px 0 0; padding:5px 9px; font-size:11.5px; line-height:1.45;
  border-radius:var(--radius-sm);
  background:var(--accent-bg); color:var(--accent);
}
.renews b { font-family:"IBM Plex Mono", ui-monospace, monospace; }

/* Where a reschedule came from - typed on the Reschedules page, or raised by a
   decision on Approvals. Said quietly and on its own line under the date,
   because it is the answer to the question AFTER "what is owed": which screen
   this row is answered on. Loud enough to find, not loud enough to read first. */
.rsfrom {
  display:block; font-size:10px; letter-spacing:.04em; text-transform:uppercase;
  color:var(--faint); margin-top:2px;
}
.hist .badge.warn { font-size:10px; margin-left:4px; vertical-align:1px; }

/* A link button that reads as the text it replaced.
   The date in a planner row opens the session, so it is a real button - but it
   is also the date, and styling it like a link would put six blue underlines
   down a column that is meant to be read as a column of dates. It keeps the
   row's mono face and says what it is on hover; the underline appears then. */
.linkbtn.asis {
  font: inherit; color: inherit; padding: 0; border: 0; background: none;
  cursor: pointer; text-decoration: none;
}
.linkbtn.asis:hover, .linkbtn.asis:focus-visible {
  text-decoration: underline; color: var(--accent);
}

/* What was decided about a cancellation, on the row it happened on. After the
   outcome, because the outcome is the fact and this is its consequence.

   ONLY "waiting" IS COLOURED, and it borrows --wait like every other
   outstanding thing in the app rather than introducing a sixth state colour.
   The other four are context somebody reads once; a row of five different
   colours in a 390px panel is a row nobody reads at all. */
.badge.dec { font-size:10px; padding:0 4px; vertical-align:1px; }
.badge.dec.wait {
  background:var(--wait-bg); color:var(--wait); border-color:transparent;
}
.badge.dec.owed { background:var(--accent-bg); color:var(--accent); border-color:transparent; }
.badge.dec.quiet { color:var(--faint); }

/* The packages a child holds, on the Approvals table. They wrap rather than
   stretch the column: a child on two courses is ordinary here. */
.pkgcell { white-space:normal; line-height:1.8; }
.pkgcell .badge { margin-right:3px; }

/* The switch between what is still owed and everything. Above the table
   rather than in a filter bar: this panel has one filter and a bar built for
   one control reads as a bar somebody forgot to finish. */
.rsonlyowed {
  display:flex; align-items:center; gap:8px; margin:8px 0 2px;
  font-size:12.5px; color:var(--faint); cursor:pointer;
}
.rsonlyowed input { width:auto; margin:0; }

/* The owed badges are buttons now, so they need a button's defaults undone -
   a browser gives one its own font, border and background, none of which a
   badge wants. The cursor is the part that matters: it is the only thing on a
   chip that says this one can be pressed. */
button.badge { font:inherit; border-width:1px; cursor:pointer; }
button.badge:hover { filter:brightness(1.12); }

/* A booking whose reschedule nobody has answered yet. The clinic's waiting
   colour, the one the queue bar and the Reschedules flag already use, so it
   reads as the same state in a third place rather than a new one to learn.

   A WORD RATHER THAN INITIALS, unlike RS beside it. RS says what this session
   IS; this says it may not survive the week, and two two-letter badges an inch
   apart meaning very different things is what owedchip_test.mjs was written
   about. It is rare enough to afford the letters: only a hand-written
   reschedule that is still waiting, with a session already booked against it. */
.badge.pending {
  background:var(--wait-bg); color:var(--wait); border:1px solid var(--wait);
  letter-spacing:.03em;
}

/* Who is actually in this hour that the plan does not know about. Below
   everything, and separated by a rule, because it answers a different question
   from the rest of the panel: above is what repeats every week, here is what
   happened once. */
/* ---- THE DAY AND THE TIME THIS PANEL IS ABOUT -----------------------
   Asked for with the panel pinned beside the WhatsApp box: "add a filter on
   'who planned here' popup window so that i can select time slots & date".
   It was a panel you could only open from a cell, so changing hour meant
   shutting it and finding another cell on the grid behind it.

   The times come from gridRows(), so a merged "2:40 / 2:45" is one entry
   here exactly as it is one row on the Calendar - see plannerPick().

   The date box keeps the 130px floor the inbox filter measured: a native
   date control reports no overflow and clips its digits in silence, so the
   row wraps rather than letting it shrink.

   div.slotpick, not .slotpick: Therapist availability's row has a SPAN
   of that class round its time list, and these rules reached it - a line
   under the list and its arrow painted over (28 October 2026). */
div.slotpick {
  display:flex; flex-wrap:wrap; align-items:center; gap:6px;
  margin:-4px 0 12px; padding-bottom:10px;
  border-bottom:1px solid var(--line);
}
div.slotpick input[type="date"] {
  flex:0 0 auto; width:150px; min-width:130px; padding:5px 9px;
}
div.slotpick select {
  flex:1 1 92px; min-width:92px; max-width:100%;
  border:1px solid var(--line); background:var(--ground); color:var(--ink);
  font:inherit; font-size:12px; border-radius:var(--radius-pill);
  padding:5px 8px;
}

.slotextra { margin-top:14px; padding-top:10px; border-top:1px solid var(--line); }
.slotextra h4 {
  margin:0 0 6px; font-size:12px; font-weight:700; letter-spacing:.04em;
  text-transform:uppercase; color:var(--faint);
}
.slotextra h4 .sub { margin-left:6px; letter-spacing:0; }

/* The way out of "Who is planned here" for somebody it does not plan for.
   First thing in the panel, because the panel answers a question they have
   already asked and the answer is "not here" - so the next step should be the
   first thing they read rather than something under a list of other people. */
.slotadd {
  display:flex; align-items:center; gap:10px; flex-wrap:wrap;
  padding:0 0 10px; margin-bottom:10px; border-bottom:1px solid var(--line);
}
.slotadd .hint { flex:1; min-width:170px; margin:0; }

/* Answering a cancellation from inside the booking form. It sits UNDER the
   row rather than beside it, because the row is already a date, a number, a
   therapist and a state, and a dropdown on the end of that line is where the
   eye stops finding the name. */
.queuerow { display:grid; gap:0; }
.decidehere {
  display:flex; align-items:center; gap:7px; flex-wrap:wrap;
  padding:6px 10px 2px 34px;
}
.decidehere select { max-width:210px; }
.decidehere input[type="text"] { flex:1; min-width:150px; }
.decidehere .warnline { flex-basis:100%; margin:4px 0 0; }

/* The two halves of the reschedule panel. They hold rows that look alike -
   dates, therapists, numbers - and picking from one settles a debt while
   picking from the other raises one, so the heading is doing real work rather
   than decorating. It is the only thing on screen that separates them now the
   Type control no longer asks. */
.reschgroup { margin-top:10px; }
.reschgroup h4 {
  margin:0; font-size:12px; font-weight:700; letter-spacing:.04em;
  text-transform:uppercase; color:var(--faint);
}
.reschgroup .givenlist { margin-top:5px; }
#bReschSay { margin:2px 0 0; }

/* ------------------------------------- what this session is making up ----

   TWO TABS WHERE THERE WERE TWO HEADINGS. What is still outstanding opens;
   the history of cancelled sessions is behind the second one, because it is
   long - 120 sessions for the child who needs it most - and it was burying
   the short list above it.

   The segmented control is the app's own, so it looks like the one on the
   Reschedules page and reads as a choice rather than as two buttons. */
.makeuptabs { margin-top:8px; }
/* The count rides inside the label. NOT `.tally`, which is taken by the run of
   figures on a table row - one name, two meanings, from opposite ends of the
   stylesheet, is exactly what webcheck refuses. */
.tabnum {
  margin-left:6px; padding:0 6px; border-radius:var(--radius-pill);
  background:var(--line-soft); color:var(--muted);
  font-size:11px; font-weight:700; font-variant-numeric:tabular-nums;
}
.seg .btn[aria-pressed="true"] .tabnum { background:var(--accent); color:#fff; }
/* Empty is not a nought. There is nothing to say when a tab holds nothing, and
   an empty pill is a smudge next to the word. */
.tabnum:empty { display:none; }

/* WHICH LIST A ROW CAME FROM. The two mean different things on Save - one
   settles an approval, the other fills in a move already written down - and
   with them in one list the badge is the only thing that says which.

   BOTH ARE LABELLED. A badge on one and nothing on the other reads as a
   property of those rows rather than as where they came from. */
.srcpill {
  display:inline-block; vertical-align:1px; margin:0 4px 0 2px;
  padding:1px 7px; border-radius:var(--radius-pill);
  font-size:10px; font-weight:700; letter-spacing:.03em; text-transform:uppercase;
  background:var(--accent-bg); color:var(--accent);
}
.srcpill.hand { background:var(--wait-bg); color:var(--wait); }
/* IN PIECES: a debt the clinic is already part way through making up, so
   picking it asks how long THIS piece is worth. The RS colours, because that
   is what a make-up wears everywhere else in the app - not the amber beside
   it, which says "recorded by hand" on the same row and would make two
   different facts look like one. */
.srcpill.part { background:var(--rs-bg); color:var(--rs); }

/* Seven at a time. Full width so it reads as the end of the list rather than
   as a control belonging to the last row. */
.morebtn {
  margin-top:6px; width:100%; justify-content:center;
  font-variant-numeric:tabular-nums;
}

/* THE REASON A SESSION WAS CALLED OFF, under the row that says when. It is
   what tells two cancellations of one child apart - six dates and six
   therapists say very little, and "they were away at a wedding" says which
   one is being moved. Its own line rather than run on after the therapist:
   these are a sentence somebody typed and they wrap. */
.lostwhy { display:block; margin-top:2px; color:var(--faint); font-style:normal; }
.lostwhy .dim { opacity:.75; }

/* The sessions leading up to the one being moved. Read only - there is
   nothing to pick here, the numbers are the point - so no radio, no hover,
   and a row rather than a control. */
.prevbox { margin-top:10px; }
.prevbox > label { display:block; margin-bottom:2px; }
.prevlist { margin-top:6px; display:grid; gap:4px; }
.prevrow {
  display:flex; align-items:baseline; gap:10px; flex-wrap:wrap;
  padding:5px 10px; border:1px solid var(--line);
  border-radius:var(--radius-sm); background:var(--sunken); font-size:13px;
}
.prevrow b { min-width:34px; }
.prevrow i { color:var(--faint); font-style:normal; margin-left:auto; }
#bHandMore { margin-top:7px; }

/* How many cancellations are waiting for a decision, on the Owed now tab
   itself. An administrator asked why reception's reschedules were "not
   visible" - they were on that tab all along, and the count naming them was
   only readable once you were already there. */
.waitdot {
  display:inline-block; margin-left:7px; min-width:18px; padding:1px 6px;
  border-radius:999px; background:var(--wait); color:var(--on-wait, #1a1200);
  font-size:11px; font-weight:700; line-height:16px; text-align:center;
}

/* ------------------------------------------------- Reschedules, as cards ---
   Asked for with a mockup on 8 October 2026 - "apply this for reschedules
   page" - and with the same condition as the three before it: "you don't have
   to change any utility of functionality its just UI change". So every id,
   every handler and every rule on that page is where it was; what moved is
   the walls round them.

   THE COUNTS WERE A RUN OF SMALL TEXT. "15 to reschedule · 42 pending · 6
   allowed · 2 settled" is five facts in one sentence, read left to right, and
   the figure somebody acts on is the part that disappears - the same reading
   the Calendar's Stats row was sent back for. They are tiles now, and the two
   TABS are the first two of them, which is what the mockup draws. */
.rstiles { display:flex; flex-wrap:wrap; gap:10px; justify-content:flex-end; }
.rstile {
  --a:var(--accent);
  position:relative; display:inline-flex; align-items:center; gap:10px;
  background:var(--surface); border:1px solid var(--line);
  border-radius:var(--radius); padding:10px 14px; box-shadow:var(--shadow-xs);
  min-width:132px; text-align:left;
}
/* The tile's colour reaches its own tile. .cardicon sets --a on ITSELF, which
   beats inheritance, so the inline colour on the tile would never arrive. */
.rstile .cardicon { --a:inherit; }
.rsfig { display:flex; flex-direction:column; min-width:0; }
.rsfig b { font-size:19px; font-weight:650; line-height:1.15; letter-spacing:-.02em; }
.rsfig span {
  font-size:10.5px; font-family:"IBM Plex Mono", ui-monospace, monospace;
  letter-spacing:.08em; text-transform:uppercase; color:var(--faint); margin-top:3px;
}
/* A TAB IS A TILE YOU CAN PRESS, and has to look pressable and look pressed.
   Without the second half the page has two identical rows of furniture and
   nothing says which list is on screen. */
button.rstile {
  appearance:none; font:inherit; color:inherit; cursor:pointer;
}
button.rstile:hover { border-color:var(--faint); }
button.rstile[aria-pressed="true"] {
  border-color:color-mix(in srgb, var(--a) 55%, var(--line));
  background:color-mix(in srgb, var(--a) 10%, var(--surface));
}
button.rstile[aria-pressed="true"] .rsfig span { color:var(--muted); }
/* The waiting count rides the tab rather than sitting in the row, because the
   question it answers is asked from the OTHER tab. */
.rstile .waitdot { position:absolute; top:-7px; right:-6px; margin:0; }
/* An empty list naming every other list its rows are on (23 October 2026):
   a button each, wrapping as one group. */
.rswhere { display:flex; flex-wrap:wrap; justify-content:center; gap:8px; margin-top:4px; }

/* The search and the filters in one card. The bar keeps its own class, so
   foldFilters() goes on folding it; the row above is a .toolbar because that
   is where foldFilters looks for somewhere to put its button. */
.rsfilters { padding:13px 14px; }
.rsfilters .toolbar { margin:0; }
.rsfilters .toolbar .search { flex:1 1 280px; min-width:0; max-width:420px; }
.rsfilters .filterbar { margin-top:11px; }
/* THE CAPTION GOES ABOVE THE CONTROL, which is the mockup's shape and is what
   lets seven of them sit in a row without the captions stealing the width.
   Stacked only inside this card: the same pair in a toolbar elsewhere is a
   caption beside a date and is right as it is. */
.rsfilters .filterbar .fpair {
  display:flex; flex-direction:column; align-items:stretch; gap:5px;
  flex:1 1 150px; white-space:normal;
}
.rsfilters .filterbar { flex-wrap:wrap; align-items:flex-end; gap:11px 12px; }

/* The note about where these rows come from: about the whole tab, so it is a
   card above the table rather than a first line inside it, where it read as a
   caption for the first row. */
.rsnote { display:flex; align-items:flex-start; gap:11px; padding:12px 14px; }
.rsnote p { margin:0; font-size:13px; color:var(--muted); line-height:1.55; }

/* A date somebody can find in a column of forty dates, and a therapist read
   by her own colour - which is how this clinic has told its staff apart since
   the spreadsheet recorded them by the colour of the text. */
.datecell { display:inline-flex; align-items:center; gap:8px; white-space:nowrap; }
.datecell svg { width:14px; height:14px; color:var(--faint); flex:none; }
.whocell { display:inline-flex; align-items:center; gap:8px; white-space:nowrap; }
.whodot {
  --a:var(--muted);
  flex:none; width:23px; height:23px; border-radius:50%;
  display:inline-flex; align-items:center; justify-content:center;
  font-size:11px; font-weight:700; color:#fff;
  background:color-mix(in srgb, var(--a) 85%, transparent);
}
.pill-btn.tiny { padding:2px 9px; font-size:11px; }
/* The mark inside a state pill. The WORDS are unchanged - statewords_test
   pins them - so the icon sits beside them rather than in them. */
.pill svg { width:12px; height:12px; margin-right:5px; vertical-align:-2px; }
.rsowed th.actions { text-align:right; }
.rsowed td:last-child { text-align:right; }
@media (max-width: 760px) {
  .rstiles { justify-content:flex-start; }
  .rstile { min-width:0; flex:1 1 140px; }
}

/* The template this run goes out as, chosen where the run is made. A row
   rather than a form field: it sits inside the Ready to send panel, between
   the count and the button, which is the second somebody is deciding. */
.tplpick { display:flex; flex-wrap:wrap; align-items:center; gap:9px; margin:10px 0 4px; }
.tplpick label { font-size:12px; color:var(--muted); }
.tplpick select { min-width:0; max-width:100%; flex:0 1 320px; }
.tplwords { margin:2px 0 0; }
.tplwords summary { cursor:pointer; font-size:12px; color:var(--muted); }
.tplwords summary:hover { color:var(--ink); }
.tplwords .wabubble { margin:8px 8px 6px 0; }
.tplwords .hint { margin:0; }

/* What a logged message said, opened in the History row rather than linked
   away to - the use of the column is showing a parent that this went to them
   on this day, and that is done with the row in front of you. */
.saidwhat summary { cursor:pointer; font-size:12px; color:var(--muted); }
.saidwhat summary:hover { color:var(--ink); }
.saidwhat .wabubble { margin:7px 8px 2px 0; }

/* ------------------------------------------ Services & pricing, as cards ---
   Asked for with a mockup on 9 October 2026 - "make services & billing page
   something like this" - under the condition the three redesigns before it
   carried: "you don't have to change any utility of functionality its just UI
   change". Every id, every handler and every rule on both pages is where it
   was.

   WHAT THE MOCKUP IS ACTUALLY ABOUT. A price list is read ACROSS a row, out
   loud, on the telephone, and compared DOWN a column when somebody is
   deciding what to charge - and this one was nine columns of plain text. Five
   of those nine hold a short word out of a small set (the kind, the length,
   the frequency, the allowance, how long it is valid), and five short words
   in a row read as one grey sentence per line. Each is a pill now, so the eye
   finds "Assessment" or "6/week" by its shape before it has read anything.
   The two that are MONEY stay plain, bold and mono: those are the figures
   somebody reads out, and a pill round a price makes it look like a label.

   TWO THINGS IN THE MOCKUP ARE DELIBERATELY ABSENT. It draws a sort caret on
   five headings and a kebab on every row. Sorting this table would fight the
   thing it is ordered BY - the order IS the price list, kept by hand with the
   arrows in edit mode, which servicerate_test pins - and a kebab that opens
   nothing is the blank-button fault icons.js's own note describes. The row's
   controls stay where they were, in edit mode. */

/* A CARD WITH A WATERMARK BEHIND ITS HEAD. Named for the shape rather than
   for the page, because the Ready to send card on Messages is the same thing
   with the WhatsApp mark in it - and a class called `svcard` sitting on the
   Messages page is exactly the sort of thing the next reader has to go and
   look up.

   On a priced section the colour comes from its POSITION on the page and
   never from its name: `group_name` is free text the clinic types - "SE or
   OT" is one of theirs - so a table keyed on the name would leave the next
   section they invent with no colour at all, and renaming one would change
   its. */
.markcard { --a:var(--accent); position:relative; overflow:hidden; }
.markcard .cardhead { margin-bottom:14px; padding-right:84px; }
/* The mark itself: large, faint, bleeding off the right edge. Decoration, so
   it is hidden from anything reading the page aloud and takes no clicks. */
.cardmark {
  position:absolute; top:-20px; right:-24px; width:130px; height:130px;
  color:var(--a); opacity:.085; pointer-events:none;
}
.cardmark .navicon { width:100%; height:100%; stroke-width:1; }

/* A short value out of a small set. The colour is the pill's own --a, so one
   rule serves the kind, the length, the frequency, the allowance and the
   validity - and the quiet grey is what it falls BACK to rather than a case
   anybody has to remember to write. */
.tagpill {
  --a:var(--muted);
  display:inline-flex; align-items:center; gap:5px; white-space:nowrap;
  padding:3px 9px; border-radius:var(--radius-pill);
  font-size:11.5px; font-weight:600; line-height:1.5; color:var(--a);
  background:color-mix(in srgb, var(--a) 13%, transparent);
  border:1px solid color-mix(in srgb, var(--a) 26%, transparent);
}
.tagpill .navicon { width:13px; height:13px; }
/* The bars say the frequency as well as the words do - one bar a session, so
   a six is visibly more than a two before the label has been read. */
.tagpill .bars { display:inline-flex; align-items:flex-end; gap:1.5px; height:11px; }
.tagpill .bars i { width:2.5px; border-radius:1px; background:currentColor; }

@media (max-width: 760px) {
  .headside { margin-left:0; }
  .cardmark { display:none; }
  .markcard .cardhead { padding-right:0; }
}

/* ------------------------------------------------- Therapies, as a grid ---
   Asked for with a mockup on 9 October 2026 - "make bottom section of
   'Services & pricing' page something like this" - under the condition the
   six redesigns before it carried: "you don't have to change any utility of
   functionality its just UI change". Every class the handlers read - thName,
   thCode, thSort, thOn, data-thsave, data-thdel, #thAdd and its three ids -
   is exactly where it was.

   A FIELD HAS TO LOOK LIKE A FIELD, which is the note .form2 already carries
   and which this table never got: its boxes were bare inputs in table cells,
   so a row of six editable things read as a row of text. */
.thtable input[type="text"], .thtable input[type="number"] {
  border:1px solid var(--line); background:var(--ground); color:var(--ink);
  border-radius:var(--radius-sm); padding:6px 10px; font:inherit;
  font-size:13px; min-height:34px;
}
.thtable input[type="text"]:hover, .thtable input[type="number"]:hover {
  border-color:var(--faint);
}
.thtable input:disabled { opacity:.6; }
/* The name is the wide one and still not the whole column: a box stretched
   across 400px of a table reads as a cell that has lost its edges. */
.thtable td:first-child input { width:100%; max-width:300px; }
.thtable .check { gap:8px; }
.thtable .check input { width:17px; height:17px; }
.thtable td:last-child { white-space:nowrap; }
.thtable td:last-child .btn + .btn { margin-left:7px; }

/* ADDING ONE IS THE LAST THING ON THE CARD, so it is a band across the foot
   of it rather than a two-column form floating inside. The negative margins
   are the panel's own padding, which is what lets the band reach the card's
   edges and take its bottom corners. */
.thadd {
  display:flex; flex-wrap:wrap; align-items:flex-end; gap:11px 16px;
  margin:16px -19px -17px; padding:14px 19px;
  background:var(--sunken); border-top:1px solid var(--line);
  border-radius:0 0 var(--radius) var(--radius);
}
.thadd .thfield { flex:1 1 200px; min-width:0; }
.thadd .thfield.wide { flex:2 1 300px; }
.thadd label {
  display:block; margin-bottom:5px; font-size:12px; font-weight:500;
  color:var(--muted);
}
.thadd input {
  width:100%; border:1px solid var(--line); background:var(--ground);
  color:var(--ink); border-radius:var(--radius-sm); padding:8px 10px;
  font:inherit; font-size:14px; min-height:40px;
}
.thadd input:hover { border-color:var(--faint); }
.thadd .btn { flex:none; min-height:40px; }
.thadd .hint { flex:1 1 100%; margin:2px 0 0; }
.thadd .err { flex:1 1 100%; margin:0; }

/* ALARMS (0291). The list on Your login, and the card that rings.
   The card is its own element on the body and not a modal(): modal() closes
   whatever dialog is open, and an alarm going off must not throw away a
   half-typed booking. Above every dialog (100-150) and below a toast (200),
   so "Snoozed until 9:10 AM" is still seen. */
.alarmform { margin:6px 0 12px; }
.alarmdays { display:flex; flex-wrap:wrap; gap:6px 14px; margin-top:4px; }
.alarmdays .check input { width:auto; }
.alarmlist { list-style:none; margin:4px 0 0; padding:0; }
.alarmlist { display:flex; flex-direction:column; gap:10px; }
/* A CARD PER ALARM (14 October 2026, from the clinic's picture): the time in
   a block of its own colour - each alarm takes the next of a few in turn, set
   on the row as --a - its label and days beside it, then On, Change and a
   red-tinted Delete. Wraps on a narrow panel rather than squeezing the time. */
.alarmrow {
  --a:var(--accent);
  display:flex; flex-wrap:wrap; align-items:center; gap:10px 14px; padding:10px 12px;
  background:var(--raised); border:1px solid var(--line-soft); border-radius:var(--radius);
}
.alarmrow.off .alarmtime, .alarmrow.off .alarmwhat { opacity:.5; }
.alarmtime {
  flex:none; display:inline-flex; align-items:center; gap:9px; min-width:8.2em;
  padding:9px 14px; border-radius:var(--radius-sm);
  font-size:18px; font-weight:700; font-variant-numeric:tabular-nums; color:var(--ink);
  background:color-mix(in srgb, var(--a) 24%, transparent);
  border:1px solid color-mix(in srgb, var(--a) 55%, transparent);
}
.alarmtime .navicon { width:18px; height:18px; color:var(--a); flex:none; }
.alarmwhat { flex:1 1 150px; min-width:0; display:flex; flex-direction:column; gap:3px; }
/* The days read as one sentence after their mark, wrapping under it - not a
   mark alone on a line above words that wrapped away from it. */
.alarmwhat .sub > .navicon {
  width:14px; height:14px; color:var(--faint); vertical-align:-2px; margin-right:5px;
}
/* On, Change and Delete stay together, at the row's end, and drop under the
   time as one group when the panel is too narrow for one line. */
.alarmacts { flex:none; display:flex; align-items:center; gap:8px; margin-left:auto; }
/* A sound box and the button that plays it, side by side (0297). */
.soundpick { display:flex; align-items:center; gap:6px; }
.soundpick select { flex:1; min-width:0; }
/* The administrator's clinic-wide sound, a line of its own under the head. */
.alarmdefault {
  display:flex; flex-wrap:wrap; align-items:center; gap:6px 10px; margin:0 0 12px;
  font-size:12.5px;
}
.alarmdefault label { font-weight:600; }
/* A role alarm's branch, changed in its own sentence on an administrator's
   list - small enough to read as part of the line, still a real select. */
.alarmat {
  width:auto; min-height:0; height:auto; display:inline-block; vertical-align:baseline;
  padding:1px 6px; font-size:12px; border-radius:var(--radius-sm);
  background:var(--ground); color:var(--ink); border:1px solid var(--line);
}
.alarmwhat b { overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
.alarmon { flex:none; display:flex; align-items:center; gap:8px; }
.alarmonword { font-size:13px; font-weight:600; color:var(--faint); min-width:2em; }
.alarmon:has(input:checked) .alarmonword { color:var(--ok); }
/* An alarm for a role (0293) seen by somebody it reaches: theirs to hear,
   the administrator's to change, said where the controls would be. */
.alarmby { flex:none; font-size:12px; white-space:nowrap; }
.alarmon .switch input { width:100%; }
.alarmnone { margin:6px 0 2px; }
.alarmring {
  position:fixed; inset:0; z-index:190; display:flex; align-items:center;
  justify-content:center; padding:16px; background:rgba(12,16,20,.55);
}
.alarmcard {
  width:min(360px, 100%); max-height:calc(100vh - 32px); overflow:auto;
  background:var(--raised); color:var(--ink); border-radius:var(--radius-lg);
  box-shadow:var(--shadow-md); padding:20px; text-align:center;
}
.alarmbell { display:inline-flex; width:56px; height:56px; border-radius:50%;
  align-items:center; justify-content:center; background:var(--wait-bg); color:var(--wait);
  animation:alarmshake 1s ease-in-out infinite; }
.alarmbell svg { width:28px; height:28px; }
@keyframes alarmshake {
  0%, 100% { transform:rotate(0); } 20% { transform:rotate(-14deg); }
  40% { transform:rotate(12deg); } 60% { transform:rotate(-8deg); } 80% { transform:rotate(5deg); }
}
@media (prefers-reduced-motion: reduce) { .alarmbell { animation:none; } }
.alarmone { margin-top:14px; display:flex; flex-direction:column; gap:4px; }
.alarmone + .alarmone { border-top:1px solid var(--line-soft); padding-top:14px; }
.alarmone > b { font-size:24px; font-variant-numeric:tabular-nums; }
.alarmone .actions { display:flex; gap:10px; justify-content:center; margin-top:10px; }
.alarmone .actions .btn { min-height:44px; flex:1 1 0; }
@media (max-width: 520px) {
  .alarmrow { flex-wrap:wrap; }
  .alarmwhat { flex-basis:calc(100% - 6.5em); }
}
/* The Calendar's alarm clock reads as one only a little larger than a
   stroked icon: at 17px its bells and face are a red blob. */
/* Out-ranks .toolbar .btn.icon .navicon, which holds every bar icon to 17px. */
.toolbar .btn.icon.alarmbtn .navicon { width:22px; height:22px; }

/* ---- who changed what (0312): a record's History and the Change log ----
   One drawing for both (changesHtml in changes.js). A line is who, what and
   when on one row, then each field that moved: the old value struck through,
   the new one bold. Automatic and system lines are quieter, because they are
   what followed somebody's change rather than the change. */
.chgblock { margin-top:6px; }
.chgbar { display:flex; align-items:center; gap:10px; flex-wrap:wrap; }
.chgbar .btn .navicon { width:15px; height:15px; }
.chgbody { margin-top:8px; max-height:340px; overflow:auto;
  border:1px solid var(--line); border-radius:var(--radius-sm);
  background:var(--sunken); padding:8px 10px; }
.chgfoot { display:flex; gap:8px; flex-wrap:wrap; margin-top:8px; }
.chglist { list-style:none; margin:0; padding:0; display:grid; gap:8px; }
.chg { background:var(--surface); border:1px solid var(--line-soft);
  border-radius:var(--radius-sm); padding:7px 10px; }
.chg.automatic, .chg.system { opacity:.78; }
.chghead { display:flex; align-items:baseline; gap:6px; flex-wrap:wrap; }
.chghead .grow { flex:1; }
.chghead .sub { color:var(--muted); font-size:12px; }
.chgact { color:var(--ink); }
.chgact a { color:var(--accent); }
.chgfields { list-style:none; margin:5px 0 0; padding:0; display:grid; gap:2px;
  font-size:13px; }
.chgfields li { overflow-wrap:anywhere; }
.chgf { color:var(--muted); margin-right:4px; }
.chgf::after { content:':'; }
.chgfields s { color:var(--muted); }
.chgto { color:var(--faint); }
.chgday { margin:14px 0 6px; font-size:13px; color:var(--muted); font-weight:600; }
#clBody { padding:0 2px 16px; }
/* ADDED GREEN, CHANGED BLUE, REMOVED RED (24 October 2026). A line carries its
   kind as data-act; its edge, its verb and its values take the colour - the
   new value of a change, a record added whole, a record removed whole. The
   old value of a change stays struck and quiet: it is not what was done. */
.chg[data-act="add"] { --chgc:var(--ok); }
.chg[data-act="mod"] { --chgc:var(--info); }
.chg[data-act="del"] { --chgc:var(--bad); }
.chg[data-act] { border-left:3px solid var(--chgc); }
.chg[data-act] .chgverb { color:var(--chgc); font-weight:600; }
.chg[data-act="add"] .chgfields b, .chg[data-act="mod"] .chgfields b { color:var(--chgc); }
.chg[data-act="del"] .chgfields s { color:var(--chgc); }
/* The choice of which, the same on the Change log and in every History: a
   pill each, with its colour as a dot and, pressed, as its tint. */
.chgacts { display:inline-flex; flex-wrap:wrap; gap:4px; vertical-align:middle; }
.chgpick { --chgc:var(--accent); display:inline-flex; align-items:center; gap:6px;
  min-height:28px; padding:0 11px; border:1px solid var(--line); border-radius:999px;
  background:var(--surface); color:var(--ink); font:inherit; font-size:12.5px; cursor:pointer; }
.chgpick.add { --chgc:var(--ok); }
.chgpick.mod { --chgc:var(--info); }
.chgpick.del { --chgc:var(--bad); }
.chgpick.add::before, .chgpick.mod::before, .chgpick.del::before {
  content:""; width:8px; height:8px; border-radius:50%; background:var(--chgc); }
.chgpick[aria-pressed="true"] { border-color:var(--chgc); color:var(--chgc); font-weight:600;
  background:color-mix(in srgb, var(--chgc) 14%, var(--surface)); }
.chgpick:focus-visible { outline:2px solid var(--chgc); outline-offset:1px; }
/* In a History block the choice stays in view while its lines scroll. */
.chgtop { position:sticky; top:-8px; z-index:1; margin:-8px -10px 8px; padding:8px 10px 6px;
  background:var(--sunken); }
.clacts { display:inline-flex; align-items:center; gap:6px; flex-wrap:wrap; }
/* A page's own History (Services, Policies, Expenses, Stock, Templates) sits
   under the page's head, closed; a bill's opens in a row under the bill. */
.chgblock.pagechg { margin:4px 0 12px; }
tr.chgrow > td { background:var(--sunken); padding:8px 10px; }
tr.chgrow .chgbody { margin-top:0; max-height:300px; }

/* ------------------------------------------------------------- Reports --- */
/* The form beside the paper it prints on. The paper is SVG in the PDF's own
   points (reports.js pageSvg), so it is sized by width alone and every line
   lands where the file puts it; the column scrolls on its own and stays in
   view while the form below it is typed into. */
.rpwrap { display:grid; grid-template-columns:minmax(0, 1fr) minmax(320px, 0.95fr); gap:16px; align-items:start; }
.rpleft { min-width:0; }
.rpleft > .panel { margin-top:16px; }
.rpform .hint { max-width:none; }
.rpform .hint { display:block; margin-top:5px; font-size:12px; color:var(--muted); font-weight:400; }
/* THE REPORT'S EDITOR (19 October 2026): a box that is typed into like the
   textarea it replaced, with a bar of the formatting a report can print. The
   box is the paper's colours whatever the theme - white, black words - since
   a colour picked here is the colour it prints, and black on a dark theme's
   ground would be read as "this text has no colour". */
.rpwrite { display:grid; gap:6px; }
.rpwritehead { font-size:12px; font-weight:500; color:var(--muted); }
.rpedit {
  min-height:280px; max-height:70vh; overflow:auto; border:1px solid var(--faint); background:#fff; color:#111;
  border-radius:var(--radius-sm); padding:10px 12px; font-family:Helvetica, Arial, sans-serif; font-size:14px;
  line-height:1.5; overflow-wrap:anywhere; position:relative;
}
.rpedit:focus { outline:2px solid var(--accent); outline-offset:-1px; }
.rpedit[contenteditable="false"] { background:#fafafa; }
.rpedit.blank::before { content:attr(data-placeholder); color:#8a8f98; pointer-events:none; position:absolute; }
.rpedit ul, .rpedit ol { margin:2px 0; padding-left:24px; }
.rpedit table.rttable, .rpedit table { border-collapse:collapse; width:100%; margin:6px 0; table-layout:fixed; }
.rpedit th, .rpedit td { border:1px solid #555; padding:4px 6px; vertical-align:top; font-size:13px; min-width:30px; }
.rpedit th { background:#E9EEF3; font-weight:bold; text-align:left; }
.rptools { display:flex; flex-wrap:wrap; gap:6px; align-items:center; padding:5px; border:1px solid var(--line);
  border-radius:var(--radius-sm); background:var(--sunken); }
.rtgroup { display:inline-flex; gap:2px; align-items:center; }
.rtgroup + .rtgroup { padding-left:6px; border-left:1px solid var(--line); }
.rptools .rtbtn { padding:5px 8px; min-width:32px; min-height:30px; justify-content:center; font-size:14px; line-height:1; }
.rptools .rtbtn svg { width:16px; height:16px; }
.rptools .rtbtn[aria-pressed="true"] { background:var(--accent-bg, rgba(59, 130, 246, .14)); border-color:var(--accent); color:var(--accent); }
.rptools .rtbtn b, .rptools .rtbtn i, .rptools .rtbtn u { font-family:Georgia, serif; font-size:15px; }
.rtletter { font-weight:700; font-family:Georgia, serif; font-size:15px; border-bottom:3px solid #C62828; line-height:1; padding:0 1px; }
.rtword { margin-left:4px; font-size:13px; }
.rtpopwrap { position:relative; display:inline-flex; }
.rtpop { position:absolute; top:calc(100% + 4px); left:0; z-index:20; padding:8px; width:max-content; max-width:calc(100vw - 32px); background:var(--card, var(--ground));
  border:1px solid var(--line); border-radius:var(--radius-sm); box-shadow:0 8px 24px rgba(0, 0, 0, .18); display:grid; gap:8px; }
.rtpop[hidden] { display:none; }
.rtpop.rtright { left:auto; right:0; }
.rtswatches { display:flex; gap:5px; }
.rtswatch { width:26px; height:26px; border-radius:6px; border:1px solid rgba(0, 0, 0, .25); cursor:pointer; padding:0; }
.rtswatch:hover, .rtswatch:focus-visible { outline:2px solid var(--accent); outline-offset:1px; }
.rtnone { font-size:12px; padding:4px 8px; }
.rttablepop { width:max-content; }
.rtgrid { display:grid; grid-template-columns:repeat(8, 18px); gap:3px; }
.rtcell { width:18px; height:18px; border:1px solid var(--faint); border-radius:3px; background:var(--ground); padding:0; cursor:pointer; }
.rtcell.on { background:var(--accent); border-color:var(--accent); }
.rtgridsay { font-size:12px; color:var(--muted); max-width:180px; }
.rtsize { display:flex; flex-wrap:wrap; gap:6px; align-items:end; }
.rtsize label { margin:0; display:grid; gap:2px; font-size:11px; }
.rtsize input { width:62px; border:1px solid var(--faint); background:var(--ground); color:var(--ink);
  border-radius:var(--radius-sm); padding:5px 6px; font:inherit; font-size:13px; }
.rtsize .btn { padding:5px 10px; }
.rptabletools { display:flex; flex-wrap:wrap; gap:6px; align-items:center; padding:5px 8px; border:1px dashed var(--line);
  border-radius:var(--radius-sm); }
.rptabletools[hidden] { display:none; }
.rptabletools .sub { display:inline-flex; align-items:center; gap:5px; font-size:12px; }
.rptabletools .sub svg { width:14px; height:14px; }
.rptabletools .btn { padding:4px 9px; font-size:13px; }
.rptabletools .btn svg { width:14px; height:14px; }
.rtwhy { color:var(--wait); }
.rpextra { margin-top:14px; display:grid; gap:7px; }
.rpextrahead { font-size:13px; }
.rpextra > .hint { margin:-4px 0 2px; font-size:12px; color:var(--muted); }
.rpfield { display:grid; grid-template-columns:minmax(0, 0.8fr) minmax(0, 1.2fr); gap:8px; }
.rpfield input {
  width:100%; border:1px solid var(--faint); background:var(--ground); color:var(--ink);
  border-radius:var(--radius-sm); padding:7px 10px; font:inherit; font-size:14px;
}
.rpfield.hasdel { grid-template-columns:minmax(0, 0.8fr) minmax(0, 1.2fr) auto; }
.rpfielddel { padding:5px 8px; }
.rpfielddel svg { width:15px; height:15px; }
.rpfielddel:hover { border-color:var(--bad); color:var(--bad); }
.rpfieldadd { justify-self:start; padding:5px 12px; font-size:13px; }
.rpbold { display:grid; gap:7px; margin-top:8px; }
.rpbold:empty { display:none; }
.rpboldlist { display:grid; gap:4px; }
.rpboldrow { display:grid; grid-template-columns:minmax(0, 1fr) auto auto; gap:10px; align-items:center;
  padding:5px 8px; border:1px solid var(--line); border-radius:var(--radius-sm); }
.rpboldsay { font-size:13px; overflow-wrap:anywhere; color:var(--ink); }
.rpboldsay .on { font-weight:700; }
.rpboldtick { display:inline-flex; align-items:center; gap:4px; font-size:12px; color:var(--muted); white-space:nowrap; margin:0; }
.rpboldall { display:flex; flex-wrap:wrap; gap:6px; }
.rpboldall .btn { padding:4px 10px; font-size:12px; }
.rpform .actions { margin-top:14px; flex-wrap:wrap; }
/* --rptop is the app bar's height (reports.js watchBar): sticky parts stick
   below it, or they slide under it. */
.rppreview { position:sticky; top:calc(var(--rptop, 0px) + 12px); margin-top:16px; }
.rpprevhead { display:flex; align-items:center; gap:8px; margin:0 2px 8px; }
.rpprevhead b { display:inline-flex; align-items:center; gap:6px; font-size:13px; }
.rpprevhead svg { width:16px; height:16px; }
.rppages {
  max-height:calc(100vh - 110px - var(--rptop, 0px)); overflow:auto; padding:4px 10px 10px;
  background:var(--sunken); border:1px solid var(--line); border-radius:var(--radius);
}
.rppaper { margin:10px 0 4px; }
.rpsvg {
  display:block; width:100%; height:auto; background:#fff;
  box-shadow:0 1px 3px rgba(0,0,0,.18), 0 6px 18px rgba(0,0,0,.08);
}
.rppaper figcaption { text-align:center; font-size:11px; color:var(--muted); margin-top:5px; }
.rpdesign .cardhead .btn { flex:none; }
/* The head's button goes under the words when they would be squeezed into a
   column a word wide - on a phone, "Give Sector 28 its own" beside the title. */
.rpdesign .cardhead, .rplist .cardhead { flex-wrap:wrap; }
.rpdesign .cardhead .cardtitle, .rplist .cardhead .cardtitle { flex:1 1 220px; min-width:0; }
.rpdesigngrid { border:0; padding:0; margin:0; min-width:0; display:block; }
.rpdesigngrid[disabled] { opacity:.55; }
/* A picture's Change is a label round a file box, which a disabled fieldset
   does not grey: the box inside is shut, so the label must look it too. */
.rpdesigngrid[disabled] label.btn { opacity:.5; pointer-events:none; }
/* Two to a row: four in a row squeezed each card to ~140px on the clinic's
   tablet, its buttons stacked under each other. */
.rppics { display:grid; grid-template-columns:repeat(2, minmax(0, 1fr)); gap:10px; }
.rppic { display:flex; flex-direction:column; gap:7px; min-width:0;
  padding:10px; border:1px solid var(--line); border-radius:var(--radius-sm); }
.rppicview {
  height:72px; display:flex; align-items:center; justify-content:center;
  border-radius:6px; overflow:hidden; border:1px dashed var(--line);
  /* a checkerboard, so a picture with a clear background shows that it has one */
  background:repeating-conic-gradient(var(--sunken) 0 25%, var(--surface) 0 50%) 0 0 / 14px 14px;
}
.rppicview img { max-width:100%; max-height:100%; object-fit:contain; }
.rppicview .sub { font-size:11px; }
.rppicwords { display:flex; flex-direction:column; gap:1px; min-width:0; }
.rppicwords .sub { font-size:12px; color:var(--muted); }
.rppicbtns { display:flex; gap:6px; flex-wrap:wrap; }
.rpblend { display:flex; align-items:flex-start; gap:6px; font-size:12px; color:var(--ink); line-height:1.3; }
.rpblend input { margin:1px 0 0; flex:none; }
/* The lock, under the head: amber while locked - it explains why every box
   below is shut - and quiet while not. */
.rplock { display:flex; align-items:center; gap:10px; flex-wrap:wrap; margin:0 0 12px;
  padding:9px 12px; border:1px solid var(--line); border-radius:var(--radius-sm); font-size:13px; }
.rplock > svg { width:18px; height:18px; flex:none; color:var(--muted); }
.rplock > span { flex:1 1 220px; min-width:0; }
.rplock.on { border-color:color-mix(in srgb, var(--wait) 55%, transparent);
  background:color-mix(in srgb, var(--wait) 12%, transparent); }
.rplock.on > svg { color:var(--wait); }
.rppicbtns .btn { padding:5px 9px; }
.rppicbtns .btn svg { width:15px; height:15px; }
/* minmax(0, ...) on every track: a range input and a textarea both carry a
   minimum width of their own, and a plain 1fr lets that push the whole panel
   past its column - which the first build did, readouts and all. */
.rpwmform { display:grid; grid-template-columns:repeat(3, minmax(0, 1fr)); gap:12px 16px;
  align-content:start; margin-top:14px; }
.rpwmform > label { display:block; min-width:0; font-size:12px; font-weight:500; color:var(--muted); }
.rpwmform > .rpwide { grid-column:1 / -1; }
.rpwmform textarea {
  box-sizing:border-box; width:100%; margin-top:4px; border:1px solid var(--faint); background:var(--ground);
  color:var(--ink); border-radius:var(--radius-sm); padding:7px 10px; font:inherit; font-size:14px; resize:vertical;
}
.rpwmform input[type=color] {
  display:block; margin-top:4px; width:56px; height:34px; padding:2px;
  border:1px solid var(--faint); border-radius:var(--radius-sm); background:var(--ground);
}
.rpcolour { display:flex; gap:8px; align-items:center; margin-top:4px; }
.rpcolour input[type=color] { margin-top:0; }
.rpslide { display:flex; gap:8px; align-items:center; margin-top:6px; min-width:0; }
.rpslide input[type=range] { flex:1 1 auto; width:0; min-width:0; accent-color:var(--accent); }
.rpslide output { flex:none; min-width:42px; text-align:right; font-variant-numeric:tabular-nums;
  font-size:12.5px; color:var(--ink); }
.rpwmform > .btn { justify-self:start; align-self:end; }
/* The style picker leads the watermark's controls: which of the four decides
   which of the rest are shown, so it is read first. */
.rpstylerow { display:flex; flex-wrap:wrap; align-items:center; gap:8px 12px; }
.rpstylecap { font-size:12px; font-weight:500; color:var(--muted); }
.rpstyles { flex-wrap:wrap; }
.rpstyles .btn { padding:6px 11px; font-size:13px; }
.rppicnote { margin:0; font-size:12.5px; color:var(--muted); }
.rpwmform > label.rpstagger { display:flex; align-items:center; gap:7px; font-size:13px; color:var(--ink);
  align-self:end; padding-bottom:4px; }
.rpwmform > label.rpstagger input { width:auto; margin:0; }
.rpdesign > .actions, .rpdesign > .err { margin-top:12px; }
/* Links in the PDF: a row each - what it opens, the address, which picture,
   where on it - with the reason it does not work yet, or what it opens, under
   it. On a phone the address takes a line of its own. */
.rplinks { margin-top:16px; display:grid; gap:8px; }
.rplinkshead { display:flex; flex-direction:column; gap:2px; }
.rplinkshead b { display:inline-flex; align-items:center; gap:6px; font-size:13px; }
.rplinkshead b svg { width:16px; height:16px; }
.rplinkshead .sub { font-size:12px; color:var(--muted); }
.rplinkrow { display:grid; grid-template-columns:120px minmax(0, 1fr) 110px auto auto; gap:6px; align-items:center;
  padding:8px; border:1px solid var(--line); border-radius:var(--radius-sm); }
.rplinkrow select, .rplinkrow input {
  width:100%; border:1px solid var(--faint); background:var(--ground); color:var(--ink);
  border-radius:var(--radius-sm); padding:6px 8px; font:inherit; font-size:13.5px; min-width:0;
}
.rplinkrow .btn { padding:5px 9px; white-space:nowrap; }
.rplinkrow .btn svg { width:14px; height:14px; }
.rplinkwhy { grid-column:1 / -1; font-size:12px; color:var(--muted); overflow-wrap:anywhere; }
.rplinkadd { display:flex; flex-wrap:wrap; gap:6px; }
.rplinkadd .btn { padding:5px 10px; font-size:13px; }
/* The spot picker: the picture large, a box dragged over it. touch-action
   none, or a finger's drag is taken for scrolling the dialog. */
.rpspot { position:relative; touch-action:none; user-select:none; -webkit-user-select:none; cursor:crosshair;
  border:1px solid var(--line); border-radius:6px; overflow:hidden; background:#fff;
  /* AS BIG AS THE PICTURE, NO BIGGER, so a box drawn in it is a box on the
     picture; and the picture short enough that Cancel and Use this spot stay
     on screen. Found on the clinic's tablet held sideways: the signature at
     full width ran past the foot of the screen, and a finger on the picture
     draws rather than scrolls, so the buttons could not be reached. */
  width:fit-content; max-width:100%; margin:6px auto 12px; }
.rpspot img { display:block; max-width:100%; max-height:calc(100vh - 230px); width:auto; height:auto; pointer-events:none; }
.rpspotbox { position:absolute; border:2px solid var(--accent); background:color-mix(in srgb, var(--accent) 18%, transparent);
  border-radius:3px; pointer-events:none; }
/* A link's box on the paper: nothing at all, unless the letterhead is open. */
.rppages .rplink { fill:transparent; stroke:none; }
.rppages.showlinks .rplink { fill:rgba(59, 130, 246, .14); stroke:#3b82f6; stroke-width:1; stroke-dasharray:3 2; }
.rplist .cardhead .searchbox { flex:0 1 280px; }
.rplist .cardhead .searchbox .search { width:100%; }
.rptable td.rpacts { white-space:nowrap; }
.rptable td.rpacts .btn { padding:4px 9px; }
.rptable td.rpacts .btn svg { width:14px; height:14px; }
.rptable tr.rpcurrent > td { background:color-mix(in srgb, var(--accent) 9%, transparent); }
.rpempty { margin:4px 2px; }

/* The Write / Preview switch: only where the columns have stacked, and then
   it keeps itself in reach at the top while the form scrolls under it. */
.rpmobtabs { display:none; }
@media (max-width: 980px) {
  .rpwrap { grid-template-columns:minmax(0, 1fr); }
  .rppreview { position:static; }
  .rppages { max-height:none; }
  .rpmobtabs { display:flex; position:sticky; top:calc(var(--rptop, 0px) + 6px); z-index:5; margin:12px 0 0; padding:6px;
    background:var(--ground); border:1px solid var(--line); border-radius:var(--radius); width:max-content; max-width:100%; }
  .rpmobtabs .btn { padding:6px 14px; }
  .rpmobtabs .btn svg { width:15px; height:15px; }
  .rpwrap:not(.paper) > .rppreview { display:none; }
  .rpwrap.paper > .rpleft { display:none; }
}
@media (max-width: 560px) {
  .rplinkrow { grid-template-columns:minmax(0, 1fr) minmax(0, 1fr); }
  .rplinkrow [data-lu] { grid-column:1 / -1; }
  .rppics { grid-template-columns:minmax(0, 1fr); }
  .rpwmform { grid-template-columns:minmax(0, 1fr); }
  .rpfield { grid-template-columns:minmax(0, 1fr); }
  /* The name and its Remove on one line, the value under them. */
  .rpfield.hasdel { grid-template-columns:minmax(0, 1fr) auto; }
  .rpfield.hasdel [data-fv] { grid-column:1 / -1; order:2; }
  .rpfield.hasdel .rpfielddel { order:1; }
  .rpboldrow { grid-template-columns:minmax(0, 1fr) auto; }
  .rpboldsay { grid-column:1 / -1; }
  .rtswatches { flex-wrap:wrap; max-width:200px; }
}

/* --------------------------------------------------------- lavender --- */

/* LAVENDER - a soft purple tint over the whole app, gentle and quiet.
   A third theme beside Light and Dark, chosen on the theme button, and a
   LIGHT one whatever the device is set to: it is excluded from the dark
   blocks in app.css and above, the way Light is, so a phone set to dark
   cannot paint over it. Nothing here applies under any other theme.

   THE CHIPS ARE NOT TOUCHED. The page's own colours (--surface, --sunken,
   --line ...) are tinted, and a chip reads some of them - so every chip and
   tag sets the plain theme's values back at its own edge (the reset just
   below), and everything inside a chip is drawn exactly as the plain theme
   draws it. The colours that carry a meaning - --ok, --bad, --wait, --tent,
   --canc, --imm, --miss, the therapy colours, RS, the therapist fills - are
   never redefined here.

   THE ACCENT IS PURPLE AND THE CODES ARE NOT. Where --accent carries a
   meaning rather than chrome - a child still owed a session, a booked
   outcome, an owed decision, "owed now" in the reschedule queue
   (.srcpill), a package progress bar on track - it stays the app's
   green (--mean), so a code never changes colour with a theme.

   EVERY SELECTOR STARTS html:where([data-theme="lavender"]): scoped to the
   theme, outweighing the rule it replaces by one element and no more. */
:root[data-theme="lavender"] {
  color-scheme:light;
  --ground:#F6F3FC; --surface:#FFFFFF; --raised:#FBFAFE; --sunken:#F1ECFB;
  --ink:#1E1733; --muted:#5B5375; --faint:#8E86A8;
  --line:#E6DFF5; --line-soft:#F0EBFA;
  --accent:#6D4FC2; --accent-ink:#FFFFFF; --accent-bg:#F1ECFB; --accent-hover:#5A3EAD;
  --mean:#1F6F5C; --mean-bg:#E8F3EF;
  --stripe:#FBF9FE;
  --bar:#EDE7F9; --lav-shadow:0 2px 10px -4px rgba(91,63,175,.22);
  --shadow-xs:0 1px 2px rgba(59,42,107,.06);
  --shadow-sm:0 1px 2px rgba(59,42,107,.07), 0 2px 6px rgba(59,42,107,.05);
  --shadow-md:0 2px 4px rgba(59,42,107,.06), 0 10px 24px rgba(59,42,107,.10);
  --shadow:0 12px 36px rgba(59,42,107,.20);
  --ring:0 0 0 3px rgba(109,79,194,.24);
}

/* EVERY CHIP AND TAG IS THE PLAIN THEME INSIDE - every badge (the plain
   one, NO PACKAGE among them, and every decision pill), the Kids history
   mini-calendar's cells (.hd) and the slot panel's owed pills (.owed).
   A neutral state in a coded set - Booked, Pending, No package - keeps
   the plain theme's grey, or it would read as a different state in each
   theme. Custom properties inherit, so setting the plain theme's values
   back at a chip's own edge gives every part inside it - a white chip's
   fill, its border, its number plate, its shadow - the value the plain
   theme gives it. The coloured variants read --ok, --wait, --bad, --rs,
   --ind and --pkg, which no theme redefines, so they are untouched. */
html:where([data-theme="lavender"]) .sess,
html:where([data-theme="lavender"]) .openpill,
html:where([data-theme="lavender"]) .tchip,
html:where([data-theme="lavender"]) .stat,
html:where([data-theme="lavender"]) .misskid,
html:where([data-theme="lavender"]) .offpill,
html:where([data-theme="lavender"]) .occchip,
html:where([data-theme="lavender"]) .badge,
html:where([data-theme="lavender"]) .badge.dec,
html:where([data-theme="lavender"]) .hd,
html:where([data-theme="lavender"]) .owed {
  --surface:#FFFFFF; --raised:#FAFBFC; --sunken:#F0F2F5; --ground:#F6F7F9;
  --line:#E2E6EB; --line-soft:#EEF1F4; --ink:#141A21; --muted:#5A6672; --faint:#8C98A4;
  --accent:#1F6F5C; --accent-bg:#E8F3EF;
  --shadow-xs:0 1px 2px rgba(16,24,32,.05);
  --shadow-sm:0 1px 2px rgba(16,24,32,.06), 0 1px 3px rgba(16,24,32,.04);
  --shadow-md:0 2px 4px rgba(16,24,32,.05), 0 8px 20px rgba(16,24,32,.07);
  --ring:0 0 0 3px rgba(31,111,92,.25);
}
/* .sess sets no colour of its own and inherits the body's theme ink, so
   the colour is set back here - on the chip, a history cell and an owed
   pill ONLY: .misskid, .openpill and the rest set theirs from --kidink at
   one class, and .badge from --muted, which a colour on the list above
   would beat. The full-colour chip's --kidink still wins, and every
   coloured .owed is one class heavier than this. */
html:where([data-theme="lavender"]) .sess,
html:where([data-theme="lavender"]) .hd,
html:where([data-theme="lavender"]) .owed { color:var(--ink); }

/* Where the accent is a code, not chrome: the app's green, always. */
html:where([data-theme="lavender"]) .badge.owes,
html:where([data-theme="lavender"]) .outcome.scheduled,
html:where([data-theme="lavender"]) .badge.dec.owed,
html:where([data-theme="lavender"]) .srcpill { color:var(--mean); background:var(--mean-bg); }
/* The progress bar's on-track fill is the green of a three-state code;
   .near (amber) and .over (red) are one class heavier and still win. */
html:where([data-theme="lavender"]) .progress .bar i { background:var(--mean); }

/* The bar across the top, tinted. --bar is also what the status bar reads
   (index.html, paintThemeColour), so the two stay one colour. */
html:where([data-theme="lavender"]) .appnav { background:var(--bar); border-bottom-color:var(--line); }

/* Rounder panes with a soft violet shadow instead of a drawn line. */
html:where([data-theme="lavender"]) .toolbar,
html:where([data-theme="lavender"]) .filterbar,
html:where([data-theme="lavender"]) .panel,
html:where([data-theme="lavender"]) .gridwrap {
  border-radius:var(--radius-lg); box-shadow:var(--lav-shadow); border-color:var(--line-soft);
}
html:where([data-theme="lavender"]) .headside.toolbar { box-shadow:none; border:0; }
html:where([data-theme="lavender"]) .modal-box { border-radius:18px; }
html:where([data-theme="lavender"]) .modal { background:rgba(30,20,60,.42); }

/* Tabs: the chosen one filled in the accent, the rest in a lavender wash. */
html:where([data-theme="lavender"]) .tabrow .btn { background:var(--sunken); border-color:transparent; }
html:where([data-theme="lavender"]) .navlink.current { background:var(--accent-bg); border-radius:var(--radius-sm) var(--radius-sm) 0 0; }

/* ------------------------------------------------------------ blush --- */

/* BLUSH - a light pink tint over the whole app, soft and warm.
   A theme beside Light and Dark, chosen on the theme button, and a
   LIGHT one whatever the device is set to: it is excluded from the dark
   blocks in app.css and above, the way Light is, so a phone set to dark
   cannot paint over it. Nothing here applies under any other theme.

   THE CHIPS ARE NOT TOUCHED. The page's own colours (--surface, --sunken,
   --line ...) are tinted, and a chip reads some of them - so every chip and
   tag sets the plain theme's values back at its own edge (the reset just
   below), and everything inside a chip is drawn exactly as the plain theme
   draws it. The colours that carry a meaning - --ok, --bad, --wait, --tent,
   --canc, --imm, --miss, the therapy colours, RS, the therapist fills - are
   never redefined here.

   THE ACCENT IS ROSE AND THE CODES ARE NOT. Where --accent carries a
   meaning rather than chrome - a child still owed a session, a booked
   outcome, an owed decision, "owed now" in the reschedule queue
   (.srcpill), a package progress bar on track - it stays the app's
   green (--mean), so a code never changes colour with a theme. The rose is
   a deep berry rather than a bright pink on purpose: it lives on buttons,
   tabs and today's heading, never on a chip, and a bright magenta there
   would read as one of the therapists' own pinks.

   EVERY SELECTOR STARTS html:where([data-theme="blush"]): scoped to the
   theme, outweighing the rule it replaces by one element and no more. */
:root[data-theme="blush"] {
  color-scheme:light;
  --ground:#FDF4F7; --surface:#FFFFFF; --raised:#FFFAFC; --sunken:#FBEAF0;
  --ink:#2A1720; --muted:#6E5260; --faint:#A08592;
  --line:#F3DCE5; --line-soft:#F8E9EF;
  --accent:#B4386E; --accent-ink:#FFFFFF; --accent-bg:#FBEAF0; --accent-hover:#992C5C;
  --mean:#1F6F5C; --mean-bg:#E8F3EF;
  --stripe:#FFF9FB;
  --bar:#FCE7EF; --blush-shadow:0 2px 10px -4px rgba(180,56,110,.22);
  --shadow-xs:0 1px 2px rgba(110,40,72,.06);
  --shadow-sm:0 1px 2px rgba(110,40,72,.07), 0 2px 6px rgba(110,40,72,.05);
  --shadow-md:0 2px 4px rgba(110,40,72,.06), 0 10px 24px rgba(110,40,72,.10);
  --shadow:0 12px 36px rgba(110,40,72,.20);
  --ring:0 0 0 3px rgba(180,56,110,.24);
}

/* EVERY CHIP AND TAG IS THE PLAIN THEME INSIDE - every badge (the plain
   one, NO PACKAGE among them, and every decision pill), the Kids history
   mini-calendar's cells (.hd) and the slot panel's owed pills (.owed).
   A neutral state in a coded set - Booked, Pending, No package - keeps
   the plain theme's grey, or it would read as a different state in each
   theme. Custom properties inherit, so setting the plain theme's values
   back at a chip's own edge gives every part inside it - a white chip's
   fill, its border, its number plate, its shadow - the value the plain
   theme gives it. The coloured variants read --ok, --wait, --bad, --rs,
   --ind and --pkg, which no theme redefines, so they are untouched. */
html:where([data-theme="blush"]) .sess,
html:where([data-theme="blush"]) .openpill,
html:where([data-theme="blush"]) .tchip,
html:where([data-theme="blush"]) .stat,
html:where([data-theme="blush"]) .misskid,
html:where([data-theme="blush"]) .offpill,
html:where([data-theme="blush"]) .occchip,
html:where([data-theme="blush"]) .badge,
html:where([data-theme="blush"]) .badge.dec,
html:where([data-theme="blush"]) .hd,
html:where([data-theme="blush"]) .owed {
  --surface:#FFFFFF; --raised:#FAFBFC; --sunken:#F0F2F5; --ground:#F6F7F9;
  --line:#E2E6EB; --line-soft:#EEF1F4; --ink:#141A21; --muted:#5A6672; --faint:#8C98A4;
  --accent:#1F6F5C; --accent-bg:#E8F3EF;
  --shadow-xs:0 1px 2px rgba(16,24,32,.05);
  --shadow-sm:0 1px 2px rgba(16,24,32,.06), 0 1px 3px rgba(16,24,32,.04);
  --shadow-md:0 2px 4px rgba(16,24,32,.05), 0 8px 20px rgba(16,24,32,.07);
  --ring:0 0 0 3px rgba(31,111,92,.25);
}
/* .sess sets no colour of its own and inherits the body's theme ink, so
   the colour is set back here - on the chip, a history cell and an owed
   pill ONLY: .misskid, .openpill and the rest set theirs from --kidink at
   one class, and .badge from --muted, which a colour on the list above
   would beat. The full-colour chip's --kidink still wins, and every
   coloured .owed is one class heavier than this. */
html:where([data-theme="blush"]) .sess,
html:where([data-theme="blush"]) .hd,
html:where([data-theme="blush"]) .owed { color:var(--ink); }

/* Where the accent is a code, not chrome: the app's green, always. */
html:where([data-theme="blush"]) .badge.owes,
html:where([data-theme="blush"]) .outcome.scheduled,
html:where([data-theme="blush"]) .badge.dec.owed,
html:where([data-theme="blush"]) .srcpill { color:var(--mean); background:var(--mean-bg); }
/* The progress bar's on-track fill is the green of a three-state code;
   .near (amber) and .over (red) are one class heavier and still win. */
html:where([data-theme="blush"]) .progress .bar i { background:var(--mean); }

/* The bar across the top, tinted. --bar is also what the status bar reads
   (index.html, paintThemeColour), so the two stay one colour. */
html:where([data-theme="blush"]) .appnav { background:var(--bar); border-bottom-color:var(--line); }

/* Rounder panes with a soft rose shadow instead of a drawn line. */
html:where([data-theme="blush"]) .toolbar,
html:where([data-theme="blush"]) .filterbar,
html:where([data-theme="blush"]) .panel,
html:where([data-theme="blush"]) .gridwrap {
  border-radius:var(--radius-lg); box-shadow:var(--blush-shadow); border-color:var(--line-soft);
}
html:where([data-theme="blush"]) .headside.toolbar { box-shadow:none; border:0; }
html:where([data-theme="blush"]) .modal-box { border-radius:18px; }
html:where([data-theme="blush"]) .modal { background:rgba(60,20,38,.42); }

/* Tabs: the chosen one filled in the accent, the rest in a pink wash. */
html:where([data-theme="blush"]) .tabrow .btn { background:var(--sunken); border-color:transparent; }
html:where([data-theme="blush"]) .navlink.current { background:var(--accent-bg); border-radius:var(--radius-sm) var(--radius-sm) 0 0; }


/* ------------------------------------------------ the other pages on a phone
   20 October 2026, asked straight after Calendar and Planning: "check all
   other pages, fix them". Every page was opened on the clinic's handset
   (406px) and photographed, and tools/mobilecheck.py was run over it: nothing
   scrolled sideways and nothing sat off the edge, so what is fixed here is
   the screen being SPENT on furniture, text broken where it should not be,
   and two targets too small for a thumb. Phone only; a desktop is untouched. */
@media (max-width: 760px) {
  /* THE PAGE BAND (Reports, Billing, Messages, Reschedules, Services,
     Policies): a 24px title and its sentence took 150px before the first
     control. The title stays, smaller; the sentence goes - the page's own
     controls say what it is for, and the Calendar and Planning dropped theirs
     the same way. */
  .pagetop { gap:8px; margin-bottom:10px; }
  .pagetop .pagehead h2 { font-size:19px; }
  .pagetop .pagehead .sub { display:none; }
  /* SETTINGS' OWN HEAD, the same furniture under another name (25 October
     2026): under the bar, the thirteen tabs and Change history, each tab
     opened on "SETTINGS", its name again and a sentence - about 40% of the
     phone before the first setting. The eyebrow says what the bar above
     already says; the name stays, smaller; the sentence goes, unless it
     holds an i, whose explanation would go with it. */
  #setBody .pagehead { margin-bottom:8px; }
  #setBody .pagehead .eyebrow { display:none; }
  #setBody .pagehead h2 { font-size:19px; }
  #setBody .pagehead .sub:not(:has(.hintbtn)) { display:none; }

  /* ONE ROW OF TABS, swiped sideways. Messages' nine tabs wrapped onto four
     rows and Settings' thirteen onto four - about 200px each, a quarter of
     the phone, before the tab's own page began. Neither bar is redrawn when
     a tab is pressed (only aria-pressed moves), so where it was swiped to
     stays put. The Approvals page's three sections (26 October 2026) too:
     two rows of tabs at 375px. */
  #msgTabs, #setTabs, #apSections {
    flex-wrap:nowrap; overflow-x:auto; max-width:100%; min-width:0;
    overscroll-behavior-x:contain; scrollbar-width:none;
  }
  #msgTabs::-webkit-scrollbar, #setTabs::-webkit-scrollbar,
  #apSections::-webkit-scrollbar { display:none; }
  #msgTabs > *, #setTabs > *, #apSections > * { flex:none; }

  /* RESCHEDULES' FIVE FIGURES were three rows of two, 330px, before the list
     they count. Three to a row, the mark dropped - the colour still tells
     them apart. */
  .rstiles { display:grid; grid-template-columns:repeat(3, minmax(0, 1fr)); gap:6px; }
  .rstile { padding:7px 8px; gap:6px; min-width:0; }
  .rstile .cardicon { display:none; }
  .rsfig b { font-size:17px; }
  .rsfig span { font-size:9.5px; letter-spacing:.04em; }

  /* DAY MESSAGES' COUNTS were one to a row - .cards.wide asks 196px a tile so
     five fit on a laptop, and a phone has room for one - so five figures took
     340px under the Send button, the list a screen and a half down. Two to a
     row, the mark a little smaller; .cards.wide is drawn by that page alone. */
  .cards.wide { grid-template-columns:repeat(2, minmax(0, 1fr)); gap:8px; }
  .cards.wide .tile { padding:10px 12px; gap:9px; min-width:0; }
  .cards.wide .tile .cardicon { width:28px; height:28px; }
  .cards.wide .tile .n { font-size:22px; }

  /* STAFF MESSAGES: the message column was squeezed to 100px, one or two
     words a line, so every row was a column of words. It keeps a width that
     reads; the table scrolls sideways inside its own box, as tables here do. */
  .rows.smsg td.wrap { min-width:14rem; }

  /* A FIGURE IS NOT BROKEN ACROSS LINES. Attendance's "113 done · 22 C" sat
     on three lines in a column too narrow for it; every numeric cell keeps
     together and the table scrolls instead. */
  table.rows td.num { white-space:nowrap; }

  /* HELP: each card's open arrow wrapped onto a line of its own under the
     title and its sentence, an empty row in every card. The arrow sits
     beside them now. */
  .wf > summary {
    display:grid; grid-template-columns:minmax(0, 1fr) auto;
    column-gap:12px; row-gap:4px; padding:12px 14px;
  }
  .wf > summary > * { grid-column:1; }
  .wf > summary::after { grid-column:2; grid-row:1 / span 2; align-self:center; }

  /* HELP'S SHORT FORMS were 437px in a 343px box, swiped sideways for the
     last column, because "Where" never wraps and the first column is held at
     120px for a two-letter badge. Here "Where" wraps and the first column is
     its badge's width, so the table fits the screen and nothing is pinned
     because nothing has to be. (The Dashboard's "unpinned table" in
     mobilecheck was this one: the Dashboard waits behind its PIN screen and
     the page under it was still Help.) Two marks are long - "OT / SE / SP /
     BT" and "- not set -" - and held the badge column at 139px for marks
     two letters wide, so those wrap inside their own pill rather than
     taking the explanation's room. */
  .rows.legend td:first-child { width:auto; white-space:normal; }
  .rows.legend td:first-child .badge { display:inline-block; max-width:5.5em; }
  .rows.legend td:last-child { white-space:normal; }
}

/* TWO TARGETS TOO SMALL FOR A THUMB (tools/mobilecheck.py on the handset):
   the Change log's links to a session or a package, 16px tall, and the "x"
   that takes a time out of a merged row on Settings, 18x15. A transparent
   pad takes the tap and nothing moves - the technique .linkbtn::after uses -
   and only on a touch screen, where a pad cannot steal a mouse's click. */
@media (pointer: coarse) {
  .chgact a { position:relative; }
  .chgact a::after { content:""; position:absolute; inset:-8px -2px; }
  .mergex { position:relative; }
  .mergex::after { content:""; position:absolute; inset:-8px -7px; }
  /* AND A HELD REMINDER'S NAME on Day messages (21 October 2026), the link
     to that session: 15 to 21px tall, found the first morning the list had
     anything held - the day before it was empty, so mobilecheck had nothing
     to measure. */
  .rcname { position:relative; }
  .rcname::after { content:""; position:absolute; inset:-8px -4px; }
}

/* THE PACKAGE WINDOW OVER ANOTHER PAGE (openPackagePopup in packages.js): the
   grid it is a remote control for, drawn off the screen, unseen and out of
   reach. Wide enough that the row lays out as it would on its own page. */
.pkgpophost { position:fixed; left:-20000px; top:0; width:1600px; visibility:hidden;
  pointer-events:none; }

/* ------------------------------------------------------------- motion ---
   ASKED FOR AS THREE THINGS (local trial, 24 October 2026): a chip another
   desk has just changed says so; a save says it worked; a popup or a side
   panel arrives rather than appearing. Every one of them is short - a window
   in under a fifth of a second, a glow in two - because this is a tool used
   all day and motion that makes somebody wait is motion they come to hate.
   Every one stops under prefers-reduced-motion (the block at the end). */

/* A WINDOW ARRIVES - AND IS THERE AT ONCE. The first version faded the
   backdrop and the box in from nothing over a fifth of a second, and the night
   it went live the clinic said the app seemed slow: a window that starts
   invisible is a window being waited for. So nothing starts invisible. The box
   is drawn at once, already mostly there, and settles its last few pixels in
   a tenth of a second; the backdrop is not animated at all (its own rule says
   why). `translate`, not `transform`: a dialog somebody has dragged keeps its
   place in its own transform, and the arrival must not fight it. */
.modal > .modal-box { animation:tgrise .11s cubic-bezier(.2, .8, .2, 1) both; }
@keyframes tgrise { from { opacity:.6; translate:0 5px; } }

/* A PANEL SETTLES IN FROM ITS OWN SIDE, a little - enough to say where it
   came from, drawn at once and mostly there, like a window. A minimized one
   that is brought back arrives the same way, since its display comes back and
   the animation with it. */
@keyframes tgslide-r { from { opacity:.6; translate:10px 0; } }
@keyframes tgslide-l { from { opacity:.6; translate:-10px 0; } }

/* The chip's menu, its status list and the date picker settle in quicker
   still, and from half there. Opacity and nothing else: each measures itself
   the moment it is put on the page to decide where it sits, and a box that
   starts smaller or lower than it ends would be placed by the wrong size. */
@keyframes tgpop { from { opacity:.5; } }

/* ANOTHER DESK CHANGED THIS ONE. A ring in the accent that breathes out over
   two seconds and is gone - the live redraw would wipe anything that stayed. */
.sess.livechanged { animation:tglive 2.2s ease-out 1; }
@keyframes tglive {
  0%   { box-shadow:inset 0 0 0 2px var(--accent), 0 0 0 0 color-mix(in srgb, var(--accent) 45%, transparent); }
  25%  { box-shadow:inset 0 0 0 2px var(--accent), 0 0 0 7px color-mix(in srgb, var(--accent) 0%, transparent); }
  60%  { box-shadow:inset 0 0 0 2px var(--accent), 0 0 0 0 transparent; }
  100% { box-shadow:inset 0 0 0 0 transparent; }
}

/* THIS BROWSER JUST SAVED IT. The same ring in the app's "done" green, with a
   tick in the corner that pops and fades - the chip that changed is where the
   eye goes when the dialog shuts. */
.sess.justsaved { animation:tgsaved 1.8s ease-out 1; position:relative; }
.sess.justsaved::after {
  /* In the middle of the chip: a chip clips anything past its own edge, and a
     corner holds the menu's dots on one side and the number on the other. */
  content:""; position:absolute; inset:0; margin:auto; width:28px; height:28px;
  border-radius:50%; background:var(--ok) center/17px no-repeat
    url("data:image/svg+xml;utf8,%3Csvg%20xmlns='http://www.w3.org/2000/svg'%20viewBox='0%200%2024%2024'%20fill='none'%20stroke='white'%20stroke-width='3.2'%20stroke-linecap='round'%20stroke-linejoin='round'%3E%3Cpath%20d='m5%2012.5%204.8%204.8L19%207'/%3E%3C/svg%3E");
  box-shadow:0 0 0 3px var(--surface), 0 4px 12px rgba(0,0,0,.25);
  animation:tgtickpop 1.8s ease-out 1 both; pointer-events:none; z-index:2;
}
@keyframes tgsaved {
  0%, 35% { box-shadow:inset 0 0 0 2px var(--ok), 0 0 0 4px var(--ok-bg); }
  100%    { box-shadow:inset 0 0 0 0 transparent; }
}
@keyframes tgtickpop {
  0%   { opacity:0; scale:.4; }
  14%  { opacity:1; scale:1.15; }
  24%  { scale:1; }
  75%  { opacity:1; }
  100% { opacity:0; }
}

/* "IT WORKED", IN THE MESSAGE: a tick in a green disc that draws itself in
   front of the words (toastDone in ui.js decides which messages get one). */
.toast.done { display:flex; align-items:center; gap:9px; }
.toasttick {
  flex:none; width:20px; height:20px; border-radius:50%; background:var(--ok);
  display:inline-flex; align-items:center; justify-content:center;
  animation:tgtickin .22s var(--ease) both;
}
.toasttick svg {
  width:13px; height:13px; fill:none; stroke:#fff; stroke-width:3.2;
  stroke-linecap:round; stroke-linejoin:round;
  stroke-dasharray:24; stroke-dashoffset:24;
  animation:tgdraw .32s .12s var(--ease) forwards;
}
@keyframes tgtickin { from { scale:.4; opacity:0; } }
@keyframes tgdraw { to { stroke-dashoffset:0; } }

/* ------------------------------------------- the status list beside Edit --- */
.chipmenu-pop .mrow { display:flex; align-items:stretch; gap:2px; }
.chipmenu-pop .mrow > [data-pick] { flex:1 1 auto; width:auto; }
/* The chevron is a button of its own, for a finger: 40px wide, the menu
   row's full height. It turns to point at the list it opens. */
.chipmenu-pop .msubopen {
  flex:none; width:40px; align-items:center; justify-content:center; padding:0;
  color:var(--muted);
}
.chipmenu-pop .msubopen svg { width:16px; height:16px; rotate:-90deg; }
.chipmenu-pop [data-pick="edit"][aria-expanded="true"] { background:var(--accent-bg); color:var(--accent); }
.chipmenu-pop [data-pick="edit"][aria-expanded="true"] .mi { color:inherit; }
.chipmenu-pop.sub { min-width:236px; }
.chipmenu-pop.sub button { align-items:center; }
.chipmenu-pop.sub button[aria-disabled="true"] { opacity:.5; cursor:not-allowed; }
.chipmenu-pop.sub button[aria-disabled="true"]:hover { background:none; color:inherit; }
.chipmenu-pop.sub button[aria-checked="true"] { font-weight:600; }
.chipmenu-pop .mtick { margin-left:auto; display:flex; color:var(--accent); }
.chipmenu-pop .mtick svg { width:16px; height:16px; }
/* Each status wears the colour it wears on a chip, so the list reads as the
   chip's own words. */
.stdot {
  flex:none; width:11px; height:11px; border-radius:50%;
  background:var(--line); box-shadow:inset 0 0 0 1px rgba(0,0,0,.12);
}
.stdot.st-scheduled { background:transparent; box-shadow:inset 0 0 0 2.5px var(--accent); }
.stdot.st-attended { background:var(--ok); }
.stdot.st-cancelled { background:var(--canc); }
.stdot.st-immediate { background:var(--imm); }
.stdot.st-unmarked { background:var(--faint); }
.stdot.st-already_given { background:var(--muted); }
.stdot.st-tentative { background:transparent; box-shadow:inset 0 0 0 2px var(--tent);
  outline:1px dashed var(--muted); outline-offset:1px; }

/* -------------------------------------------------------- the date picker ---
   datepick.js. A month in tiles, in the app's own colours - the accent fills
   the chosen day where the picture had blue, as every redesign here has
   worn the app's theme rather than the mockup's. Above dialogs (100) and
   panels (90), since it opens from a box inside either. */
.tgdate {
  position:fixed; z-index:130; width:304px; max-width:calc(100vw - 16px);
  padding:14px 14px 12px; border-radius:var(--radius-lg);
  background:var(--surface); color:var(--ink);
  border:1px solid var(--line); box-shadow:var(--shadow-md), 0 18px 40px rgba(0,0,0,.18);
  animation:tgpop .08s ease-out both;
  font-size:13px;
}
.tgd-head { display:flex; align-items:center; gap:6px; margin-bottom:10px; }
.tgd-title {
  flex:1; appearance:none; border:0; background:none; font:inherit;
  font-weight:650; font-size:14.5px; color:var(--ink); cursor:pointer;
  padding:6px 8px; border-radius:var(--radius-sm); letter-spacing:-.01em;
}
.tgd-title:hover, .tgd-title:focus-visible { background:var(--sunken); outline:none; }
.tgd-nav {
  appearance:none; flex:none; width:34px; height:34px; display:inline-flex;
  align-items:center; justify-content:center; padding:0; cursor:pointer;
  border:1px solid var(--line); border-radius:50%; background:var(--raised); color:var(--ink);
}
.tgd-nav:hover, .tgd-nav:focus-visible { border-color:var(--accent); color:var(--accent); outline:none; }
.tgd-nav svg { width:16px; height:16px; fill:none; stroke:currentColor; stroke-width:2.2;
  stroke-linecap:round; stroke-linejoin:round; }
.tgd-week, .tgd-days { display:grid; grid-template-columns:repeat(7, 1fr); gap:4px; }
.tgd-week span { text-align:center; font-size:11.5px; font-weight:600; color:var(--muted);
  padding:2px 0 6px; }
.tgd-day {
  appearance:none; position:relative; aspect-ratio:1; min-height:34px; padding:0;
  display:inline-flex; align-items:center; justify-content:center;
  font:inherit; font-variant-numeric:tabular-nums; font-weight:550; color:var(--ink);
  background:var(--sunken); border:1.5px solid transparent; border-radius:10px; cursor:pointer;
  transition:background-color .12s, border-color .12s, color .12s;
}
.tgd-day:hover { border-color:var(--accent); }
.tgd-day:focus-visible { outline:none; border-color:var(--accent); box-shadow:var(--ring); }
.tgd-day.out { background:transparent; color:var(--faint); }
.tgd-day.today { border-color:var(--line); color:var(--accent); font-weight:700; }
.tgd-day.picked { background:var(--accent); border-color:var(--accent); color:var(--accent-ink); font-weight:700; }
.tgd-day:disabled { opacity:.35; cursor:not-allowed; border-color:transparent; }
.tgd-dot {
  position:absolute; left:50%; bottom:4px; width:5px; height:5px; margin-left:-2.5px;
  border-radius:50%; background:var(--canc);
}
.tgd-day.picked .tgd-dot { background:var(--accent-ink); }
.tgd-months { display:grid; grid-template-columns:repeat(3, 1fr); gap:6px; padding:4px 0 2px; }
.tgd-month {
  appearance:none; font:inherit; font-weight:550; padding:12px 0; cursor:pointer;
  background:var(--sunken); color:var(--ink); border:1.5px solid transparent; border-radius:10px;
}
.tgd-month:hover, .tgd-month:focus-visible { border-color:var(--accent); outline:none; }
.tgd-month.picked { background:var(--accent); color:var(--accent-ink); }
.tgd-foot { display:flex; align-items:center; gap:8px; margin-top:12px; padding-top:10px;
  border-top:1px solid var(--line-soft); }
.tgd-foot .grow { flex:1; }
.tgd-link {
  appearance:none; border:0; background:none; font:inherit; font-weight:600;
  color:var(--accent); cursor:pointer; padding:6px 4px; border-radius:var(--radius-xs);
}
.tgd-link:hover, .tgd-link:focus-visible { text-decoration:underline; outline:none; }
.tgd-link:disabled { color:var(--faint); cursor:not-allowed; text-decoration:none; }
.tgd-foot .btn { padding:7px 12px; }
@media (pointer: coarse) {
  .tgdate { width:min(340px, calc(100vw - 16px)); }
  .tgd-day { min-height:40px; }
  .tgd-link { padding:10px 6px; }
}

/* STILL, FOR WHOEVER ASKED FOR STILL. The arrivals simply happen; the two
   glows become a plain ring that the same timer takes away; the tick is
   drawn whole. */
@media (prefers-reduced-motion: reduce) {
  .modal, .modal > .modal-box, .sidepanel, .sidepanel.left, .chipmenu-pop, .tgdate,
  .toasttick { animation:none; }
  .toasttick svg { animation:none; stroke-dashoffset:0; }
  .sess.livechanged { animation:none; box-shadow:inset 0 0 0 2px var(--accent); }
  .sess.justsaved { animation:none; box-shadow:inset 0 0 0 2px var(--ok); }
  .sess.justsaved::after { animation:none; }
  .tgd-day { transition:none; }
}

/* CLOSE, MINIMIZE AND EXPAND ON EVERY POPUP (ui.js, winControls; 24 October
   2026), drawn as they were asked for: a red circle with a cross, a yellow one
   with a bar, a green one with the expand arrows - in every dialog's head,
   every side panel's, and the WhatsApp windows'. The same colours in both
   themes, the mark always showing. They are the window's own buttons, so the
   older rules for a head's .x and .min are spoken over here. A 22px circle with
   a pad to 30px, and the pads of neighbours meet without overlapping. */
.wctl { display:inline-flex; align-items:center; gap:8px; flex:none; }
.wctl > .wcb {
  appearance:none; position:relative; flex:none; padding:0 !important; margin:0;
  width:22px !important; height:22px !important; border-radius:50% !important;
  border:1px solid rgba(0,0,0,.22) !important; cursor:pointer; line-height:1;
  display:inline-flex; align-items:center; justify-content:center;
  color:rgba(0,0,0,.62) !important; font-size:0;
}
.wctl > .wcb::after { content:''; position:absolute; inset:-4px; }
.wctl > .wcb svg { width:13px; height:13px; stroke-width:2.4; }
.wctl > .x { background:#ff5f57 !important; }
.wctl > .wc-min, .wctl > .min { background:#febc2e !important; }
.wctl > .wc-max { background:#28c840 !important; }
.wctl > .wcb:hover { filter:brightness(.93); color:rgba(0,0,0,.85) !important; }
.wctl > .wcb:focus-visible { outline:2px solid var(--accent); outline-offset:2px; }
/* Expanded to fill the screen: the box takes the window less a margin, and
   nothing offers to move or resize it until it is put back to its size. */
/* Expanded, the window still keeps clear of a phone's status bar and its
   navigation buttons - both drawn over the installed app (0 on a laptop). */
.modal.maxed > .modal-box {
  position:fixed !important; margin:0 !important;
  inset:calc(10px + env(safe-area-inset-top)) calc(10px + env(safe-area-inset-right))
        calc(10px + env(safe-area-inset-bottom)) calc(10px + env(safe-area-inset-left)) !important;
  width:auto !important; height:auto !important; max-width:none !important; max-height:none !important;
  display:flex; flex-direction:column;
}
.modal.maxed .modal-body { flex:1 1 auto; overflow:auto; }
.modal.maxed .mgrip, .modal.maxed .mgrip-e, .modal.maxed .mgrip-s,
.sidepanel.maxed .pgrip { display:none; }
.sidepanel.maxed {
  left:10px !important; right:10px !important;
  top:calc(64px + env(safe-area-inset-top)) !important; bottom:calc(10px + env(safe-area-inset-bottom)) !important;
  width:auto !important; height:auto !important; max-width:none !important; max-height:none !important;
}
/* The box takes the focus itself on a touch screen (modal() in ui.js), and
   a ring round the whole popup would say nothing. */
.modal-box[tabindex="-1"]:focus { outline:none; }
/* Minimized: kept, out of sight, and back from the dock. */
.modal.minimized { display:none !important; }
.windock {
  position:fixed; left:16px; bottom:calc(16px + env(safe-area-inset-bottom)); z-index:101;
  display:flex; flex-wrap:wrap-reverse; gap:8px; max-width:calc(100vw - 110px);
  pointer-events:none;
}
.windock .docked {
  pointer-events:auto; display:inline-flex; align-items:center; gap:8px;
  max-width:260px; min-height:38px; padding:5px 14px 5px 5px; cursor:pointer;
  border:1px solid var(--line); border-radius:var(--radius-pill);
  background:var(--surface); color:var(--ink); box-shadow:var(--shadow-md);
  font:inherit; font-size:13px; font-weight:600;
}
.windock .docked:hover { border-color:color-mix(in srgb, var(--accent) 55%, var(--line)); }
.windock .docked:focus-visible { outline:2px solid var(--accent); outline-offset:2px; }
.windock .dockmark {
  width:28px; height:28px; border-radius:50%; flex:none;
  display:inline-flex; align-items:center; justify-content:center;
  color:var(--a); background:color-mix(in srgb, var(--a) 16%, transparent);
}
.windock .dockmark svg { width:15px; height:15px; }
.windock .docktitle { overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }

/* THE PAYMENT DETAILS AS SETS (0362), on Messages, Payment messages: a row a
   set - whether it is the one enabled, its QR, its UPI ID and number, and
   Change / Remove. The enabled one is edged in the app's green, the colour a
   switch that is on wears here. */
.paysets { display:flex; flex-direction:column; gap:8px; margin:6px 0 10px; }
.payset {
  display:flex; align-items:center; gap:12px; flex-wrap:wrap;
  padding:8px 10px; border:1px solid var(--line); border-radius:10px;
  background:var(--raised);
}
.payset.on { border-color:var(--ok); box-shadow:inset 3px 0 0 var(--ok); }
.paysetuse { display:inline-flex; align-items:center; gap:6px; font-weight:600; min-width:90px; }
.paysetqr { width:56px; height:56px; object-fit:contain; border-radius:6px; background:#fff; }
.paysetqr.none {
  display:inline-flex; align-items:center; justify-content:center;
  font-size:11px; color:var(--muted); background:var(--sunken);
}
.paysetwords { display:flex; flex-direction:column; gap:2px; flex:1 1 180px; min-width:0; }
.paysetwords span { color:var(--muted); font-size:13px; overflow-wrap:anywhere; }
.paysetdo { display:flex; gap:6px; }
#psQrNow .paysetqr { width:120px; height:120px; margin:4px 0 6px; }
.camein .cameinto { display:inline-flex; align-items:center; gap:10px; }
.camein .paysetqr { width:36px; height:36px; flex:none; }
.camein tr.quiet td { color:var(--muted); }
.paysetwords .paysetnear { color:var(--wait); font-weight:600; }
.limitwarn { margin:8px 0 14px; padding-left:18px; line-height:1.5; }
.limitwarn li + li { margin-top:8px; }

/* ------------------------------------------- phone cards (25 October 2026)
   From the review on the clinic's phone (406px wide): two tables were read a
   column at a time on a screen showing one or two of their columns, with the
   rest a swipe away to the right. On a phone each row becomes a card made of
   its OWN cells - the same buttons, the same boxes, the same handlers - laid
   out one under another instead of in a row. Nothing is drawn twice and
   nothing is removed from the page; a laptop and the tablet are untouched. */

/* RESCHEDULES, the To-reschedule list and the four beside it. The child, the
   state and Book on the first line; the date with its Open, the time and the
   number; who it was cancelled with and what is left; who has taken the child
   so far. Each small fact wears its column's name (data-l), since the head
   row is not drawn. #view in front because compact pages pad these cells
   with a selector of its own, and on a phone the card's spacing is the
   card's. */
@media (max-width: 760px) {
  #view table.rsowed, #view table.rsowed tbody { display:block; }
  #view table.rsowed thead { display:none; }
  #view table.rsowed tr {
    display:flex; flex-wrap:wrap; align-items:center; gap:6px 12px;
    padding:10px 12px; border-top:1px solid var(--line-soft);
  }
  #view table.rsowed tbody tr:first-child { border-top:0; }
  #view table.rsowed td {
    display:block; padding:0; border:0; text-align:left; position:static;
  }
  #view table.rsowed td.rs-kid { order:1; flex:1 1 auto; min-width:0; font-size:15px; }
  #view table.rsowed td.rs-act { order:2; flex:none; text-align:right; }
  #view table.rsowed td.rs-state { order:3; flex:1 1 100%; }
  #view table.rsowed td.rs-date { order:4; }
  #view table.rsowed td.rs-time { order:5; }
  #view table.rsowed td.rs-num { order:6; }
  #view table.rsowed td.rs-with { order:7; }
  #view table.rsowed td.rs-left { order:8; }
  #view table.rsowed td.rs-seen { order:9; flex:1 1 100%; }
  #view table.rsowed td[data-l]::before {
    content:attr(data-l); display:block; font-family:inherit;
    font-size:10.5px; font-weight:600; letter-spacing:.04em; color:var(--muted);
  }
}

/* ONE CHIP, SHORTER (body.phonechips, Settings, General - off until switched
   on). The time is the size of the rest of the chip again rather than half as
   big again, and the outcome tag is smaller: measured on a Monday's 73 chips
   at 375px, the middle chip went from 61px to 52px and the day's grid from
   6,300px to 5,659 - about a sixth more children before each swipe.
   NOTHING IS HIDDEN - every badge, the therapist and the note stay - which is
   why it can be a switch rather than a decision about what reception needs.
   The bigger time was asked for by the clinic, so it stays the default. */
@media (max-width: 760px) {
  body.phonechips .sess { padding:4px 8px 4px 13px; }
  body.phonechips .sess-meta { font-size:12.5px; margin-top:1px; }
  body.phonechips .sess-meta .outcome { font-size:10px; padding:1px 5px; letter-spacing:.04em; }
  body.phonechips .sess-note { margin-top:2px; }
}

/* PACKAGES & BILLING AS CARDS (body.pkgcards, Settings, General - on unless
   switched off). Measured on the phone the grid was 7.6 screens wide and a
   screen showed the child and the therapy. Each row is a card of its own
   cells: the child across the top with the row's buttons, then the therapy,
   the size, the progress, the status, the payment and the amount.

   THE ROW IS STILL THE ROW, and that is the whole of why this is safe. The
   package window copies its boxes from the row and types into them, and
   Save reads every [data-f] in it (readRow) - so the cells this card does not
   show are HIDDEN, never removed: they are in the row, the window still finds
   them, and Save still writes them. Reaching for a box on the card opens the
   window on that box, as reaching for it in the grid does.

   #view in front so the copy of the grid the Calendar's "Package & Billing"
   window draws off the screen (#pkgPopupHost, on the body) is left alone. */
@media (max-width: 760px) {
  body.pkgcards #view #pkgList table.rows.edit,
  body.pkgcards #view #pkgList table.rows.edit tbody { display:block; width:100%; min-width:0; }
  body.pkgcards #view #pkgList table.rows.edit thead { display:none; }
  body.pkgcards #view #pkgList table.rows.edit tr {
    display:flex; flex-wrap:wrap; align-items:flex-end; gap:8px 10px;
    padding:12px; border-top:1px solid var(--line-soft);
  }
  body.pkgcards #view #pkgList table.rows.edit tbody tr:first-child { border-top:0; }
  /* No table.rows.edit in this one, on purpose: the hiding rules below must
     outweigh it, and each of them is this selector and one class more. */
  body.pkgcards #view #pkgList td {
    display:block; padding:0; border:0; position:static; background:none;
    box-shadow:none; text-align:left; min-width:0;
  }
  /* What the card leaves out - still in the row, for the window and Save. */
  body.pkgcards #view #pkgList td:has(> [data-f="name"]),
  body.pkgcards #view #pkgList td:has(> [data-f="parent_phone"]),
  body.pkgcards #view #pkgList td:has(> [data-f="send_messages"]),
  body.pkgcards #view #pkgList td:has(> [data-f="kind"]),
  body.pkgcards #view #pkgList td.paidcell,
  body.pkgcards #view #pkgList td.startcell,
  body.pkgcards #view #pkgList td:has(> [data-f="full_payment"]),
  body.pkgcards #view #pkgList td.lastcell,
  body.pkgcards #view #pkgList td:has(> [data-f="policy_id"]),
  body.pkgcards #view #pkgList td.paidsofar,
  body.pkgcards #view #pkgList td:has(> [data-f="comments"]) { display:none; }
  /* The child and the row's buttons share the top line. */
  body.pkgcards #view #pkgList td.kidcell { order:1; flex:1 1 auto; font-size:15px; }
  body.pkgcards #view #pkgList td.nowrap { order:2; flex:none; }
  body.pkgcards #view #pkgList td:has(> [data-f="therapy_name"]) { order:3; flex:1 1 60%; }
  body.pkgcards #view #pkgList td:has(> [data-f="size"]) { order:4; flex:0 1 30%; }
  body.pkgcards #view #pkgList td.progress { order:5; flex:1 1 100%; }
  body.pkgcards #view #pkgList td:has(> [data-f="status"]) { order:6; flex:1 1 30%; }
  body.pkgcards #view #pkgList td:has(> [data-f="payment"]) { order:7; flex:1 1 30%; }
  body.pkgcards #view #pkgList td:has(> [data-f="amount"]) { order:8; flex:1 1 30%; }
  body.pkgcards #view #pkgList td select, body.pkgcards #view #pkgList td input:not([type="hidden"]) {
    width:100%; max-width:none;
  }
  body.pkgcards #view #pkgList td:has(> [data-f="therapy_name"])::before { content:"Therapy"; }
  body.pkgcards #view #pkgList td:has(> [data-f="size"])::before { content:"Sessions"; }
  body.pkgcards #view #pkgList td:has(> [data-f="status"])::before { content:"Status"; }
  body.pkgcards #view #pkgList td:has(> [data-f="payment"])::before { content:"Payment"; }
  body.pkgcards #view #pkgList td:has(> [data-f="amount"])::before { content:"Amount"; }
  body.pkgcards #view #pkgList td:has(> [data-f])::before {
    display:block; margin-bottom:2px;
    font-size:10.5px; font-weight:600; letter-spacing:.04em; color:var(--muted);
  }
}

/* ------------------------------------------------------ compact pages ---
   (25 October 2026) compactPages() in main.js, while "Compact pages" on
   Settings, General is on - so off, nothing here applies. Measured at
   1366x768 before: Reschedules 3 rows of 114 on screen, Stock's list 337px
   down, the Change log 15,000px long, one Approvals card a row. */
/* A page's sentence is the "i" beside its name, and the band one line. */
body.compact #view > .pagetop > .pagehead > p.sub[data-hinted] { display:none; }
body.compact #view > .pagetop > .pagehead h2 { margin:0; }
body.compact #view > .pagetop { align-items:center; }
/* Reschedules: a row a line. The state is one word (owedRow), Sessions taken
   two pills and "+N", and the cells' padding a little less. */
body.compact table.rsowed td { padding-top:6px; padding-bottom:6px; }
body.compact table.rsowed td, body.compact table.rsowed th { padding-left:8px; padding-right:8px; }
/* "assigned", not "assigned therapist": the words stay in the markup and the
   pill's own edge says whose it is. */
body.compact table.rsowed .seenrole { font-size:0; }
body.compact table.rsowed .seenrole::before { content:"assigned"; font-size:10.5px; }
body.compact table.rsowed .pill { white-space:nowrap; }
body.compact table.rsowed .seenwith { flex-wrap:nowrap; }
body.compact table.rsowed .seenwith.all { flex-wrap:wrap; }
.seenwith:not(.all) > .seenextra { display:none; }
.seenmore {
  appearance:none; font:inherit; font-size:12px; cursor:pointer; white-space:nowrap;
  padding:2px 8px; border:1px dashed var(--line); border-radius:var(--radius-pill);
  background:none; color:var(--muted);
}
.seenmore:hover { border-color:var(--accent); color:var(--ink); }
body.compact .rsnote { padding:8px 12px; cursor:pointer; }
body.compact .rsnote:not(.open) p {
  display:-webkit-box; -webkit-line-clamp:1; -webkit-box-orient:vertical; overflow:hidden;
}
/* Stock: the four tiles as a strip - the figure beside its label, the line
   under it beside both. */
@media (min-width: 761px) {
  body.compact .stktiles { margin-bottom:10px; }
  body.compact .stktiles .tile { padding:8px 12px; gap:10px; align-items:center; }
  body.compact .stktiles .tile .tfig { display:flex; align-items:baseline; flex-wrap:wrap; gap:0 8px; }
  body.compact .stktiles .tile .tfig .l { margin:0; order:0; }
  body.compact .stktiles .tile .n { font-size:20px; order:1; }
  body.compact .stktiles .tile .tfig .sub { margin:0; order:2; flex-basis:100%; }
  body.compact .stktiles .cardicon { width:30px; height:30px; }
  /* Messages: the day's card a little slimmer round its one row. */
  body.compact #view > .msgbar { padding:5px 12px; margin-top:8px; }
  body.compact #view > .msgbar > .toolbar { padding-top:4px; padding-bottom:4px; }
}
/* Approvals: two purchase-request cards a row where two fit; a long one
   still takes the whole row (LONG_CARD). */
body.compact #apStock .stkreqs {
  grid-template-columns:repeat(auto-fit, minmax(min(100%, 560px), 1fr));
  /* Short cards fill in beside each other round a long one, which takes a
     row of its own: in time order a long one between two short ones left
     each of them alone on its row. */
  grid-auto-flow:row dense;
}
/* The Change log and every History: a long value two lines until "more",
   and on the log the day stays in view while its lines scroll. */
.chgmore { display:none; }
/* A record added or removed whole as a wrapped run of "Field: value" rather
   than one a line: they were 234px each, 28 of the log's 100, and the log
   halved (14,887px to 7,792, measured). A change - before and after - keeps a
   line of its own. */
body.compact .chg[data-act="add"] .chgfields, body.compact .chg[data-act="del"] .chgfields {
  display:flex; flex-wrap:wrap; gap:2px 16px;
}
body.compact .chgfields li.chglong { position:relative; }
body.compact .chgfields li.chglong:not(.open) {
  display:-webkit-box; -webkit-line-clamp:2; -webkit-box-orient:vertical; overflow:hidden;
}
body.compact .chgmore {
  display:inline; appearance:none; border:0; background:none; padding:0 2px; margin-left:6px;
  font:inherit; font-size:12px; color:var(--accent); cursor:pointer;
}
body.compact .chgfields li.chglong:not(.open) .chgmore {
  position:absolute; right:0; bottom:0; padding-left:18px;
  background:linear-gradient(to right, transparent, var(--surface) 14px);
}
body.compact #clBody .chgday {
  position:sticky; top:var(--clnav, 0px); z-index:3; margin:10px 0 4px; padding:6px 2px;
  background:var(--ground);
}
