/* ==========================================================================
   RTWEB density layer

   Purpose
   -------
   The stylesheets that came before this one accumulated roughly 2,000
   `!important` declarations across eleven files, so editing a component in one
   place did not change the screen. Chasing each file individually is what
   produced the fragmentation in the first place.

   This layer loads last and states the density contract once, globally:
   compact controls, a tight box-to-text gap, close information lines, and no
   decorative pills or shadows. Component files keep owning layout and colour;
   this file only owns scale.

   Excluded surfaces (owner request): Schedule, Regular Class Planner, and
   English Check keep their own scale and are opted out at the bottom.

   Values come from the token scale in edu-theme.css. Do not hardcode here.
   ========================================================================== */

/* --- Typography ---------------------------------------------------------- */

body.app-body {
  font-size: var(--text-md);
  line-height: var(--leading-normal);
}

body.app-body .page-title,
body.app-body h1 {
  font-size: var(--text-2xl) !important;
  font-weight: 700 !important;
  line-height: var(--leading-tight) !important;
  letter-spacing: -0.01em !important;
  margin: 0 0 var(--space-3) !important;
}

body.app-body h2 {
  font-size: var(--text-xl) !important;
  font-weight: 600 !important;
  line-height: var(--leading-tight) !important;
  margin: 0 0 var(--space-3) !important;
}

body.app-body h3,
body.app-body h4,
body.app-body .card h3,
body.app-body .card h4 {
  font-size: var(--text-lg) !important;
  font-weight: 600 !important;
  line-height: var(--leading-snug) !important;
  margin: 0 0 var(--space-3) !important;
}

body.app-body h5,
body.app-body h6 {
  font-size: var(--text-md) !important;
  font-weight: 600 !important;
  margin: 0 0 var(--space-2) !important;
}

body.app-body .page-subtitle,
body.app-body .section-note {
  font-size: var(--text-sm) !important;
  line-height: var(--leading-snug) !important;
  margin: 0 0 var(--space-4) !important;
}

body.app-body p {
  line-height: var(--leading-normal);
}

/* --- Controls ------------------------------------------------------------ */

body.app-body button,
body.app-body .btn,
body.app-body input:not([type="checkbox"]):not([type="radio"]),
body.app-body select,
body.app-body textarea {
  min-height: var(--control-h-lg) !important;
  border-radius: var(--radius-sm) !important;
  font-size: var(--text-md) !important;
  line-height: var(--leading-snug) !important;
  box-shadow: none !important;
}

body.app-body button,
body.app-body .btn {
  padding: var(--space-2) var(--space-5) !important;
  font-weight: 500 !important;
  letter-spacing: 0 !important;
  transform: none !important;
}

body.app-body input:not([type="checkbox"]):not([type="radio"]),
body.app-body select,
body.app-body textarea {
  padding: var(--space-2) var(--space-4) !important;
}

body.app-body textarea {
  min-height: calc(var(--control-h-lg) * 2) !important;
}

/* --- Boxes: shrink the gap between the frame and its text ---------------- */

body.app-body .card,
body.app-body .panel,
body.app-body .table-wrap,
body.app-body .hero-panel {
  border-radius: var(--card-radius) !important;
  box-shadow: none !important;
}

/* --- Wide tables: compress instead of running off the card ---------------

   Measured on /masterdata (9 columns): the table asked for 1126px inside an
   840px card, and 1368px inside 640px, so the right-hand columns were only
   reachable by scrolling. app-layout.css sets "table-layout: auto;
   min-width: 100%", and auto layout sizes to content, not to the container.

   Four candidates were measured at 1440/1100/900/700px. Wrapping the headers
   saved 0px, and capping cell width saved 34px of 312px; only fixed layout
   actually honours the container. Fixed layout costs row height, so it is
   applied only below 1200px, where the overflow starts. Wide desktop keeps the
   compact content-sized rows.

   Columns that hold one atomic value keep their single line: a timestamp,
   a credit figure or a status dot must not be split across two rows.
   ----------------------------------------------------------------------- */

@media (max-width: 1200px) {
  body.app-body .table-wrap > table,
  body.app-body .table-wrap > form > table {
    table-layout: fixed !important;
    width: 100% !important;
    min-width: 0 !important;
  }

  body.app-body .table-wrap > table th,
  body.app-body .table-wrap > table td,
  body.app-body .table-wrap > form > table th,
  body.app-body .table-wrap > form > table td {
    white-space: normal !important;
    overflow-wrap: break-word !important;
  }

  /* ops-controls.css styles headers through
     "body.app-body:not(.regular-planner-body):not(.menu-english_check) table th",
     which is a longer selector than the one above, so the header kept
     white-space: nowrap and its 12px side padding and pushed the row past the
     card. Matching that specificity is what lets the header wrap. */
  body.app-body:not(.regular-planner-body):not(.menu-english_check) .table-wrap > table th,
  body.app-body:not(.regular-planner-body):not(.menu-english_check) .table-wrap > table td,
  body.app-body:not(.regular-planner-body):not(.menu-english_check) .table-wrap > form > table th,
  body.app-body:not(.regular-planner-body):not(.menu-english_check) .table-wrap > form > table td {
    white-space: normal !important;
    overflow-wrap: break-word !important;
  }

  /* Identifier columns carry the longest strings, so give the first column
     roughly twice a plain column and let the rest share what is left.

     This assumes the first column holds the identifier. The reading library
     table leads with a 42px checkbox and an 84px cover thumbnail instead, so
     the heuristic spent 18% of the row on the checkbox: measured at a 1100px
     viewport the select column took 185px while the book title was squeezed to
     113px, the same share as the two-digit chapter count. That table states its
     own column widths in ops-ui.css, so it opts out here. */
  body.app-body .table-wrap > table:not(.reader-library-table) th:first-child,
  body.app-body .table-wrap > table:not(.reader-library-table) td:first-child {
    width: 18% !important;
  }

  /* A row-action cell holds one control; it never needs a share of the text
     budget. Same for a trailing status marker. */
  body.app-body .table-wrap > table:not(.reader-library-table) th:last-child,
  body.app-body .table-wrap > table:not(.reader-library-table) td:last-child {
    width: 8% !important;
  }

  body.app-body .table-wrap > table td:last-child > .btn,
  body.app-body .table-wrap > table td:last-child > button,
  body.app-body .table-wrap > table td:last-child > a {
    white-space: nowrap !important;
  }
}

