/* ==========================================================================
   Onward SHARS Medicaid -- application shell
   ==========================================================================

   Loaded by includes/header.php for EVERY page, after tsbs_styl.css, so the
   rules here win on equal specificity. This is the ONE place app-screen
   styling lives; pages must not carry their own <style> blocks or colour
   literals.

   Design intent (see Docs/DESIGN-DECISIONS.md): the login screen established a
   white card floating on a tinted canvas, navy headings, an amber accent and a
   6px radius. This file applies that same language to the app by restyling the
   classes the pages ALREADY use -- `.content`, `.content-panel`,
   `.content-head`, `.nongrid-form`, `.button` -- rather than asking each page
   to adopt new markup. That is deliberate: ~35 screens share those class names,
   so one definition here upgrades all of them and there is nothing per-page to
   keep in sync.

   Every colour resolves to a --brand-* token from the :root block in
   tsbs_styl.css. No hex literals below except pure white/black and the two
   translucent shadow/scrim values, which are neutral by definition.
   ========================================================================== */

:root {
  /* Shell metrics, centralized so panels, grids and cards align to the same
     grid without each screen re-deciding. */
  --ow-radius: 6px;
  --ow-radius-lg: 10px;
  --ow-gap: 20px;
  --ow-shell-max: 1200px;
  --ow-canvas: #f7f9fc; /* matches --tint in login.css */
  --ow-shadow: 0 2px 14px rgba(11, 61, 119, 0.08);
  --ow-shadow-hover: 0 4px 20px rgba(11, 61, 119, 0.13);
}

/* --------------------------------------------------------------------------
   Canvas
   --------------------------------------------------------------------------
   tsbs_styl.css paints #content-frame with images/epic-bkg-gray2.png -- the
   grey snowflake wallpaper. It is EPIC-era decoration and reads as noise
   behind the new white cards, so every page gets the flat tinted canvas the
   login screen uses. `.bare-frame` (login / set password) keeps its own
   treatment from login.css and is excluded.
   -------------------------------------------------------------------------- */
#content-frame:not(.bare-frame) {
  background-image: none;
  background-color: var(--ow-canvas);
  padding: 0 30px 40px;
}

/* #wrapper is `flex: 1 0 auto` so it absorbs the leftover viewport height and
   keeps the footer at the bottom -- but the canvas colour lives on
   #content-frame INSIDE it, so on a short page the wrapper's own white
   background showed as a band between the content and the footer. Making the
   wrapper a column flex container and letting the frame grow paints the canvas
   over that leftover space instead. */
#wrapper {
  display: flex;
  flex-direction: column;
}

#wrapper > #content-frame {
  flex: 1 0 auto;
}
/* --------------------------------------------------------------------------
   Account bar (post-login only)
   --------------------------------------------------------------------------
   The strip above the logo. Only rendered when logged in: header.php omits
   #toolbar-links on Log In / Register / Electronic Signature, and every rule
   here is scoped to `#toolbar:has(#toolbar-links)` so the pre-login bar keeps
   its original 19px sliver.

   What was wrong with the original:
   - `width: 1100px; text-align: center` on a full-bleed navy strip, while
     #header widens past 1200px. So the content floated mid-strip, sharing no
     edge with the logo or nav below it: three rows, three alignments.
   - Identity and actions were both 11px white caps, distinguished only by a
     faint underline. Nothing read as clickable.
   - 19px tall with 4px/3px padding, so 11px text nearly touched both edges.

   The redesign: one row, two groups. Identity left (avatar, name, id, district)
   as quiet information; actions right as real buttons with hover states. Aligned
   to the same 1100px content column the logo and nav use, so all three line up.
   -------------------------------------------------------------------------- */
#toolbar:has(#toolbar-links) {
  height: auto;
  min-height: 38px;
  display: flex;
  align-items: center;
  /* A hairline of the amber accent, matching the footer's top edge, so the bar
     reads as deliberate chrome rather than a stray blue line. */
  box-shadow: inset 0 -1px 0 rgba(255, 255, 255, 0.09);
}

/* Share the content column with #header. tsbs_styl.css sets #header to
   `width: 1100px; margin: 0 auto` and only switches it to `100%` + 50px padding
   below 1200px, so the bar has to do both to stay aligned at every width. */
#toolbar:has(#toolbar-links) #toolbar-links {
  box-sizing: border-box;
  width: 1100px;
  margin: 0 auto;
  padding: 0;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 20px;
  text-align: left;
  font-size: 12px;
}

@media (max-width: 1200px) {
  #toolbar:has(#toolbar-links) #toolbar-links {
    width: 100%;
    padding: 0 50px;
  }
}

@media (max-width: 1100px) {
  #toolbar:has(#toolbar-links) #toolbar-links {
    padding: 0 35px;
  }
}

#toolbar-links .tb-identity,
#toolbar-links .tb-actions {
  display: flex;
  align-items: center;
  min-width: 0;
}

/* The identity group absorbs the free space so the actions group is pushed hard
   against the right edge, level with the nav below it. Without `flex: 1 1 auto`
   the identity group only sized to its content and the leftover space landed
   between the two groups, leaving the actions ~255px short of the edge.

   The last action also needs its own 11px padding discounted, or the button's
   text -- rather than its box -- is what sits 11px inside the column edge, and
   the bar reads as misaligned against the nav. */
#toolbar-links .tb-identity {
  gap: 14px;
  flex: 1 1 auto;
}

#toolbar-links .tb-actions {
  gap: 2px;
  flex: 0 0 auto;
  margin-right: -20px;
}

/* Reset the float-era spacing; the flex `gap` owns it now. */
#toolbar:has(#toolbar-links) .top-button {
  margin: 0;
  padding: 0;
  line-height: 1.3;
}

/* --- identity ----------------------------------------------------------- */

#toolbar-links .tb-user {
  display: flex;
  align-items: center;
  gap: 9px;
  min-width: 0;
}

/* Initials rather than a generic person glyph: it identifies the actual signed-in
   user, which is the one thing this bar exists to confirm. */
#toolbar-links .tb-avatar {
  flex: 0 0 auto;
  width: 24px;
  height: 24px;
  border-radius: 50%;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  font-size: 10px;
  font-weight: 700;
  letter-spacing: 0.02em;
  color: #ffffff;
  background-color: rgba(255, 255, 255, 0.16);
}

#toolbar-links .tb-who {
  display: flex;
  align-items: baseline;
  gap: 8px;
  min-width: 0;
}

#toolbar-links .tb-who b {
  color: #ffffff;
  font-weight: 600;
  font-size: 12px;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

/* The id is a lookup key, not part of the name. The original ran it together as
   "(ID: 989946)", which read as a suffix of the person. */
#toolbar-links .tb-meta {
  flex: 0 0 auto;
  font-size: 10px;
  font-weight: 500;
  letter-spacing: 0.04em;
  color: rgba(255, 255, 255, 0.55);
}

/* District and Spoof Practitioner: context selectors, so they sit between the
   name and the actions, separated by a hairline rather than more whitespace.

   The selector carries `.top-button.top-select` in the markup, and the
   `.top-button` reset above zeroes padding -- so this needs the same two-class
   specificity to win, not `.top-select` alone. */
#toolbar:has(#toolbar-links) .top-button.top-select {
  display: flex;
  align-items: center;
  gap: 7px;
  padding: 0 0 0 14px;
  border-left: 1px solid rgba(255, 255, 255, 0.16);
  font-size: 10px;
  font-weight: 600;
  letter-spacing: 0.06em;
  color: rgba(255, 255, 255, 0.5);
  text-transform: uppercase;
  white-space: nowrap;
}

#toolbar-links .top-select .ui-selectmenu-button.ui-button {
  margin: 0;
  padding: 3px 8px;
  height: auto;
  min-height: 0;
  width: auto !important;
  max-width: 190px;
  border: 1px solid rgba(255, 255, 255, 0.2);
  border-radius: 4px;
  background: rgba(255, 255, 255, 0.08);
  color: #ffffff;
  font-family: inherit;
  font-size: 11px;
  font-weight: 600;
  letter-spacing: 0;
  text-transform: none;
  transition: background-color 0.15s ease, border-color 0.15s ease;
}

#toolbar-links .top-select .ui-selectmenu-button.ui-button:hover,
#toolbar-links .top-select .ui-selectmenu-button.ui-button:focus {
  background: rgba(255, 255, 255, 0.16);
  border-color: rgba(255, 255, 255, 0.34);
}

#toolbar-links .top-select .ui-selectmenu-text {
  margin-right: 18px;
  color: #ffffff;
}

#toolbar-links .top-select .ui-selectmenu-icon.ui-icon {
  opacity: 0.7;
}

/* --- actions ------------------------------------------------------------ */

#toolbar-links .tb-actions .top-button > a {
  display: inline-flex;
  align-items: center;
  /* Tight, so each icon reads as belonging to its own label rather than
     floating between two of them. The glyphs already carry side bearings inside
     their em box, so the optical gap is wider than this number. */
  gap: 2px;
  padding: 6px 11px;
  border-radius: 5px;
  color: rgba(255, 255, 255, 0.88);
  font-size: 11px;
  font-weight: 600;
  letter-spacing: 0.04em;
  text-decoration: none;
  transition: background-color 0.15s ease, color 0.15s ease;
}

#toolbar-links .tb-actions .top-button > a:hover,
#toolbar-links .tb-actions .top-button > a:focus-visible {
  background-color: rgba(255, 255, 255, 0.15);
  color: #ffffff;
  text-decoration: none;
}

#toolbar-links .tb-actions .top-button > a:focus-visible {
  outline: 2px solid rgba(255, 255, 255, 0.55);
  outline-offset: 1px;
}

/* `align-items: center` centres each glyph's BOX; the ink inside those boxes is
   not centred the same way, so the icons render visibly high next to all-caps
   labels that have no descenders.

   Measured from the actual painted pixels -- screenshot the anchor at 8x device
   scale, scan for light-on-navy rows, and compare the icon's ink band with the
   label's: icon centre 104, text centre 113, i.e. the icon sits 9 device px
   (1.13 CSS px) high. Nudged down 1px to match.

   ONE rule for every icon on the bar, deliberately. fa-power-off has a taller
   outline than the others (121 device px of ink at 12x against the label's 100)
   and looked like it needed its own value, but measuring said otherwise: 1.5px
   and 1.25px both tipped it low, and the shared 1px lands all three within
   0.06 CSS px. A per-glyph selector here would be dead weight and would imply
   future icons need their own entry.

   Two measurement traps, both hit while arriving at this:
   - Drawing the glyph to a <canvas> reports 0.28px, not 1.13px, because the icon
     font is not available in a canvas 2D context and it silently measures a
     fallback glyph. Measure the rendered page.
   - Scanning screenshot rows with a brightness threshold shifts with whatever
     threshold you pick, since antialiasing then counts as solid ink. Use an
     intensity-weighted centroid instead. */
#toolbar-links .tb-actions .fa {
  font-size: 11px;
  opacity: 0.7;
  position: relative;
  top: 1px;
}

/* fa-power-off is the exception, and it needs the extra px for a reason worth
   recording: the glyph is a ring with a STEM protruding above it. The stem drags
   both the ink bounding box and the intensity centroid upward, so every
   whole-glyph metric reports it as already centred (-0.05px) while the ring --
   the shape the eye actually tracks -- sits visibly high.

   Measured on the ring alone, excluding the stem by taking only rows at least
   55% as wide as the widest row. Verified by rendering at 1px, 2px and 3px with
   a hairline through the label's optical centre: at 1px the ring is above the
   line, at 3px its underside drops below the letters' baseline, at 2px the line
   bisects the ring the same way it bisects the O and the G. */
#toolbar-links .tb-actions .fa-power-off {
  top: 2px;
}

#toolbar-links .tb-actions .top-button > a:hover .fa {
  opacity: 1;
}

/* Log out is the only destructive action on the bar. Neutral at rest so it does
   not shout, red on hover so it is unmistakable before the click. */
#toolbar-links .tb-actions .tb-logout {
  margin-left: 6px;
}

#toolbar-links .tb-actions .tb-logout:hover,
#toolbar-links .tb-actions .tb-logout:focus-visible {
  background-color: var(--brand-danger);
  color: #ffffff;
}

/* --- narrow screens ----------------------------------------------------- */
/* Below 940px tsbs_styl.css converts the bar into a collapsible stacked menu
   driven by #toolbar-toggle: .top-button becomes `display: none; width: 100%`
   and only reappears under `#toolbar-links.toggle-open`. A flex row fights that,
   so hand the layout back and let the original rules run. */
@media (max-width: 940px) {
  #toolbar:has(#toolbar-links) {
    display: block;
    min-height: 0;
  }

  #toolbar:has(#toolbar-links) #toolbar-links {
    display: block;
    width: 100%;
    padding: 0;
    text-align: center;
  }

  #toolbar-links .tb-identity,
  #toolbar-links .tb-actions,
  #toolbar-links .tb-user,
  #toolbar-links .tb-who {
    display: block;
  }

  #toolbar-links .tb-avatar {
    display: none; /* the stacked rows are labelled; initials add nothing */
  }

  #toolbar-links .tb-actions .top-button > a {
    display: block;
    padding: 0;
    border-radius: 0;
  }

  /* Each stacked item is already a full-width bar with its own separator; a
     hover fill on top of that reads as a selected state. */
  #toolbar-links .tb-actions .top-button > a:hover,
  #toolbar-links .tb-actions .tb-logout:hover {
    background-color: transparent;
    color: #ffffff;
  }

  #toolbar-links .top-select {
    display: block;
    padding-left: 0;
    border-left: none;
  }
}

/* --------------------------------------------------------------------------
   Header (logo + nav), post-login only
   --------------------------------------------------------------------------
   Three problems in the original, all visible at once:

   1. No bottom edge. The white header met the tinted canvas with nothing between
      them, so the nav floated in whitespace with no containing shape -- while the
      toolbar above it has a clear bottom and the footer has its amber rule.
   2. `#header-links { margin: 20px 0 30px }` is asymmetric, which pushed the nav
      optically high against the logo AND left ~30px of dead space underneath.
      Centring the two against each other fixes both at once.
   3. Only the active nav item was styled, so the group read as one filled button
      and four plain labels with no sign the rest were interactive.
   -------------------------------------------------------------------------- */
/* Scoped by `:has(#header-links)`: header.php only emits that nav block when a
   session exists, so the login and register pages keep their original centred
   logo with no border. */
#header:has(#header-links) {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 30px;
  padding: 18px 0 16px;
  /* The edge the header was missing. Same hairline as the card borders, so the
     chrome reads as a stack of defined bands rather than fading into the canvas. */
  border-bottom: 1px solid var(--brand-line);
}

@media (max-width: 1200px) {
  #header:has(#header-links) {
    padding: 18px 50px 16px;
  }
}

/* Vertically centred against the logo instead of floated with a lopsided
   margin. `float: right` still applies from tsbs_styl.css but is inert once the
   parent is a flex container.

   `white-space: nowrap` is load-bearing: tsbs_styl.css lays the nav out as
   inline-blocks sized by their text, and making the parent a flex container let
   the items shrink below that width, so "Configure Onward" and "Create Sessions"
   wrapped onto two lines. */
#header:has(#header-links) #header-links {
  display: flex;
  align-items: center;
  gap: 4px;
  margin: 0;
  float: none;
  white-space: nowrap;
}

#header:has(#header-links) .header-column {
  display: flex;
  flex: 0 0 auto;
}

/* Every item gets the same shape, so the inactive ones read as controls too --
   the active pill then means "you are here" rather than "this is the only
   button". */
#header:has(#header-links) .header-button {
  padding: 9px 15px;
  border-radius: var(--ow-radius);
  font-size: 13px;
  font-weight: 600;
  color: var(--brand-navy);
  background-color: transparent;
  transition: background-color 0.15s ease, color 0.15s ease;
}

#header:has(#header-links) .header-column:hover .header-button,
#header:has(#header-links) .header-button:hover {
  background-color: var(--brand-blue-soft);
  color: var(--brand-navy);
}

#header:has(#header-links) .header-button.active,
#header:has(#header-links) .header-button.active:hover {
  background-color: var(--brand-navy);
  color: #ffffff;
}

/* Below 940px tsbs_styl.css turns the nav into a drawer: `#header-links` goes
   `display: none` and is revealed only by `#header.open-menu`, with the columns
   stacked full-width and the submenus expanded inside it.

   Every rule above has to stand down here. Left applying, `display: flex` beat
   the `display: none` and forced the whole menu open permanently -- the submenus
   rendered as tall navy columns floating over the page, and the row overflowed
   horizontally. Restate the drawer's own values rather than merely unsetting
   ours, because `:has()` outranks the plain selectors it is overriding. */
/* Between 940 and 1100 the logo plus a single-line nav is wider than the
   viewport: at 980 the nav alone measures 729px. Let it wrap under the logo
   rather than overflow, keeping the desktop pill styling. */
@media (max-width: 1100px) {
  #header:has(#header-links) {
    flex-wrap: wrap;
    justify-content: center;
    gap: 10px 4px;
  }

  #header:has(#header-links) #header-links {
    flex-wrap: wrap;
    justify-content: center;
  }
}

@media (max-width: 940px) {
  #header:has(#header-links) {
    display: block;
    padding: 20px;
    border-bottom: none;
  }

  #header:has(#header-links) #header-links {
    display: none;
    width: 100%;
    margin: 0;
    white-space: normal;
  }

  #header.open-menu:has(#header-links) #header-links {
    display: block;
  }

  #header:has(#header-links) .header-column {
    display: block;
    flex: none;
  }

  /* The drawer's items are full-width rows with their own separators; the pill
     shape and hover fill from the desktop rules read as a selected state on top
     of that. */
  #header:has(#header-links) .header-button {
    padding: 10px;
    border-radius: 0;
    font-size: 14px;
  }

  #header:has(#header-links) .header-column:hover .header-button,
  #header:has(#header-links) .header-button:hover,
  #header:has(#header-links) .header-button.active {
    background-color: transparent;
    color: var(--brand-navy);
  }
}

/* The gap between the app header and the content card.

   It used to come from `.content-header`'s 22px top padding, which meant the spacing
   depended on a page happening to emit `<div class="content-header">` - a spacer div
   with no other purpose on several screens. Settings and Preferences does not emit one,
   so its card sat flush against the header divider: measured GAP=0 there against 36px
   on My Account, Contact Us and Configure.

   Declared on the FRAME, not on the card, and only when no `.content-header` is present.
   Putting it on the card and cancelling it per-sibling does not work: Customize
   Activities and Manage IEP Goals wrap their card in a `<form>`, so the card is not a
   sibling of the header and the cancelling rule never matched - measured 73px there,
   both gaps stacked. `:has()` asks the question that actually matters, which is whether
   the PAGE emitted a header at all, regardless of how deeply the card is wrapped. */
#content-frame:not(.bare-frame):not(:has(.content-header)) {
  padding-top: 36px;
}

/* --------------------------------------------------------------------------
   Page header -- the row above the content card
   -------------------------------------------------------------------------- */
#content-frame:not(.bare-frame) .content-header {
  max-width: var(--ow-shell-max);
  min-height: 0;
  padding: 22px 0 14px;
  display: block;
  text-align: right;
}

#content-frame:not(.bare-frame) .content-header a {
  font-size: 13px;
  font-weight: 600;
  color: var(--brand-blue);
}

/* --------------------------------------------------------------------------
   The content card
   --------------------------------------------------------------------------
   `.content` is the wrapper on essentially every screen. Giving it the card
   treatment here is what makes an untouched page look redesigned.
   -------------------------------------------------------------------------- */
#content-frame:not(.bare-frame) .content {
  max-width: var(--ow-shell-max);
  border: 1px solid var(--brand-line);
  border-radius: var(--ow-radius-lg);
  box-shadow: var(--ow-shadow);
  overflow: hidden;
}

/* `.content h1` is the in-card page title on most screens. */
#content-frame:not(.bare-frame) .content h1 {
  align-items: center;
  margin: 0;
  padding: 18px 26px 16px;
  font-size: 20px;
  font-weight: 600;
  color: var(--brand-navy);
  background-color: var(--brand-blue-soft);
  border-bottom: 1px solid var(--brand-line);
}

/* tsbs_styl.css pads `.content.account/.caseload/.review/.sessions/.log-in` by
   `20px 25px 60px`, which insets the heading band by the card's own padding so it
   floats short of the card edge instead of spanning it like on every other screen.
   Pulling the h1 out by that padding restores the full-width band. Listed by class
   because the compensation has to match the padding those rules set. */
#content-frame:not(.bare-frame) .content.account > h1,
#content-frame:not(.bare-frame) .content.caseload > h1,
#content-frame:not(.bare-frame) .content.review > h1,
#content-frame:not(.bare-frame) .content.sessions > h1,
#content-frame:not(.bare-frame) .content.log-in > h1 {
  margin: -20px -25px 20px;
}

#content-frame:not(.bare-frame) .content h1 .fa,
#content-frame:not(.bare-frame) .content h1 .fa-solid {
  margin-right: 10px;
  color: var(--brand-blue);
}

/* The rule tsbs_styl.css draws after h1 text is redundant once the heading has
   its own tinted band. */
#content-frame:not(.bare-frame) .content h1:after {
  display: none;
}

/* --------------------------------------------------------------------------
   Home-style panels
   --------------------------------------------------------------------------
   Home (and Configure) lay out `.content-panel` as table cells divided by
   1px rules. Switching the parent to flex turns them into separate tiles,
   which suits the card language and removes the full-height divider lines.
   -------------------------------------------------------------------------- */
#content-frame:not(.bare-frame) .content.home {
  display: flex;
  flex-wrap: wrap;
  gap: var(--ow-gap);
  padding: var(--ow-gap);
  /* tsbs_styl.css gives `.content` a negative right margin to cancel the gutter
     of the old table-cell panel layout. With flex tiles there is no such gutter,
     so the margin just pushed the row past the viewport -- it was the source of
     horizontal overflow at every width below 1400. */
  margin: 0 auto;
  width: 100%;
  box-sizing: border-box;
  background-color: transparent;
  border: none;
  border-radius: 0;
  box-shadow: none;
  overflow: visible;
}

