/* ReSouris — Sidebar nav system (UX redesign 2026-05-18)
 *
 * Layout: persistent left sidebar (collapsible groups) + top bar
 * (brand + search + sign-out). Mobile (≤768px): sidebar slides off-
 * canvas as a drawer; backdrop closes on tap. Pure CSS + minimal
 * vanilla JS for the drawer toggle.
 *
 * a11y posture inherits from layout.css:
 *   * `<details>/<summary>` for groups — keyboard-native, no JS needed
 *   * Each link carries aria-current="page" when active
 *   * Sidebar landmark uses role="navigation" + aria-label
 *   * Drawer uses aria-expanded on the hamburger + ESC closes
 *
 * Visual idiom shares tokens.css palette (slate dark theme).
 */

/* ============================================== Body shell ============= */

.app-body--has-sidebar {
  display: flex;
  flex-direction: column;
  min-height: 100vh;
}

.app-body--has-sidebar .app-shell {
  display: flex;
  flex-direction: row;
  flex: 1 0 auto;
  max-width: none;          /* override layout.css 1400px cap; sidebar+main fill viewport */
  padding: 0;
  width: 100%;
}

.app-body--has-sidebar .app-main {
  flex: 1 1 auto;
  min-width: 0;             /* prevents table-overflow blowing layout */
  padding: var(--space-6) var(--space-6);
  max-width: 1180px;        /* keep line-length sensible on wide screens */
}
/* Admin-list pages (anything that renders an .entity-table) get the
   full viewport width: columns are visually-dense and the line-length
   cap is for prose, not tables. :has() supported in all modern
   browsers (Chrome 105+, Firefox 121+, Safari 15.4+). */
.app-body--has-sidebar .app-main:has(.entity-table) {
  max-width: none;
}

.app-body--has-sidebar .app-footer {
  width: 100%;
  max-width: 1400px;
  margin: 0 auto;
  padding: var(--space-4) var(--space-6);
}

/* ============================================== Top bar ============== */

.app-topbar {
  display: flex;
  align-items: center;
  gap: var(--space-4);
  padding: var(--space-3) var(--space-6);
  border-bottom: 1px solid var(--border-subtle);
  background: var(--bg);
  position: sticky;
  top: 0;
  z-index: 50;
}

/* ⚠️ THE MARK AND THE WORDMARK ARE ONE LOCKUP. `.app-topbar` is a flex row with
   a uniform `--space-4` gap between its items, which is right for hamburger,
   brand, search and sign-out -- and wrong between an icon and the word it
   belongs to, where it reads as two separate controls rather than a logo.
   Pulling the pair back to ~8px is the whole fix. */
.app-topbar .app-brand__mark {
  margin-right: calc(0.5rem - var(--space-4));
}
.app-topbar .app-brand {
  margin: 0;
  font-size: var(--text-lg);
}

.app-topbar__search {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  flex: 1 1 auto;
  max-width: 480px;
  margin-left: var(--space-4);
}

.app-topbar__search-input {
  flex: 1 1 auto;
  min-width: 0;
  padding: var(--space-2) var(--space-3);
  font-size: var(--text-sm);
  background: var(--surface);
  border: 1px solid var(--border-subtle);
  border-radius: var(--radius-md);
  color: var(--text-primary);
}

.app-topbar__search-input:focus {
  outline: none;
  box-shadow: var(--focus-ring);
  border-color: var(--accent);
}

.app-topbar__search-input::placeholder {
  color: var(--text-disabled);
}

