/* 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;
}

/* ============================================== 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));
}