/* The table now matches the card width exactly, so the last few pixels of
   overflow come from the cells themselves: 12px of side padding per cell plus
   strings with no break opportunity (hashes, user agents, class codes). Below
   760px the horizontal padding steps down and long words may break anywhere,
   which is what removed the final 7-47px on /masterdata and /logs. */
@media (max-width: 760px) {
  body.app-body:not(.regular-planner-body):not(.menu-english_check) .table-wrap > table th,
  body.app-body:not(.regular-planner-body):not(.menu-english_check) .table-wrap > table td,
  body.app-body:not(.regular-planner-body):not(.menu-english_check) .table-wrap > form > table th,
  body.app-body:not(.regular-planner-body):not(.menu-english_check) .table-wrap > form > table td {
    padding: var(--space-2) var(--space-3) !important;
    overflow-wrap: anywhere !important;
  }
}

body.app-body .card,
body.app-body .panel {
  padding: var(--card-padding) !important;
  margin-bottom: var(--space-4) !important;
}

/* Metric tiles: a fixed column count, not auto-fit.

   auto-fit derives the count from available width, so when the sidebar hides at
   900px the content area jumped from 674px to 868px and the row went from four
   tiles to six -- the count increased as the window got smaller. Measured at
   1440/1200/1100/1024/950/900px it produced 8/6/5/5/4/6 columns.

   A metric row is a fixed set of figures, so the count should step down
   deliberately instead of tracking pixels. */
body.app-body .stat-grid {
  grid-template-columns: repeat(4, minmax(0, 1fr)) !important;
  gap: var(--space-3) !important;
}

body.app-body .stat-card {
  padding: var(--space-3) var(--space-4) !important;
  border-radius: var(--radius-sm) !important;
  box-shadow: none !important;
}

body.app-body .stat-label {
  font-size: var(--text-xs) !important;
  font-weight: 600 !important;
  letter-spacing: 0.02em !important;
  text-transform: none !important;
  margin-bottom: var(--space-1) !important;
}

body.app-body .stat-value {
  font-size: var(--text-xl) !important;
  font-weight: 700 !important;
  line-height: var(--leading-tight) !important;
}

body.app-body .stat-note {
  font-size: var(--text-xs) !important;
  margin-top: var(--space-1) !important;
}

/* --- Tables: the densest surface in the product -------------------------- */

body.app-body th,
body.app-body td {
  padding: var(--space-2) var(--space-4) !important;
  font-size: var(--text-md) !important;
  line-height: var(--leading-snug) !important;
}

body.app-body th {
  font-size: var(--text-sm) !important;
  font-weight: 600 !important;
  text-transform: none !important;
  letter-spacing: 0 !important;
}

/* --- Badges: square, quiet, and small ----------------------------------- */

body.app-body .hero-chip,
body.app-body .section-kicker,
body.app-body .badge,
body.app-body .chip,
body.app-body .pill,
body.app-body .tag {
  border-radius: var(--radius-xs) !important;
  padding: 0 var(--space-3) !important;
  min-height: var(--control-h-sm) !important;
  font-size: var(--text-xs) !important;
  font-weight: 600 !important;
  letter-spacing: 0 !important;
  text-transform: none !important;
  box-shadow: none !important;
}

/* --- Rhythm: collapse the dead space between stacked items -------------- */

body.app-body .btn-row,
body.app-body .form-row,
body.app-body .filter-row,
body.app-body .table-actions {
  gap: var(--space-3) !important;
}

/* --- Button rows: size to the label, not to a fixed basis ----------------

   Measured on /counseling: app-layout.css gives every button in a .btn-row a
   152px flex basis below 900px. Three language buttons whose labels need
   44-63px were each stretched to 172px, so a row that fits in one line broke
   into three. A "Save" pair needing 402px split across two lines inside a
   1130px row for the same reason.

   Buttons now take their content width and the row wraps only when the labels
   genuinely do not fit. Rows that intentionally fill the width (a lone submit,
   an empty-state call to action) keep their own rules further down this file.
   ----------------------------------------------------------------------- */

body.app-body .btn-row {
  display: flex !important;
  flex-wrap: wrap !important;
  align-items: center !important;
}

body.app-body .btn-row > .btn,
body.app-body .btn-row > button,
body.app-body .btn-row > a {
  flex: 0 0 auto !important;
  width: auto !important;
  min-width: 0 !important;
  max-width: 100% !important;
  white-space: nowrap !important;
}

/* Buttons sitting side by side must share one height. On /counseling a
   .compact-btn (26px) sat next to a .btn primary (32px), which read as a 3px
   baseline slip. The row stretches its children so the tops and bottoms line
   up regardless of which size class each button carries. */
body.app-body .btn-row {
  align-items: stretch !important;
}

body.app-body .btn-row > .btn,
body.app-body .btn-row > button,
body.app-body .btn-row > a {
  min-height: var(--control-h-lg) !important;
  align-items: center !important;
  justify-content: center !important;
}

/* A single button may still stretch when the form is a stacked mobile column,
   because a lone control reads as the form's primary action. */
@media (max-width: 560px) {
  body.app-body .btn-row > :only-child {
    flex: 1 1 auto !important;
    width: 100% !important;
  }
}

body.app-body .mobile-stack,
body.app-body .form-grid,
body.app-body .filter-grid {
  gap: var(--space-4) !important;
}

body.app-body .card > * + * {
  margin-top: var(--space-3);
}

body.app-body ul,
body.app-body ol {
  margin: 0 0 var(--space-3);
  padding-left: var(--space-7);
}

body.app-body li + li {
  margin-top: var(--space-1);
}

/* --- Collapsibles and dialogs ------------------------------------------- */

body.app-body details {
  border-radius: var(--radius-sm) !important;
}

body.app-body summary {
  min-height: var(--control-h-md) !important;
  padding: var(--space-2) var(--space-4) !important;
  border-radius: var(--radius-sm) !important;
  font-size: var(--text-md) !important;
  font-weight: 600 !important;
}