/* ⚠️ **THE AUTO MARGIN GOES ON THE FIRST ITEM OF THE RIGHT-HAND CLUSTER, NOT ON
   THE LAST ITEM OF THE BAR.** It used to sit on `.app-topbar__sign-out`, which
   in a flex row means sign-out alone absorbs the free space and flies to the
   right edge — leaving the theme toggle packed against the search box with 742
   measured pixels of nothing between them. Marcus, 2026-09-29: *"the theme
   change icons on the top bar are just AFTER 'Search'. With the Operator
   Console they were just BEFORE the 'Sign out' link."*

   ⚠️ **THE TEMPLATE WAS ALREADY IN THE REQUESTED ORDER**, which is why reading
   `_topbar.html` would have closed this as already done. A layout fault, not an
   ordering one — see v1.38.

   ⚠️ **SCOPED WITH `>` BECAUSE `.theme-toggle` IS SHARED WITH THE OPERATOR
   CONSOLE.** Its rules live in `layout.css` so both apps use one definition; an
   unscoped rule here would also move the console's toggle and the one on its
   profile page, which is not in a top bar at all. */
.app-topbar > .theme-toggle {
  margin-left: auto;
}

/* ============================================== Alert badge ========== */

/* ⚠️ **THE AUTO MARGIN HAS TO MOVE WHEN THE ALERT IS PRESENT, AND THE NOTE
   ABOVE IS WHY.** The rule it documents puts `margin-left: auto` on the FIRST
   item of the right-hand cluster. The alert renders before the theme toggle,
   so it becomes that first item — and leaving the auto margin on the toggle
   would give TWO items an auto margin, which in flex splits the free space
   between them and strands the alert in the middle of the bar. That is the
   same class of layout fault v1.38 fixed here, so it is pre-empted rather than
   rediscovered.

   ⓘ Equal specificity to the rule above (0,2,0), so ORDER decides it. This
   must stay below. */
.app-topbar > .app-topbar__alert { margin-left: auto; }
.app-topbar__alert ~ .theme-toggle { margin-left: 0; }

.app-topbar__alert {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  padding: 2px 10px 2px 8px;
  border-radius: 999px;
  border: 1px solid var(--border-subtle);
  background: var(--surface-raised);
  color: var(--text-secondary);
  font-size: var(--text-xs);
  font-weight: 600;
  line-height: 1.6;
  text-decoration: none;
}

.app-topbar__alert:hover,
.app-topbar__alert:focus-visible {
  border-color: var(--accent);
  color: var(--text-primary);
}

/* ⛔ **RED IS FOR `alert`, AND ONLY FOR `alert`.** `Notice.severity` decides
   this, not the count: an icon that goes red for anything at all stops being
   read, and then the one thing that needed acting on looks like the other
   four. */
.app-topbar__alert--alarm {
  border-color: var(--danger);
  background: var(--danger);
  color: #FFFFFF;
}

.app-topbar__alert--alarm:hover,
.app-topbar__alert--alarm:focus-visible {
  border-color: var(--danger);
  color: #FFFFFF;
  filter: brightness(1.08);
}

/* ⓘ The glyph is a text "!" in a ring rather than an icon file: it inherits
   the colour of whichever state above is active, so there is no second place
   to keep the two in step. */
.app-topbar__alert-glyph {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 15px;
  height: 15px;
  border-radius: 50%;
  border: 1.5px solid currentColor;
  font-size: 10px;
  font-weight: 800;
  line-height: 1;
}

.app-topbar__alert-count { font-variant-numeric: tabular-nums; }

/* ============================================== Hamburger ============ */

.app-topbar__hamburger {
  display: none;            /* hidden on desktop; shown via media query */
  flex-direction: column;
  justify-content: center;
  gap: 4px;
  width: 40px;
  height: 40px;
  padding: 0;
  background: transparent;
  border: 1px solid var(--border-subtle);
  border-radius: var(--radius-md);
  cursor: pointer;
}

.app-topbar__hamburger:focus {
  outline: none;
  box-shadow: var(--focus-ring);
}

.app-topbar__hamburger-bar {
  display: block;
  height: 2px;
  background: var(--text-primary);
  margin: 0 auto;
  width: 20px;
  transition: transform var(--motion-fast) var(--ease-standard);
}