#content-frame:not(.bare-frame) .content.home .content-panel,
#content-frame:not(.bare-frame) .content.home .content-panel:last-child {
  display: flex;
  flex-direction: column;
  align-items: center;
  flex: 1 1 240px;
  min-width: 240px;
  padding: 30px 22px 26px;
  background-color: #ffffff;
  border: 1px solid var(--brand-line);
  border-right: 1px solid var(--brand-line);
  border-radius: var(--ow-radius-lg);
  box-shadow: var(--ow-shadow);
  transition: box-shadow 0.15s ease, transform 0.15s ease;
}

#content-frame:not(.bare-frame) .content.home .content-panel:hover {
  background-color: #ffffff;
  box-shadow: var(--ow-shadow-hover);
  transform: translateY(-2px);
}

/* The 120px black-bordered circle is the most dated element on the page.
   Reduced, de-bordered and tinted to match the login card's badge. */
#content-frame:not(.bare-frame) .fontawesome-icon.circle-yes {
  height: 64px;
  width: 64px;
  font-size: 28px;
  line-height: 64px;
  border: none;
  background-color: var(--brand-blue-soft);
  color: var(--brand-blue);
}

/* `.alert` marks the panel carrying the pending-work counts. Amber, matching
   the accent button on the login card, so it reads as "needs attention"
   without using the error colour. */
#content-frame:not(.bare-frame) .alert .fontawesome-icon.circle-yes {
  background-color: var(--brand-amber-soft);
  color: var(--brand-amber-dark);
}

#content-frame:not(.bare-frame) .content-panel h2 {
  margin: 16px 0 12px;
  font-size: 16px;
  font-weight: 600;
  letter-spacing: 0.01em;
}

#content-frame:not(.bare-frame) .content-panel ul {
  width: 100%;
  font-size: 13px;
  line-height: 20px;
}

#content-frame:not(.bare-frame) .content-panel li {
  margin-top: 9px;
  font-weight: 500;
  color: var(--brand-ink);
}

/* --------------------------------------------------------------------------
   Account panels (My Account, Maintain Practitioner Status)
   --------------------------------------------------------------------------
   Both screens build their layout from inline percentage widths on floats:
   `.column` at 45%/55%, `.account-panel` at 50%/60%/40% inside it. The
   percentages compound, so at 360px the right column was 152px and its panel
   91px, leaving `#newDist` (154px fixed, plus a 65px inline left margin)
   hanging 72px outside the card. Inline styles beat any stylesheet, so the
   widths cannot be overridden directly.

   Flex resolves it at the cause: on a flex item `flex-basis` decides the main
   size ahead of `width`, and `float` is ignored. The columns then size from the
   card rather than from each other, and wrap instead of shrinking.
   -------------------------------------------------------------------------- */
#content-frame:not(.bare-frame) .content.account {
  display: flex;
  flex-wrap: wrap;
  gap: 20px;
  /* `flex-start`, deliberately. The two columns hold different content - My Account's
     left column is two 82px panels, its right is one 235px panel - so equalising their
     heights only moves the ragged edge inside the taller column, where the last panel
     then stops 51px short of its own box. Measured both ways before choosing. */
  align-items: flex-start;
  padding: 20px 25px 40px;
}

/* Only the flex behaviour is screen-specific; the negative margin that pulls the
   heading band out to the card edge is the shared rule above. */
#content-frame:not(.bare-frame) .content.account > h1 {
  flex: 1 1 100%;
}

#content-frame:not(.bare-frame) .content.account > .column {
  display: flex !important;
  flex-direction: column;
  gap: 20px;
  flex: 1 1 280px;
  float: none !important;
  width: auto !important;
  min-width: 0;
}

#content-frame:not(.bare-frame) .content.account .account-panel,
#content-frame:not(.bare-frame) .content.account .account-panel:last-child {
  float: none !important;
  width: auto !important;
  margin: 0 !important;
  min-width: 0;
  border: 1px solid var(--brand-line);
  border-radius: var(--ow-radius);
  background-color: #ffffff;
}

/* Configure Onward puts its panels directly in the card rather than inside a
   `.column`, and they are navigation tiles, not form sections. Give them a tile
   basis so three of them share the row evenly instead of inheriting the
   two-column basis above and bunching at the left with dead space beside them.
   `align-content: flex-start` stops a single wrapped row stretching to the card's
   full height. */
#content-frame:not(.bare-frame) .content.account > .account-panel {
  flex: 1 1 260px;
  padding: 22px 24px;
  transition: box-shadow 0.15s ease, transform 0.15s ease;
}

/* Same wrapping-row-with-gap trap as Contact Us: once the tiles wrap, the one on
   the second line grows against that line's space less the gap, so a 2-tile card
   measured 508px and 488px between 620px and 480px. Below the point where two
   tiles no longer fit, make it a column so each takes the full width.
   Measured fit: two tiles are 274px each at 700px, and stop fitting at ~620px. */
@media (max-width: 640px) {
  #content-frame:not(.bare-frame) .content.account {
    flex-direction: column;
    /* The card sets `align-items: flex-start` for the row layout; in a column that
       is the CROSS axis, so the tiles would shrink to their text width (measured
       217px and 187px) instead of filling. */
    align-items: stretch;
  }

  /* `flex: 1 1 260px` sizes the main axis, which is now height. Reset the basis so
     a tile is as tall as its content rather than 260px. */
  #content-frame:not(.bare-frame) .content.account > .account-panel {
    flex: 0 0 auto;
  }
}

#content-frame:not(.bare-frame) .content.account > .account-panel:hover {
  box-shadow: var(--ow-shadow-hover);
  transform: translateY(-2px);
}

#content-frame:not(.bare-frame) .content.account > .account-panel h3 a {
  color: var(--brand-blue);
  text-decoration: none;
}

#content-frame:not(.bare-frame) .content.account > .account-panel h3 a:hover {
  text-decoration: underline;
}

/* The panels carry `<br/>` spacers between them in the markup; the flex gap
   above already separates them. */
#content-frame:not(.bare-frame) .content.account > .column > br {
  display: none;
}

#content-frame:not(.bare-frame) .content.account .account-panel h3 {
  margin: 0 0 6px;
  font-size: 15px;
  color: var(--brand-navy);
}

/* `.description` in tsbs_styl.css is `text-align: center`, which centres a label
   sitting under a left-aligned h3 and centres the nested h3 of the "Request
   Access" block. Each district row and both request blocks are separate
   `.description` elements whose inner button floats right, leaving a 106px gap
   from the input beside it. A flex row fixes both: real spacing, and the district
   name stays on one line with its Remove button. Scoped to these panels rather
   than changing the global rule. */
#content-frame:not(.bare-frame) .content.account .account-panel .description {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 10px;
  margin-top: 8px;
  text-align: left;
  color: var(--brand-muted);
}

/* `.description` carries two different things on this screen, and they were painted
   the same: helper sentences under a link ("Change your password.") and the user's own
   assigned DISTRICT NAMES, which are real data. Muted grey at 12px made "TEXAS ISD"
   read as a disabled hint rather than as the district the account is attached to.

   Told apart structurally, since the markup gives no class to work with: a helper panel
   puts its `<h3>` BEFORE the description as a sibling, while a district row is the
   first `.description` in a panel whose heading sits outside it. `:first-of-type`
   after an `h3` sibling selects the helper case, so the data case is what is left. */
#content-frame:not(.bare-frame) .content.account .account-panel > h3 + .description {
  color: var(--brand-muted);
}

#content-frame:not(.bare-frame) .content.account .account-panel > .description {
  color: var(--brand-ink);
  font-size: 13px;
}

#content-frame:not(.bare-frame) .content.account .account-panel .description h3 {
  flex: 1 1 100%;
  margin-top: 18px;
  text-align: left;
  color: var(--brand-navy);
}

/* `align-items: center` centres the MARGIN box, so the inline `margin-top: 16px`
   on #newDist and the 15px on `.post-label` shifted their centres 8px below the
   buttons beside them, which carry no top margin. The margins existed to push
   each control clear of the old float layout; the flex row spaces them now. */
#content-frame:not(.bare-frame) .content.account .account-panel .description input,
#content-frame:not(.bare-frame) .content.account .account-panel .description select,
#content-frame:not(.bare-frame) .content.account .account-panel .description .button {
  margin-top: 0 !important;
  margin-bottom: 0 !important;
}

/* A district row is "NAME ... Remove District" with no field in it, so that button
   belongs at the far edge. The Add rows contain an input or select and their button
   belongs beside it, at the flex gap. */
#content-frame:not(.bare-frame) .content.account .account-panel .description .button {
  float: none !important;
  margin-left: auto !important;
  margin-right: 0 !important;
  flex: 0 0 auto;
}

#content-frame:not(.bare-frame) .content.account .account-panel .description:has(input) .button,
#content-frame:not(.bare-frame) .content.account .account-panel .description:has(select) .button {
  margin-left: 0 !important;
}

/* The district name is a bare text node, so it has no box to shrink and a long
   name pushed the button onto its own line while `margin-left: auto` kept it hard
   right, reading as unrelated to the name above it. `nowrap` on the row keeps the
   pair together; the button already refuses to shrink. */
#content-frame:not(.bare-frame) .content.account .account-panel .description:not(:has(input)):not(:has(select)) {
  flex-wrap: nowrap;
}

#content-frame:not(.bare-frame) #newDist {
  margin-left: 0 !important;
  width: 100% !important;
  max-width: 260px;
  box-sizing: border-box;
}

/* --------------------------------------------------------------------------
   Maintain Practitioner Status - the two rosters
   --------------------------------------------------------------------------
   These panels are direct children of `.content.account`, so without a class of
   their own they inherit Configure Onward's navigation-tile rules: a 260px tile
   basis, and a hover lift on something that is not a link. `.ow-roster` is the
   discriminator - the structure is identical on both screens, so nothing else
   can tell a list panel from a tile.

   Two equal halves rather than tiles, and each roster scrolls: the account holds
   42 active and 23 inactive practitioners, so an unbounded list runs the card
   past 2000px and the second roster's rows sit far below the first's.
   -------------------------------------------------------------------------- */
#content-frame:not(.bare-frame) .content.account > .ow-roster {
  --ow-roster-row: 43px;
  --ow-roster-rows: 12;

  flex: 1 1 320px;
  padding: 18px 20px;
  transition: none;
  align-self: stretch;
}

/* The rows scroll, not the panel. An earlier version scrolled the panel itself
   and derived its height from the heading, but the heading measures 28.25px - a
   fraction of the font, not a value this file controls - so the derived height
   landed mid-row anyway (row 29.7% visible, its button 19.8%). Putting the rows
   in their own box makes the height depend only on the row pitch, which IS set
   here, and the heading no longer needs `position: sticky` to survive a scroll.

   Sizing to a whole number of rows is the point: an arbitrary max-height cuts
   some row in half, and a button clipped at 95% reads as a rendering fault
   rather than as "scroll for more". */
#content-frame:not(.bare-frame) .content.account > .ow-roster .ow-roster-list {
  max-height: calc(var(--ow-roster-row) * var(--ow-roster-rows));
  min-height: calc(var(--ow-roster-row) * 3);
  overflow-y: auto;
  /* The list scrolls, and nothing said so. Maintain Practitioner Status shows 41 active
     and 23 inactive but renders 12 rows each, with 1247px and 473px below the fold -
     reachable, yet the overlay scrollbar only appears once the pointer is already inside
     the box, so the first impression is a list that simply ends. `scrollbar-gutter`
     reserves the track whether or not the bar is showing, which both reveals the bar in
     supporting browsers and stops the rows shifting sideways when it appears. */
  scrollbar-gutter: stable;
  scrollbar-width: thin;
  scrollbar-color: var(--brand-line) transparent;
}

/* WebKit and Blink ignore `scrollbar-width`, so the same thin track is restated with
   the vendor pseudo-elements. Kept next to the standard properties above rather than
   in a separate block, so the two cannot drift apart. */
#content-frame:not(.bare-frame) .content.account > .ow-roster .ow-roster-list::-webkit-scrollbar {
  width: 10px;
}

#content-frame:not(.bare-frame) .content.account > .ow-roster .ow-roster-list::-webkit-scrollbar-thumb {
  background-color: var(--brand-line);
  border: 3px solid #ffffff;
  border-radius: 5px;
}

#content-frame:not(.bare-frame) .content.account > .ow-roster .ow-roster-list::-webkit-scrollbar-thumb:hover {
  background-color: var(--brand-muted);
}

#content-frame:not(.bare-frame) .content.account > .ow-roster:hover {
  box-shadow: none;
  transform: none;
}

/* The wrapping-flex-row-with-gap trap, third occurrence after Contact Us and
   Configure Onward: once the two panels wrap, the one on the second line grows
   against that line's space LESS the gap, so they came out 20px apart (measured
   658 against 638) across a 125px band.

   The breakpoint is derived, not guessed. Two panels fit while the card's content
   box holds `320 + 320 + gap` = 660px, and the card is `viewport - 60` (page
   margins) `- 50` (its own padding), so the row breaks at viewport 770px. Swept
   at 5px steps: equal down to 775px, 20px apart from 770px. The shared 640px
   query below is Configure's 260px-tile point and fires 130px too late here -
   which is the reason to derive the number from the basis rather than reuse
   another screen's.

   Keep this in step with `flex-basis` above: change one and the other moves.

   `flex: 0 0 auto` alone is not enough, and the reason is the same one Configure
   hit: the card's `align-items: flex-start` is the CROSS axis, so the panels stay
   side by side and shrink to their content (measured 376.7 and 362 - unequal by
   text, not by gap). The direction has to become a column, and `align-items` has
   to become `stretch`, for the basis to mean full width. */
@media (max-width: 770px) {
  #content-frame:not(.bare-frame) .content.account:has(> .ow-roster) {
    flex-direction: column;
    align-items: stretch;
  }

  #content-frame:not(.bare-frame) .content.account > .ow-roster {
    flex: 0 0 auto;
  }
}

/* Below 380px the names wrap, so rows stop being uniform and "12 rows tall" no
   longer lands on a row boundary - measured a cut at 74.1% at 380px and 9.4% at
   360px. A cap over variable-height rows cannot be clean, so drop it and let the
   page scroll instead of the list. Measured wrap point: uniform at 400px, first
   wrapped row at 380px. */
@media (max-width: 380px) {
  #content-frame:not(.bare-frame) .content.account > .ow-roster .ow-roster-list {
    max-height: none;
    overflow-y: visible;
  }

  /* A 276px row splits ~120px of name against a 150px button, so a name runs to
     three lines beside a one-line button. Stack them: the row reads "who", then
     "what you can do about it". The shared rule sets `nowrap` on rows with no
     field, which has to be released for the pair to stack at all.

     Zebra striping cannot carry the grouping once the row is two lines tall - on
     an unstriped row the button sits under a name with nothing joining them, so
     it reads as floating between two rows. Every row gets its own outline here
     and the alternating fill is dropped. */
  #content-frame:not(.bare-frame) .content.account > .ow-roster .description {
    flex-wrap: wrap !important;
    align-items: stretch;
    padding: 8px 10px;
    border: 1px solid var(--brand-line);
    background-color: transparent !important;
  }

  #content-frame:not(.bare-frame) .content.account > .ow-roster .description + .description {
    margin-top: 8px;
  }

  /* The transparent fill above is `!important` to beat the zebra rule, so hover
     has to be restated at the same weight or it stops giving any feedback. */
  #content-frame:not(.bare-frame) .content.account > .ow-roster .description:hover {
    background-color: var(--brand-blue-soft) !important;
  }

  #content-frame:not(.bare-frame) .content.account > .ow-roster .description .button {
    margin-left: 0 !important;
    flex: 1 1 100%;
  }

  #content-frame:not(.bare-frame) .ow-pract-name {
    flex: 1 1 100%;
  }
}

#content-frame:not(.bare-frame) .content.account > .ow-roster h3 {
  display: flex;
  align-items: center;
  gap: 8px;
  padding-bottom: 8px;
  border-bottom: 1px solid var(--brand-line);
}

/* The count belongs in the heading: "42 active" is the question this screen
   answers, and reading it off a scrolled list means counting rows. */
#content-frame:not(.bare-frame) .ow-roster-count {
  padding: 1px 8px;
  border-radius: 10px;
  background-color: var(--brand-blue-soft);
  color: var(--brand-navy);
  font-size: 12px;
  font-weight: 600;
}

/* Zebra rows give the pairing between a name and its button, which is what the
   old `float: right` with a -10px margin was reaching for and missing. The row is
   already a flex row from the shared `.description` rule above. */
/* `min-height`, never `height`: the pitch has to be a set number for the list
   height above to be a whole number of rows, but a fixed height is wrong the
   moment a name wraps. At 360px the names run to three lines, and with `height`
   the text overflowed the row - zebra stripes out of step with their text, and
   the button outside its own tinted band. A wrapped row grows instead. */
#content-frame:not(.bare-frame) .content.account > .ow-roster .description {
  margin-top: 0;
  box-sizing: border-box;
  min-height: var(--ow-roster-row);
  padding: 4px 10px;
  border-radius: 6px;
  color: var(--brand-ink);
}

#content-frame:not(.bare-frame) .content.account > .ow-roster .description:nth-of-type(odd) {
  background-color: var(--ow-canvas);
}

#content-frame:not(.bare-frame) .content.account > .ow-roster .description:hover {
  background-color: var(--brand-blue-soft);
}

/* The name needs a box of its own so a long one wraps inside the row instead of
   pushing the button out; the shared rule sets `nowrap` on rows with no field. */
#content-frame:not(.bare-frame) .ow-pract-name {
  flex: 1 1 auto;
  min-width: 0;
}

/* The empty-state line is prose, not a row, so it must not inherit the fixed row
   pitch - its text wraps at narrow widths. */
#content-frame:not(.bare-frame) .ow-empty {
  height: auto !important;
  padding: 14px 10px !important;
  color: var(--brand-muted) !important;
  font-style: italic;
}

/* --------------------------------------------------------------------------
   Customize Activities
   --------------------------------------------------------------------------
   The Add and Delete rows each pair a button with a field. `tsbs_styl.css` gives
   every control `margin-top: 15px` while their heights differ - button 29px, text
   input 35px, select 38px - so the pairs sat up to 4.6px off a shared baseline
   (measured `#Del` cy=650.1 against `#delAct` cy=654.7).

   A flex row with `align-items: center` pairs each button to its own field and
   removes the vertical margins that caused the drift, the same fix as My Account.
   The page's `style="width:70% !important"` on #newItem stays: it is what stops
   the globally-scoped `select, textarea, input[type=password]` rule in
   tsbs_styl.css (broken comma in a `.log-in` selector) forcing the field to 100%.
   -------------------------------------------------------------------------- */
#content-frame:not(.bare-frame) .content.caseload .form-left-half,
#content-frame:not(.bare-frame) .content.caseload .form-right-half {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 10px;
}

/* `tsbs_styl.css:346` gives these `width: 49%` with `float: left` / `float: right`,
   sized for the registration captcha - two 306px panels that genuinely fill a form.
   On Manage IEP Goals the same classes hold one dropdown each, so the halves took half
   the card regardless of content and the two controls ended up **305px apart** at
   opposite ends of a 1843px row, with STUDENT at x=129 and IEP DATES at x=1054.

   Sized to content here and laid out as a row, so the label/control pairs sit together
   and the surplus goes to the right-hand end rather than between them. Scoped to a card
   that contains a grid, so registration and the other `.form-*-half` screens - which do
   want two equal columns - are untouched. */
#content-frame:not(.bare-frame) .content.caseload:has(.ow-grid-scroll) > form > .desc,
#content-frame:not(.bare-frame) .content.caseload:has(#gbox_list) > form > .desc {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 12px 28px;
}

#content-frame:not(.bare-frame) .content.caseload:has(.ow-grid-scroll) .form-left-half,
#content-frame:not(.bare-frame) .content.caseload:has(.ow-grid-scroll) .form-right-half,
#content-frame:not(.bare-frame) .content.caseload:has(#gbox_list) .form-left-half,
#content-frame:not(.bare-frame) .content.caseload:has(#gbox_list) .form-right-half {
  width: auto;
  min-width: 0;
  float: none;
  margin-bottom: 0;
  flex: 0 1 auto;
}

/* A select in one of those halves sizes to its own longest option, with room for the
   dropdown arrow. The IEP picker shipped with an inline `width: 190px` that cut
   "08/06/2025 - 07/31/2026" to "...07/31/202" - a DATE RANGE missing its last digit,
   which is worse than a truncated label because the remaining text still looks like a
   valid value. The inline width is removed from the page in the same change.

   `min-width: fit-content` is the floor; `max-width` keeps a long student name from
   taking the whole row. Note `scrollWidth` does NOT catch this - it excludes the arrow,
   so the probe read `truncated: false` on a visibly cut value and only the screenshot
   showed it. */
#content-frame:not(.bare-frame) .content.caseload:has(.ow-grid-scroll) .form-left-half > select,
#content-frame:not(.bare-frame) .content.caseload:has(.ow-grid-scroll) .form-right-half > select,
#content-frame:not(.bare-frame) .content.caseload:has(#gbox_list) .form-left-half > select,
#content-frame:not(.bare-frame) .content.caseload:has(#gbox_list) .form-right-half > select {
  width: auto;
  min-width: fit-content;
  max-width: 100%;
  padding-right: 28px;
}

/* A control in a flex row that centres must not carry an asymmetric vertical margin.

   `align-items: center` centres the MARGIN box, not the border box, and
   `tsbs_styl.css:1058` gives every `select` in the app `margin-top: 15px` with no bottom
   margin. The selector there reads `.log-in input[type="text"], select, textarea, ...`
   and the commas make `select` top-level, so a rule written for the login card reaches
   every screen. Centring that lopsided box puts the visible control half the margin below
   the text beside it: measured **-8px** on `STUDENT:` (Manage IEP Goals) and
   `SERVICE AREA:` (Customize Activities) at every width, growing to -51.5px at 480px
   where the row wraps and the label keeps the first line to itself.

   Scoped to `.form-*-half`, NOT to the grid-guarded selector above. That guard asks
   whether the screen has a grid, which has nothing to do with whether a row centres -
   Activities has the identical row and no grid, so the first version of this fix left it
   drifting.

   That 15px was doing TWO jobs, and removing it alone traded one defect for another: it
   also separated the row from the heading band above. Zeroed on the control, the dropdown
   came to rest 3px under the h1 on Manage IEP Goals and touching it at **0px** on
   Customize Activities. The spacing is therefore moved up to the ROW, where it separates
   the row from what precedes it without entering the centring arithmetic - a margin on
   the flex container is outside the box `align-items` centres, a margin on a flex item is
   inside it. Same 15px, one job each.

   Fixed here rather than on the global rule because that rule also supplies
   `width: 100%`, `display: block` and the control font to all 22 selects in the app.
   Measured before choosing: 2 of the 22 drift, 20 are correct - so the narrow fix
   corrects exactly the two and cannot regress the rest. Same cause as the input/button
   pairs on My Account and the label/field pairs in the caseload dialog. */