body.app-body dialog,
body.app-body .modal,
body.app-body .modal-panel {
  border-radius: var(--card-radius) !important;
  box-shadow: 0 4px 16px rgba(8, 42, 34, 0.12) !important;
}

/* --- Keep action rows from reflowing as the viewport changes ------------- */

body.app-body .card-header-row {
  flex-wrap: nowrap;
  gap: var(--space-4) !important;
}

body.app-body .card-header-row > .btn-row {
  flex-wrap: nowrap;
  flex-shrink: 0;
  justify-content: flex-end;
}

@media (max-width: 720px) {
  body.app-body .card-header-row,
  body.app-body .card-header-row > .btn-row {
    flex-wrap: wrap;
  }
}

/* --- Action rows beside a heading ---------------------------------------

   Measured on /workflow at 582px: .wf-ops-topline is a 558px flex row holding
   a title block and .wf-ops-actions. The action group was left with 284px while
   its three buttons needed 284px, so the last one wrapped to a second line even
   though the row had free space to the right.

   Below the point where a title and its actions stop fitting side by side, the
   group moves under the title and keeps all buttons on one line.
   ----------------------------------------------------------------------- */

body.app-body .wf-ops-actions,
body.app-body .card-header-row > .btn-row {
  flex-wrap: nowrap !important;
  flex-shrink: 0 !important;
  align-items: center !important;
  gap: var(--space-3) !important;
}

body.app-body .wf-ops-actions > button,
body.app-body .wf-ops-actions > .btn {
  flex: 0 0 auto !important;
  white-space: nowrap !important;
}

@media (max-width: 900px) {
  body.app-body .wf-ops-topline {
    flex-direction: column !important;
    align-items: stretch !important;
    gap: var(--space-4) !important;
  }

  body.app-body .wf-ops-actions {
    width: 100% !important;
    justify-content: flex-start !important;
  }
}

/* Both of these are grids below 900px, not flex rows.

   Measured at 470px: .wf-ops-topline resolved to a single 252.9px column and
   .wf-ops-actions to "123.5px 123.5px", so three buttons became 2 + 1 and the
   group could never exceed 253px. Flex properties had no effect here because
   neither element was a flex container. The fix has to speak grid: let the
   topline column fill the line, and let the action group flow in one row.
   ----------------------------------------------------------------------- */
@media (max-width: 900px) {
  body.app-body .wf-ops-topline {
    grid-template-columns: minmax(0, 1fr) !important;
  }

  body.app-body .wf-ops-actions {
    display: flex !important;
    grid-template-columns: none !important;
    width: 100% !important;
    min-width: 0 !important;
  }

  /* The buttons inherited width:100% from the grid layout they were in, so once
     the container became a flex row all three still measured the full 446px and
     stacked on top of each other. In a row they size to their label. */
  body.app-body .wf-ops-actions > button,
  body.app-body .wf-ops-actions > .btn,
  body.app-body .wf-ops-actions > a {
    width: auto !important;
    min-width: 0 !important;
    flex: 0 0 auto !important;
  }
}

/* Very narrow: let the group wrap rather than overflow the viewport. */
@media (max-width: 420px) {
  body.app-body .wf-ops-actions,
  body.app-body .card-header-row > .btn-row {
    flex-wrap: wrap !important;
  }
}

/* --- Opt-out: surfaces the owner asked to leave alone --------------------

   render_html tags <body> with menu-<current_menu>, so the excluded screens can
   be skipped wholesale instead of guarding every rule above. Anything scoped to
   body.app-body simply stops matching on these three menus.
   ------------------------------------------------------------------------- */

body.app-body.menu-regular_planner,
body.app-body.menu-english_check {
  --density-layer: off;
}

/* --- Real dashboard/workflow class names --------------------------------

   The generic .stat-grid rules above do not reach the operator screens: those
   use ops-* and wf-* names, and each is redefined in up to five files. The
   density contract has to name them explicitly.
   ----------------------------------------------------------------------- */

body.app-body .ops-stats-grid,
body.app-body .wf-stat-grid {
  display: grid !important;
  grid-template-columns: repeat(4, minmax(0, 1fr)) !important;
  gap: var(--space-3) !important;
}

/* Step the count down at the two places a four-across row stops fitting. */
@media (max-width: 720px) {
  body.app-body .stat-grid,
  body.app-body .ops-stats-grid,
  body.app-body .wf-stat-grid {
    grid-template-columns: repeat(2, minmax(0, 1fr)) !important;
  }
}

@media (max-width: 420px) {
  body.app-body .stat-grid,
  body.app-body .ops-stats-grid,
  body.app-body .wf-stat-grid {
    grid-template-columns: minmax(0, 1fr) !important;
  }
}

body.app-body .ops-stat-card,
body.app-body .wf-stat-card {
  padding: var(--space-3) var(--space-4) !important;
  border-radius: var(--radius-sm) !important;
  box-shadow: none !important;
}

/* Queue and attention lists carried one item per oversized card. */
body.app-body .wf-live-panel,
body.app-body .wf-focus-card,
body.app-body .wf-queue-card {
  gap: var(--space-3) !important;
  padding: var(--space-4) !important;
  border-radius: var(--card-radius) !important;
  box-shadow: none !important;
}

body.app-body .wf-focus-card h3,
body.app-body .wf-panel-head h3 {
  margin: 0 !important;
  font-size: var(--text-lg) !important;
  line-height: var(--leading-snug) !important;
  letter-spacing: 0 !important;
}

body.app-body .wf-focus-card p {
  font-size: var(--text-sm) !important;
  line-height: var(--leading-snug) !important;
}

body.app-body .wf-focus-meta {
  font-size: var(--text-xs) !important;
  font-weight: 600 !important;
}

/* The two-column operator grid left half the row empty when one side was
   short. Let panels size to content instead of a fixed half. */
body.app-body .wf-live-grid {
  grid-template-columns: repeat(auto-fit, minmax(320px, 1fr)) !important;
  gap: var(--space-4) !important;
  align-items: start !important;
}