/* ============================================== Sidebar ============== */

.app-sidebar {
  flex: 0 0 240px;
  width: 240px;
  padding: var(--space-4) 0;
  border-right: 1px solid var(--border-subtle);
  background: var(--bg);
  overflow-y: auto;
  position: sticky;
  top: 57px;                /* below sticky top bar */
  align-self: flex-start;
  max-height: calc(100vh - 57px);
}

/* ⚠️ THE GUEST SIDEBAR IS AN EMPTY LANDMARK AND MUST OCCUPY NO SPACE.
 *
 * `_sidebar.html` renders an empty `<nav>` on unauthenticated pages to keep the
 * WCAG 2.2 AA `navigation` landmark that the a11y baseline asserts. It inherits
 * `.app-sidebar { flex: 0 0 240px }` above — and `.app-shell` is
 * `flex-direction: column` for guests, because only `.app-body--has-sidebar`
 * flips it to `row`. In a COLUMN, `flex-basis: 240px` is a HEIGHT.
 *
 * So an empty nav reserved a 240px-tall block above the content on every signed
 * out page, pushing the sign-in card 332px down a 1105px viewport and forcing a
 * scroll to reach it. Measured in the browser, not inferred: the card is 813px
 * and fits comfortably; `.auth-shell`'s own `min-height: 60vh` was never the
 * binding constraint.
 *
 * `height: 0` rather than `display: none` — the element must stay in the
 * accessibility tree for the landmark to count.
 */
.app-sidebar--guest {
  flex: 0 0 auto;
  width: 0;
  height: 0;
  padding: 0;
  border-right: 0;
}

/* With the phantom 240px gone the card sits at the top of the pane, because
 * `.auth-shell`'s own `min-height: 60vh` is shorter than the card. Let the
 * guest pane grow into whatever the chrome leaves and centre inside it —
 * NO subtracted constant, because the topbar and footer are token-sized and
 * any number written here would be wrong the first time a token changed.
 *
 * `margin-block: auto` on the card rather than `align-items: center` alone:
 * when the viewport is shorter than the card, auto margins collapse and the
 * card stays fully reachable, where centring would clip its top off-screen.
 */
.app-body--guest .app-main {
  display: flex;
  flex-direction: column;
  flex: 1 1 auto;
}

.app-body--guest .auth-shell {
  flex: 1 1 auto;
  min-height: 0;
}

.app-body--guest .auth-card {
  margin-block: auto;
}

.app-sidebar__groups {
  list-style: none;
  padding: 0;
  margin: 0;
}

.app-sidebar__group {
  margin: 0 0 var(--space-1) 0;
  padding: 0 var(--space-2);
}

.app-sidebar__details {
  position: relative;
  border-radius: var(--radius-md);
}

.app-sidebar__group.is-active > .app-sidebar__details > .app-sidebar__summary {
  background: var(--accent-soft);
}
.app-sidebar__group.is-active > .app-sidebar__details > .app-sidebar__summary
  .app-sidebar__group-label {
  color: var(--accent);
  font-weight: 700;
}

.app-sidebar__summary {
  list-style: none;
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: var(--space-2) var(--space-3);
  font-size: var(--text-sm);
  font-weight: 600;
  color: var(--text-secondary);
  cursor: pointer;
  border-radius: var(--radius-md);
  user-select: none;
  transition: background-color var(--motion-fast) var(--ease-standard);
}

.app-sidebar__summary::-webkit-details-marker {
  display: none;
}

.app-sidebar__summary:hover {
  background: var(--surface);
  color: var(--text-primary);
}

.app-sidebar__summary:focus {
  outline: none;
  box-shadow: var(--focus-ring);
}