#content-frame:not(.bare-frame) .content .form-left-half > select,
#content-frame:not(.bare-frame) .content .form-right-half > select,
#content-frame:not(.bare-frame) .content .form-left-half > input,
#content-frame:not(.bare-frame) .content .form-right-half > input {
  margin-top: 0;
  margin-bottom: 0;
}

/* The vertical gap between stacked form rows. One value for the whole app.

   `tsbs_styl.css` gives `.form-left-half` / `.form-right-half` `margin-bottom: 5px`.
   Overridden here rather than edited there because that file is the legacy base and
   the token belongs with the rest of the shared layout - one edit reaches every screen
   that stacks these rows.

   The gap is the ROW's bottom margin, not a margin on the control: the rule above
   zeroes control margins so `align-items: center` has a symmetric box to centre (an
   asymmetric margin drops the control below its label). Spacing therefore lives on the
   wrapper and centring lives on the item - one job each. */
#content-frame:not(.bare-frame) .content .form-left-half,
#content-frame:not(.bare-frame) .content .form-right-half {
  margin-bottom: 7px;
}

/* A `.label` introducing a control needs the same gap below it whatever the control is.

   `tsbs_styl.css` gives `.label` only `margin-top: 15px`, so the space beneath it came
   from the CONTROL's own top margin - and the rule above deliberately zeroes that on
   `input`/`select` so `align-items: center` has a symmetric box. A `textarea` is not in
   that selector and kept its 15px, which is why Account Registration's two Terms &
   Conditions columns disagreed: measured **15px** under "Please carefully read our
   electronic signature agreement" against **0px** under "Indicate acceptance of
   electronic signature terms & conditions", leaving the signature box flush against
   its own label.

   Putting the gap on the LABEL fixes both columns with one value and cannot re-break
   the centring, because a margin below the label is outside the box `align-items`
   centres. Verified before adding: Register is the only screen in the shell with a
   `.label` immediately followed by a control, so this corrects exactly that screen -
   the other eight files using the class render none inside a `.form-*-half`. */
#content-frame:not(.bare-frame) .content .form-left-half > .label,
#content-frame:not(.bare-frame) .content .form-right-half > .label {
  margin-bottom: 15px;
}

/* Only the FIRST row in its container. The 15px separates a dropdown row from the
   heading band above it, so it belongs solely to a row that has a heading above it -
   and a row that follows another `.form-*-half` has a sibling above it instead.

   Without `:first-child` this fired on every row containing a select, which put a
   hole in the middle of an even form: Account Registration's five rows sat on the
   shared row gap and the SCHOOL DISTRICT / SERVICE AREA row measured **20px**, 4x its
   neighbours, purely because it happens to hold the two dropdowns.

   Measured across every select row in the app before narrowing it. The rows this
   rule is FOR are first-child (`#stud` on Manage IEP Goals, `#svcarea` on Customize
   Activities - both report no previous element sibling); the rows it was breaking
   are not (`#district_id`/`#credentials` on Registration). `#delAct`, `#goalStudent`
   and `#iepStudent` are mid-container too and never needed the offset. */
#content-frame:not(.bare-frame) .content .form-left-half:first-child:has(> select),
#content-frame:not(.bare-frame) .content .form-right-half:first-child:has(> select) {
  margin-top: 15px;
}

/* Edit Caseload matches the selector above (it has a `#gbox_list` too) but must NOT be
   content-sized. Its `#iepStudent` is filled from JS after load, and `fit-content` on a
   `<select>` measures the SELECTED option rather than the longest - so it collapsed to
   the width of whatever happened to be selected. Measured against the pre-change
   baseline: 454px before, 75px at 360px after, with "SELECT STUDENT IN CASELOAD"
   needing 178px. Caught by SELECT-TRUNCATED on the run that introduced it.

   Restored to filling its half, which is what that screen was built for - the two
   halves there hold a full-width picker each, not a short label-and-dropdown pair.

   The parent is a FLEX container, so `width: 100%` alone does nothing here - a later
   `flex: 1 1 0` on the same element resolves the basis to 0 and the select collapses to
   its content. The flex shorthand is what has to change, and `min-width` has to come off
   `0` or the item is still allowed to shrink below its text. */
#content-frame:not(.bare-frame) .content.caseload .form-left-half > select#iepStudent,
#content-frame:not(.bare-frame) .content.caseload .form-right-half > select#iepStudent {
  flex: 1 1 auto;
  width: 100%;
  min-width: 12em;
}

#content-frame:not(.bare-frame) .content.caseload .form-left-half > .button,
#content-frame:not(.bare-frame) .content.caseload .form-right-half > .button {
  margin-top: 0;
  margin-bottom: 0;
  vertical-align: baseline;
  flex: 0 0 auto;
}

/* The field must size from what is LEFT after the button and gap, not from a
   percentage of the whole column. The select inherits `width: 100%` from a global
   rule in tsbs_styl.css (a broken comma in a `.log-in` selector leaks `select`,
   `textarea` and `input[type=password]` app-wide), measured against the full
   column - so at 1200px the pair needed 524px of a 533px column and wrapped
   anyway, leaving a button 41.9px from the field it belongs to.

   `flex: 1 1 0` with `min-width: 0` lets the field shrink to the space that is
   actually free. The page also carried `style="width:70% !important"` on the text
   input to fight the same global rule; that is removed, so no `!important` is
   needed here either. */
#content-frame:not(.bare-frame) .content.caseload .form-left-half > input[type='text'],
#content-frame:not(.bare-frame) .content.caseload .form-right-half > select {
  flex: 1 1 0;
  min-width: 0;
  width: auto;
  margin-top: 0;
  margin-bottom: 0;
  vertical-align: baseline;
}

/* --------------------------------------------------------------------------
   Training and Forms
   --------------------------------------------------------------------------
   These rules were a `<style>` block in the page, which Rule 3 forbids. They are
   scoped under `.training` rather than hoisted, because bare `.left` / `.right` /
   `.month` / `.left35` are generic enough to collide - `.column` and `.row:after`
   already exist globally in tsbs_styl.css, and nine screens use class names
   containing "left" or "right". Verified that bare `.left` and `.right` appear on
   no other screen.

   The page also carried `style='width:780px'` on the card, which capped it well
   inside the shell and is now removed; the card sizes with every other screen.
   -------------------------------------------------------------------------- */
#content-frame:not(.bare-frame) .content.training {
  display: block;
  padding: 20px 25px 40px;
}

#content-frame:not(.bare-frame) .content.training .row {
  display: flex;
  flex-wrap: wrap;
  gap: 24px;
  align-items: flex-start;
}

#content-frame:not(.bare-frame) .content.training .row:after {
  content: none;
}

#content-frame:not(.bare-frame) .content.training .column {
  float: none;
  min-width: 0;
}

#content-frame:not(.bare-frame) .content.training .row > .left,
#content-frame:not(.bare-frame) .content.training .row > .right {
  flex: 1 1 340px;
  width: auto;
}

/* The bus-log tables inside each column pair a month label with its PDF/EXCEL
   links, so they stay a two-column grid rather than becoming flex items. */
#content-frame:not(.bare-frame) .content.training .row .row {
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto;
  gap: 0 14px;
  align-items: start;
}

#content-frame:not(.bare-frame) .content.training .left35,
#content-frame:not(.bare-frame) .content.training .month {
  width: auto;
}

/* The Contact link was a `float: right` div placed BEFORE the h1, so it escaped the
   heading band and overlapped the card edge. It now sits inside the h1, which is a
   flex row, and the link is pushed to the band's right edge.

   A growing title, not `margin-left: auto` on the link, does the pushing. An auto
   margin resolves to a USED pixel value on the single-line layout and that value
   stays applied when the link wraps to its own line: measured
   `margin-left: 161.828px` at 360px, indenting the wrapped link to x=204.8
   instead of aligning it under the title at x=88.2.

   `justify-content: space-between` was tried instead and is wrong here: the icon
   and the title are separate flex items, so it opened the gap between THEM too -
   the title landed at x=830 at 1903px. The title therefore carries its own
   `.h1-title` span (added in the page) so it is a real flex item that can absorb
   the free space; `flex-grow` on it pins the link right with no margin to leak
   onto the wrapped line. */
#content-frame:not(.bare-frame) .content.training > h1 {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  /* Free-space distribution gives no MINIMUM spacing - measured 1014px at
     1400px collapsing to 2px at 360px, where the title and the link touched.
     A gap holds the floor once there is no free space left to distribute. */
  gap: 6px 18px;
}

#content-frame:not(.bare-frame) .content.training > h1 .h1-title {
  flex: 1 1 auto;
}

#content-frame:not(.bare-frame) .content.training > h1 .training-link {
  /* `.training-link` carries `margin-left: 10px` globally for the in-list links;
     in the heading band that offset re-indents the wrapped line. */
  margin-left: 0;
  font-size: 12px;
}

#content-frame:not(.bare-frame) .content.training h3 {
  margin: 0 0 4px;
  font-size: 17px;
  color: var(--brand-navy);
}

#content-frame:not(.bare-frame) .content.training h6 {
  display: block;
  margin: 22px 0 10px;
  padding-bottom: 6px;
  color: var(--brand-navy);
  border-bottom: 1px solid var(--brand-line);
}

#content-frame:not(.bare-frame) .content.training .row > .column > h3 + h6 {
  margin-top: 16px;
}

/* The PCS column has no `h3`, so its first `h6` started above the "BUS LOGS"
   heading beside it and the two tables read as unrelated.

   Set the margin directly on that h6 rather than padding the column. Padding was
   tried and under-delivers: it stops the h6's own 22px top margin collapsing
   through the column edge, so part of the padding is consumed and the heading
   lands short. Two failed attempts before this - a `::before` spacer (grows the
   h6's box, putting the gap above its own underline) and `padding-top` (margin
   collapsing) - so measure the result, do not assume the offset arrives intact.

   Target: columns 0 and 1 put their first h6 at y=294. */
#content-frame:not(.bare-frame) .content.training .row > .column:not(:has(> h3)) > h6:first-child {
  margin-top: 35px;
}

/* Months with no bus log on file. These sit in the same list as `.training-link`,
   which is --brand-blue (6.07:1 on white), so this is the "same list, not
   clickable" state and must stay in the blue family while clearly receding.
   --brand-muted is a desaturated blue-grey and measures 4.26:1 - readable, and
   quieter than the links beside it.
   Measured alternatives, all rejected on contrast: --brand-amber 2.03:1,
   --brand-amber-dark 2.59:1, navy at 45% opacity 2.45:1. The original #e9482d
   also made the non-links louder than the links, which was backwards. */
#content-frame:not(.bare-frame) .content.training .noLink {
  font-size: 12px;
  font-weight: bold;
  color: var(--brand-muted);
  cursor: default;
  text-decoration: none;
  margin-left: 10px;
  text-transform: uppercase;
}

/* The video and slideshow panels are toggled by JS and are wider than the column
   they sit in, so they scroll rather than push the card open. */
#content-frame:not(.bare-frame) .content.training iframe,
#content-frame:not(.bare-frame) .content.training #RMTS {
  max-width: 100%;
}

/* --------------------------------------------------------------------------
   Contact Us
   --------------------------------------------------------------------------
   Two blocks of contact detail. The page previously centred both and pushed the
   right one across with an inline `margin: 0 75px 0 0px`, so the columns lined up
   with nothing. They now use the shared `.form-*-half` classes, which the
   `max-width: 700px` rule further down already stacks.

   The contact strings are left exactly as they are: they name TSBS while the
   footer names Onward Learning, which is the unresolved business question tracked
   in Docs/TSBS-REFERENCES.md, not a styling decision.
   -------------------------------------------------------------------------- */
#content-frame:not(.bare-frame) .ow-contact {
  padding: 26px 25px 30px;
  background-color: transparent;
}

/* The two blocks separate their lines with `<br/><br/>` pairs, so every empty line
   is scaled by line-height. At 1.85 those doubled gaps left the taller block
   overhanging the shorter by ~60px. 1.55 keeps the paragraphs readable without
   multiplying the markup's own spacing, and `align-items: stretch` on the flex row
   below makes both cards the same height regardless of content. */
#content-frame:not(.bare-frame) .ow-contact .desc {
  display: flex;
  flex-wrap: wrap;
  gap: 20px;
  align-items: stretch;
}

#content-frame:not(.bare-frame) .ow-contact .form-left-half,
#content-frame:not(.bare-frame) .ow-contact .form-right-half {
  box-sizing: border-box;
  flex: 1 1 280px;
  float: none;
  width: auto;
  min-width: 0;
  margin: 0;
  padding: 22px 24px;
  text-align: left;
  background-color: var(--brand-blue-soft);
  border: 1px solid var(--brand-line);
  border-radius: var(--ow-radius);
  line-height: 1.55;
}

#content-frame:not(.bare-frame) .ow-contact b {
  display: block;
  margin-bottom: 4px;
  font-size: 14px;
  color: var(--brand-navy);
}

/* --------------------------------------------------------------------------
   Radio and checkbox rows
   --------------------------------------------------------------------------
   The markup writes radios as `<input> LABEL TEXT</input>` - text as a sibling
   node, not in a <label> - so there is nothing to bind a click to and no box to
   space. Giving the group a flex column and the accent colour makes the options
   read as a set. Applies to Settings and Preferences, Calendar View and Create
   Sessions, which all use bare radios.
   -------------------------------------------------------------------------- */
/* `normalize.css` gives every radio and checkbox `position: relative; top: 2.5px`
   - a legacy nudge for inline layout that shifts the painted control 2.5px below
   its own box. It applies app-wide, so every one of these controls is 2.5px lower
   than it measures, and any per-screen margin written to "centre" one is really
   compensating for this. Found by enumerating the matching rules rather than by
   guessing an offset, after two wrong nudges on the Settings radios.
   `vertical-align` handles the inline case, which is what the shift was for. */
#content-frame:not(.bare-frame) .content input[type='radio'],
#content-frame:not(.bare-frame) .content input[type='checkbox'] {
  accent-color: var(--brand-navy);
  width: 15px;
  height: 15px;
  margin: 0 8px 0 0;
  position: static;
  top: auto;
  vertical-align: -2px;
  cursor: pointer;
}

/* The option text is a bare sibling text node, not wrapped in a <label>, so flex
   or grid on this container would separate each radio from its own words. It stays
   a block: the `<br>` tags in the markup already break the lines, and the radios
   are inline-block against their text.

   The option text is wrapped in a <label> in the markup, which is what lets each
   option become its own flex row: the radio stays beside its words, and a long
   option wrapping at narrow widths keeps its continuation lines aligned with the
   first rather than running back under the radio. The label also makes the text
   itself clickable, which bare text nodes never were. */
#content-frame:not(.bare-frame) #goalRB {
  padding: 16px 18px;
  background-color: var(--brand-blue-soft);
  border: 1px solid var(--brand-line);
  border-radius: var(--ow-radius);
}

/* Direct child only: the option labels also contain a span. */
#content-frame:not(.bare-frame) #goalRB > span {
  display: block;
  margin-bottom: 10px;
  color: var(--brand-navy);
}

/* `.ow-radio` itself is not screen-specific - only the panel chrome above is. It
   is used by Settings and Preferences (`#goalRB`) and by the recurrence dialog on
   Review Sessions - Calendar View (`#recurRB`), which had the same bare-text-node
   markup. Scoping it to one id would mean a second copy for the second screen.

   Deliberately NOT prefixed with `#content-frame`: jQuery UI reparents a dialog to
   be a direct child of `<body>` when it opens, so anything inside one leaves the
   content frame and every `#content-frame`-scoped rule silently stops applying.
   Measured on the recurrence dialog - `closest('#content-frame')` returned null and
   the three options rendered inline as one run of text. Any component that can
   appear in a dialog needs the same treatment. */
.ow-radio {
  display: flex;
  align-items: flex-start;
  gap: 9px;
  margin: 0 0 8px;
  line-height: 1.45;
  cursor: pointer;
}

.ow-radio:last-child {
  margin-bottom: 0;
}

/* The row is `align-items: flex-start` so a wrapped option keeps its control on
   the first line. A 15px control against a 17.4px line box then sits 2.4px high,
   which a `margin-top` used to paper over - wrongly, because the real offset was
   normalize.css's `top: 2.5px` (see the rule above). With that removed, the
   control is centred on the FIRST line only. `align-self: center` is wrong here -
   it centres on the whole label, so a wrapped option pulled the control 8.9px low
   at 480px and 17.6px at 360px, away from the line it belongs to. Sizing the
   control's own box to the line height and centring the 15px glyph inside it with
   `box-sizing: content-box` plus symmetric padding keeps it on line one at every
   width. Measured drift: 0.19px unwrapped and wrapped alike. */
.ow-radio input[type='radio'] {
  margin: 0;
  flex: 0 0 auto;
  align-self: flex-start;
  box-sizing: content-box;
  padding: calc((1.45em - 15px) / 2) 0;
  /* normalize.css's `position: relative; top: 2.5px` reaches here too, and the
     app-wide reset that cancels it is `#content-frame`-scoped, so a radio in a
     dialog would keep the 2.5px shift. */
  position: static;
  top: auto;
  width: 15px;
  height: 15px;
  accent-color: var(--brand-navy);
  cursor: pointer;
}

/* --------------------------------------------------------------------------
   Forms
   --------------------------------------------------------------------------
   `.nongrid-form` is the standard form container on the account, settings,
   activity and report screens.

   The padding matches the `.content.account/.caseload/.review/.sessions/.log-in`
   rule in tsbs_styl.css, because some screens carry both classes on one element -
   settings.php is `content log-in nongrid-form`. With 26px horizontally the heading
   band's negative margin under-compensated by 1px on those screens and over-
   compensated nowhere else, so the band sat 2px inside the card instead of 1px.

   The BOTTOM value has to match for the same reason: at 26px here against the card's
   40px, Contact Us and Settings ended with a 27px tail while My Account, Configure and
   the PAR report ended with 41px - the same card component finishing three different
   ways depending on which classes a page happened to put on it. Keep all three equal.
   -------------------------------------------------------------------------- */
#content-frame:not(.bare-frame) .nongrid-form {
  box-sizing: border-box;
  padding: 22px 25px 40px;
  background-color: #ffffff;
}

#content-frame:not(.bare-frame) .nongrid-form label {
  color: var(--brand-ink);
}

#content-frame:not(.bare-frame) .nongrid-form input[type='text'],
#content-frame:not(.bare-frame) .nongrid-form input[type='password'],
#content-frame:not(.bare-frame) .nongrid-form input[type='email'],
#content-frame:not(.bare-frame) .nongrid-form input[type='number'],
#content-frame:not(.bare-frame) .nongrid-form select,
#content-frame:not(.bare-frame) .nongrid-form textarea {
  padding: 9px 11px;
  font-family: inherit;
  font-size: 13px;
  color: var(--brand-ink);
  background-color: #ffffff;
  border: 1px solid var(--brand-line);
  border-radius: var(--ow-radius);
  transition: border-color 0.15s ease, box-shadow 0.15s ease;
}

#content-frame:not(.bare-frame) .nongrid-form input[type='text']:focus,
#content-frame:not(.bare-frame) .nongrid-form input[type='password']:focus,
#content-frame:not(.bare-frame) .nongrid-form input[type='email']:focus,
#content-frame:not(.bare-frame) .nongrid-form input[type='number']:focus,
#content-frame:not(.bare-frame) .nongrid-form select:focus,
#content-frame:not(.bare-frame) .nongrid-form textarea:focus {
  outline: none;
  border-color: var(--brand-blue);
  box-shadow: 0 0 0 3px rgba(22, 101, 168, 0.12);
}

/* --------------------------------------------------------------------------
   Reusable content blocks
   --------------------------------------------------------------------------
   Generic on purpose -- any screen that needs an intro paragraph, a boxed
   warning or a read-only generated value uses these rather than inventing its
   own markup. Added when Reset Password needed them; reused from there on.
   -------------------------------------------------------------------------- */

/* Intro paragraph under a page title. */
#content-frame:not(.bare-frame) .ow-lede {
  margin: 0 0 18px;
  font-size: 13px;
  line-height: 1.6;
  color: var(--brand-muted);
}

/* Boxed aside. `.ow-callout-warn` is for policy/compliance text that must not
   be skimmed past -- amber, because it is a caution rather than an error. */
#content-frame:not(.bare-frame) .ow-callout {
  margin: 0 0 20px;
  padding: 14px 18px;
  font-size: 13px;
  line-height: 1.6;
  color: var(--brand-ink);
  background-color: var(--brand-blue-soft);
  border-left: 4px solid var(--brand-blue);
  border-radius: 0 var(--ow-radius) var(--ow-radius) 0;
}

#content-frame:not(.bare-frame) .ow-callout-warn {
  background-color: #fdf6e7;
  border-left-color: var(--brand-amber);
}

#content-frame:not(.bare-frame) .ow-callout-title {
  display: block;
  margin-bottom: 8px;
  font-size: 13px;
  font-weight: 700;
  color: var(--brand-navy);
}

#content-frame:not(.bare-frame) .ow-callout ol,
#content-frame:not(.bare-frame) .ow-callout ul {
  margin: 8px 0 0;
  padding-left: 22px;
}

#content-frame:not(.bare-frame) .ow-callout li {
  margin-bottom: 5px;
}

#content-frame:not(.bare-frame) .ow-callout li:last-child {
  margin-bottom: 0;
}

/* A field label above its control. */
#content-frame:not(.bare-frame) .ow-field {
  margin-bottom: 16px;
}