/* --- Kill the reserved-height floors ------------------------------------

   Measured on the operator dashboard: a metric tile held 56px of content but
   was pinned to 79px by min-height:68px plus grid gap. Those floors are what
   made small text sit inside a large empty box, so they are released here and
   the tiles are allowed to size to their content.
   ----------------------------------------------------------------------- */

body.app-body .wf-stat-card,
body.app-body .ops-stat-card,
body.app-body .stat-card {
  min-height: 0 !important;
  gap: var(--space-1) !important;
}

body.app-body .wf-stat-label,
body.app-body .ops-stat-label {
  font-size: var(--text-xs) !important;
  line-height: var(--leading-tight) !important;
  font-weight: 600 !important;
  letter-spacing: 0.02em !important;
  text-transform: none !important;
}

body.app-body .wf-stat-value,
body.app-body .ops-stat-value {
  font-size: var(--text-xl) !important;
  line-height: var(--leading-tight) !important;
  font-weight: 700 !important;
  letter-spacing: -0.01em !important;
}

body.app-body .wf-stat-copy,
body.app-body .ops-stat-copy {
  font-size: var(--text-xs) !important;
  line-height: var(--leading-tight) !important;
}

/* --- Metric tiles: one layout regardless of value ------------------------

   edu-theme.css switches a tile to a two-column layout once its value is zero
   (.is-zero), so on narrow widths a "0" tile put its label and number side by
   side while every neighbour stacked them. The row then read as broken
   alignment. Tiles now use the same stacked layout at every width and for
   every value.
   ----------------------------------------------------------------------- */

body.app-body .wf-stat-card,
body.app-body .wf-stat-card.is-zero,
body.app-body .ops-stat-card,
body.app-body .ops-stat-card.is-zero {
  display: grid !important;
  grid-template-columns: minmax(0, 1fr) !important;
  align-items: start !important;
  justify-items: start !important;
  align-content: start !important;
  min-height: 0 !important;
  gap: var(--space-1) !important;
}

body.app-body .wf-stat-card.is-zero .wf-stat-value,
body.app-body .ops-stat-card.is-zero .ops-stat-value {
  font-size: var(--text-xl) !important;
}

/* Tiles share a row, so they should also share a height. */
body.app-body .wf-stat-grid,
body.app-body .ops-stats-grid {
  align-items: stretch !important;
}

/* Same floor problem on list rows: three lines of text in a tall card. */
body.app-body .wf-queue-item,
body.app-body .wf-attention-item,
body.app-body .review-queue-item,
body.app-body .ops-list-item {
  min-height: 0 !important;
  padding: var(--space-3) var(--space-4) !important;
  border-radius: var(--radius-sm) !important;
  gap: var(--space-1) !important;
  box-shadow: none !important;
}

/* --- Live list rows (measured: 86px tall for two lines of text) ---------- */

body.app-body .wf-live-item,
body.app-body .wf-live-head {
  gap: var(--space-2) !important;
}

body.app-body .wf-live-item {
  padding: var(--space-3) var(--space-4) !important;
  border-radius: var(--radius-sm) !important;
  box-shadow: none !important;
}

body.app-body .wf-live-list {
  gap: var(--space-2) !important;
}

body.app-body .wf-live-item strong {
  font-size: var(--text-md) !important;
  line-height: var(--leading-snug) !important;
  font-weight: 600 !important;
}

body.app-body .wf-live-item small,
body.app-body .wf-live-meta {
  font-size: var(--text-xs) !important;
  line-height: var(--leading-tight) !important;
}

/* --- Shell chrome: measured leftovers ------------------------------------

   Audited in-browser on the operator dashboard. Each floor below was still
   reserving vertical space that no content needed.
   ----------------------------------------------------------------------- */

body.app-body .sidebar-head {
  min-height: 0 !important;
  margin-bottom: var(--space-4) !important;
}

body.app-body .brand-mark {
  font-size: var(--text-lg) !important;
  line-height: var(--leading-tight) !important;
}

body.app-body .brand-subtitle {
  font-size: var(--text-xs) !important;
  letter-spacing: 0.04em !important;
}

body.app-body .nav-link {
  min-height: var(--control-h-md) !important;
  padding: var(--space-2) var(--space-4) !important;
  margin-bottom: 1px !important;
  gap: var(--space-4) !important;
  font-weight: 500 !important;
}

body.app-body .nav-link-icon {
  width: 16px !important;
  height: 16px !important;
}

body.app-body .topbar {
  min-height: 0 !important;
  padding: var(--space-3) var(--space-5) !important;
}

body.app-body .topbar-mini-field {
  min-height: var(--control-h-md) !important;
  padding: 0 var(--space-3) !important;
}

/* The globe icon and the language select were laid out on the text baseline,
   which left the icon 9px below the control once the 44px floor was removed.
   Centre both on the field's cross axis instead. */
body.app-body .topbar-mini-field {
  display: inline-flex !important;
  align-items: center !important;
  gap: var(--space-3) !important;
}

body.app-body .topbar-mini-field > svg,
body.app-body .topbar-mini-field > .ui-icon {
  flex: 0 0 auto !important;
  align-self: center !important;
}

body.app-body .topbar-mini-field select {
  align-self: center !important;
  min-height: var(--control-h-md) !important;
  border: 0 !important;
  background-color: transparent !important;
}

/* A tap-target floor belongs on touch input, not on every pointer. */
@media (pointer: coarse) {
  body.app-body .nav-link,
  body.app-body button,
  body.app-body .btn {
    min-height: 40px !important;
  }
}

/* --- Popups, combos, and toolbars ---------------------------------------

   Audited on /users and /students. These are the surfaces the owner reported
   as never changing: modal cards, dropdown panels, search combos, toolbars.
   ----------------------------------------------------------------------- */

body.app-body .picker-modal-card,
body.app-body .modal-card,
body.app-body .ops-combo-panel,
body.app-body .operation-toolbar {
  border-radius: var(--card-radius) !important;
}

body.app-body .picker-modal-card,
body.app-body .modal-card {
  padding: var(--card-padding) !important;
}

body.app-body .ops-combo-native,
body.app-body .ops-combo-filter,
body.app-body .ops-combo-input,
body.app-body .students-search-input,
body.app-body .students-filter-select {
  min-height: var(--control-h-lg) !important;
  border-radius: var(--radius-sm) !important;
  font-size: var(--text-md) !important;
  padding: var(--space-2) var(--space-4) !important;
}