.app-sidebar__chevron {
  font-size: 10px;
  /* ⚠️ NOT --text-disabled: a chevron is not a disabled control, and that token
     is valued for a state WCAG 2.2 §1.4.3 exempts. It measured 2.45:1 in light
     and 3.75:1 in dark — 16 of this screen's 18 light-mode failures. v1.35 §9.2. */
  /* ⚠️ AND NOT --text-muted EITHER, WHICH §9.2 PUT HERE AND MEASURED AT 4.6:1 —
     against `--bg`. The sidebar's OPEN row is tinted, and on that row the same
     glyph measured 4.00:1: **145 failures across 145 screens**, the single
     largest contrast defect in the product. The background it fails on, #E0ECF3,
     appears as a literal in NO file — it is `--accent-soft` composited over
     `--bg`, so only a rendered audit can see it.
     --text-secondary clears every light background in play with headroom rather
     than a hairline: 7.24:1 on --bg, 6.30:1 on the tinted row, 6.92:1 on
     --surface-raised; dark 7.05:1 and 5.68:1. v1.35 §10.3.
     ⚠️ Measure a foreground against the background the element ACTUALLY HAS, in
     every state — resting, hover, [open], [aria-current]. */
  color: var(--text-secondary);
  transition: transform var(--motion-fast) var(--ease-standard);
}

.app-sidebar__details[open] .app-sidebar__chevron {
  transform: rotate(90deg);
}

.app-sidebar__items {
  list-style: none;
  padding: var(--space-1) 0 var(--space-2) var(--space-3);
  margin: 0;
}

.app-sidebar__link {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  padding: var(--space-2) var(--space-3);
  font-size: var(--text-sm);
  color: var(--text-secondary);
  text-decoration: none;
  border-left: 2px solid transparent;
  border-radius: 0 var(--radius-md) var(--radius-md) 0;
  transition: background-color var(--motion-fast) var(--ease-standard),
              color var(--motion-fast) var(--ease-standard),
              border-color var(--motion-fast) var(--ease-standard);
}

.app-sidebar__link-icon {
  display: inline-flex;
  flex: 0 0 auto;
  width: 16px;
  height: 16px;
  align-items: center;
  justify-content: center;
}
.app-sidebar__link-icon svg {
  width: 16px;
  height: 16px;
}
.app-sidebar__link-label {
  flex: 1 1 auto;
  min-width: 0;
}

.app-sidebar__link:hover {
  background: var(--surface);
  color: var(--text-primary);
}

.app-sidebar__link[aria-current="page"] {
  background: var(--accent-soft);
  color: var(--accent);
  border-left-color: var(--accent);
  font-weight: 600;
}

.app-sidebar__link:focus {
  outline: none;
  box-shadow: var(--focus-ring);
}

/* ============================================== Backdrop ============== */

.app-sidebar__backdrop {
  position: fixed;
  inset: 0;
  background: rgba(15, 23, 42, 0.7);
  z-index: 40;
  display: none;
}

.app-body--sidebar-open .app-sidebar__backdrop {
  display: block;
}

/* ============================================== Breadcrumbs ========== */
/* ============================================== Visually-hidden ======= */

.visually-hidden {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip: rect(0, 0, 0, 0);
  white-space: nowrap;
  border: 0;
}

/* ============================================== Mobile/tablet breakpoint
   Sidebar collapses to a hamburger-drawer below 1200px so phones AND
   tablet-portrait + small-laptop windows get the full-width content
   area. Above 1200px the persistent sidebar shows. */