#content-frame:not(.bare-frame) .ow-field-label {
  display: block;
  margin-bottom: 6px;
  font-size: 12px;
  font-weight: 600;
  letter-spacing: 0.02em;
  text-transform: uppercase;
  color: var(--brand-muted);
}

/* Read-only value the app generated (e.g. a temporary password). Monospace so
   characters that look alike are distinguishable when it is transcribed.

   `max-width` and `box-sizing` are load-bearing: an <input> with no width sizes
   from its `size` attribute, which defaults to 20 characters, and at 16px
   monospace that is wider than a 328px card - measured 18px past the card edge at
   360px. The padding and border have to come out of the width, not add to it. */
#content-frame:not(.bare-frame) input.ow-generated {
  font-family: 'Consolas', 'Menlo', monospace;
  font-size: 16px;
  font-weight: 700;
  letter-spacing: 0.06em;
  color: var(--brand-navy);
  background-color: var(--brand-blue-soft);
  border: 1px dashed var(--brand-blue);
  border-radius: var(--ow-radius);
  padding: 10px 12px;
  box-sizing: border-box;
  max-width: 100%;
}

/* Actions row at the bottom of a form. `clear: both` because the form columns
   above it (.form-quarter and friends) are floats -- without it the divider
   sits alongside the column instead of below it. */
#content-frame:not(.bare-frame) .ow-actions {
  clear: both;
  margin-top: 22px;
  padding-top: 18px;
  border-top: 1px solid var(--brand-line);
}

/* `.block` is `display:block` in tsbs_styl.css, which makes an action button
   stretch the full card width. Shrink it to its label. */
#content-frame:not(.bare-frame) .ow-actions .button.block {
  display: inline-block;
  width: auto;
  padding: 10px 22px;
}

/* A standalone primary submit that is not inside .ow-actions. */
/* "SUBMIT E-SIGNATURE AUTHORIZATION" sized this button 23px past the card edge at
   360px. `max-width` alone is the wrong fix and was measured doing the wrong
   thing: an <input type="button"> renders its value as a single unwrappable text
   run, so constraining the box clipped 50px of the label instead of the box - the
   user could no longer read what they were authorizing, which is worse than the
   overflow.

   `white-space: normal` is what gives the value a line-break opportunity; without
   it every other property here is decoration. The label is legal text, so it is
   wrapped rather than shortened or abbreviated. */
#content-frame:not(.bare-frame) .button.ow-submit {
  display: inline-block;
  margin-top: 4px;
  padding: 11px 24px;
  box-sizing: border-box;
  max-width: 100%;
  white-space: normal;
  height: auto;
  line-height: 1.35;
}

/* Legal / agreement text shown in a readonly <textarea>. Sized so the box
   ends on a blank line rather than mid-sentence -- a clipped final word reads
   as a rendering fault, and this is text the user is being asked to have
   read. Resizable vertically so a long agreement can be opened up. */
#content-frame:not(.bare-frame) textarea.ow-legal {
  display: block;
  width: 100%;
  max-width: 900px;
  height: 260px;
  box-sizing: border-box;
  margin-bottom: 20px;
  padding: 16px 18px;
  font-family: inherit;
  font-size: 12.5px;
  line-height: 1.65;
  color: var(--brand-ink);
  background-color: #fcfdfe;
  border: 1px solid var(--brand-line);
  border-radius: var(--ow-radius);
  resize: vertical;
}

/* The signature input is an attestation, so it is given the weight of one:
   wider, larger text, and an amber underline that reads as a signature line
   rather than a generic form field. */
#content-frame:not(.bare-frame) input#signature {
  display: block;
  width: 100%;
  max-width: 420px;
  box-sizing: border-box;
  margin-bottom: 6px;
  padding: 12px 14px;
  font-family: inherit;
  font-size: 17px;
  color: var(--brand-navy);
  background-color: #ffffff;
  border: 1px solid var(--brand-line);
  border-bottom: 2px solid var(--brand-amber);
  border-radius: var(--ow-radius) var(--ow-radius) 0 0;
}

#content-frame:not(.bare-frame) input#signature:focus {
  outline: none;
  border-color: var(--brand-blue);
  border-bottom-color: var(--brand-amber-dark);
}

#content-frame:not(.bare-frame) .ow-required-note {
  margin: 0 0 16px;
  font-size: 11px;
  color: var(--brand-muted);
}

/* Divider heading inside a long form. On the registration page this replaced a
   `<span style="color:white">` that existed only to add vertical space -- it
   rendered as white-on-white text, i.e. content nobody could read. */
#content-frame:not(.bare-frame) .ow-section-head {
  clear: both;
  margin: 26px 0 16px;
  padding-top: 18px;
  font-size: 15px;
  font-weight: 600;
  color: var(--brand-navy);
  border-top: 1px solid var(--brand-line);
}

/* `.form-quarter` is a fixed 25% float from tsbs_styl.css. At 1920px that is
   ~460px, which is fine, but the selects inside it inherit no width and
   collapse to their content. Giving the column a floor and its controls full
   width keeps a single-column form readable at any viewport without each
   screen restating it.

   The floor needs a ceiling. A bare `min-width: 320px` beats the parent's width,
   so below a ~380px viewport the column grew past the card and took its contents
   with it - `#newPwd` measured 43px outside the card at 360px, and no `max-width`
   on the input could pull it back because the input was the victim, not the cause.
   `min(320px, 100%)` keeps the floor where there is room for it and yields to the
   card where there is not. */
#content-frame:not(.bare-frame) .nongrid-form .form-quarter {
  box-sizing: border-box;
  min-width: min(320px, 100%);
}

#content-frame:not(.bare-frame) .ow-field > select,
#content-frame:not(.bare-frame) .ow-field > input {
  width: 100%;
  box-sizing: border-box;
}

/* jQuery UI replaces the <select> with a button + menu; the real element is
   hidden, so the width has to land on the generated widget too. */
#content-frame:not(.bare-frame) .ow-field .ui-selectmenu-button {
  width: 100%;
  box-sizing: border-box;
}

/* --------------------------------------------------------------------------
   Buttons
   --------------------------------------------------------------------------
   `.button` / `.input-button` are the app-wide action classes. They were
   amber-on-amber at 12px; navy is now the primary action colour so the
   default button matches the login card's Sign In.
   -------------------------------------------------------------------------- */
#content-frame:not(.bare-frame) .button,
#content-frame:not(.bare-frame) .input-button input,
#content-frame:not(.bare-frame) input[type='submit'],
#content-frame:not(.bare-frame) input[type='button'] {
  font-family: inherit;
  font-size: 13px;
  font-weight: 600;
  letter-spacing: 0.03em;
  border-radius: var(--ow-radius);
  cursor: pointer;
  transition: background-color 0.15s ease, border-color 0.15s ease, color 0.15s ease;
}

/* Most `.button`s in the app are anchors, so they inherit the link underline.
   The pages work around it with an inline `style='text-decoration:none'` -
   repeated on a dozen buttons across the screens visited so far. Declared once
   here instead; the inline copies are removed as each screen is redesigned. */
#content-frame:not(.bare-frame) a.button,
#content-frame:not(.bare-frame) a.button:hover {
  text-decoration: none;
}

/* --------------------------------------------------------------------------
   Plain data tables - `.ow-table`
   --------------------------------------------------------------------------
   For the handful of real `<table>` elements that are not jqGrid. A previous
   pass wrote this against `table.data-table`, a class that **matches nothing
   anywhere in the app** - so it styled nothing. Renamed to the `.ow-*`
   convention every other component here uses, and written to work on a bare
   `<table>` inside a card so a page only has to add the class.

   Not applied to the table-as-layout markup that predates this redesign
   (`esigpdf.php` builds a PDF, `mailer.php` builds an HTML email body - email
   clients cannot reach this stylesheet and need table layout anyway).
   -------------------------------------------------------------------------- */
#content-frame:not(.bare-frame) .content .ow-table {
  width: 100%;
  border-collapse: collapse;
  font-size: 13px;
  color: var(--brand-ink);
}

#content-frame:not(.bare-frame) .content .ow-table th {
  padding: 10px 12px;
  background-color: var(--brand-blue-soft);
  border-bottom: 1px solid var(--brand-line);
  color: var(--brand-navy);
  font-weight: 600;
  text-align: left;
  white-space: nowrap;
}

#content-frame:not(.bare-frame) .content .ow-table td {
  padding: 9px 12px;
  border-bottom: 1px solid var(--brand-line);
  vertical-align: middle;
}

#content-frame:not(.bare-frame) .content .ow-table tbody tr:nth-child(even) td {
  background-color: #fafcfe;
}

#content-frame:not(.bare-frame) .content .ow-table tbody tr:last-child td {
  border-bottom: none;
}

/* A wide table scrolls inside its own wrapper rather than widening the page.
   This is the same principle the grid block below enforces: content that does
   not fit gets its own scroller, never a wider `<body>`. */
#content-frame:not(.bare-frame) .content .ow-table-scroll {
  width: 100%;
  max-width: 100%;
  overflow-x: auto;
  border: 1px solid var(--brand-line);
  border-radius: var(--ow-radius);
  background-color: #ffffff;
}

/* --------------------------------------------------------------------------
   jqGrid skin
   --------------------------------------------------------------------------
   ui.jqgrid.css + jquery-ui.theme.css paint the grid with the jQuery UI
   default (grey gradients, 1px ridged borders). These rules recolour it to the
   navy/blue palette. Layout properties (widths, heights, positioning) are left
   alone on purpose -- jqGrid measures its own DOM in JS and reflows from those
   values, so changing them breaks column alignment and the resize handles.
   Colour, font and border-colour are safe; box metrics are not.
   -------------------------------------------------------------------------- */
#content-frame:not(.bare-frame) .ui-jqgrid {
  border-color: var(--brand-line);
  border-radius: var(--ow-radius);
  font-family: inherit;
}

/* Caption bar above the grid. */
#content-frame:not(.bare-frame) .ui-jqgrid .ui-jqgrid-titlebar,
#content-frame:not(.bare-frame) .ui-jqgrid .ui-jqgrid-caption {
  background: var(--brand-navy);
  background-image: none;
  border-color: var(--brand-navy);
  color: #ffffff;
  font-weight: 600;
}

/* Column headers.

   This block used to set colour ONLY - `background`, `border-color`, `color`,
   `font-weight` - and that was the whole defect behind "the table still looks old".
   The grid inherited the vendor's typography and spacing: a **10px** header label with
   `padding: 0 2px`, and **11px** cells with `padding: 4px 4px 3px`, against the
   `.ow-table` standard of 13px with `10px 12px`. Colour alone does not modernise a
   table; the density is what reads as a 1990s spreadsheet.

   Two definitions of "table" had drifted apart - `.ow-table` for plain markup and this
   skin for the grids - so the numbers below are lifted from `.ow-table` deliberately.
   Change one and change the other.

   Measured before: header 10px / `0 2px`, cell 11px / `4px 4px 3px`, a vertical rule on
   every cell, row heights ragged at 46/46/33/46/33/33/33/30px. */
#content-frame:not(.bare-frame) .ui-jqgrid .ui-jqgrid-hdiv,
#content-frame:not(.bare-frame) .ui-jqgrid .ui-jqgrid-htable th,
#content-frame:not(.bare-frame) .ui-jqgrid .ui-jqgrid-labels th.ui-th-column {
  background: var(--brand-blue-soft);
  background-image: none;
  border-color: var(--brand-line);
  color: var(--brand-navy);
  font-weight: 600;
}

/* Header typography and spacing. jqGrid puts the label in a `div.ui-th-div` inside the
   `th`, so the padding goes on that div - the vendor sets `padding: 0` on the `th` and
   the div would otherwise sit flush against the cell edge.

   `white-space: nowrap` is kept from the vendor, deliberately. A first attempt used
   `normal` to match `.ow-table th`, and "GOAL/OBJECTIVE TEXT" then wrapped inside a
   narrow column and took the whole header strip from 25px to **80px** - worse than the
   cramped 10px it replaced. `.ow-table` can wrap because its columns size to content;
   a jqGrid column is a fixed width from `colModel`, so its header cannot.

   12px rather than `.ow-table`'s 13px for the same reason: at 13px the label needs more
   horizontal room than several of these columns have. The header is the one place the two
   table definitions legitimately differ, because only one of them has fixed columns. */
#content-frame:not(.bare-frame) .ui-jqgrid .ui-jqgrid-labels th.ui-th-column div.ui-th-div,
#content-frame:not(.bare-frame) .ui-jqgrid .ui-jqgrid-htable th div.ui-th-div {
  padding: 4px 4px;
  font-size: 12px;
  line-height: 1.25;
  white-space: normal;
  overflow: visible;
  word-break: normal;
  overflow-wrap: break-word;
}

/* Three attempts here, and the measurements decided each one:

     `white-space: normal` + 13px + 12px padding -> "GOAL/OBJECTIVE TEXT" wrapped and
        the header strip went 25px -> **80px**. Worse than the cramped original.
     `nowrap` + `ellipsis` + 10px padding        -> header 42px, but "MY GOAL" rendered
        as "MY ..." in its 55px column. A truncated header hides which column you are in.
     `normal` + 12px + 4px padding               -> header 42px, "MY GOAL" on two lines,
        nothing truncated. This one.

   The difference between the first and the last is the font and the padding, not the
   wrapping: a jqGrid column is a FIXED width from the page's `colModel`, and those
   numbers were chosen against the vendor's 2px padding, so the header only ever has as
   much room as the page already allotted. `.ow-table` can afford 13px/12px because its
   columns size to their content; this one cannot, and that is the one place the two
   table definitions legitimately differ.

   The action columns have EMPTY headers (`lock`, `del`, `edit`, `add` on Manage IEP
   Goals). jqGrid measures a fixed 16px box for those divs, so padding a label-less one
   put 23px of content inside a 16px box - `CONTENT-OVER-BOX x4` at all nine widths, the
   gate catching a defect I had just introduced.

   The padding therefore sits on the `th`, not on the div. Two attempts at selecting only
   the blank divs both failed, and the reason is worth recording: a column label is a TEXT
   NODE, and CSS cannot select on one. `:empty` does not match (the div holds a
   290-character `<span class="s-ico">` sort-arrow block, so `textContent` is `""` while
   `innerHTML` is not), and `:not(:has(> :not(.s-ico)))` matched EVERY header including
   the labelled ones, because none of them has a non-icon element child. Verified against
   the rendered DOM both times.

   Putting it on the `th` was tried third and is WORSE: jqGrid sizes header and body
   cells independently from `colModel`, so header padding shifts the header row only and
   every column drifts cumulatively - measured 0, 4, 8, 12, 16, 20px against the body.
   `COL-MISALIGNED` on all six columns. Header padding must never go on the `th`.

   So it stays on the div, and the blank case is handled by giving the div a `min-height`
   instead of padding: a labelled div grows to its text, a blank one stays at the 16px
   box jqGrid measured for it and nothing overflows. */
#content-frame:not(.bare-frame) .ui-jqgrid .ui-jqgrid-labels th.ui-th-column div.ui-th-div,
#content-frame:not(.bare-frame) .ui-jqgrid .ui-jqgrid-htable th div.ui-th-div {
  box-sizing: border-box;
  min-height: 30px;
  display: flex;
  align-items: center;
  justify-content: center;
}

/* The action columns have no header text, and jqGrid still paints each one as its own
   filled, bordered cell - four grey boxes stacked at the top-left corner, which read as
   empty columns the user is missing something in rather than as a header for the
   lock/delete/edit buttons below.

   They keep the header FILL (so the strip is one continuous band) and lose the vertical
   dividers between them, so the group reads as a single blank run. Matched by "no
   element child other than the sort icon" is not possible here - a label is a text node,
   which CSS cannot see - so this uses the ids the pages actually emit. */
#content-frame:not(.bare-frame) .ui-jqgrid .ui-jqgrid-labels th#list_lock,
#content-frame:not(.bare-frame) .ui-jqgrid .ui-jqgrid-labels th#list_del,
#content-frame:not(.bare-frame) .ui-jqgrid .ui-jqgrid-labels th#list_edit,
#content-frame:not(.bare-frame) .ui-jqgrid .ui-jqgrid-labels th#list_add {
  border-right-color: transparent;
}

/* The header's bottom edge. The vendor leaves `border-bottom-width: 0`, so there was no
   rule separating the header strip from the first data row - measured `0px`. */
#content-frame:not(.bare-frame) .ui-jqgrid .ui-jqgrid-hdiv,
#content-frame:not(.bare-frame) .ui-jqgrid .ui-jqgrid-labels th.ui-th-column {
  border-bottom: 1px solid var(--brand-line);
}

/* Data rows: zebra striping instead of jQuery UI's flat grey, plus the typography and
   the horizontal-rules-only treatment from `.ow-table td`.

   `border-right: none` is the single biggest change to how the table reads. jQuery UI
   draws a vertical rule on every cell, which is what makes a grid look like a
   spreadsheet; a modern table separates rows, not columns. */
#content-frame:not(.bare-frame) .ui-jqgrid tr.jqgrow td {
  padding: 9px 6px;
  border-color: var(--brand-line);
  border-right: none;
  border-left: none;
  color: var(--brand-ink);
  font-size: 13px;
  line-height: 1.45;
  vertical-align: middle;
}

/* 6px rather than `.ow-table`'s 12px, because a jqGrid column is a FIXED width from the
   page's `colModel` and those numbers were picked against the vendor's 2px padding.
   At 12px the action columns lost 20px of their 35px, and the lock, trash and ADD OBJ
   buttons were clipped mid-glyph - visible in a render, and invisible to a clipping
   check that only measures against scrolling ancestors.

   A cell holding a control gets the tighter treatment still: the button carries its own
   padding, so the cell only needs to keep it off the column edge. Text cells keep the
   roomier 12px, which is where the readability gain actually is. */
#content-frame:not(.bare-frame) .ui-jqgrid tr.jqgrow td:has(> .input-button),
#content-frame:not(.bare-frame) .ui-jqgrid tr.jqgrow td:has(> input[type='checkbox']) {
  padding: 6px 3px;
  text-align: center;
}

#content-frame:not(.bare-frame) .ui-jqgrid tr.jqgrow td:not(:has(> .input-button)):not(:has(> input)) {
  padding: 9px 12px;
}

#content-frame:not(.bare-frame) .ui-jqgrid tr.jqgrow.ui-row-ltr:nth-child(even) td {
  background-color: var(--brand-row-alt);
}

/* Row pitch. Measured 57/57/38/57/57/38/38/38 on Manage IEP Goals: the height came
   entirely from whether the text column happened to wrap to a second line, so rows
   alternated by content rather than sitting on a rhythm.

   The floor goes on the CELL as `height`, not `min-height`. In table layout
   `min-height` on a `td` is ignored - measured, it left the rows at 57/38 exactly as
   before - whereas `height` on a table cell is already defined as a MINIMUM the row
   grows past when the content needs more. That is the property that does what
   `min-height` reads like it should.

   With `box-sizing: border-box` and the 9px padding above, 44px holds one 13px line at
   1.45 with room to spare, and a cell that wraps to two lines still expands. */
#content-frame:not(.bare-frame) .ui-jqgrid tr.jqgrow td {
  box-sizing: border-box;
  height: 44px;
}

#content-frame:not(.bare-frame) .ui-jqgrid tr.ui-state-hover td,
#content-frame:not(.bare-frame) .ui-jqgrid tr.jqgrow:hover td {
  background-color: var(--brand-blue-soft);
}

/* Selected row. jqGrid sets .ui-state-highlight on the <tr>. */
#content-frame:not(.bare-frame) .ui-jqgrid tr.ui-state-highlight td,
#content-frame:not(.bare-frame) .ui-jqgrid tr.ui-state-highlight {
  background: #dbe9f5;
  background-image: none;
  border-color: var(--brand-blue);
  color: var(--brand-navy);
}

/* Pager and toolbar. */
#content-frame:not(.bare-frame) .ui-jqgrid .ui-jqgrid-pager,
#content-frame:not(.bare-frame) .ui-jqgrid .ui-pg-table,
#content-frame:not(.bare-frame) .ui-jqgrid .ui-jqgrid-toppager {
  background: #ffffff;
  background-image: none;
  border-color: var(--brand-line);
  color: var(--brand-ink);
  font-size: 12px;
}

#content-frame:not(.bare-frame) .ui-jqgrid .ui-pg-input,
#content-frame:not(.bare-frame) .ui-jqgrid .ui-pg-selbox {
  color: var(--brand-ink);
  border: 1px solid var(--brand-line);
  border-radius: 3px;
}

/* Search / filter toolbar row. */
#content-frame:not(.bare-frame) .ui-jqgrid .ui-search-toolbar th {
  background: #ffffff;
  background-image: none;
}

#content-frame:not(.bare-frame) .ui-jqgrid .ui-search-toolbar input,
#content-frame:not(.bare-frame) .ui-jqgrid .ui-search-toolbar select {
  border: 1px solid var(--brand-line);
  border-radius: 3px;
  color: var(--brand-ink);
}

/* --------------------------------------------------------------------------
   A grid that does not fit scrolls itself, it does not widen the page
   --------------------------------------------------------------------------
   Four screens carry the same copy-pasted `loadComplete` block:

     var listWidth = parseInt($("#gbox_list").width())+150;
     var bodyWidth = parseInt($("#content-frame").css('width'));
     if(bodyWidth < listWidth){ document.body.style.width=listWidth+'px'; }

   `shrinkToFit:false` with fixed `width:` per column gives a grid that does not
   shrink, so the page was made wider instead: measured `document.body.style.width
   = "970px"` on Third Party Practitioner at every viewport below 900px, which
   scrolls the header, the card and the footer sideways along with the grid.

   No stylesheet can beat an inline width written by script, so the assignment is
   removed from the pages and replaced with `Onward.fitGrid()`, which gives the
   grid's own wrapper a horizontal scroller. These rules are the CSS half; they
   also stand alone if the script does not run.
   -------------------------------------------------------------------------- */
#content-frame:not(.bare-frame) .content .ui-jqgrid {
  max-width: 100%;
}

/* `.ow-grid-scroll` is added by `Onward.fitGrid()` around `#gbox_*`. The wrapper
   scrolls; the grid keeps the exact width jqGrid measured, because changing that
   breaks its column alignment. */