/* The combo trigger measured 36px on /students below 900px: a 16px iOS-guard
   font size gives a 21.6px line box, and 6px of padding on each side pushes
   the content past the 32px floor. Capping the box and tightening the line
   height keeps it level with the search field beside it. */
body.app-body .ops-combo-button {
  height: var(--control-h-lg) !important;
  min-height: var(--control-h-lg) !important;
  max-height: var(--control-h-lg) !important;
  padding: 0 var(--space-4) !important;
  line-height: var(--leading-snug) !important;
}

body.app-body .ops-combo-option {
  min-height: var(--control-h-md) !important;
  padding: var(--space-2) var(--space-4) !important;
  font-size: var(--text-md) !important;
}

body.app-body .operation-toolbar {
  padding: var(--space-3) var(--space-4) !important;
  gap: var(--space-3) !important;
}

body.app-body .hero-title {
  font-size: var(--text-2xl) !important;
  font-weight: 700 !important;
  line-height: var(--leading-tight) !important;
  letter-spacing: -0.01em !important;
}

/* The layout-mode switch reserved a tap floor on every pointer type. */
body.app-body .layout-mode-option {
  min-height: var(--control-h-md) !important;
}

/* --- Filter grids: keep a control next to its own label -----------------

   Measured on /users: the grid resolved to three ~371px columns, and the
   searchable combo landed in a cell of its own, one column away from the
   "Role" label it belongs to. Auto-fit columns sized to content keep a label
   and its control together and stop the row from stretching to fill width.
   ----------------------------------------------------------------------- */

body.app-body .filter-grid {
  /* A fixed 240px cap left ~230px of the row empty on /users while the
     two fields sat cramped, and auto-fit re-flowed the column count as the
     sidebar hid. Explicit column counts per width keep the fields filling
     the row and stepping down predictably. */
  grid-template-columns: repeat(4, minmax(0, 1fr)) !important;
  justify-content: stretch !important;
  align-items: end !important;
  gap: var(--space-4) !important;
}

/* Two- and three-field rows should not leave the remaining columns blank. */
body.app-body .filter-grid:has(> :last-child:nth-child(1)) {
  grid-template-columns: minmax(0, 1fr) !important;
}

body.app-body .filter-grid:has(> :last-child:nth-child(2)) {
  grid-template-columns: repeat(2, minmax(0, 1fr)) !important;
}

body.app-body .filter-grid:has(> :last-child:nth-child(3)) {
  grid-template-columns: repeat(3, minmax(0, 1fr)) !important;
}

@media (max-width: 720px) {
  body.app-body .filter-grid,
  body.app-body .filter-grid:has(> :last-child:nth-child(3)) {
    grid-template-columns: repeat(2, minmax(0, 1fr)) !important;
  }
}

@media (max-width: 420px) {
  body.app-body .filter-grid,
  body.app-body .filter-grid:has(> :last-child:nth-child(2)),
  body.app-body .filter-grid:has(> :last-child:nth-child(3)) {
    grid-template-columns: minmax(0, 1fr) !important;
  }
}

body.app-body .filter-grid > label,
body.app-body .filter-grid > div {
  gap: var(--space-2) !important;
  min-width: 0 !important;
}

body.app-body .filter-grid > label > span {
  font-size: var(--text-xs) !important;
  font-weight: 600 !important;
  color: var(--edu-text-muted) !important;
}

/* The filter grid now sizes its own columns, so the row itself should use the
   full card width instead of shrinking to content. */
body.app-body .query-form > .btn-row {
  width: auto !important;
}

/* --- Combo controls ------------------------------------------------------

   ops-controls.js now leaves the upgraded combo inside its own <label>, so a
   filter field is a single column again: caption on top, control beneath. Only
   the internal stacking needs stating here.
   ----------------------------------------------------------------------- */

body.app-body .ops-combo-label {
  display: flex !important;
  flex-direction: column !important;
  gap: var(--space-2) !important;
  min-width: 0 !important;
}

/* Measured on /schedule: inside a two-column smart-form-grid the label
   collapsed to zero width, so "Teacher" wrapped one letter per line. A combo
   field owns its whole cell and its caption must stay on one line. */
body.app-body .ops-combo-label {
  width: 100% !important;
  min-width: 100% !important;
}

body.app-body .ops-combo-label > span,
body.app-body .ops-combo-label > strong {
  white-space: nowrap !important;
  font-size: var(--text-xs) !important;
  font-weight: 600 !important;
  color: var(--edu-text-muted) !important;
}

/* Filter rows should size to their fields rather than stretch to the card. */
body.app-body .smart-form-grid {
  grid-template-columns: repeat(auto-fit, minmax(160px, 220px)) !important;
  justify-content: start !important;
  align-items: end !important;
  gap: var(--space-4) !important;
}

/* Cards that render no content should not reserve a band of empty space. */
body.app-body .card:empty,
body.app-body .panel:empty {
  display: none !important;
}

/* A <details> used as a card keeps its summary as the visible affordance, so the
   summary must never collapse. Measured on /schedule: the "Advanced schedule &
   class tools" summary rendered at 0x0, which read as an empty bordered band. */
body.app-body details.card > summary,
body.app-body details > summary.mobile-card-summary {
  display: flex !important;
  align-items: center !important;
  width: auto !important;
  min-width: 0 !important;
  min-height: var(--control-h-md) !important;
  padding: 0 !important;
  cursor: pointer;
}

/* Same collapse affected plain <summary> and checkbox labels that carry their
   text after the control. Both are affordances, so neither may shrink away. */
body.app-body summary {
  width: auto !important;
  min-width: 0 !important;
}

body.app-body label:has(> input[type="checkbox"]),
body.app-body label:has(> input[type="radio"]) {
  display: inline-flex !important;
  align-items: center !important;
  gap: var(--space-3) !important;
  width: auto !important;
  min-width: 0 !important;
  flex-direction: row !important;
}

/* --- Schedule day panel --------------------------------------------------

   Measured at 1440px: the "Teacher" caption sat in a flex row with no basis of
   its own, so it was squeezed to 41px and broke across two lines while the
   select beside it kept its width. Captions in this row keep one line and the
   row is allowed to wrap instead.
   ----------------------------------------------------------------------- */