@media (max-width: 1200px) {
  .app-topbar__hamburger {
    display: inline-flex;
  }

  .app-topbar__search {
    display: none;          /* search collapses; surfaced via /ui/admin/searchlight directly */
  }

  .app-sidebar {
    position: fixed;
    top: 57px;
    left: 0;
    bottom: 0;
    width: 280px;
    transform: translateX(-100%);
    /* ⚠️ WAS `var(--motion-normal)`, WHICH IS NOT A TOKEN. The scale is
       fast / normal / slow, so the whole `transition` declaration was invalid
       at computed-value time and fell back to its initial `all 0s` -- the
       mobile drawer snapped open instead of sliding, on the one surface where
       the movement tells you where the panel came from. Nothing failed: an
       undefined custom property is not an error, it just quietly wins as
       nothing. The prefers-reduced-motion block zeroes these tokens, so using
       the real one also restores that opt-out, which the invalid value had
       been bypassing. */
    transition: transform var(--motion-normal) var(--ease-standard);
    z-index: 45;
    box-shadow: 4px 0 12px rgba(0, 0, 0, 0.35);
  }

  .app-body--sidebar-open .app-sidebar {
    transform: translateX(0);
  }

  .app-body--has-sidebar .app-main {
    padding: var(--space-4) var(--space-4);
  }
}

@media (prefers-reduced-motion: reduce) {
  .app-sidebar,
  .app-sidebar__chevron,
  .app-topbar__hamburger-bar {
    transition: none;
  }
}

/* ---- Zoom: the roster, and nothing else ----------------------------------
 *
 * Marcus, 2026-09-01: "an option to Zoom into the roster. This will effectively
 * hide the navigation and the top bar and fill the screen with the roster only.
 * May still need scroll bars but it is a better experience for a browser."
 *
 * ⚠️ A ROSTER GRID IS THE ONE THING IN THIS PRODUCT THAT IS WIDER THAN A PAGE.
 * Every other surface is prose or a table that wraps; a week x rooms grid has a
 * real width and the sidebar is 260px of it. Hiding chrome is not a preference
 * here, it is how the artefact fits.
 *
 * `display: none` rather than skipping the includes, so there is one layout
 * rather than two that drift. The main pane also drops its 1180px reading cap,
 * which exists for line length in prose and is exactly wrong for a grid.
 */
.app-body--zoom .app-topbar,
.app-body--zoom .app-sidebar,
.app-body--zoom .app-footer,
.app-body--zoom .breadcrumbs {
  display: none;
}
.app-body--zoom .app-shell {
  max-width: none;
  padding: 0;
}
.app-body--zoom .app-main,
.app-body--has-sidebar.app-body--zoom .app-main {
  max-width: none;
  padding: var(--space-3) var(--space-4);
}
/* The exit control has to stay reachable once the chrome is gone.
 *
 * ⚠️ **IT USED TO EAT THE TEXT BEHIND IT.** Marcus, 2026-09-18: *"the 'Exit
 * zoom' button obscures the text below it."* It was `position: sticky` plus
 * `float: right` — and a floated element that has stuck is out of normal flow,
 * so the page-header's meta line ran underneath it. Measured at 1600px: the
 * button sat at x=1480 over the tail of "attempt 1, solver 9.15.6755".
 *
 * ⚠️ **`fixed`, NOT `sticky`.** Sticky only holds while its containing block is
 * in view; this is the one way out of a mode with no top bar, no sidebar and no
 * breadcrumb, so it must not be able to scroll away at all.
 *
 * ⚠️ **AND THE CORNER IS RESERVED RATHER THAN OVERLAID.** Taking the control out
 * of flow stops it pushing anything, but on its own that only moves the overlap
 * — the header would still run under it. The padding below is what actually
 * fixes the report; the icon shape only makes the reserved strip narrow.
 */
.roster-zoom__exit {
  position: fixed;
  top: var(--space-3);
  right: var(--space-3);
  z-index: 20;
}
/* ⚠️ THE SHAPE COMES FROM `btn--icon`, NOT FROM RULES HERE. `_icons.html` emits
   no width/height on purpose — "size + colour are CSS-driven via .btn--icon" —
   so an icon in a button without that class renders at zero and the control
   comes out as an empty outlined box. Which is exactly what the first attempt
   shipped, and only a screenshot showed it. */
.app-body--zoom .page-header {
  padding-right: calc(32px + var(--space-4));
}