#content-frame:not(.bare-frame) .content .ow-grid-scroll {
  width: 100%;
  max-width: 100%;
  min-width: 0;
  overflow-x: auto;
  overflow-y: hidden;
  -webkit-overflow-scrolling: touch;
}

/* `.content.account` is the flex card built for My Account's two-column panels, and
   Edit Practitioner Account puts a grid in it. A 977px grid then widens the whole
   flex line, and every item on that line stretches to match: measured at 360px, the
   `h1` came out 1027px and `#year-select` 977px inside a 328px card, which put its
   two buttons at x=553 and x=628 - CLIPPED-CONTROL by up to 360px.

   Nothing was setting a width on `#year-select` - enumerating every matching rule
   found none, and there was no inline style and no script writing one. It was being
   sized by the line. So the fix is to stop the grid contributing its content width
   to that line, not to constrain the items it dragged along.

   `flex-basis: 100%` makes the grid wrapper take the full card width and its own
   line; `min-width: 0` lets it shrink below the grid's intrinsic width so the
   scroller inside it can do its job. */
#content-frame:not(.bare-frame) .content > .ow-grid-scroll,
#content-frame:not(.bare-frame) .content > *:has(> .ow-grid-scroll) {
  flex: 1 1 100%;
  min-width: 0;
  max-width: 100%;
}

/* Siblings on the grid's former line must not be stretched by it either. Scoped to
   cards that actually contain a grid so no other flex layout changes. */
#content-frame:not(.bare-frame) .content:has(.ow-grid-scroll) > #year-select {
  flex: 0 1 auto;
  min-width: 0;
  max-width: 100%;
}

#content-frame:not(.bare-frame) .content .ow-grid-scroll > .ui-jqgrid {
  max-width: none;
}

/* Both pagers must track the card, not the grid's measured width. jqGrid writes
   that width inline on `#pager` and on `#list_toppager`, so this needs
   `!important`: the value being overridden is an inline style that the widget
   rewrites on every load.

   The bottom pager is moved outside the scroller by `Onward.fitGrid`. The TOP
   pager cannot be - jqGrid renders it inside `#gview_list`, part of the widget's
   own DOM - so it stays in the scroller and is sized to the visible area instead.
   That matters for reachability, not just tidiness: its controls are laid out
   around `#list_toppager_center`, so at 360px the rows-per-page select sat at
   x=567 in an 823px pager, and scrolling the wrapper fully right moved it to
   x=20.1 against a wrapper starting at x=42 - past the left edge and still not
   visible. No scroll position could reach it.

   The vendor CSS builds both pagers as `white-space: nowrap; overflow: hidden`
   with `min-height: 28px` - a strip designed never to wrap. An earlier attempt
   here set `height: auto; overflow: visible` to let the controls flow onto a
   second line, and the clipped-control count went from 2 to 4: overriding a
   widget's layout model produces more overflow, not less. Only the width is
   corrected now; the pager clips its own content exactly as designed.

   Both pagers live inside the grid box that the scroller wraps, so `width: 100%`
   resolves against the grid's 820px, not the card - measured, the bottom pager ran
   from x=43 to x=863 in a 360px viewport. They are pinned to the scroller's visible
   width instead, which `fitGrid` publishes as a custom property because CSS cannot
   reference an ancestor's used width.

   `position: sticky` was tried here and does not work: it pins an element only
   once scrolling would carry it past the given edge, so a pager whose static
   position is already 273px into the 820px grid stays at 273px - measured at
   x=315 in a 360px viewport with **0 of its controls on screen**.

   The bottom pager is the page's own `<div id='pager'>`, declared as a SIBLING of
   the table; jqGrid relocates it into the grid box on init. `fitGrid` moves it
   back out, below the scroller, where it sits in the card's flow at card width and
   needs no width override at all.

   The top pager cannot be moved - jqGrid builds it inside `#gview_list` as part of
   the widget - so it keeps the visible-width treatment below. */
/* Sized to `--ow-grid-inner` - the measured width INSIDE the grid box - rather than to
   the scroller. The strip is nested two 1px border pairs below the scroller
   (`.ow-grid-scroll` > `#gbox_*` > `#gview_*` > the strip), so the scroller's width is
   4px too wide for it. Measured on Manage IEP Goals: 1791 scroller, 1789 grid box,
   1787 view, 1785 strip.

   An earlier version subtracted a literal `- 4px` from the scroller. That was right on
   this screen and only this screen - the nesting is not identical everywhere, and on
   Third Party Practitioner an equivalent assumption came out 1px over, which Chromium
   paints as a full-width horizontal scrollbar. Measuring the parent is exact wherever
   the arithmetic is a guess. */
#content-frame:not(.bare-frame) .content .ow-grid-scroll .ui-jqgrid-toppager,
#content-frame:not(.bare-frame) .content .ow-grid-scroll #list_toppager {
  box-sizing: border-box;
  width: calc(var(--ow-grid-visible, 100%) - 4px) !important;
  max-width: 100%;
  position: sticky;
  left: 0;
  background-color: #ffffff;
  color: var(--brand-ink);
}

/* Outside the scroller now, so it tracks the card. The inline width jqGrid wrote
   still has to be overridden.

   `tsbs_styl.css` fills both pagers with `--brand-navy-dark` via the id selectors
   `#list_toppager, #pager`, which outrank the class-based grid skin above - so the
   strip painted navy while its own buttons are also navy, leaving the arrows
   almost invisible against their background. Matched by id here so the pager reads
   like every other control strip in the redesign: white surface, navy buttons. */
/* Aligned to the TABLE above it, not to the card.

   Measured on Manage IEP Goals at 1903px: the header and body strips run 58 to 1843,
   and this one ran 56 to 1847 - hanging 2px left and 4px right of the table it belongs
   to, which reads as a footer that does not line up with its own grid.

   The cause is that the two were sized against different boxes. `fitGrid` moves this
   strip out of the scroller so its controls stay reachable at 360px, and it was then
   given `width: 100%` - the CARD's width, while everything above it is the GRID's.
   Where the grid does not fill the card those are different numbers.

   `min()` keeps both requirements. The strip takes the grid's own width so its edges
   meet the table's, and never exceeds the card, which is what the Third Party
   Practitioner suite asserts at all seven widths (`the pager tracks the card, not the
   grid width` - there the grid is narrower than the card and a strip sized purely to
   the grid came out 56px short).

   Width and offset are BOTH needed, and they are set so this strip's own BORDER lands
   on the same x as the table's. The grid's visible vertical rules are `#gview_*`'s
   border at 57 and 1844; the body div inside it has none. Sizing this strip to the body
   div put its border at 58 and 1842 - one pixel inboard on each side, drawing a short
   vertical line that does not meet the table's above it.

   So it spans the same box as `#gview_*` (the scroller less `#gbox_*`'s border pair)
   and carries the matching 1px border, which continues the table's rules downward
   instead of starting new ones beside them. `margin-left` rather than a position
   offset, so the strip still participates in the card's flow. */
#content-frame:not(.bare-frame) .content #pager.ow-grid-pager,
#content-frame:not(.bare-frame) .content .ow-grid-pager {
  box-sizing: border-box;
  width: min(calc(100% - 1px), calc(var(--ow-grid-visible, 100%) - 4px)) !important;
  margin-left: 1px;
  max-width: calc(100% - 1px);
  background-color: #ffffff;
  border: 1px solid var(--brand-line);
  border-top: none;
  color: var(--brand-ink);
  /* `fitGrid` moves this out of `.ui-jqgrid`, so it stops inheriting the widget's
     `font-size: 12px` and picks up the card's 16px instead. The vendor sizes the
     pager's internals in `em` (`.ui-pg-input { font-size: .9em }`), so on Edit
     Practitioner Account every control came out about a third larger than the top
     pager's - measured 16px against 12px, with a 57px strip against 39px. The font
     context has to travel with the element. */
  font-size: 12px;
}

/* The two pagers used to render differently - 18x46 slab buttons 2px apart in the
   bottom strip against 32x25 buttons 5px apart in the top one, in a 57px strip
   against 39px - and an earlier version of this comment called that the widget's own
   rendering and left it. That inference was wrong, and from the right evidence:
   enumerating the rules matching the bottom button returned **none**, which means the
   styling was LOST, not that the vendor owns it. Zero matched rules is the signature
   of a selector that no longer reaches the element, not of a self-sizing widget.

   Every rule that sizes a pager button in `tsbs_styl.css` is scoped under
   `.ui-jqgrid` (`.ui-jqgrid .ui-jqgrid-pager .ui-pg-button`, and the
   `.ui-paging-pager` variant that supplies `padding: 0 6px` and `text-align:
   center`). `Onward.fitGrid()` relocates the bottom pager OUT of `.ui-jqgrid` so it
   stays reachable at narrow widths, and that single move dropped all of them - so the
   button fell back to the vendor's `padding: 1px`, the strip grew, the gaps collapsed
   to 2px, and the rows-per-page select ended up with a white border on white.

   Restated below at `.ow-grid-pager`, the class `fitGrid` adds when it moves the
   element, so the relocated strip keeps the styling it had in place. This is the same
   lesson as the `font-size: 12px` above, which was already kept for exactly this
   reason: anything inherited or scoped to an ancestor travels with the ancestor. See
   "Moving an element moves it out of its inherited context" in Docs/CODING-RULES.md.

   The button carries no height of its own (`ui.jqgrid.css` says `height: auto`) and
   neither does the row, so the 46px was the table row stretching to its tallest cell.
   Two wrong answers came first, each killed by a measurement: `padding` (moved the
   width 18px to 28px, height unchanged) and `font-size` (reached the browser, 12px to
   9px, height unchanged). The row floor is the rows-per-page SELECT, and the
   arithmetic closes exactly:

     29px select + 15px margin-top + 2px border-spacing = 46px      (bottom)
     25px select +  0px margin     + 5px border-spacing = 25px      (top, row = tallest cell)

   Three separate `.ui-jqgrid`-scoped rules are lost by the move, two of them the
   vendor's own:

     ui.jqgrid.css   `.ui-jqgrid .ui-pg-selbox { margin: 0; height: inherit }`
     ui.jqgrid.css   `.ui-jqgrid td select { margin: 0 }`
     tsbs_styl.css   `.ui-jqgrid ... .ui-paging-pager { border-spacing: 5px }`

   With those gone the select falls through to `tsbs_styl.css`'s generic
   `.log-in input[type="text"], select, textarea { margin-top: 15px }` - a rule written
   for the login form, reaching a pager control 15px at a time. Same shape as the 2.15
   export buttons: a legacy form rule applying somewhere nobody looked.

   Restated below so the relocated strip keeps what it had. `font-size: 9px` is kept
   too - it was genuinely lost, it is simply not what drove the height. Note the strip
   itself still wants 12px; that is the rule above, and only the buttons take 9px. */
#content-frame:not(.bare-frame) .content .ow-grid-pager td.ui-pg-button.ui-corner-all {
  padding: 0 6px;
  font-size: 9px;
  text-align: center;
  vertical-align: middle;
}

/* A LABELLED nav button (ADD GOAL, and whatever a page adds via `navButtonAdd`) sits in
   its own row in the relocated strip, with no icon siblings to hold that row up - so it
   collapsed to its 10.35px line box and rendered as a 67x10 sliver against the top
   pager's 76x22. The icon buttons are unaffected because their 16px sprite sets the row.

   The floor is the icon buttons' 30px, not 22px, and that is the whole difference
   between the two strips: in the TOP pager this button shares a row with 30px arrows
   that stretch it, while in the footer it sits alone and stopped at its own minimum -
   measured 30px against 20.1px for the same ADD GOAL button. A minimum that only
   happens to be met in one of the two places is not a shared definition.

   `height`, not `min-height`. These buttons are `<td>` elements, and `min-height` does
   not apply to a `display: table-cell` box - it computed as 30px and the element still
   rendered 20.14px. On a table cell `height` is already treated as a minimum the row
   grows past, so a longer label still fits. Same trap as the data-row pitch above. */
#content-frame:not(.bare-frame) .content .ow-grid-pager td.ui-pg-button.ui-corner-all:not(:has(.ui-icon)),
#content-frame:not(.bare-frame) .content .ui-jqgrid-toppager td.ui-pg-button.ui-corner-all:not(:has(.ui-icon)) {
  box-sizing: border-box;
  height: 30px;
  padding: 4px 8px;
  line-height: 1.35;
}

/* The paging arrows sit on the darker navy in the top strip; matched here so the
   relocated copy does not lose that distinction from the labelled buttons. */
#content-frame:not(.bare-frame) .content .ow-grid-pager .ui-paging-pager td.ui-pg-button.ui-corner-all {
  background-color: var(--brand-navy-dark);
}

/* `.ui-pg-selbox` gets its border from a `.ui-jqgrid`-scoped rule too, so the
   relocated strip rendered a white border on the white pager fill - a control with
   no visible edge, which is the same defect class as the multiselect buttons. */
#content-frame:not(.bare-frame) .content .ow-grid-pager .ui-pg-selbox,
#content-frame:not(.bare-frame) .content .ow-grid-pager .ui-pg-input {
  border: 1px solid var(--brand-line);
  border-radius: 3px;
  color: var(--brand-ink);
}

/* Pager controls are NOT form fields. `.nongrid-form input, .nongrid-form select`
   carries `padding: 9px 11px` for real form inputs, and once the `non-grid-form` typo
   was corrected on these screens that rule started reaching the pager's rows-per-page
   selector - 22px of horizontal padding inside a 24px box, leaving **2px** for the
   text, so "20" rendered clipped. Caught by SELECT-TRUNCATED, which is exactly the
   defect class it was added for, on the change that introduced it.
   Scoped tight: only the two pager controls, in either strip. */
#content-frame:not(.bare-frame) .nongrid-form .ui-jqgrid-pager .ui-pg-selbox,
#content-frame:not(.bare-frame) .nongrid-form .ui-jqgrid-toppager .ui-pg-selbox,
#content-frame:not(.bare-frame) .nongrid-form .ow-grid-pager .ui-pg-selbox,
#content-frame:not(.bare-frame) .content .ui-jqgrid-pager .ui-pg-selbox,
#content-frame:not(.bare-frame) .content .ui-jqgrid-toppager .ui-pg-selbox,
#content-frame:not(.bare-frame) .content .ow-grid-pager .ui-pg-selbox {
  width: auto;
  min-width: fit-content;
  padding: 2px 4px;
  font-size: 12px;
}

#content-frame:not(.bare-frame) .nongrid-form .ui-jqgrid-pager .ui-pg-input,
#content-frame:not(.bare-frame) .nongrid-form .ui-jqgrid-toppager .ui-pg-input,
#content-frame:not(.bare-frame) .nongrid-form .ow-grid-pager .ui-pg-input,
#content-frame:not(.bare-frame) .content .ui-jqgrid-pager .ui-pg-input,
#content-frame:not(.bare-frame) .content .ui-jqgrid-toppager .ui-pg-input,
#content-frame:not(.bare-frame) .content .ow-grid-pager .ui-pg-input {
  padding: 2px 4px;
  font-size: 12px;
}

/* The last of the strip-height difference. `ui.jqgrid.css` gives the pager's three
   layout cells `padding: 0 1px` under `.ui-jqgrid .ui-pg-table td`; outside `.ui-jqgrid`
   the relocated strip falls back to the browser's `padding: 1px`, adding 2px of vertical
   padding that the top strip does not have. Measured: bottom cells 52px against the
   top's 50px, which is the 57-vs-52 strip difference once the borders are counted. */
#content-frame:not(.bare-frame) .content .ow-grid-pager .ui-pg-table > tbody > tr > td {
  padding: 0 1px;
}

/* The right-hand gap. `ui.jqgrid.css` pads the pager's RIGHT layout cell
   `0 5px 0 1px` under a `.ui-jqgrid`-scoped selector, so the relocated footer lost it
   and its contents finished hard against the strip edge while the header's kept a 5px
   air gap. Measured: header right cell `0px 5px 0px 1px`, footer `0px 1px`.

   Matched by the id suffix jqGrid emits (`#pager_right`, `#list_toppager_right`)
   because the three layout cells carry no distinguishing class. */
#content-frame:not(.bare-frame) .content .ow-grid-pager > .ui-pager-control > table.ui-pg-table > tbody > tr > td[id$='_right'] {
  padding: 0 5px 0 1px;
}

/* The footer's contents have to span the strip, the way the header's do.

   jqGrid writes an inline width on `.ui-pg-table` computed from the GRID's width at the
   moment it builds the strip. In the top pager that number is the strip's own width and
   it fills; in the relocated bottom strip it is stale - measured 628px of content inside
   a 1785px strip, leaving 1157px of empty white and the ADD GOAL button stranded on the
   left while "View 1 - 8 of 8" sat in the middle of nowhere.

   `fitGrid` already clears that inline width in JS, which is necessary but not
   sufficient: with it gone the table falls back to `width: auto`, and an auto table
   shrinks to its content. It needs to be told to span. `!important` because the value
   being overridden is inline.

   Scoped with `>` to the OUTER table only. The strip nests a `.ui-pg-table` inside each
   of its three layout cells for the button groups, and those inherit a descendant
   selector - measured, the ADD GOAL button went from 76px to 432px wide and the page
   input from 92px to 434px, because every nested cell stretched too. Only the table
   that spans the strip should span it.

   `table-layout: fixed` on the outer table so the three layout cells divide the strip
   the way the header's do. Without it an auto table sizes the cells to their content
   and the last one overhangs - measured the footer's contents running 32.6px PAST the
   strip while the header's finished flush.

   `border-collapse` and zero `border-spacing` because the relocated strip falls back to
   the browser's default 2px separated borders once it leaves `.ui-jqgrid`. That spacing
   is what pushed its first layout cell to x=61 against the header's 58, and opened 2px
   holes between the three cells where the header has none. */
#content-frame:not(.bare-frame) .content .ow-grid-pager > .ui-pager-control,
#content-frame:not(.bare-frame) .content .ow-grid-pager > .ui-pager-control > table.ui-pg-table {
  box-sizing: border-box;
  width: 100% !important;
  table-layout: fixed;
  border-collapse: collapse;
  border-spacing: 0;
}

/* And the buttons themselves. The top strip's icon buttons measured 32x30 against the
   relocated strip's 28x23, because `.ui-jqgrid .ui-jqgrid-toppager .ui-paging-pager
   .ui-pg-button` supplies `padding: 0 6px` on top of the vendor cell padding and gives
   the 16px sprite a taller line box - and that selector, like every other one that
   sizes this widget, dies at the `.ui-jqgrid` boundary the move crosses.

   A fixed `width`/`height` rather than padding, because the two strips inherit
   DIFFERENT cell padding from the vendor (`.ui-paging-pager` adds `0 6px` in the top
   one only), so equal padding still gave unequal boxes - measured 34x34 against 30x30
   after a padding-only attempt. Pinning the box is legitimate here: the content is a
   fixed 16px sprite, so there is nothing that can grow. Restated for both strips so
   they cannot drift apart again. */
#content-frame:not(.bare-frame) .content .ow-grid-pager td.ui-pg-button.ui-corner-all:has(.ui-icon),
#content-frame:not(.bare-frame) .content .ui-jqgrid-toppager td.ui-pg-button.ui-corner-all:has(.ui-icon),
#content-frame:not(.bare-frame) .content .ui-jqgrid-pager td.ui-pg-button.ui-corner-all:has(.ui-icon) {
  box-sizing: border-box;
  width: 30px;
  height: 30px;
  padding: 0;
  line-height: 30px;
  text-align: center;
}

/* The three rules the move actually cost, restated. The `margin: 0` is the one that
   collapses the 57px strip back to 39px: without it the login form's
   `select { margin-top: 15px }` reaches this control and the row grows by 15px. */
#content-frame:not(.bare-frame) .content .ow-grid-pager .ui-pg-selbox,
#content-frame:not(.bare-frame) .content .ow-grid-pager td select,
#content-frame:not(.bare-frame) .content .ow-grid-pager td input {
  margin: 0;
}

#content-frame:not(.bare-frame) .content .ow-grid-pager .ui-paging-pager,
#content-frame:not(.bare-frame) .content .ow-grid-pager .navtable {
  border-collapse: separate;
  border-spacing: 5px;
}

#content-frame:not(.bare-frame) .content .ui-jqgrid-pager {
  border-radius: 0 0 var(--ow-radius) var(--ow-radius);
}

/* jqGrid emits 12px `ui-pg-button` cells either side of the page-number input as
   spacers. They carry no id and no content, and the navy button fill painted them
   as solid squares floating between the arrows and the word "Page".

   Told apart from real buttons by `ui-corner-all`, which jqGrid puts on the
   clickable ones only - checked against the rendered DOM rather than assumed:
   real buttons are `ui-pg-button ui-corner-all ui-state-disabled` at 32px, the
   spacers are `ui-pg-button ui-state-disabled` at 12px with no id. */
#content-frame:not(.bare-frame) .content .ui-jqgrid-pager td.ui-pg-button:not(.ui-corner-all),
#content-frame:not(.bare-frame) .content .ui-jqgrid-toppager td.ui-pg-button:not(.ui-corner-all) {
  background: none;
  background-color: transparent;
  border: none;
}

/* `tsbs_styl.css:1397` gives every `.ui-pg-button` in either pager
   `background-color: var(--brand-navy)` and declares **no `color`**, so the label
   falls back to the inherited `--brand-ink` (#2d3748): navy text on a navy fill,
   measured **1.11** on Manage IEP Goals and Objectives' top-pager "ADD GOAL".

   Only text-labelled pager buttons are affected, which is why this surfaced on one
   screen and not on the two grid screens done before it. The paging arrows are
   `span.ui-icon` jQuery UI **sprite images** - `content` computes to `none`, so
   `color` cannot reach them and their nominal 1.11 is a false positive. Measured
   across four grid screens: 1 real failure, 0 on Third Party Practitioner, Edit
   Practitioner Account and Student Selection.

   Fixed by making both copies of the button match, which needs the fill restated as
   well as the label. A first attempt set only `color: #ffffff` on the button class
   and traded 1.11 navy-on-navy for **1.00 white-on-white**, because the two copies
   differ by BACKGROUND, not by class: the legacy selector is
   `.ui-jqgrid .ui-jqgrid-pager .ui-pg-button`, and `Onward.fitGrid()` relocates the
   bottom pager OUT of `.ui-jqgrid` to keep it reachable, so the bottom copy matched
   no background rule at all and computed `transparent` on the white card. Enumerating
   the matching rules on both copies is what showed it - one matched the navy rule,
   the other matched none.

   So the fill is restated here at a selector that survives the move, and the label
   follows it. `:not(.ui-corner-all)` spacers keep the transparent treatment above -
   they are excluded from both halves of this rule.

   `text-transform` and `font-weight` are restated for the same reason and were found
   the same way. `tsbs_styl.css` scopes both under `.ui-jqgrid .ui-jqgrid-pager`, so the
   relocated bottom pager lost them too - on Create Sessions the top pager's button read
   `DELETE ALL` at 80px bold while its twin below read `Delete ALL` at 70px in the
   source's own casing. Enumerating the rules matching each copy returned three for the
   top and **zero** for the bottom, which is the signature of a selector that no longer
   reaches the element rather than of a widget styling itself. Fourth rule lost to that
   relocation; each has had to be restated here. */