body.app-body .schedule-day-panel {
  flex-wrap: wrap !important;
  align-items: center !important;
  gap: var(--space-3) var(--space-4) !important;
}

body.app-body .schedule-day-panel > .muted {
  flex: 0 0 auto !important;
  white-space: nowrap !important;
  font-size: var(--text-xs) !important;
  font-weight: 600 !important;
  color: var(--edu-text-muted) !important;
}

/* Each filter is its own flex row, so a caption that wraps shifts only that
   row and the group stops sharing a baseline. Measured 23px of drift between
   the Day and Teacher panels. Stop the wrap and pin them to one baseline. */
body.app-body .schedule-day-panel {
  flex-wrap: nowrap !important;
  align-items: baseline !important;
}

body.app-body .schedule-day-panel > select,
body.app-body .schedule-day-panel > .ops-combo {
  flex: 0 1 auto !important;
  min-width: 0 !important;
}

body.app-body .ops-combo {
  width: 100% !important;
  min-width: 0 !important;
}

/* --- Utilities -----------------------------------------------------------

   Stage 2 of the UI pass retires inline style="" attributes from the Python
   render code. Measured in scope: 1,196 attributes across 674 distinct values,
   of which 552 set a single property (mostly margins and flex rows).

   Inline styles beat every rule that is not !important, so leaving them in
   place is what made earlier CSS-only passes invisible. These utilities give
   the render code a token-aligned replacement, so spacing can never drift back
   to arbitrary pixel values.
   ----------------------------------------------------------------------- */

body.app-body .u-mt-0 { margin-top: 0 !important; }
body.app-body .u-mt-1 { margin-top: var(--space-2) !important; }
body.app-body .u-mt-2 { margin-top: var(--space-3) !important; }
body.app-body .u-mt-3 { margin-top: var(--space-4) !important; }
body.app-body .u-mt-4 { margin-top: var(--space-5) !important; }

body.app-body .u-mb-0 { margin-bottom: 0 !important; }
body.app-body .u-mb-1 { margin-bottom: var(--space-2) !important; }
body.app-body .u-mb-2 { margin-bottom: var(--space-3) !important; }
body.app-body .u-mb-3 { margin-bottom: var(--space-4) !important; }
body.app-body .u-mb-4 { margin-bottom: var(--space-5) !important; }

body.app-body .u-m-0 { margin: 0 !important; }

/* Horizontal rows: the second most repeated inline pattern. */
body.app-body .u-row {
  display: flex !important;
  align-items: center !important;
  gap: var(--space-4) !important;
}

body.app-body .u-row-between {
  display: flex !important;
  align-items: center !important;
  justify-content: space-between !important;
  gap: var(--space-4) !important;
}

body.app-body .u-row-wrap { flex-wrap: wrap !important; }
body.app-body .u-grow { flex: 1 1 auto !important; min-width: 0 !important; }
body.app-body .u-hidden { display: none !important; }