#content-frame:not(.bare-frame) .content .ui-jqgrid-pager td.ui-pg-button.ui-corner-all,
#content-frame:not(.bare-frame) .content .ui-jqgrid-toppager td.ui-pg-button.ui-corner-all,
#content-frame:not(.bare-frame) .content .ow-grid-pager td.ui-pg-button.ui-corner-all {
  background-color: var(--brand-navy);
  color: #ffffff;
  text-transform: uppercase;
  font-weight: 700;
}

/* Disabled paging arrows. jqGrid marks them `ui-state-disabled` the moment there is
   only one page, and the rule above then painted them the SAME solid navy as a live
   button - measured all eight arrows on a one-page grid at `rgb(9,47,92)`, opacity 1,
   indistinguishable from enabled. A control that cannot be clicked must not look like
   one that can.

   A pale fill with a muted arrow rather than `opacity`, which would also fade the
   button's border and leave it looking like a rendering artefact. The sprite is a
   background image and cannot take `color`, so it is dimmed with a filter - the one
   case where the icon's own paint has to be reached rather than its text.

   `not-allowed` states it a second way for a pointer, and the vendor already sets
   `pointer-events: none` on these, so nothing here changes what is clickable. */
#content-frame:not(.bare-frame) .content .ui-jqgrid-pager td.ui-pg-button.ui-corner-all.ui-state-disabled,
#content-frame:not(.bare-frame) .content .ui-jqgrid-toppager td.ui-pg-button.ui-corner-all.ui-state-disabled,
#content-frame:not(.bare-frame) .content .ow-grid-pager td.ui-pg-button.ui-corner-all.ui-state-disabled {
  background-color: #eef1f5;
  border-color: var(--brand-line);
  color: var(--brand-muted);
  cursor: not-allowed;
}

#content-frame:not(.bare-frame) .content .ui-jqgrid-pager td.ui-pg-button.ui-state-disabled .ui-icon,
#content-frame:not(.bare-frame) .content .ui-jqgrid-toppager td.ui-pg-button.ui-state-disabled .ui-icon,
#content-frame:not(.bare-frame) .content .ow-grid-pager td.ui-pg-button.ui-state-disabled .ui-icon {
  filter: grayscale(1) opacity(0.45);
}

/* Centre the arrow sprite in its button. `ui.jqgrid.css` gives the TOP strip's icons
   `display: block; margin: 2px`, which pins a 16px sprite to the left of a 30px cell -
   measured 2px of gap on the left against 12px on the right, a 10px skew, while the
   relocated footer's `inline-block` icons centred correctly at 7/7.

   `inline-block` with auto side margins so the sprite sits on the cell's own
   `text-align: center`, matching what the footer already does by accident of having
   lost the vendor rule. The negative top margin the footer carries is kept out of it:
   vertical centring comes from the 30px line box, which both strips share. */
#content-frame:not(.bare-frame) .content .ui-jqgrid-pager td.ui-pg-button .ui-icon,
#content-frame:not(.bare-frame) .content .ui-jqgrid-toppager td.ui-pg-button .ui-icon,
#content-frame:not(.bare-frame) .content .ow-grid-pager td.ui-pg-button .ui-icon {
  display: inline-block;
  margin-left: auto;
  margin-right: auto;
  float: none;
}

#content-frame:not(.bare-frame) .content .ui-jqgrid-pager td.ui-pg-button.ui-corner-all .fa,
#content-frame:not(.bare-frame) .content .ui-jqgrid-toppager td.ui-pg-button.ui-corner-all .fa,
#content-frame:not(.bare-frame) .content .ow-grid-pager td.ui-pg-button.ui-corner-all .fa {
  color: #ffffff;
}

/* Icon SIZE, restated for the relocated pager. `tsbs_styl.css:1601` sizes these
   under `.ui-jqgrid .ui-jqgrid-pager`, which `Onward.fitGrid()` moves the bottom
   pager out of, so the icon fell back to the button's own 9px with no padding -
   measured 11.66px wide at the top against 7.08px below, which is the whole of the
   5px width difference between the two copies of DELETE ALL.

   Worth recording how that was isolated, because the obvious reading was wrong: the
   two labels measured 64.02px and 59.44px and the computed font-families differed
   (`Quicksand` against `Quicksand, sans-serif`), so the font looked like the cause.
   Rendering the SAME string in each element's own context measured **49.89px in
   both** - the text was never the difference. It was the `<i class="fa fa-trash">`
   in front of it. Measure the part you suspect on its own before believing it.

   The font-stack difference was real and is fixed at its cause in `tsbs_styl.css`,
   just not the cause of this. Fifth rule lost to the pager relocation. */
#content-frame:not(.bare-frame) .content .ow-grid-pager .ui-pg-button .fa {
  font-size: 11px;
  padding-right: 3px;
}
#content-frame:not(.bare-frame) .content .ow-grid-pager .ui-paging-pager .ui-pg-button .fa {
  font-size: 14px;
  padding-right: 0;
}

/* Below 420px even a full-width pager strip cannot hold the paging buttons, the
   record count and the rows-per-page select on one `nowrap` line - measured, the
   select was clipped by 77px by the pager's own `overflow: hidden`.

   The top pager is a DUPLICATE of the bottom one (`toppager:true` plus the pages'
   own `cloneToTop`), so hiding it at that width removes an unreachable control
   rather than a capability: every function it offers remains on the bottom pager,
   which sits outside the scroller and stays fully visible. Chosen over overriding
   the widget's `nowrap`/`overflow` model, which measurably made things worse. */
/* The duplicate top pager goes at 520px, not 420px. The 420 was picked by eye and left
   a 100px band broken: swept at 5-20px steps, the strip's rows-per-page selector sits
   inside its parent down to 520px (over by -1.3px) and hangs outside from 480px
   (+28.8), reaching +70px at 425px - and the strip is `overflow: hidden`, so those are
   not merely ugly, the control is clipped away and cannot be clicked. Confirmed with
   `elementsFromPoint`, which returned `DIV.content` where the select should have been.

   Hiding it removes no capability: it is a DUPLICATE of the bottom pager, which sits
   outside the scroller and keeps every control. Derived from the measurement rather
   than reused from another component - see "Derive the breakpoint from the basis" in
   Docs/CODING-RULES.md. */
@media (max-width: 520px) {
  #content-frame:not(.bare-frame) .content .ui-jqgrid-toppager {
    display: none;
  }
}

@media (max-width: 420px) {

  /* `#pager_left/_center/_right` are split 25/50/25 by `!important` in
     `tsbs_styl.css`, sized for a wide strip. At 276px that leaves the record count
     ("View 1 - 3 of 3") half-cut by the pager's own `overflow: hidden`. It is
     informational, not a control - every button, the page input and the rows
     selector stay visible - so it gives up its quarter rather than showing a
     truncated number, which would read as a wrong count. */
  #content-frame:not(.bare-frame) .content .ui-jqgrid-pager .ui-paging-info {
    display: none;
  }

  /* `tsbs_styl.css` splits `#pager_left/_center/_right` 25/50/25 with `!important`,
     sized for a wide strip. At 276px that arithmetic puts the centre cell at x=90 with
     a 264px table, so it ends at 352 against a strip that ends at 318 - the
     rows-per-page selector hangs 31px outside and `CLIPPED-CONTROL` fires.

     The left cell is empty at this width (`ADD GOAL` is the only thing that lives there
     and it wraps to its own line), so the centre takes the whole strip and the paging
     controls centre inside it. Measured at 360px: table 262px inside 276px, nothing
     past the edge. */
  #content-frame:not(.bare-frame) .content .ui-jqgrid-pager #pager_center,
  #content-frame:not(.bare-frame) .content .ow-grid-pager #pager_center {
    width: 100% !important;
    padding: 0 !important;
  }

  #content-frame:not(.bare-frame) .content .ui-jqgrid-pager #pager_left,
  #content-frame:not(.bare-frame) .content .ow-grid-pager #pager_left,
  #content-frame:not(.bare-frame) .content .ui-jqgrid-pager #pager_right,
  #content-frame:not(.bare-frame) .content .ow-grid-pager #pager_right {
    width: 0 !important;
    padding: 0 !important;
  }

  /* A strip whose left cell holds LABELLED buttons needs its rows to stack instead.

     The `width: 0` above assumes an empty left cell, which is true of every grid screen
     but Create Sessions - that one puts DELETE ALL and CLEAR SORT there. Squeezed to
     zero inside a `table-layout: fixed` strip they do not shrink, they overlap: measured
     at 360px, four pairs painting on top of each other, `DELETE ALL [46..94]` across
     `[46..76]` and `[78..108]`, with CLEAR SORT's label running over the paging arrows.
     None of it happens at 480px or above, where the strip is 392px and the row fits.

     The cells become blocks so the row wraps to as many lines as it needs, which is the
     one thing the vendor's `nowrap` model cannot do. Scoped by `:has()` to strips that
     actually carry a labelled button, so the strips the arithmetic above already fits
     are untouched - and `overflow: visible` with `height: auto` so the extra lines are
     not cut by the strip's own `overflow: hidden`.

     Overriding the widget's layout model was avoided everywhere else in this file and is
     the right answer here for a specific reason: the earlier attempt failed because it
     tried to make a strip of ICON buttons wrap, which the vendor sizes to a single row
     and re-measures. These are text buttons in the pager's left cell, which the widget
     writes once and never measures again. */
  #content-frame:not(.bare-frame) .content .ow-grid-pager:has(#pager_left .ui-pg-button .fa) > .ui-pager-control > table.ui-pg-table,
  #content-frame:not(.bare-frame) .content .ow-grid-pager:has(#pager_left .ui-pg-button .fa) > .ui-pager-control > table.ui-pg-table > tbody,
  #content-frame:not(.bare-frame) .content .ow-grid-pager:has(#pager_left .ui-pg-button .fa) > .ui-pager-control > table.ui-pg-table > tbody > tr {
    display: block;
    width: 100% !important;
  }

  #content-frame:not(.bare-frame) .content .ow-grid-pager:has(#pager_left .ui-pg-button .fa) > .ui-pager-control > table.ui-pg-table > tbody > tr > td {
    display: block;
    width: 100% !important;
    text-align: center;
  }

  #content-frame:not(.bare-frame) .content .ow-grid-pager:has(#pager_left .ui-pg-button .fa),
  #content-frame:not(.bare-frame) .content .ow-grid-pager:has(#pager_left .ui-pg-button .fa) > .ui-pager-control {
    height: auto !important;
    overflow: visible;
    white-space: normal;
  }

  /* The pager table must not exceed the strip either - jqGrid writes its width inline
     from the grid's measured width, which is far wider than the card at this size.
     `Onward.fitGrid()` clears those inline values on the cells it moved; this catches
     the table itself and any pager the JS has not touched. */
  #content-frame:not(.bare-frame) .content .ow-grid-pager table.ui-pg-table,
  #content-frame:not(.bare-frame) .content .ui-jqgrid-toppager table.ui-pg-table,
  #content-frame:not(.bare-frame) .content .ui-jqgrid-pager table.ui-pg-table {
    max-width: 100%;
  }

  /* The "Page [ ] of N" cell is the one that does not fit. Measured at 360px it took
     **102.3px** of a 276px strip - 42% of the row for a single-digit input - because the
     vendor sizes `.ui-pg-input` for a wide strip and the surrounding cell inherits its
     `white-space: pre`. The paging BUTTONS are the controls that must survive; the page
     number is a display of state the user rarely edits, so it gives up its width first.
     Everything then fits inside the strip with nothing clipped. */
  #content-frame:not(.bare-frame) .content .ow-grid-pager .ui-pg-input,
  #content-frame:not(.bare-frame) .content .ui-jqgrid-toppager .ui-pg-input,
  #content-frame:not(.bare-frame) .content .ui-jqgrid-pager .ui-pg-input {
    width: 26px !important;
    min-width: 0;
    padding: 2px 3px;
  }

  #content-frame:not(.bare-frame) .content .ow-grid-pager td[id^='input_'],
  #content-frame:not(.bare-frame) .content .ui-jqgrid-toppager td[id^='input_'],
  #content-frame:not(.bare-frame) .content .ui-jqgrid-pager td[id^='input_'] {
    white-space: normal;
    padding: 0 2px;
  }

  /* The relocated strip needs both of the above by its own class, because
     `Onward.fitGrid()` moves it out of `.ui-jqgrid` and the two selectors above stop
     reaching it. Verified from the computed style, not assumed. */
  #content-frame:not(.bare-frame) .content .ow-grid-pager .ui-paging-info {
    display: none;
  }

  /* And it gives up the restored 5px border-spacing here. That value is correct at
     every width where the strip has room, but at 276px the editable branch of Manage
     IEP Goals carries an extra ADD GOAL button, and 5px across its eight cells pushed
     the rows-per-page select 15px past the viewport - measured, and caught as a
     regression from restoring the spacing in the first place. 2px is the vendor
     default and every control fits. The read-only branch, with no ADD GOAL, was
     unaffected either way. */
  #content-frame:not(.bare-frame) .content .ow-grid-pager .ui-paging-pager,
  #content-frame:not(.bare-frame) .content .ow-grid-pager .navtable {
    border-spacing: 2px;
  }
}

/* jqGrid's own `.ui-jqgrid-bdiv` gets `overflow-x: hidden` from the same
   `loadComplete` block. With the wrapper scrolling, that hiding is what keeps a
   second nested scrollbar from appearing. Declared here so it survives the
   removal of the JS. */
#content-frame:not(.bare-frame) .content .ow-grid-scroll .ui-jqgrid-bdiv {
  overflow-x: hidden;
}

/* The JS also wrote `#gview_list` left and right borders as `#a6a9ac`, inline,
   on every load - a grey that predates the palette and that no stylesheet could
   override. Owned here now.

   `box-sizing: border-box` on both the view and the grid box is load-bearing, not
   tidiness. jqGrid computes a width and writes it inline (`#gbox_list` at 1789px);
   these borders then added 2px on top, so the box painted at 1791px, `#gview_list`
   overhung the scroller by 1px, and Chromium painted a **full-width horizontal
   scrollbar** across the bottom of a table that fits. A 1px overflow is enough - the
   earlier reading of `scrollWidth - clientWidth = 1` was dismissed as sub-pixel noise
   and it was the whole defect. Border-box makes the borders live inside the width the
   widget calculated. */
#content-frame:not(.bare-frame) .content .ui-jqgrid-view {
  box-sizing: border-box;
  border-left: 1px solid var(--brand-line);
  border-right: 1px solid var(--brand-line);
}

#content-frame:not(.bare-frame) .content .ui-jqgrid,
#content-frame:not(.bare-frame) .content .ui-jqgrid-view,
#content-frame:not(.bare-frame) .content .ui-jqgrid-hdiv,
#content-frame:not(.bare-frame) .content .ui-jqgrid-bdiv {
  box-sizing: border-box;
  max-width: 100%;
}

/* --------------------------------------------------------------------------
   uix-multiselect (Edit Caseload, and any other two-list picker)
   --------------------------------------------------------------------------
   The SELECT ALL / DESELECT ALL controls sit on the widget's own header strip.
   tsbs_styl.css paints the strip `--brand-navy-dark` (#092f5c) and the buttons
   `--brand-navy` (#0b3d77): two navies about 8% apart in luminance, with
   `border: none` on the button. The label is legible against its own fill (3.98)
   so a contrast check passes, but there is no visible edge between button and
   strip, so at normal size the controls read as floating text rather than
   buttons -- users reported them as "not visible at all".

   Fixed at the boundary rather than by recolouring the text: give the control a
   lighter fill and a hairline so it reads as a raised surface on the strip.
   -------------------------------------------------------------------------- */
/* The strip lays its three children out as floats with a hardcoded
   `margin-top: 8px`. Because the label (29.8px), the search box (24px) and the
   button (30px) are all different heights, an equal top margin gives three
   different centres: 414.8, 419.9 and 422.9 -- the button sat 8px below the
   label and 3px below the search box.

   Flex row with `align-items: center` centres them against each other by
   construction, so no per-element offset is needed and it stays correct if any
   of the three changes height. */
#content-frame:not(.bare-frame) .uix-multiselect .ui-widget-header {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 8px 10px;
  /* The widget writes an INLINE height on this strip in JS - measured 36px on
     Customize Activities against 30.86px on Edit Caseload, because Activity
     initialises the widget twice and it re-measures a different state. Inline
     styles beat any stylesheet, so padding cannot govern the total: with
     content-box the strip came out 5px taller than the widget's own arithmetic
     expects, and the next list's strip then painted 6px over the rows above it.
     border-box folds the padding into the height the widget declared. */
  box-sizing: border-box;
}

/* The widget emits the button FIRST and the label second, relying on
   `float: right` to reverse them visually. Dropping the float restores DOM
   order, which read "SELECT ALL / search / 59 UNASSIGNED" -- backwards. `order`
   puts the count first and the controls last without touching the markup, so the
   count stays the paragraph's subject and the actions read as what you do to it. */
#content-frame:not(.bare-frame) .uix-multiselect .ui-widget-header div.header-text {
  flex: 1 1 auto;
  padding: 0;
  order: 1;
}

#content-frame:not(.bare-frame) .uix-multiselect .ui-widget-header .uix-search {
  order: 2;
}

#content-frame:not(.bare-frame) .uix-multiselect .ui-widget-header .uix-control-right,
#content-frame:not(.bare-frame) .uix-multiselect .ui-widget-header .uix-control-left {
  order: 3;
}

#content-frame:not(.bare-frame) .uix-multiselect .ui-widget-header .uix-search,
#content-frame:not(.bare-frame) .uix-multiselect .ui-widget-header .uix-control-right,
#content-frame:not(.bare-frame) .uix-multiselect .ui-widget-header .uix-control-left {
  float: none;
  margin: 0;
  flex: 0 0 auto;
}

#content-frame:not(.bare-frame) .uix-multiselect .ui-widget-header .uix-control-right,
#content-frame:not(.bare-frame) .uix-multiselect .ui-widget-header .uix-control-left {
  background-color: rgba(255, 255, 255, 0.14);
  border: 1px solid rgba(255, 255, 255, 0.35);
  border-radius: var(--ow-radius);
  color: #ffffff;
  cursor: pointer;
  transition: background-color 0.15s ease, border-color 0.15s ease;
}

#content-frame:not(.bare-frame) .uix-multiselect .ui-widget-header .uix-control-right:hover,
#content-frame:not(.bare-frame) .uix-multiselect .ui-widget-header .uix-control-left:hover {
  background-color: rgba(255, 255, 255, 0.26);
  border-color: rgba(255, 255, 255, 0.6);
}

/* The two lists are `position: absolute` at a fixed 440px each with
   `left: 440px !important` on the second, so the widget needs 880px and cannot
   reflow. The widget sizes itself in JS from those values, so they cannot be
   made fluid without breaking its internal maths.

   The absolute lists do not contribute to their parent's scroll width, so
   `overflow-x` on the widget itself does nothing -- what actually overflowed was
   `#multiselect-container`, whose 880px stretched `.content` to 932px inside a
   600px viewport. Scroll the container that has the real width instead, and let
   the card stay within the viewport.

   Pre-existing behaviour, surfaced only once the audit swept narrow widths. */
#content-frame:not(.bare-frame) #multiselect-container {
  max-width: 100%;
  overflow-x: auto;
  overflow-y: hidden;
}

/* The scrolling list is sized by the widget in JS, not from here - see
   `Onward.fitMultiselect` in includes/js/onward.js, which rounds its height down to a
   whole number of rows. Four CSS attempts are recorded there and why each failed. Only
   the scrollbar treatment is styling, so only that stays in this file. */
#content-frame:not(.bare-frame) .uix-multiselect .uix-list-container {
  scrollbar-gutter: stable;
  scrollbar-width: thin;
  scrollbar-color: var(--brand-line) transparent;
}

#content-frame:not(.bare-frame) .uix-multiselect .uix-list-container::-webkit-scrollbar {
  width: 10px;
}

#content-frame:not(.bare-frame) .uix-multiselect .uix-list-container::-webkit-scrollbar-thumb {
  background-color: var(--brand-line);
  border: 3px solid #ffffff;
  border-radius: 5px;
}

/* Above 980px tsbs_styl.css pins the widget to 880px with two 440px columns, so
   it sat as an 880px block in a 1340px card with 460px unused. The widths are CSS
   only -- the widget reads no width option and clicking a row still posts
   save.php with percentages applied -- so the same 100%/50% treatment the narrow
   breakpoint already uses works here.

   The columns abut exactly (seam 0px), so the boundary is drawn with an inset
   shadow: a `border-left` grew the box to 441px and pushed its right edge past
   the container. */
@media (min-width: 981px) {
  #content-frame:not(.bare-frame) #multiselect-container,
  #content-frame:not(.bare-frame) .multiselect,
  #content-frame:not(.bare-frame) .uix-multiselect.ui-widget {
    width: 100% !important;
  }

  #content-frame:not(.bare-frame) .multiselect-available-list {
    width: 50% !important;
    left: 0 !important;
  }

  #content-frame:not(.bare-frame) .multiselect-selected-list {
    width: 50% !important;
    left: 50% !important;
    box-shadow: inset 1px 0 0 var(--brand-line);
  }
}