/* Text roles, so a size is never chosen ad hoc. */
body.app-body .u-text-xs { font-size: var(--text-xs) !important; }
body.app-body .u-text-sm { font-size: var(--text-sm) !important; }
body.app-body .u-muted { color: var(--edu-text-muted) !important; }
body.app-body .u-strong { font-weight: 600 !important; }
body.app-body .u-danger { color: #b3261e !important; }

/* Extra single-purpose utilities, added from a second measurement pass over
   the render code. Each one replaces an inline declaration that appeared five
   or more times. */

body.app-body .u-mt-5 { margin-top: var(--space-6) !important; }
body.app-body .u-mb-5 { margin-bottom: var(--space-6) !important; }
body.app-body .u-mb-block { margin: 0 0 var(--space-6) !important; }

body.app-body .u-text-md { font-size: var(--text-md) !important; }
body.app-body .u-text-lg { font-size: var(--text-lg) !important; }
body.app-body .u-text-xl { font-size: var(--text-xl) !important; }

body.app-body .u-ink { color: var(--edu-text-dark) !important; }
body.app-body .u-bare { text-decoration: none !important; }
body.app-body .u-left { text-align: left !important; }

/* Small inline status pills carried their own padding and radius. */
body.app-body .u-tag {
  display: inline-flex !important;
  align-items: center !important;
  padding: 0 var(--space-3) !important;
  min-height: var(--control-h-sm) !important;
  border-radius: var(--radius-xs) !important;
  font-size: var(--text-xs) !important;
  font-weight: 600 !important;
}

/* Metric figures rendered as one-off 18px bold blocks. */
body.app-body .u-figure {
  margin: 0 !important;
  font-size: var(--text-xl) !important;
  font-weight: 700 !important;
  line-height: var(--leading-tight) !important;
  color: var(--edu-text-dark) !important;
}

/* --- Shared component shells --------------------------------------------

   Styles for the helpers in readingtown/ui_components.py. A screen that uses
   a helper gets this treatment automatically, so new work cannot reintroduce
   a one-off empty state or badge.
   ----------------------------------------------------------------------- */

body.app-body .ui-empty {
  display: flex !important;
  align-items: center !important;
  gap: var(--space-3) !important;
  margin: 0 !important;
  padding: var(--space-4) var(--space-5) !important;
  border: 1px dashed var(--edu-border) !important;
  border-radius: var(--radius-sm) !important;
  color: var(--edu-text-muted) !important;
  font-size: var(--text-sm) !important;
  line-height: var(--leading-snug) !important;
}

body.app-body .ui-card-heading {
  display: flex !important;
  align-items: baseline !important;
  gap: var(--space-4) !important;
  min-width: 0 !important;
}

body.app-body .ui-card-heading h4 {
  margin: 0 !important;
}

body.app-body .ui-card-note {
  font-size: var(--text-xs) !important;
  color: var(--edu-text-muted) !important;
  white-space: nowrap !important;
}

body.app-body .ui-collapsible > summary {
  display: flex !important;
  align-items: center !important;
  gap: var(--space-3) !important;
  min-height: var(--control-h-md) !important;
  width: auto !important;
  font-size: var(--text-md) !important;
  font-weight: 600 !important;
  cursor: pointer;
}

body.app-body .ui-collapsible-body {
  margin-top: var(--space-4) !important;
}

/* Badges: one shape, tone only changes colour. */
body.app-body .ui-badge {
  display: inline-flex !important;
  align-items: center !important;
  min-height: var(--control-h-sm) !important;
  padding: 0 var(--space-3) !important;
  border: 1px solid transparent !important;
  border-radius: var(--radius-xs) !important;
  font-size: var(--text-xs) !important;
  font-weight: 600 !important;
  letter-spacing: 0 !important;
  text-transform: none !important;
  white-space: nowrap !important;
}

body.app-body .ui-badge.is-neutral {
  background: var(--edu-bg) !important;
  border-color: var(--edu-border) !important;
  color: var(--edu-text-muted) !important;
}

body.app-body .ui-badge.is-ok {
  background: var(--edu-success-soft) !important;
  border-color: var(--edu-success) !important;
  color: var(--edu-success) !important;
}

body.app-body .ui-badge.is-warn {
  background: var(--edu-warning-soft) !important;
  border-color: var(--edu-warning) !important;
  color: var(--edu-warning) !important;
}

body.app-body .ui-badge.is-info {
  background: var(--edu-primary-soft) !important;
  border-color: var(--edu-primary) !important;
  color: var(--edu-primary) !important;
}

body.app-body .ui-badge.is-danger {
  background: #fdeceb !important;
  border-color: #b3261e !important;
  color: #b3261e !important;
}

/* --- Empty states: 21 class names, one appearance ------------------------

   Measured in the render code: empty-msg (131), family-empty (33), wf-empty,
   portal-empty, review-empty and sixteen more, each with its own padding and
   colour. New code should call render_empty_state(), but the existing names are
   aliased here so every screen already looks the same.

   Slot placeholders are excluded: .empty-slot-plus is an add button, not a
   message.
   ----------------------------------------------------------------------- */

body.app-body .empty-msg,
body.app-body .family-empty,
body.app-body .family-empty-state,
body.app-body .family-reading-inline-empty,
body.app-body .wf-empty,
body.app-body .workflow-signal-empty,
body.app-body .workflow-auto-empty,
body.app-body .portal-empty,
body.app-body .review-empty,
body.app-body .teacher-homework-empty,
body.app-body .student-shell-empty,
body.app-body .lesson-preset-empty,
body.app-body .lesson-storage-empty,
body.app-body .reader-library-empty,
body.app-body .reader-access-empty,
body.app-body .reader-zip-empty,
body.app-body .storage-history-empty,
body.app-body .library-step-empty {
  display: flex !important;
  align-items: center !important;
  gap: var(--space-3) !important;
  margin: 0 !important;
  padding: var(--space-4) var(--space-5) !important;
  border: 1px dashed var(--edu-border) !important;
  border-radius: var(--radius-sm) !important;
  background: transparent !important;
  color: var(--edu-text-muted) !important;
  font-size: var(--text-sm) !important;
  font-style: normal !important;
  font-weight: 400 !important;
  line-height: var(--leading-snug) !important;
  text-align: left !important;
  box-shadow: none !important;
  min-height: 0 !important;
}

/* A compact variant sits inside a row rather than owning a block. */
body.app-body .family-empty-state.compact,
body.app-body .family-reading-inline-empty {
  padding: var(--space-2) var(--space-4) !important;
  font-size: var(--text-xs) !important;
}

/* An empty state that also offers actions is a block, not a single line.

   Measured on the parent portal at 1440px: .family-empty was a flex row, so the
   .btn-row inside it received 230px out of 1122px and its two buttons split
   across lines while the row had ~900px free. Empty states that contain more
   than a message stack their parts and let each part use the full width.
   ----------------------------------------------------------------------- */
body.app-body .family-empty:has(.btn-row),
body.app-body .family-empty:has(button),
body.app-body .family-empty:has(form),
body.app-body .empty-msg:has(.btn-row),
body.app-body .wf-empty:has(.btn-row),
body.app-body .portal-empty:has(.btn-row) {
  display: flex !important;
  flex-direction: column !important;
  align-items: flex-start !important;
  gap: var(--space-4) !important;
}

body.app-body .family-empty > .btn-row,
body.app-body .empty-msg > .btn-row,
body.app-body .wf-empty > .btn-row,
body.app-body .portal-empty > .btn-row {
  width: 100% !important;
  flex-wrap: nowrap !important;
}

@media (max-width: 420px) {
  body.app-body .family-empty > .btn-row,
  body.app-body .empty-msg > .btn-row,
  body.app-body .wf-empty > .btn-row,
  body.app-body .portal-empty > .btn-row {
    flex-wrap: wrap !important;
  }
}

/* --- Status markers: 64 class names, one shape ---------------------------

   badge (51), family-pill (13), review-chip (12), family-badge (11) and sixty
   more variants each picked their own height, radius, and letter spacing. The
   pill silhouette in particular is what read as AI slop.

   Colour stays with the owning component; only geometry and type are unified.
   Rows of markers (*-chip-row, chip-grid) are containers, not markers, so they
   only get the gap treatment.
   ----------------------------------------------------------------------- */

body.app-body .badge,
body.app-body .family-pill,
body.app-body .family-badge,
body.app-body .family-app-badge,
body.app-body .family-feed-badge,
body.app-body .family-feedback-role-badge,
body.app-body .review-chip,
body.app-body .review-app-badge,
body.app-body .result-level-chip,
body.app-body .student-lesson-pill,
body.app-body .workflow-meta-pill,
body.app-body .start-meta-pill,
body.app-body .status-pill,
body.app-body .wf-item-badge,
body.app-body .drive-badge,
body.app-body .rr-chip,
body.app-body .wf-hero-kicker,
body.app-body .wf-panel-kicker,
body.app-body .wf-focus-kicker,
body.app-body .section-kicker,
body.app-body .ops-chip,
body.app-body .chip-label {
  display: inline-flex !important;
  align-items: center !important;
  min-height: var(--control-h-sm) !important;
  padding: 0 var(--space-3) !important;
  border-radius: var(--radius-xs) !important;
  font-size: var(--text-xs) !important;
  font-weight: 600 !important;
  letter-spacing: 0 !important;
  text-transform: none !important;
  white-space: nowrap !important;
  box-shadow: none !important;
}

/* --- Kickers: keep the uppercase eyebrow, drop the pill -------------------

   .wf-hero-kicker and .wf-panel-kicker were 999px pills in the legacy teal
   pair at letter-spacing 0.08em, which is the pill silhouette the owner
   called out. They stay as small uppercase labels in the deep-green family so
   a section still reads as labelled without the lozenge.
   ----------------------------------------------------------------------- */

body.app-body .wf-hero-kicker,
body.app-body .wf-panel-kicker,
body.app-body .wf-focus-kicker,
body.app-body .section-kicker {
  min-height: 0 !important;
  padding: 0 !important;
  border: 0 !important;
  background: transparent !important;
  color: var(--edu-text-muted) !important;
  font-size: var(--text-xs) !important;
  font-weight: 700 !important;
  letter-spacing: 0.06em !important;
  text-transform: uppercase !important;
}

/* Marker rows: spacing only. */
body.app-body .chip-grid,
body.app-body .chip-field,
body.app-body .rr-chip-row,
body.app-body .review-inspector-chip-row {
  display: flex !important;
  flex-wrap: wrap !important;
  gap: var(--space-2) !important;
}

/* --- Remaining per-screen geometry --------------------------------------

   Audited in-browser across dashboard, students, counseling, payments,
   resources, and logs. These are the last visible surfaces that still chose
   their own radius, type size, or height floor.
   ----------------------------------------------------------------------- */

/* page-title resolved to 22px because a longer selector won; match it. */
body.app-body .page-container > .page-title,
body.app-body .drive-shell .page-title,
body.app-body h1.page-title {
  font-size: var(--text-2xl) !important;
}

/* Search and filter controls kept a 10px radius from their own rules. */
body.app-body .students-search-input,
body.app-body .students-filter-select,
body.app-body .counseling-search-input,
body.app-body .ops-combo-native,
body.app-body .ops-combo-filter {
  border-radius: var(--radius-sm) !important;
}

/* Toolbars and hero panels: card geometry, not their own. */
body.app-body .counseling-template-bar,
body.app-body .drive-hero,
body.app-body .rtweb-toast {
  border-radius: var(--radius-md) !important;
}

body.app-body .drive-hero {
  padding: var(--card-padding) !important;
  box-shadow: none !important;
}

body.app-body .drive-hero h2,
body.app-body .drive-hero .hero-title {
  font-size: var(--text-2xl) !important;
  font-weight: 700 !important;
  line-height: var(--leading-tight) !important;
}

/* Tab strips read as pills at 20px; a tab is a rectangle with a marker. */
body.app-body .drive-tab-item,
body.app-body .md-tab-item,
body.app-body .tab-item {
  border-radius: var(--radius-sm) !important;
  min-height: var(--control-h-md) !important;
  padding: 0 var(--space-4) !important;
  font-size: var(--text-md) !important;
  font-weight: 500 !important;
}

/* Height floors on rows that hold a single line of text. */
body.app-body .counseling-row,
body.app-body .counseling-template-item,
body.app-body .drive-row {
  min-height: 0 !important;
}

/* --- Table headers: stop wrapping short labels into towers ---------------

   Observed on /masterdata: "FOREIGN TEACHER" and "CREDIT DEDUCT / ABSENCE"
   wrapped onto three lines each, which pushed the header band taller than the
   data rows it labels. Dropping the uppercase transform and letting headers
   keep their natural case makes them shorter and easier to scan.
   ----------------------------------------------------------------------- */

/* Many tables here emit <th> without a <thead>, so the selector cannot rely on
   that ancestor. */
body.app-body th {
  white-space: nowrap !important;
  text-transform: none !important;
  letter-spacing: 0 !important;
  vertical-align: middle !important;
}

/* ops-controls.css forces uppercase through a longer selector, so matching its
   specificity is required rather than relying on load order alone. */
body.app-body:not(.regular-planner-body):not(.menu-english_check) table th,
body.app-body:not(.regular-planner-body):not(.menu-english_check) table thead th {
  text-transform: none !important;
  letter-spacing: 0 !important;
  white-space: nowrap !important;
  font-size: var(--text-sm) !important;
}

/* Sort links inherit the header treatment. */
body.app-body th a {
  font-size: inherit !important;
  font-weight: inherit !important;
  text-transform: none !important;
  letter-spacing: 0 !important;
}

/* Row height follows the text, not a reserved band. */
body.app-body tbody td {
  vertical-align: middle !important;
}

/* Status and count chips inside tables were full-size buttons. */
body.app-body td .badge,
body.app-body td .chip,
body.app-body td .pill {
  min-height: var(--control-h-sm) !important;
}

body.app-body td button,
body.app-body td .btn {
  min-height: var(--control-h-sm) !important;
  padding: 0 var(--space-4) !important;
  font-size: var(--text-sm) !important;
}

/* --- Icons ---------------------------------------------------------------

   Emoji were the most visible source of drift: each platform draws its own
   glyph, at its own weight, in its own colour. ui_icons.py emits these SVGs
   instead, and they inherit colour and size from the text around them.
   ----------------------------------------------------------------------- */

body.app-body {
  --icon-size: 15px;
}

body.app-body .ui-icon {
  width: var(--icon-size);
  height: var(--icon-size);
  flex-shrink: 0;
  vertical-align: -2px;
  display: inline-block;
}

/* Inside a button or badge the icon should track the label, not the box. */
body.app-body button .ui-icon,
body.app-body .btn .ui-icon,
body.app-body .badge .ui-icon,
body.app-body .chip .ui-icon {
  --icon-size: 14px;
  margin-right: var(--space-2);
}

body.app-body h1 .ui-icon,
body.app-body h2 .ui-icon,
body.app-body h3 .ui-icon,
body.app-body h4 .ui-icon {
  --icon-size: 16px;
  margin-right: var(--space-3);
}

body.app-body th .ui-icon,
body.app-body td .ui-icon,
body.app-body .u-text-xs .ui-icon,
body.app-body small .ui-icon {
  --icon-size: 13px;
}