/* Below 980px tsbs_styl.css stacks the lists at `height: 220px` with the scroller
   at `height: 180px` - arithmetic that assumes a 40px strip. Anything taller and
   the second list, positioned at `top: 220px`, starts inside the bottom of the
   first, so its navy strip paints over the last rows.

   The strip's height must therefore be pinned, not merely padded. The widget writes
   an inline `height` on it in JS after measuring the rendered box, and it measures
   differently per screen - 30.86px on Edit Caseload against 36px on Customize
   Activities, which initialises the widget twice. Padding alone was enough on
   Caseload and left Activity 6px over. `!important` is the only way to beat a
   JS-written inline style, and 40px is the value the widget's own arithmetic
   assumes. Verified on both screens. */
@media (max-width: 980px) {
  #content-frame:not(.bare-frame) .uix-multiselect .ui-widget-header {
    box-sizing: border-box;
    height: 40px !important;
    padding-top: 5px;
    padding-bottom: 5px;
  }

  #content-frame:not(.bare-frame) .uix-multiselect .uix-list-container {
    box-sizing: border-box;
  }
}

/* Below 500px tsbs_styl.css hides the button label, leaving a 31px box holding a
   9px glyph. Size the glyph to the button so the icon-only state stays legible. */
@media (max-width: 500px) {
  #content-frame:not(.bare-frame) .uix-multiselect .ui-widget-header .uix-control-right,
  #content-frame:not(.bare-frame) .uix-multiselect .ui-widget-header .uix-control-left {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 34px;
    height: 30px;
    padding: 0;
  }

  #content-frame:not(.bare-frame) .uix-multiselect .ui-widget-header .uix-control-right .fa,
  #content-frame:not(.bare-frame) .uix-multiselect .ui-widget-header .uix-control-left .fa {
    font-size: 16px;
    line-height: 1;
  }
}

/* `.form-left-half` / `.form-right-half` are 49% floats with `min-width: 306px`
   in tsbs_styl.css, added for the registration captcha but applying to all six
   screens that use these classes. Two 306px floats plus padding cannot fit under
   ~650px, so below that the second half was clipped by `.content`'s `overflow-x`.
   Stacking them is what a narrow screen wants. */
@media (max-width: 700px) {
  #content-frame:not(.bare-frame) .form-left-half,
  #content-frame:not(.bare-frame) .form-right-half {
    float: none;
    width: 100%;
    min-width: 0;
    /* `.form-left-half` also carries `margin-right: 2%`, which only made sense
       beside a floated sibling. */
    margin-right: 0;
  }

  /* Contact Us lays these out with flex. In a wrapping ROW with a gap, the item
     that wraps onto the second line grows against that line's space less the gap,
     so the two cards measured 538px and 518px despite identical CSS. Proven by
     setting `flex-direction: column` or `gap: 0`, either of which makes them equal.
     A column is what a narrow screen wants anyway. */
  #content-frame:not(.bare-frame) .ow-contact .desc {
    flex-direction: column;
  }

  #content-frame:not(.bare-frame) .ow-contact .form-left-half,
  #content-frame:not(.bare-frame) .ow-contact .form-right-half {
    width: auto;
  }
}

/* ROOT CAUSE of card overflow on every screen, not just Edit Caseload.

   tsbs_styl.css sets `.content { display: table }`, and a table box sizes to its
   content and **ignores `max-width`**. So any screen with a wide fixed-width
   child -- this multiselect at 880px, a jqGrid wider than the viewport -- stretched
   the whole card past the browser edge, and `max-width` was computed but inert.
   Verified: `.content` measured 932px inside a 600px viewport with
   `max-width: min(1200px, 100%)` applied.

   `display: block` respects max-width and is what a full-width card wants
   anyway; the table box was never load-bearing here (`.content.tier-one` uses
   flex and `.tier-two` restates table for the two screens that need it). */
#content-frame:not(.bare-frame) .content {
  display: block;
  max-width: 100%;
  overflow-x: auto;
}

/* --------------------------------------------------------------------------
   jQuery UI dialogs
   --------------------------------------------------------------------------
   Used for the Home holiday reminder and every grid's add/edit form.
   -------------------------------------------------------------------------- */
/* Every dialog in this app is initialised with a fixed pixel width - 490 on Edit
   Caseload, 350 on the recurrence dialogs - which jQuery UI writes inline, so it
   cannot shrink and Edit Caseload's overflowed a 360px viewport by 138px.

   The width is only half of it. jQuery UI computes an absolute `left` **once, when the
   dialog opens**, and never recomputes it: a dialog opened at 1400px kept
   `left: 525px` while the viewport narrowed, hanging 175px outside at 700px. A
   breakpoint cannot fix that - it just moves the width at which the stale offset stops
   showing, which is what a 560px query was doing.

   CSS can only cap the width here. Pinning `left` with `!important` does fix the
   overflow, but it also breaks dragging - the widget's draggable writes a `left` the
   rule then overrides, and the dialog stops moving (measured: no change in position
   after a 120px drag). Re-centring on resize is the widget's own `position` API, so it
   is done in Onward.keepDialogsInView in includes/js/onward.js instead. */
.ui-dialog {
  border-color: var(--brand-line);
  border-radius: var(--ow-radius-lg);
  box-shadow: 0 8px 34px rgba(11, 61, 119, 0.18);
  box-sizing: border-box;
  max-width: calc(100vw - 24px) !important;
  max-height: calc(100vh - 24px);
  overflow-y: auto;
}

.ui-dialog .ui-dialog-titlebar {
  background: var(--brand-navy);
  background-image: none;
  border-color: var(--brand-navy);
  border-radius: var(--ow-radius-lg) var(--ow-radius-lg) 0 0;
  color: #ffffff;
  font-weight: 600;
}

/* The title is a CHILD of the bar and `jquery-ui.theme.css` gives it its own colour,
   so it does not inherit the white above - it came out navy on navy, i.e. a title
   nobody can read. Measured `color` and the bar's `background-color` as the same
   `rgb(11, 61, 119)` on the recurrence dialog, with the text present and 284px wide.
   The audit gate's INVISIBLE check cannot catch this: the dialog is closed on load. */
.ui-dialog .ui-dialog-titlebar,
.ui-dialog .ui-dialog-title {
  color: #ffffff;
}

.ui-dialog .ui-dialog-titlebar-close {
  border-color: transparent;
  background: transparent;
}

.ui-dialog .ui-dialog-titlebar-close .ui-icon {
  /* The vendor's close glyph is a sprite tinted for a light bar. */
  filter: invert(1) brightness(2);
}

.ui-dialog .ui-dialog-titlebar-close:hover {
  border-color: rgba(255, 255, 255, 0.4);
  background: rgba(255, 255, 255, 0.12);
}

/* Form controls inside a dialog get none of the app's input styling, because every
   rule that would reach them is `#content-frame`-scoped and jQuery UI reparents the
   dialog to <body>. On Edit Caseload that left the two consent date fields 17.8px
   tall and unbordered against 27px siblings, which read as missing inputs rather
   than as empty ones.

   The `max-width` matters as much: those fields carry inline pixel widths that do
   not shrink - `#Ref` is `width:270px` inside a 336px dialog at 360px - and an
   inline width beats any stylesheet without it. */
.ui-dialog input[type='text'],
.ui-dialog input[type='date'],
.ui-dialog input[type='number'],
.ui-dialog input[type='password'],
.ui-dialog select,
.ui-dialog textarea {
  box-sizing: border-box;
  max-width: 100%;
  min-height: 27px;
  padding: 4px 8px;
  border: 1px solid var(--brand-line);
  border-radius: var(--ow-radius);
  background-color: #ffffff;
  color: var(--brand-ink);
  font-family: inherit;
  font-size: 13px;
}

.ui-dialog input[type='text']:focus,
.ui-dialog input[type='date']:focus,
.ui-dialog select:focus,
.ui-dialog textarea:focus {
  outline: none;
  border-color: var(--brand-blue);
  box-shadow: 0 0 0 3px rgba(22, 101, 168, 0.14);
}

/* A label-and-field pair inside a dialog needs a box of its own. Edit Caseload wrote
   them as a bare `<span>` followed by an `<input>` with no break between pairs, so at
   336px each label wrapped onto the previous field's line - "PARENTAL CONSENT DATE:"
   over an empty gap, with its input sitting beside "PARENTAL DECLINE:". Nothing
   overflowed and no measurement flagged it; only the painted image showed the labels
   attached to the wrong fields.

   The pair wraps as a unit rather than shrinking, and the row collapses to nothing
   when its children are hidden - which matters because the Personal Care branch hides
   the label and field individually with inline `display: none`. */
.ui-dialog .ow-dialog-field {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 4px 10px;
  margin-bottom: 12px;
}

.ui-dialog .ow-dialog-field:empty {
  margin-bottom: 0;
}

/* The labels carry `pad-top` / `pad-bottom` / `pad-right`, which tsbs_styl.css
   implements as MARGINS. `align-items: center` centres the margin box, so those
   margins shifted each label's centre against its own field - measured 11.5px on the
   consent rows and 32.4px on the referral row. The same cause as My Account's
   input/button pairs. The flex gap supplies the spacing instead. */
.ui-dialog .ow-dialog-field > span,
.ui-dialog .ow-dialog-field > label {
  flex: 0 0 auto;
  margin-top: 0;
  margin-bottom: 0;
  margin-right: 0;
}

.ui-dialog .ow-dialog-field > input,
.ui-dialog .ow-dialog-field > textarea,
.ui-dialog .ow-dialog-field > select {
  margin-top: 0;
  margin-bottom: 0;
}

/* Labels differ in length, so at a narrow width some pairs fit on one line and some
   wrap - "PARENTAL DECLINE:" plus its 153px field needs 331px and fits in a 336px
   dialog, while "PARENTAL CONSENT DATE:" needs 376px and does not. Two rows with
   identical rules then render differently, which reads as a bug even though each is
   individually correct. Below the point where the longest label still fits, stack
   every pair so they are consistent. */
@media (max-width: 620px) {
  .ui-dialog .ow-dialog-field {
    flex-direction: column;
    align-items: stretch;
  }

  .ui-dialog .ow-dialog-field > span {
    flex: 0 0 auto;
  }
}

/* The notes block is a heading, a checkbox option and a textarea - three stacked
   items, not a pair. */
.ui-dialog .ow-dialog-field-stacked {
  flex-direction: column;
  align-items: stretch;
}

.ui-dialog .ow-dialog-field-stacked > textarea {
  width: 100%;
}

/* Same treatment as `.ow-radio`: the option text gets a box so it is clickable and
   wraps beside its control rather than under it. */
.ow-check {
  display: flex;
  align-items: flex-start;
  gap: 9px;
  line-height: 1.45;
  cursor: pointer;
}

.ow-check input[type='checkbox'] {
  margin: 0;
  flex: 0 0 auto;
  align-self: flex-start;
  box-sizing: content-box;
  padding: calc((1.45em - 13px) / 2) 0;
  position: static;
  top: auto;
  width: 13px;
  height: 13px;
  min-height: 0;
  accent-color: var(--brand-navy);
  cursor: pointer;
}

.ui-dialog .ui-dialog-buttonpane button {
  font-family: inherit;
  font-size: 13px;
  font-weight: 600;
  color: #ffffff;
  background: var(--brand-navy);
  background-image: none;
  border: 1px solid var(--brand-navy);
  border-radius: var(--ow-radius);
  cursor: pointer;
}

.ui-dialog .ui-dialog-buttonpane button:hover {
  background: var(--brand-navy-dark);
  border-color: var(--brand-navy-dark);
}

/* --------------------------------------------------------------------------
   Tutorial overlay
   --------------------------------------------------------------------------
   Several pages carry the #overlay / #close_o pair. The close affordance is
   styled as a navy pill: as plain white text it was invisible against the
   overlay's white background, which trapped the user inside it.
   -------------------------------------------------------------------------- */
#content-frame:not(.bare-frame) #close_o {
  display: inline-block;
  padding: 8px 18px;
  font-size: 13px;
  font-weight: 600;
  color: #ffffff;
  background-color: var(--brand-navy);
  border-radius: var(--ow-radius);
  text-decoration: none;
}

#content-frame:not(.bare-frame) #close_o:hover {
  background-color: var(--brand-navy-dark);
  text-decoration: none;
}

/* --------------------------------------------------------------------------
   Narrow screens
   --------------------------------------------------------------------------
   tsbs_styl.css switches `.content-panel` to a stacked left-aligned layout
   with a collapsible list below 600px. The flex container above already
   wraps, so only the tile's own alignment needs restating.
   -------------------------------------------------------------------------- */
@media (max-width: 600px) {
  #content-frame:not(.bare-frame) {
    padding: 0 16px 30px;
  }

  #content-frame:not(.bare-frame) .content.home {
    padding: 16px;
    gap: 14px;
  }

  #content-frame:not(.bare-frame) .content.home .content-panel,
  #content-frame:not(.bare-frame) .content.home .content-panel:last-child {
    align-items: flex-start;
    padding: 22px 20px;
    border-bottom: 1px solid var(--brand-line);
  }

  #content-frame:not(.bare-frame) .content-panel .view-options {
    color: var(--brand-blue);
    font-weight: 600;
  }
}

/* --------------------------------------------------------------------------
   FullCalendar - Set Up Student Holidays and Review Sessions - Calendar View
   --------------------------------------------------------------------------
   Both screens carried a near-identical <style> block in the page: 68 lines in
   `su.php` and 89 in `index.php`, with drifted details. `su.php` asked for
   'Roboto Slab' and 'Roboto', neither of which `header.php` loads - only
   'Quicksand' - so it was silently falling back, and the two files disagreed on
   whether button groups have a divider. One definition here, so a change reaches
   both screens.

   Every hardcoded hex is replaced by a token: `#00649d` -> `--brand-navy`,
   `#dddddd` -> `--brand-line`. The weekend fill is the one that mattered most -
   see the note on `.fc-sat` / `.fc-sun` below.
   -------------------------------------------------------------------------- */
#content-frame:not(.bare-frame) .fc h2 {
  font-family: 'Quicksand', sans-serif;
  font-weight: 500;
  font-size: 20px;
  color: var(--brand-navy);
}

#content-frame:not(.bare-frame) .fc table {
  font-family: 'Quicksand', sans-serif;
}

#content-frame:not(.bare-frame) .fc-toolbar.fc-header-toolbar {
  margin-bottom: 12px;
  padding-top: 4px;
}

#content-frame:not(.bare-frame) .fc .fc-button {
  height: auto;
  padding: 7px 12px;
  border: none;
  border-radius: var(--ow-radius);
  background-color: var(--brand-navy);
  background-image: none;
  box-shadow: none;
  color: #ffffff;
  font-family: 'Quicksand', sans-serif;
  font-size: 11px;
  font-weight: 700;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  text-shadow: none;
}

#content-frame:not(.bare-frame) .fc .fc-button:hover,
#content-frame:not(.bare-frame) .fc .fc-button.fc-state-hover {
  background-color: var(--brand-navy-dark);
}

#content-frame:not(.bare-frame) .fc .fc-button.fc-state-active,
#content-frame:not(.bare-frame) .fc .fc-button.fc-state-down {
  background-color: var(--brand-blue);
}

#content-frame:not(.bare-frame) .fc .fc-button.fc-state-disabled {
  opacity: 0.5;
}

/* The prev/next buttons hold a FontAwesome glyph rather than a word, so they get
   a squarer box than the text buttons. */
#content-frame:not(.bare-frame) .fc .fc-button.fc-prev-button,
#content-frame:not(.bare-frame) .fc .fc-button.fc-next-button {
  padding: 5px 11px;
  font-size: 14px;
}

#content-frame:not(.bare-frame) .fc .fc-button-group {
  display: inline-flex;
  gap: 4px;
}

#content-frame:not(.bare-frame) .fc .fc-button-group > * {
  margin: 0;
}

#content-frame:not(.bare-frame) .fc-icon-fa {
  font-family: FontAwesome;
}

#content-frame:not(.bare-frame) .fc th.fc-day-header {
  box-sizing: border-box;
  padding: 7px 4px;
  background-color: var(--brand-navy);
  border-color: var(--brand-navy);
  color: #ffffff;
  font-size: 11px;
  font-weight: 700;
  letter-spacing: 0.06em;
  text-transform: uppercase;
}

/* Weekends were `#adadad`, the SAME mid-grey the page tells the user means "no
   school" ("click on a date to turn it gray"), so a weekend was indistinguishable
   from a holiday they had set. A weekend is not a holiday - it is a day the
   concept does not apply to - so it reads as inert canvas tint instead, and the
   holiday grey stays the only grey that means something. */
#content-frame:not(.bare-frame) .fc td.fc-sun,
#content-frame:not(.bare-frame) .fc td.fc-sat {
  background-color: var(--ow-canvas);
  border-top: 1px solid var(--brand-line);
}

#content-frame:not(.bare-frame) .fc-basic-view .fc-day-number {
  padding: 6px 8px 2px 2px;
  color: var(--brand-ink);
  font-size: 12px;
}

#content-frame:not(.bare-frame) .fc-basic-view .fc-other-month .fc-day-number {
  color: var(--brand-muted);
  opacity: 0.65;
}

#content-frame:not(.bare-frame) .fc-row .fc-content-skeleton td,
#content-frame:not(.bare-frame) .fc-row .fc-helper-skeleton td,
#content-frame:not(.bare-frame) .fc-unthemed td,
#content-frame:not(.bare-frame) .fc-unthemed th {
  border-color: var(--brand-line);
}

/* Today's cell. Amber is the app's "needs attention" colour - it marks the Home tile
   carrying pending work - and today is not a warning, it is a position. Painting it
   amber made the current date read as a flagged day and left the only warm surface on
   an otherwise blue screen. Soft blue is what marks the current selection everywhere
   else, with a navy top rule so the cell is still findable at a glance. */
#content-frame:not(.bare-frame) .fc-unthemed .fc-today {
  background-color: var(--brand-blue-soft);
  box-shadow: inset 0 2px 0 var(--brand-blue);
}

#content-frame:not(.bare-frame) .fc-day-grid-event {
  padding: 1px 5px;
  font-size: 0.75em;
}

#content-frame:not(.bare-frame) .fc-row .fc-content-skeleton tbody td.fc-more-cell {
  font-size: 0.85em;
}

/* Review Sessions - Calendar View only: timed events sit in a narrow column, so
   the vendor's default padding leaves no room for the label. */
#content-frame:not(.bare-frame) .fc-time-grid-event {
  border-width: 2px;
  padding-right: 2px;
  padding-left: 2px;
}

#content-frame:not(.bare-frame) .fc-content {
  padding-right: 1px;
  padding-left: 1px;
}

/* Row height is NOT settable here. FullCalendar measures the rendered box and
   writes `height: 154px` inline on every `.fc-row` after the CSS applies, so
   `min-height` is satisfied and ignored - the same mechanism as the
   uix-multiselect strip on Edit Caseload. Its own knob is the `aspectRatio` init
   option (default 1.35, feeding only `suggestedViewHeight` - verified in
   fullcalendar.js:12310, height and nothing else), so that is set per screen at
   the call site rather than fought with `!important` here. */

/* The toolbar is three sections holding floated children, so the title and the
   TODAY button sat on one line with a **0px** gap between them - measured the
   title's left edge at 211.3 against the button's right edge at 211.3 exactly.

   `display: block` on the sections is not the fix and was measured doing nothing:
   their children are the floats, so each section collapsed to zero height (top and
   bottom both 312.6) while the buttons and title carried on side by side. Making
   the TOOLBAR the flex container is what reflows them, because a flex item's
   `float` is ignored. */
#content-frame:not(.bare-frame) .fc-toolbar.fc-header-toolbar {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 8px 16px;
}

#content-frame:not(.bare-frame) .fc-toolbar .fc-left,
#content-frame:not(.bare-frame) .fc-toolbar .fc-right,
#content-frame:not(.bare-frame) .fc-toolbar .fc-center {
  float: none;
  display: flex;
  align-items: center;
  gap: 8px;
  margin: 0;
}

/* `.fc-center` holds the month title and should take the leftover space so the
   title sits away from the buttons rather than against them. */
#content-frame:not(.bare-frame) .fc-toolbar .fc-center {
  flex: 1 1 auto;
  justify-content: center;
}

#content-frame:not(.bare-frame) .fc-toolbar h2 {
  margin: 0;
}

/* The vendor clears the float row with an `:after`; with flex it becomes a stray
   flex item that adds a phantom gap. */
#content-frame:not(.bare-frame) .fc-toolbar.fc-header-toolbar::after {
  display: none;
}

@media (max-width: 560px) {
  /* Below this width the three sections no longer fit on one line, so centre the
     title under the buttons instead of squeezing it beside them. */
  #content-frame:not(.bare-frame) .fc-toolbar .fc-center {
    flex: 1 1 100%;
  }

  #content-frame:not(.bare-frame) .fc h2 {
    font-size: 17px;
  }

  #content-frame:not(.bare-frame) .fc th.fc-day-header {
    padding: 6px 1px;
    font-size: 9px;
    letter-spacing: 0;
  }

  #content-frame:not(.bare-frame) .fc-basic-view .fc-day-number {
    padding: 3px 4px 2px 2px;
    font-size: 11px;
  }
}

/* --------------------------------------------------------------------------
   Inline label + control pair
   --------------------------------------------------------------------------
   Nine screens write a filter control as `DISTRICT: &nbsp;<select>` - a bare text
   node followed by a field, with a non-breaking space doing the spacing. The text
   has no box, so it cannot be clicked to focus the field, it cannot be aligned
   against it, and the `&nbsp;` is the only gap. Wrapping the words in a
   `<label for>` and making the pair a flex row fixes all three, the same fix the
   Settings radios needed.

   Applied to Set Up Student Holidays so far. The other eight - Manage IEP Goals,
   Create Sessions, and the six Scheduled Reports screens - pick it up as each is
   reached; the class is here so they do not each restate it.
   -------------------------------------------------------------------------- */
#content-frame:not(.bare-frame) .ow-field-inline {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 10px;
  text-align: left;
}

#content-frame:not(.bare-frame) .ow-field-inline > label {
  flex: 0 0 auto;
  margin: 0;
  color: var(--brand-navy);
  font-size: 12px;
  font-weight: 700;
  letter-spacing: 0.04em;
  cursor: pointer;
}

/* `align-items: center` centres the margin box, and tsbs_styl.css gives every
   control `margin-top: 15px`, which would sit the field 7.5px below its label. */
#content-frame:not(.bare-frame) .ow-field-inline > select,
#content-frame:not(.bare-frame) .ow-field-inline > input {
  margin-top: 0;
  margin-bottom: 0;
  min-width: 0;
}

/* --------------------------------------------------------------------------
   Event-state legend - Review Sessions - Calendar View
   --------------------------------------------------------------------------
   The four swatches were `<input disabled>` elements with an inline
   `background-color` - a form control standing in for a decoration, which a screen
   reader announces as a disabled text field and which inherits every global input
   rule. They are now spans.

   The colours themselves are NOT restyled. `list.php` emits them as data on each
   event (`black`, `red`, `forestgreen`, `lightskyblue` at lines 77-79), so the
   legend has to match what the grid paints; changing one without the other makes
   the legend lie. They are named here so the coupling is visible from both ends.
   -------------------------------------------------------------------------- */
#content-frame:not(.bare-frame) .ow-legend {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 6px 18px;
  margin-bottom: 12px;
  text-align: left;
  text-transform: uppercase;
  font-size: 12px;
}

#content-frame:not(.bare-frame) .ow-legend-title {
  color: var(--brand-navy);
  font-weight: 700;
  letter-spacing: 0.04em;
}

#content-frame:not(.bare-frame) .ow-legend-item {
  display: inline-flex;
  align-items: center;
  gap: 7px;
  color: var(--brand-muted);
  white-space: nowrap;
}

#content-frame:not(.bare-frame) .ow-swatch {
  flex: 0 0 auto;
  width: 20px;
  height: 12px;
  border-radius: 3px;
  /* A black or red chip on white needs no outline, but lightskyblue is pale enough
     to vanish against the card. */
  box-shadow: inset 0 0 0 1px rgba(0, 0, 0, 0.16);
}

#content-frame:not(.bare-frame) .ow-swatch-review { background-color: lightskyblue; }
#content-frame:not(.bare-frame) .ow-swatch-saved { background-color: forestgreen; }
#content-frame:not(.bare-frame) .ow-swatch-error { background-color: red; }
#content-frame:not(.bare-frame) .ow-swatch-submitted { background-color: black; }

/* The unsubmitted-row key on Review Sessions.

   `--brand-row-unsubmitted` is the SAME value `index.php:1046` writes inline onto the
   row, so the key and the thing it describes cannot drift - which they already had: the
   legend was written twice with `#E3E5E7` at the top of the grid and the keyword
   `lightgrey` at the bottom, two different greys for one state on one screen. The row
   colour is written by JS per row and so cannot be moved into CSS, but naming it once
   here means the next reader changes one token rather than finding all three copies.

   `inline-block`, not the flex chip above: this legend is a run of text with the keys
   inline, while the Calendar View legend is a flex row of labelled items. */
#content-frame:not(.bare-frame) .ow-grid-legend .ow-swatch {
  display: inline-block;
  vertical-align: middle;
  margin-right: 4px;
  position: relative;
  top: -1px;
}

#content-frame:not(.bare-frame) .ow-swatch-unsubmitted {
  background-color: var(--brand-row-unsubmitted);
}

/* --------------------------------------------------------------------------
   Filter / action bar - `.top-details`
   --------------------------------------------------------------------------
   The strip above a report or grid where the district picker and the export
   buttons live. Eleven screens use it: Run PAR Report, Set Up Student Holidays,
   Manage IEP Goals, and all seven Scheduled Reports screens.

   `tsbs_styl.css` styles it as an `#f0f4f7` band with 11px text, which predates
   the palette. Restyled here rather than per screen, so one edit reaches all
   eleven. Note the pages write `class="top-details center"` and the legacy rule
   is `.top-details.center-details`, so the centring comes from the standalone
   `.center` utility - it is deliberate, and kept for the prose.
   -------------------------------------------------------------------------- */
#content-frame:not(.bare-frame) .content .top-details {
  box-sizing: border-box;
  width: 100%;
  padding: 18px 20px;
  background-color: var(--ow-canvas);
  border: 1px solid var(--brand-line);
  border-radius: var(--ow-radius);
  font-size: 13px;
  color: var(--brand-ink);
}

#content-frame:not(.bare-frame) .content .top-details b {
  font-size: 13px;
  color: var(--brand-navy);
}

/* The controls row inside the strip. `.desc` is the wrapper the pages already
   use, so the class does not have to be added to eleven files.

   Left-aligned, not centred: the strip carries prose above the controls on the
   report screens, and centred controls under left-aligned prose read as two
   unrelated blocks. The `.center` utility on the strip still centres any bare text
   node the pages put there, so nothing else moves.

   Excludes `.ow-contact`, which reuses `.desc` for two full-height content CARDS
   rather than for a row of controls. This rule's `align-items: center` beat the
   contact block's own `stretch` on source order alone - both are one class deep - and
   centring cards of unequal content left them 256px against 200px with a 28px vertical
   step between their top edges. Same class, two genuinely different components. */
#content-frame:not(.bare-frame) .content .top-details:not(.ow-contact) > .desc {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: flex-start;
  gap: 12px 16px;
  text-align: left;
}

/* `align-items: center` centres the MARGIN box, and the pad-* utilities in
   tsbs_styl.css are margins, so a label carrying `pad-bottom` would sit low
   against the control beside it. The gap spaces the row instead. */
#content-frame:not(.bare-frame) .content .top-details > .desc > label,
#content-frame:not(.bare-frame) .content .top-details > .desc > span,
#content-frame:not(.bare-frame) .content .top-details > .desc > select,
#content-frame:not(.bare-frame) .content .top-details > .desc > .button,
#content-frame:not(.bare-frame) .content .top-details > .desc > a.button {
  margin: 0;
  flex: 0 0 auto;
}

#content-frame:not(.bare-frame) .content .top-details > .desc > select {
  min-width: 0;
  max-width: 100%;
}

/* Prose above the controls takes the full width so it is not competing for the
   row, and reads left-aligned like every other body copy in the app. */
#content-frame:not(.bare-frame) .content .top-details .report-desc {
  margin-bottom: 16px;
  font-size: 13px;
  line-height: 1.55;
  text-align: left;
  color: var(--brand-muted);
}

/* --------------------------------------------------------------------------
   Scheduled Reports - the `#rptFrame` report panel
   --------------------------------------------------------------------------
   Five of the seven Scheduled Reports screens (COF, Transportation, SHARS,
   PAL, Two-Year) display their report by iframing a pre-built file, with
   `width="100%" height="500px" style="margin:auto;display:block;"` written on
   the element. `Forms/ScheduledReports/` computes nothing - the reimbursement
   maths lives in a batch process outside this repo - so the iframe IS the screen.

   The width already tracks the card once the shared `.top-details` rule applies
   (measured 238px inside a 328px card at 360px, no overflow). What needed fixing
   was the frame's own chrome: with no `border` declared the browser paints its
   default 2px inset groove, which is the only 3D edge left in the redesign.

   The page's `height="500px"` is left alone. A 500px window holding a 238px-wide
   report looks empty at 360px, but the report is a fixed-layout file scrolling
   inside its own frame, so a shorter window only shows less of it - and a `vh`
   height is worse, not better: on the 900px-tall test viewport `60vh` computed to
   540px and made the frame TALLER at 360px than the value it replaced. Nothing
   overflows or clips at 500px, so the dimension is not churned.
   -------------------------------------------------------------------------- */
#content-frame:not(.bare-frame) .content #rptFrame {
  box-sizing: border-box;
  width: 100%;
  max-width: 100%;
  margin: 4px 0 0;
  border: 1px solid var(--brand-line);
  border-radius: var(--ow-radius);
  background-color: #ffffff;
}

/* --------------------------------------------------------------------------
   Disabled buttons - `.disabledHead`
   --------------------------------------------------------------------------
   `tsbs_styl.css` sets only `background-color: DarkGrey !important` and
   `pointer-events: none`, so the white label from `.button` is inherited onto
   `#a9a9a9`: contrast **1.47**, measured at all nine widths on Run Eligibility
   Report. A disabled control should read as disabled, but at 1.47 the user cannot
   tell WHICH report is unavailable - and on that screen the description tells them
   to contact their representative when a button is disabled, so the label is the
   information they need in order to act.

   Also used by `Sessions/create.php`, which toggles the class onto live Submit and
   Cancel buttons, so this reaches screen 4.7 as well.

   `!important` on the background because that is how the legacy rule declares it;
   without it the `.button` background wins on source order.
   -------------------------------------------------------------------------- */
#content-frame:not(.bare-frame) .disabledHead,
#content-frame:not(.bare-frame) a.button.disabledHead,
#content-frame:not(.bare-frame) input.disabledHead {
  background-color: #eef1f5 !important;
  border: 1px solid var(--brand-line);
  color: var(--brand-muted);
  /* `pointer-events: none` already blocks the click; `cursor: pointer` from
     `.button` still told the user it was clickable. */
  cursor: not-allowed;
}

/* The same state, spelled two other ways, both found on Manage IEP Goals and
   Objectives. They get the identical treatment because they mean the identical
   thing - a control the user cannot use right now:

     `.input-button.disabled`   the grid's lock / delete / edit buttons, built in
                               `loadComplete` and disabled per row by `locked_yn`
                               and `used`
     `.button:disabled`        EDIT IEP DATES, which carries the native attribute

   Both were written as an inline `background-color:#f3f3f3 !important` on the
   element - a fourth grey, off-palette, and unreachable from any stylesheet.
   Measured contrast of their labels was **1.11** (white on #f3f3f3), so "EDIT
   GOAL", "EDIT OBJ" and "ADD OBJ" rendered as ghost text and the lock and trash
   glyphs all but vanished. That is worse than the 1.47 that prompted the
   `.disabledHead` rule above. `#eef1f5` with `--brand-muted` measures **3.76**,
   which clears the 3:1 floor for UI text while still reading as unavailable.

   The inline copies were deleted in the same change, because an inline style
   outranks any stylesheet - see "Removing an inline style is how you learn whether
   its replacement ever applied" in Docs/CODING-RULES.md.

   `!important` matches how the legacy rules declare these fills; without it the
   `.button` / `.input-button` background wins on source order. */
#content-frame:not(.bare-frame) .input-button.disabled,
#content-frame:not(.bare-frame) a.input-button.disabled,
#content-frame:not(.bare-frame) .button:disabled,
#content-frame:not(.bare-frame) input.button:disabled,
#content-frame:not(.bare-frame) input[type='button']:disabled,
#content-frame:not(.bare-frame) input[type='submit']:disabled {
  background-color: #eef1f5 !important;
  border: 1px solid var(--brand-line);
  color: var(--brand-muted) !important;
  cursor: not-allowed;
}

/* No `pointer-events` here on purpose: `tsbs_styl.css` line 1518 already declares
   `.disabled { background-color: #f3f3f3 !important; pointer-events: none }`
   app-wide, so the click is already blocked and only the colour needed replacing.
   Verified from the cascade rather than assumed - the computed style on a grid
   button reports `pointer-events: none` with no rule of ours contributing it.

   Worth knowing why that matters here: `delGO.php` and `lockGO.php` have no
   server-side guard on `locked_yn` or `used`, so they act on whatever id they are
   handed. The CSS is the only thing preventing a delete of a locked, in-use goal.
   Logged as GOALOBJ-DELETE-NO-STATE-GUARD; not fixed here because a server-side
   guard is billing-adjacent logic, not styling. */

/* --------------------------------------------------------------------------
   Permission denied - `.ow-denied`
   --------------------------------------------------------------------------
   Reached from `Sessions/create.php` when a practitioner opens a session header
   that is not theirs. It was a `width="700"` centred table holding one sentence,
   which overflowed every viewport below 700px, and it rendered with no toolbar,
   no navigation and a footer down to a single link - so a user who hit it had no
   way off the page but the back button.

   That was one cause, not three: `$pagename` was never set, and the toolbar, the
   nav and two footer links all gate on it. Setting it restores all of them, so
   nothing here styles the chrome back into place.
   -------------------------------------------------------------------------- */
#content-frame:not(.bare-frame) .content.ow-denied {
  max-width: 640px;
  margin-left: auto;
  margin-right: auto;
}

#content-frame:not(.bare-frame) .content.ow-denied .ow-denied-body {
  padding: 4px 0 8px;
}

#content-frame:not(.bare-frame) .content.ow-denied .ow-denied-lede {
  margin: 0 0 10px;
  font-size: 15px;
  font-weight: 600;
  line-height: 1.5;
  color: var(--brand-ink);
}

#content-frame:not(.bare-frame) .content.ow-denied .ow-denied-help {
  margin: 0 0 20px;
  font-size: 13px;
  line-height: 1.6;
  color: var(--brand-muted);
}

/* `align-items: center` centres the MARGIN box, and `.button` in `tsbs_styl.css`
   carries `margin: 15px 10px 0 0` app-wide - so the painted button sat 7.5px
   below the link's centre (measured 403.66 against 396.16) while both boxes were
   correctly "centred". The margin is zeroed on this row, which supplies its own
   spacing through `gap`; the global rule is left alone because other screens
   still lay out from it. */
#content-frame:not(.bare-frame) .content.ow-denied .ow-denied-actions {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 12px 18px;
}

#content-frame:not(.bare-frame) .content.ow-denied .ow-denied-actions > .button {
  margin: 0;
}

#content-frame:not(.bare-frame) .content.ow-denied .ow-denied-link {
  font-size: 13px;
  color: var(--brand-blue);
  text-decoration: underline;
}

#content-frame:not(.bare-frame) .content.ow-denied .ow-denied-link:hover {
  color: var(--brand-navy);
}

/* --------------------------------------------------------------------------
   Student list box - `.ow-studlist`
   --------------------------------------------------------------------------
   The tall `<select size="10">` that lists a district's active students, on
   View Active Students. The page carried `style='width:450px'` on the select
   inside a strip carrying `style='width:500px'`; an inline width beats any
   stylesheet, so the pair stayed 500px wide inside a 328px card at 360px and
   the select was clipped by 169px. Both inline widths are removed from the
   page and the sizing lives here.

   `size="10"` is a row count, not a height, so the box height comes from the
   font. Left as the widget's own value rather than pinned in px, so a longer
   font stack does not clip the tenth row.
   -------------------------------------------------------------------------- */
/* `.top-details > .desc > select` in the filter-bar block above sets
   `max-width: 100%` at equal specificity and comes later in the file, so it
   won and the list stretched to 1246px at 1400px. Matched through `.desc` here
   so the cap outranks it on specificity rather than with `!important`. */
#content-frame:not(.bare-frame) .content .top-details > .desc > select.ow-studlist,
#content-frame:not(.bare-frame) .content .ow-studlist {
  box-sizing: border-box;
  width: 100%;
  max-width: 450px;
  min-width: 0;
  padding: 4px;
  border: 1px solid var(--brand-line);
  border-radius: var(--ow-radius);
  background-color: #ffffff;
  font-size: 13px;
  color: var(--brand-ink);
}

#content-frame:not(.bare-frame) .content .ow-studlist option {
  padding: 3px 6px;
}

/* `flex-basis: 100%` asks for the whole row and `max-width` then clamps it to
   450px, so the list is as wide as it is allowed to be and the export button
   takes the space left over - beside the list while there is room for both,
   below it once 450 + 98.5 + the gap exceeds the row (measured: together at
   980px and up, wrapped at 700px and below).

   `align-items: flex-start` because the list is 230px tall and the button 28px:
   the inherited `center` would float the button halfway down a column of names. */
#content-frame:not(.bare-frame) .content .top-details > .desc.ow-studlist-row {
  align-items: flex-start;
}

#content-frame:not(.bare-frame) .content .top-details > .desc > select.ow-studlist {
  flex: 0 0 100%;
}

/* The grey rows mean "no Medicaid ID on file". The colour is written inline by
   `createOption()` as `gray`, so it cannot be tokenised from here without
   fighting the inline style - the legend quotes the same word instead. */
/* `text-align: left` explicitly: the strip carries the `.center` utility, so the
   legend inherited `center` and floated in the middle of the empty space beside
   a left-aligned list. */
#content-frame:not(.bare-frame) .content .ow-studlist-note {
  display: block;
  margin: 10px 0 0;
  font-size: 12px;
  line-height: 1.5;
  text-align: left;
  color: var(--brand-muted);
}

#content-frame:not(.bare-frame) .content .ow-studlist-note .ow-swatch-grey {
  display: inline-block;
  width: 11px;
  height: 11px;
  margin-right: 5px;
  vertical-align: -1px;
  border: 1px solid var(--brand-line);
  border-radius: 2px;
  background-color: gray;
}

/* --------------------------------------------------------------------------
   Add a Student panel - `#add-student`
   --------------------------------------------------------------------------
   Shared markup on View Active Students and Edit Caseload, so both screens
   move together.

   `tsbs_styl.css` centres the panel (`#add-student { text-align: center }`)
   and gives the box `padding: 20px 100px` with a 300px fixed field width. The
   200px of horizontal padding is what forced a 500px box to hold a 300px
   form, and the centring left the prose and the checkbox pair centred while
   every input rendered left-aligned - two alignments in one panel.
   -------------------------------------------------------------------------- */
#content-frame:not(.bare-frame) .content #add-student {
  text-align: left;
}

/* `.description` declares `text-align: center` on itself, so `text-align` on an
   ancestor cannot reach it - inheritance loses to a direct declaration. The lede
   under the h2/h3 has to be named directly or it stays centred under two
   left-aligned headings. */
#content-frame:not(.bare-frame) .content #add-student > .description {
  margin: 6px 0 0;
  font-size: 13px;
  line-height: 1.55;
  text-align: left;
  color: var(--brand-muted);
}

#content-frame:not(.bare-frame) .content #add-student-box {
  box-sizing: border-box;
  display: block;
  width: 100%;
  max-width: 520px;
  margin-top: 12px;
  padding: 20px;
  background-color: var(--ow-canvas);
  border: 1px solid var(--brand-line);
  border-radius: var(--ow-radius);
  text-align: left;
}

#content-frame:not(.bare-frame) .content #add-student-form {
  display: flex;
  flex-direction: column;
  align-items: stretch;
  gap: 8px;
  text-align: left;
}

/* `width: 300px` in `tsbs_styl.css` is a fixed basis inside a box that now
   tracks the card, so it is replaced rather than overridden per breakpoint. */
#content-frame:not(.bare-frame) .content #add-student-form input[type='text'] {
  box-sizing: border-box;
  width: 100%;
  max-width: none;
  min-width: 0;
  margin: 0;
  padding: 7px 10px;
  min-height: 32px;
  border: 1px solid var(--brand-line);
  border-radius: var(--ow-radius);
  background-color: #ffffff;
  font-size: 13px;
  color: var(--brand-ink);
}

#content-frame:not(.bare-frame) .content #add-student-form input[type='text']:focus {
  border-color: var(--brand-blue);
  outline: none;
}

/* The objection label and its checkbox are one control, so they share a row
   rather than stacking like the text fields do. */
#content-frame:not(.bare-frame) .content #add-student-form .ow-check {
  display: flex;
  align-items: center;
  gap: 8px;
  margin: 2px 0;
  font-size: 13px;
  line-height: 1.45;
  color: var(--brand-ink);
  cursor: pointer;
}

#content-frame:not(.bare-frame) .content #add-student-form .ow-check input[type='checkbox'] {
  margin: 0;
  flex: 0 0 auto;
}

#content-frame:not(.bare-frame) .content #add-student-form #polabel {
  display: inline;
  box-sizing: border-box;
  margin: 0;
  padding: 0;
  font-size: 13px;
  color: var(--brand-ink);
}

#content-frame:not(.bare-frame) .content #add-student-form .description {
  margin: 0;
  font-size: 12px;
  text-align: left;
  color: var(--brand-muted);
}

/* The district the student will be filed under, which is the one fact on this
   form the user cannot change here. It carries `.description` like the
   "*Required Fields" footnote does, so it rendered in the same muted grey at the
   same size - the consequential line and the incidental one looked identical. */
#content-frame:not(.bare-frame) .content #add-student-form .ow-form-district {
  margin-bottom: 4px;
  font-size: 13px;
  font-weight: 600;
  color: var(--brand-navy);
}

/* `.message` is empty until a validator writes into it, so it has no box to
   reserve space. `min-height` holds the line so the form does not jump by a
   row the first time an error appears. `white-space: wrap` on the page is not
   a valid keyword and was ignored; the wrapping is set here. */
/* `.message` in `tsbs_styl.css` carries `padding-top: 15px`, which on a label
   that is empty until a validator writes into it read as 33px of dead space at
   the top of the form. The gap does the spacing now. */
#content-frame:not(.bare-frame) .content #add-student-form .message {
  min-height: 18px;
  margin: 0;
  padding: 0;
  font-size: 13px;
  font-weight: bold;
  line-height: 1.4;
  white-space: normal;
  text-align: left;
  color: var(--brand-danger);
}

#content-frame:not(.bare-frame) .content #add-student-form .button {
  align-self: flex-start;
  margin-top: 6px;
}

/* --------------------------------------------------------------------------
   Footer links - orphaned separator when the row wraps
   --------------------------------------------------------------------------
   `tsbs_styl.css` draws the separators as `border-left` on each link and clears it
   with `#footer-links a:first-child`. That works on one line. Below 600px the same
   file sets `flex-wrap: wrap`, and the link that starts the second line is no longer
   `:first-child` - so it keeps its left border and paints a `|` with nothing to its
   left, floating clear of the text above it.

   CSS cannot ask "is there a sibling on my line", so the border cannot be made
   conditional. Below the wrap point the separators are dropped and a gap does the
   spacing instead, which is what a wrapped row wants anyway.
   -------------------------------------------------------------------------- */
/* The two-word labels wrapped inside their own boxes once the row got tight - at
   700px "USER GUIDE" and "CONTACT US" measured 27.6px tall against TRAINING's 13.8px,
   and the row's `align-items: center` then sat TRAINING 6.9px lower than its
   siblings. A nav label is not prose; it should wrap the ROW, not the word. */
#footer-links a {
  white-space: nowrap;
}

@media (max-width: 600px) {
  #footer-links {
    gap: 4px 0;
  }

  #footer-links a {
    border-left: none;
  }
}
